Compare commits

..
15 Commits
Author SHA1 Message Date
wyj 2c88bbc7e6 Doc: Define authoritative design document hygiene rules 2026-10-10 01:50:42 -04:00
wyj 80f9dcb3a3 Feat: Add post-tone-map mesh diagnostics with RGBA overlays
Color antialiased half-edges by ray outcome with configurable Catppuccin colors and default opacity 0.5.

Rasterize premultiplied RGBA8 overlays on the producer and composite in place after writing the clean image. Keep single-frame, movie, and replay output consistent.

Add overlay, CLI, and queue ownership regressions and document the final output architecture.
2026-10-10 01:46:32 -04:00
wyj 4053d5d0c8 Fix: Stop lens-map vertex decoding on read failure 2026-10-09 00:17:39 -04:00
wyj 7c980c35aa Benchmark: Compare quadratic entry precision and arithmetic cost
Generate plain long-double and experimental FMA variants from the production entry kernel. Compare exact-rational references, geometric validation, fallback counts and repeated timings with self-contained fixtures.

Validate cached build dependencies and reject stale timing output. Count all unconfirmed entry outcomes independently of reference classification.
2026-10-09 00:15:02 -04:00
wyj 789549ed2f Fix: Validate asymptotic entries with precise roots and fallback
Use scaled long-double quadratic arithmetic without explicit FMA. Validate entry candidates against backend geometry and localize uncertain entries along the original exterior trajectory.

Preserve conservative miss semantics and propagate concrete entry failures. Add production-sample and numerical regression coverage.
2026-10-09 00:14:54 -04:00
wyj 95c05f84d9 Fix: Distinguish concrete ray failure causes 2026-10-08 05:07:34 -04:00
wyj 76ed705d37 Fix: Report ray failure reasons and affected frames 2026-10-08 00:16:43 -04:00
wyj af8b83007f Fix: Separate normal renderer output from diagnostics 2026-10-07 22:14:56 -04:00
wyj 69a8cb5647 Feat: Add single-frame observer coordinate time option 2026-10-07 21:12:55 -04:00
wyj b3f1e837d5 Alcubierre: support superluminal v_s 2026-10-06 02:51:20 -04:00
wyj 0a46a7095b Feat: Complete adaptive geodesic tracing with DP54
Add error-controlled DP5(4) integration and trusted first-crossing localization, including non-monotonic energy thresholds and representable-time stepping.

Preserve adaptive state and independent step/time retry grants across RayPool, refinement and movie scheduling. Expose numerical controls, record actual persistent-sample costs, and add v3 lens-map provenance with legacy v2 RK4 import.

Use DP54 by default and select an 8M Schwarzschild maximum step from bounded scans and a two-run 4K comparison. Retain the conservative minimum-step guard and document critical-ray and backend capability limits. Archive self-contained benchmark inputs and raw output; keep fixed RK4 HDR references explicit.

Validation: make -B -j4 BUILD_TYPE=Debug test passed; explicit RK4 HDR references have zero differences. Bounded convergence checks, benchmark reproduction, Release build and focused reviews passed. No numerical-relativity backend is added.
2026-10-05 20:27:42 -04:00
wyj c894fdb11a Agent: add deepseek-coder sub agent 2026-10-05 06:39:26 -04:00
wyj f7380cbf75 Feat: Rework ray termination into escaped/dark/unresolved/incomplete
Replace the position capture cutoff with a camera-relative dark threshold
shared by every backend, and carry explicit outcome/reason provenance
through the ray, RayPool, adaptive mesh, lens-map and replay paths.

- eval/eval_slab return SpacetimePointStatus; remove SPACETIME_RAY_CAPTURED
  and the Schwarzschild capture radius; decouple observer construction from
  ray position.
- RayEndpoint stores RayOutcome/RayReason plus the last trusted state;
  budget exhaustion is retryable UNRESOLVED, data/integration failures are
  INCOMPLETE.
- Normal dark terminal is L - L0 >= --dark-threshold (default 8), with L0
  taken at the camera event and kept distinct from the worldtube entry
  energy; photon energy and frequency ratio are never reset.
- Implement E/D/U triangle decisions with merged budget retries, persistent
  probe witnesses promoted in place by vertex identity, conformity settling,
  and approximate-black boundary provenance with achieved-scale statistics.
- Add RayPool continuation state and per-ray step budgets.
- Bump lens-map to v2 with explicit end/outcome/reason, approx_black,
  threshold/retry/geometry provenance and per-frame retry counts; reject v1.
- Gate production output on incomplete/error results, overridable with
  --allow-incomplete.
- Update AGENTS.md, the design document and usage docs; add the termination
  oracle and regression coverage.

make -B -j4 BUILD_TYPE=Debug test passes with bit-identical reference HDRs.
2026-10-05 06:22:47 -04:00
wyj 7fde49308b Agents: add opencode code review sub agent 2026-10-05 02:44:15 -04:00
wyj 09a7417961 Feat: Add directed asymptotic escape and analytic Schwarzschild exterior
Replace radius-only escape termination with a common asymptotic exterior protocol: declared ends, moving escape worldtubes, directed inside->outside crossings, and a PENDING_ENTRY lifecycle shared by single-frame and movie tracing.

Add an analytic Carlson-integral Schwarzschild monopole exterior (angle primitive, bracketed turning radius, ingoing Kerr-Schild coordinate-time transfer, conserved-energy frequency) so a camera outside the escape sphere is traced through an entry event.

Make the lifecycle tri-state (no ends / ready / protocol error), carry end_id through the endpoint and lens mesh, validate sources in constructors via spacetime_source_finalize(), and refresh the Schwarzschild reference images for the corrected finish.
2026-10-04 03:32:28 -04:00
94 changed files with 26226 additions and 763 deletions

No files matched your search

+70
View File
@@ -0,0 +1,70 @@
---
description: 审查本项目代码变更,核实功能、修复与性能进展;复用已完成的完整测试,仅做受限专项验证,按实际影响报告可操作问题。
mode: subagent
model: openai/gpt-6.1-sol
variant: high
options:
reasoningEffort: high
permission:
edit: deny
task: deny
bash:
"*": allow
"make test*": deny
"make * test*": deny
"git add*": deny
"git commit*": deny
"git push*": deny
"git reset*": deny
"git checkout*": deny
"git restore*": deny
"git clean*": deny
---
# 角色与目标
你是本项目专属的 `code-reviewer`。进行独立、基于证据、与实际风险相称的 code review。你的职责是核实实现与宣称是否一致,发现值得修复的具体缺陷,并提出低复杂度、低开销的修正方向。
默认只审查,不修改源码、测试、文档或配置,不暂存、提交或重置 Git,不启动其他 subagent。Shell 同样受只读审查约束,不得借助脚本、重定向或其他命令绕过编辑权限。仅允许必要的专项编译/测试产生构建产物;临时探针与日志放在 `/tmp/opencode/`,遵守项目的路径与仓库卫生规则。
回复使用用户最新消息的语言;面向父 agent 的结论必须自包含,不能假定父 agent 已看到你的工具输出。
# 审查范围与进展核实
1. 先读项目 `AGENTS.md`,再读 `nr_spacetime_movie_renderer_design.md` 中与变更相关的章节。遵循当前权威设计,不把自己的偏好当作项目要求;文档与代码冲突时说明冲突及依据。
2. 确认要求审查的范围:工作区变更、指定 commit/range 或相对指定 base 的分支变更。父 agent 给出的总结只作为待核实的线索,不作为实现证据。
3. 用 `git status --short`、相关 `git diff`(包括 staged 与 unstaged)及必要的提交历史确认真实状态。新文件未必出现在普通 diff 中,必须检查审查范围内的 untracked 文件;已提交实现也不能因工作区 diff 为空而忽略。不得把无关的既有用户改动归因于本次实现。
4. 阅读变更的完整上下文,沿实际调用链检查入口、配置、数据流、错误处理和输出。新增函数、字段、CLI 选项或测试文件的存在不等于功能已接通;排查未调用实现、stub、TODO、错误的默认路径、遗漏的 build/test 注册以及仅覆盖理想路径的测试。
5. 对父 agent/用户明确描述的关键进展逐项核实,给出“代码支持”“部分支持”“与代码不符”或“证据不足”。区分已实现、已接入、已验证与性能已测量,避免把其中一项等同于全部完成。提供具体文件位置、调用链或日志证据;无法验证时如实说明,不推断其一定失败。
6. bug 修复要检查原触发条件是否真的被阻断、相关分支是否仍有相同问题;性能提升要检查热点路径确实使用优化、工作量和结果语义是否可比,以及是否存在 fallback 或开销转移。未经测量只能确认优化实现,不能确认速度提升;已有有效 benchmark 足以支撑其测量范围内的结论,不强求扩展到所有平台与输入。
7. 若审查期间代码继续变化,在结束前确认相关 diff 状态。结论限定于实际检查的版本;发现关键变化时仅复查受影响部分,不重启整轮审查或完整测试。
# 测试复用与受限验证(硬性约束)
- 父 agent 或用户明确声明已完成的 `make test` 类完整测试,必须复用该信息,绝不得重新完整运行。该约束覆盖带不同 flags 的同一 suite、其他完整测试入口、clean/rebuild 后重跑,以及拆成多个专项命令累计重跑整个 suite 等等价方式。不能为了“更放心”“独立确认”或补齐自己的测试记录而重跑。
- 明确区分“父 agent/用户报告完成”“已查看原始日志”和“本 reviewer 亲自执行”。“完成”不自动等于“通过”;没有明确结果就注明结果未提供。缺少日志不构成不信任声明或重新完整测试的理由,可以读取已有日志或指出有限的证据缺口。
- 即使没有完整测试声明,本 reviewer 也只运行受限规模的专项验证。需要完整 suite 时,将必要性与具体缺口交回父 agent/用户,不自行运行。配置中的命令拦截仅是辅助,不能利用命令包装、别名、脚本或其他工具绕过本节规则。
- 优先静态检查和既有测试/benchmark 结果。仅当某个具体疑点不能由这些证据解决时,才选择直接针对疑点的最小测试、现有测试的单例/过滤子集或小型复现。不要为样式、注释或与生产逻辑无关的低影响变更运行测试。
- 运行前先阅读 Makefile/测试入口,确认目标的依赖、默认数据规模和实际执行范围,防止“单个目标”隐式触发完整 suite、昂贵渲染或完整 benchmark。构建仅限必要目标,不做 `make clean` 或无关的全量重建。
- 每次先明确要验证的假设、命令和规模上限。默认整轮审查最多 3 次专项执行,每次超时不超过 60 秒,累计运行预算不超过 120 秒;使用小 fixture、低分辨率、少量 ray/样本、有限线程。父 agent/用户可以指定更合适的专项预算,但这不解除完整测试禁令。
- 超时、资源不足或无法在预算内复现时停止,报告验证限制,不将其直接判为产品失败。一次验证解决疑点后停止;只有新证据或失败需要解释时才继续使用剩余预算,不无限追加专项测试。
- 性能审查优先读取已有原始输出,核对命令、输入、build/cache、线程、工作量、fallback 和数值结果。确有必要时仅做小规模、同条件的对照,不将微基准结论泛化到完整 4K 视频工作负载。
# 风险校准与建议原则
- 正式 finding 必须有具体触发条件、可达路径和实际影响。先检查调用方保证、现有 guard、输入约束、数值容差、fallback 与测试,再下结论。不能仅凭“理论上可能”声称崩溃、数据损坏、物理失真或严重性能退化。
- 严重性根据实际影响、适用范围与触发可能性评估:P0 为已证实且广泛阻断的紧急问题;P1 为主要功能/物理正确性严重受损;P2 为明确可达、局部但值得修复的问题;P3 为低影响改进。P0/P1 必须有强证据,不因措辞耸动提高级别。
- 对实际上不会造成问题、已经由不变量保证安全或仅属个人风格偏好的点,不报缺陷。低严重性点不得夸大为 blocker;可选建议与正式缺陷分开,默认不堆积 nitpick,也不为了凑数制造问题。可以明确给出“未发现值得报告的缺陷”。
- 尚未证实的疑点作为待确认问题,写明缺少什么证据,不包装成已证实 bug。缺少某项测试本身不自动构成缺陷;说明它是否留下了与本次变更直接相关的、实质性的验证缺口。
- 优先最小局部修正和适用的专项 regression,不要求为低风险假设增加复杂状态机、全局防御性扫描、细粒度锁、重复热路径检查或大规模架构重写。建议成本必须与风险相称。
- 若确实存在严重正确性问题,而解决它必然涉及复杂度或性能代价,应如实说明证据与权衡;不隐藏问题,也不未经论证地指定最昂贵的方案。性能影响要有复杂度分析或测量支持,不使用无依据的倍数/百分比。
- 聚焦本次变更引入或影响的问题。既有问题仅在直接阻碍本次目标时提出,并明确注明它并非本次新增。
# 输出格式
1. **结论与范围**:简述检查的版本/变更范围,是否发现需要修复的问题,以及重要限制。测试通过不是“没有 bug”的证明;审查未发现问题也不等于全系统认证。
2. **Findings**:按严重性排序。每项包含 `[P1/P2/…] 简洁标题`、最小且相关的 `文件:行号`、触发条件、代码证据、实际影响与最小修正方向。合并同一根因,避免重复计数。不确定问题单列,不放入已证实 findings。
3. **进展核实**:对关键宣称列出“宣称 → 核实状态 → 证据/缺口”,特别区分实现、集成、测试和性能测量;用实际状态修正过于乐观的描述,也承认已经充分完成的部分。
4. **验证记录**:列出复用的完整测试声明/日志、自己实际执行的专项命令及规模/结果、未执行验证的具体限制。绝不声称自己运行了只由他人报告的测试。
保持报告精炼、可操作;没有某类内容时直接省略或用一句话说明,不输出冗长模板或泛化风险清单。
+32
View File
@@ -0,0 +1,32 @@
---
description: 搭配 GPT 父 agent 执行技术路线已初步敲定的明确任务。委派前,父 agent 必须提供明确、完整、详细、可执行的计划,包含目标、范围、已确定的技术方案、步骤、约束及验收方法;尚需探索、路线选择或关键决策的任务不得交给本 agent。有疑问时暂停并询问父 agent,否则按计划完整推进。
mode: subagent
model: deepseek/deepseek-flash
---
# 角色与分工
你是 `deepseek-coder`,作为 GPT 父 agent 的执行搭档,负责落实已经初步确定技术路线的明确任务。父 agent 负责探索、技术决策与计划制定,你负责按计划完成实现和验证,不自行承担未明确的路线选择或扩大任务范围。
# 开始前
1. 阅读当前适用的 `AGENTS.md`(包括相关目录的局部指引),以及它们和任务计划引用的设计文档、规范及必要的代码上下文,从这些文件获取实时要求。不要假定项目架构、模块边界或技术约定固定不变。
2. 检查父 agent 提供的计划是否明确、完整、详细且可执行:应包含目标与交付物、修改范围、已确定的技术方案、实施步骤、约束、依赖和验收方法。简单任务无需冗长模板,但不能缺少执行所需的信息。
3. 检查相关工作区状态,识别已有用户或其他 agent 的改动;不得覆盖、撤销或清理非你产生的修改。
# 疑问与阻塞
- 只要出现影响执行的疑问,就暂停并向父 agent 询问,包括需求含糊、计划缺漏、关键参数未定、存在需要取舍的多个方案、计划与实时指引或代码冲突,以及验证失败后需要改变技术路线。
- 说明疑问或阻塞、相关证据、需要父 agent 决定的具体事项,以及已经完成和尚未完成的部分。向父 agent 返回澄清请求,等待其明确答复后再继续;不要以猜测代替确认,也不要直接要求最终用户决策。
- 不擅自重新设计、替换已确定方案、降低验收标准或开展计划外重构。计划内不改变语义和路线的普通实现细节可依照现有代码惯例处理,无需逐项请示。
# 执行与验证
1. 没有疑问或阻塞时,按照计划持续推进全部步骤,完成实现、必要的接入、测试与要求的文档更新;不要只给出建议、留下占位实现,或在部分完成后无故停止。
2. 遵守实时项目指引及现有代码风格,保持修改聚焦,只实施计划授权的内容。不得擅自委派其他 agent、提交或推送代码,除非父 agent 明确授权。
3. 按计划执行验证并记录实际结果。失败时先做计划范围内的定位;若修复需要新决策,按上述规则暂停询问。不能通过跳过失败、修改预期或弱化测试来宣称完成。
4. 如遇工具、权限、环境或资源限制,明确报告限制及其对交付的影响,不把未执行或未通过的验证描述为成功。
# 向父 agent 交付
使用父 agent 要求的语言,默认跟随用户最新消息的语言。报告应自包含,简明列出完成内容、涉及文件、实际运行的验证命令与结果,以及剩余问题或阻塞。明确区分已完成、未完成和未经验证的事项;不能假定父 agent 已看过你的工具输出。
+20 -2
View File
@@ -59,9 +59,18 @@ catalog 内部数据保留 `(direction, temperature, amplitude)`,而非 RGB。
优先从 3+1 identities、已知 gauge RHS 或 temporal interpolant 的解析导数获得时间导数;不要为已有插值量另行做低阶 finite difference,也不要在 geodesic RHS 中计算不会使用的量。
## 黑洞终止
## 黑洞终止与暗终态
对 moving-puncture 数据,生产渲染使用经 AH calibration 得到、保守地位于 apparent horizon 内部的 puncture-centered cutoff 判定捕获。不要假定每次生产演化都会运行昂贵的 AH finder。可在未来加入 common-horizon 终止优化,但不得改变物理分类。
过去向光线不使用 horizon 内位置 cutoff、AH-calibrated puncture 小球或 armed/re-entry
状态机判定正常物理捕获。正常 dark 终态来自相机相对局域能量增长
`L - L0 = ln(alpha p^0) - ln(alpha p^0)|_start` 达到可配置阈值(默认 8,可用
`--dark-threshold` 覆盖),对所有 spacetime backend 统一生效;这是已确定需求,
不重置光子能量或频移。不同 dark reason 不制造 mesh seam。无法可靠推进的
积分必须报告具体数值失败,不得改写成 capture。轨迹仍可信但计算配额耗尽时返回可重试的
`UNRESOLVED/BUDGET_EXHAUSTED`;有限分辨率下的 triangle 决策中 `UUU` 必须追加计算,
`UUD/UDD` 达到几何停止尺度后可近似标黑并保留 triangle provenance 与面积统计。
不假定每次生产演化都会运行昂贵的 AH finder,也不依赖 capture sidecar。跨 chart、
跨 region 或穿越视界本身不是暗终态。
## 开发与验证顺序
@@ -76,6 +85,15 @@ catalog 内部数据保留 `(direction, temperature, amplitude)`,而非 RGB。
性能 benchmark 记录必须保留完整、可复制的命令及原始终端输出,不能只记录汇总耗时或吞吐量;输出中的 build/cache、输入加载、工作线程、处理数量与 fallback 等统计是后续正确归因性能变化的证据。
## 权威设计文档卫生
- 仓库级文档规则应具有跨任务适用性,不夹带单次任务的细节或案例。
- `nr_spacetime_movie_renderer_design.md` 应简明描述当前确定的架构、物理与数值约定、模块边界、数据流及 ownership;尚未确定的问题须明确标为待验证。
- 写最终设计,不写 agent 工作过程、对话经过、实现日记或备选方案淘汰史。已排除的临时设想不要改写成长期禁止条款;必要的物理与架构约束仍须保留。
- 决策依据只保留理解设计所必需的简要理由。实验过程、性能数据及详细对照放到符合仓库卫生要求的独立记录中,设计文档按需引用。
- 使用说明集中到 `usage.md`;README 保留面向使用者的简介与示例,避免在权威设计文档中重复罗列。
- 设计变更应改写并整合原有相关章节,删除过时或重复表述,检查跨章节一致性;不要通过不断追加补充段落堆积历史。
## 仓库卫生与短期产物
只有对本项目有长期记录价值、且值得进入 public repo 的测试与 benchmark 才纳入 git。
+59 -5
View File
@@ -36,7 +36,7 @@ TARGET_BASENAME := $(SPACETIME)_sky
OBJECT_DIR := $(BUILD_DIR)/obj/$(SPACETIME)
CORE_MINKOWSKI_SOURCES := $(COMMON_SOURCES) src/spacetime_minkowski.c
.PHONY: all backend clean run test tone-map-test sensor-bloom-test sensor-bloom-bench hip-psf-test hip-psf-bench fast-psf-fftw-bench minkowski schwarzschild alcubierre FORCE
.PHONY: all backend clean run test tone-map-test sensor-bloom-test sensor-bloom-bench mesh-overlay-test hip-psf-test hip-psf-bench fast-psf-fftw-bench minkowski schwarzschild alcubierre FORCE
ifneq ($(filter 0 1,$(PSF_EVENT_SINK)),$(PSF_EVENT_SINK))
$(error Unknown PSF_EVENT_SINK '$(PSF_EVENT_SINK)'; choose 0 or 1)
@@ -106,6 +106,12 @@ endif
# built for a different backend.
TEST_OUT_DIR := $(OBJECT_DIR)/$(HDR_BUILD_TAG)
TEST_TARGET := $(TEST_OUT_DIR)/test_geodesic
ADAPTIVE_GEODESIC_TEST_TARGET := $(TEST_OUT_DIR)/test_geodesic_adaptive
ASYMPTOTIC_TEST_TARGET := $(TEST_OUT_DIR)/test_asymptotic
ASYMPTOTIC_ENTRY_TEST_TARGET := $(TEST_OUT_DIR)/test_asymptotic_entry
ASYMPTOTIC_QUADRATIC_TEST_TARGET := $(TEST_OUT_DIR)/test_asymptotic_quadratic
ASYMPTOTIC_SCHWARZSCHILD_TEST_TARGET := $(TEST_OUT_DIR)/test_asymptotic_schwarzschild
TERMINATION_ORACLE_TEST_TARGET := $(TEST_OUT_DIR)/test_termination_oracle
FRAME_TEST_TARGET := $(TEST_OUT_DIR)/test_frame
SCHWARZSCHILD_TEST_TARGET := $(TEST_OUT_DIR)/test_schwarzschild
ALCUBIERRE_TEST_TARGET := $(TEST_OUT_DIR)/test_alcubierre
@@ -120,6 +126,7 @@ TONE_MAP_TEST_TARGET := $(TEST_OUT_DIR)/test_tone_map
MOVIE_OUTPUT_TEST_TARGET := $(TEST_OUT_DIR)/test_movie_output
SENSOR_BLOOM_TEST_TARGET := $(TEST_OUT_DIR)/test_sensor_bloom
SENSOR_BLOOM_BENCH_TARGET := $(TEST_OUT_DIR)/benchmark_sensor_bloom
MESH_OVERLAY_TEST_TARGET := $(TEST_OUT_DIR)/test_mesh_overlay
ifeq ($(PSF_BACKEND),hip)
TARGET := $(BUILD_DIR)/$(TARGET_BASENAME)_hip
@@ -204,12 +211,40 @@ $(TEST_OUT_DIR): | $(BUILD_DIR)
$(TEST_TARGET): tests/test_geodesic.c $(CORE_MINKOWSKI_SOURCES) $(CPU_FFTW_SOURCES) | $(TEST_OUT_DIR)
$(CC) $(CPPFLAGS) $(BUILD_CPPFLAGS) $(CFLAGS) $(BUILD_CFLAGS) $(OPENMP_FLAGS) -Isrc $^ $(LDLIBS) -o $@
# Explicit adaptive DP5(4) core and RayPool regression. The test textually
# includes both analytic providers (renaming spacetime_create_default), so the
# link line deliberately omits src/spacetime_{minkowski,schwarzschild}.c.
$(ADAPTIVE_GEODESIC_TEST_TARGET): tests/test_geodesic_adaptive.c $(COMMON_SOURCES) $(CPU_FFTW_SOURCES) | $(TEST_OUT_DIR)
$(CC) $(CPPFLAGS) $(BUILD_CPPFLAGS) $(CFLAGS) $(BUILD_CFLAGS) $(OPENMP_FLAGS) -DGEODESIC_EVENT_TESTING -Isrc $^ $(LDLIBS) -o $@
$(ASYMPTOTIC_TEST_TARGET): tests/test_asymptotic.c $(CORE_MINKOWSKI_SOURCES) $(CPU_FFTW_SOURCES) | $(TEST_OUT_DIR)
$(CC) $(CPPFLAGS) $(BUILD_CPPFLAGS) $(CFLAGS) $(BUILD_CFLAGS) $(OPENMP_FLAGS) -Isrc $^ $(LDLIBS) -o $@
# The test includes asymptotic.c to cover its private floating-point kernel.
$(ASYMPTOTIC_QUADRATIC_TEST_TARGET): tests/test_asymptotic_quadratic.c $(CORE_MINKOWSKI_SOURCES) $(CPU_FFTW_SOURCES) | $(TEST_OUT_DIR)
$(CC) $(CPPFLAGS) $(BUILD_CPPFLAGS) $(CFLAGS) $(BUILD_CFLAGS) $(OPENMP_FLAGS) -Isrc $(filter-out src/asymptotic.c,$^) $(LDLIBS) -o $@
$(ASYMPTOTIC_SCHWARZSCHILD_TEST_TARGET): tests/test_asymptotic_schwarzschild.c $(COMMON_SOURCES) src/spacetime_schwarzschild.c $(CPU_FFTW_SOURCES) | $(TEST_OUT_DIR)
$(CC) $(CPPFLAGS) $(BUILD_CPPFLAGS) $(CFLAGS) $(BUILD_CFLAGS) $(OPENMP_FLAGS) -DSPACETIME_SCHWARZSCHILD -Isrc $^ $(LDLIBS) -o $@
# Independent core regression for the backend-free numerical entry localizer.
# It links only the new module and the shared spacetime dispatch wrapper: no
# analytic backend, no geodesic integrator and no asymptotic.c are required,
# so it stays exercisable independently of the route integration.
$(ASYMPTOTIC_ENTRY_TEST_TARGET): tests/test_asymptotic_entry.c src/asymptotic_entry.c src/spacetime_common.c src/asymptotic_entry.h src/asymptotic.h src/geodesic.h src/spacetime.h src/observer.h | $(TEST_OUT_DIR)
$(CC) $(CPPFLAGS) $(BUILD_CPPFLAGS) $(CFLAGS) $(BUILD_CFLAGS) $(OPENMP_FLAGS) -Isrc tests/test_asymptotic_entry.c src/asymptotic_entry.c src/spacetime_common.c $(LDLIBS) -o $@
$(FRAME_TEST_TARGET): tests/test_frame.c $(CORE_MINKOWSKI_SOURCES) $(CPU_FFTW_SOURCES) | $(TEST_OUT_DIR)
$(CC) $(CPPFLAGS) $(BUILD_CPPFLAGS) $(CFLAGS) $(BUILD_CFLAGS) $(OPENMP_FLAGS) -Isrc $^ $(LDLIBS) -o $@
$(SCHWARZSCHILD_TEST_TARGET): tests/test_schwarzschild.c $(COMMON_SOURCES) src/spacetime_schwarzschild.c $(CPU_FFTW_SOURCES) | $(TEST_OUT_DIR)
$(CC) $(CPPFLAGS) $(BUILD_CPPFLAGS) $(CFLAGS) $(BUILD_CFLAGS) $(OPENMP_FLAGS) -Isrc $^ $(LDLIBS) -o $@
# Independent physics oracle for the termination policy (plan P0); links the
# analytic Schwarzschild backend and its exterior module.
$(TERMINATION_ORACLE_TEST_TARGET): tests/test_termination_oracle.c $(COMMON_SOURCES) src/spacetime_schwarzschild.c $(CPU_FFTW_SOURCES) | $(TEST_OUT_DIR)
$(CC) $(CPPFLAGS) $(BUILD_CPPFLAGS) $(CFLAGS) $(BUILD_CFLAGS) $(OPENMP_FLAGS) -DSPACETIME_SCHWARZSCHILD -Isrc $^ $(LDLIBS) -o $@
$(ALCUBIERRE_TEST_TARGET): tests/test_alcubierre.c $(COMMON_SOURCES) src/spacetime_alcubierre.c $(CPU_FFTW_SOURCES) | $(TEST_OUT_DIR)
$(CC) $(CPPFLAGS) $(BUILD_CPPFLAGS) $(CFLAGS) $(BUILD_CFLAGS) $(OPENMP_FLAGS) -Isrc $^ $(LDLIBS) -o $@
@@ -233,10 +268,10 @@ $(FAST_PSF_FFTW_TEST_TARGET): tests/test_fast_psf_fftw.c $(CORE_MINKOWSKI_SOURCE
$(TONE_MAP_TEST_TARGET): tests/test_tone_map.c src/optics.c src/optics.h $(CPU_FFTW_SOURCES) | $(TEST_OUT_DIR)
$(CC) $(CPPFLAGS) $(BUILD_CPPFLAGS) $(CFLAGS) $(BUILD_CFLAGS) $(OPENMP_FLAGS) -Isrc tests/test_tone_map.c src/optics.c $(CPU_FFTW_SOURCES) $(LDLIBS) -o $@
# The movie-output queue links production optics + fast_psf_fftw only, so it
# The movie-output queue links production optics, mesh overlay and FFTW only, so it
# needs neither a catalog nor ray tracing.
$(MOVIE_OUTPUT_TEST_TARGET): tests/test_movie_output.c src/movie_output.c src/movie_output.h src/optics.c src/optics.h $(CPU_FFTW_SOURCES) | $(TEST_OUT_DIR)
$(CC) $(CPPFLAGS) $(BUILD_CPPFLAGS) $(CFLAGS) $(BUILD_CFLAGS) $(OPENMP_FLAGS) -Isrc tests/test_movie_output.c src/movie_output.c src/optics.c $(CPU_FFTW_SOURCES) $(LDLIBS) -o $@
$(MOVIE_OUTPUT_TEST_TARGET): tests/test_movie_output.c src/movie_output.c src/movie_output.h src/mesh_overlay.c src/mesh_overlay.h src/optics.c src/optics.h $(CPU_FFTW_SOURCES) | $(TEST_OUT_DIR)
$(CC) $(CPPFLAGS) $(BUILD_CPPFLAGS) $(CFLAGS) $(BUILD_CFLAGS) $(OPENMP_FLAGS) -Isrc tests/test_movie_output.c src/movie_output.c src/mesh_overlay.c src/optics.c $(CPU_FFTW_SOURCES) $(LDLIBS) -o $@
$(FAST_PSF_FFTW_BENCH_TARGET): tests/benchmark_fast_psf_fftw.c $(CORE_MINKOWSKI_SOURCES) $(CPU_FFTW_SOURCES) | $(TEST_OUT_DIR)
$(CC) $(CPPFLAGS) $(BUILD_CPPFLAGS) $(CFLAGS) $(BUILD_CFLAGS) $(OPENMP_FLAGS) -Isrc $^ $(LDLIBS) -o $@
@@ -250,6 +285,11 @@ $(SENSOR_BLOOM_TEST_TARGET): tests/test_sensor_bloom.c src/sensor_bloom.c src/se
$(SENSOR_BLOOM_BENCH_TARGET): tests/benchmark_sensor_bloom.c src/sensor_bloom.c src/sensor_bloom.h | $(TEST_OUT_DIR)
$(CC) $(CPPFLAGS) $(BUILD_CPPFLAGS) $(CFLAGS) $(BUILD_CFLAGS) $(OPENMP_FLAGS) -Isrc tests/benchmark_sensor_bloom.c src/sensor_bloom.c $(LDLIBS) -o $@
# The mesh-overlay regression links only the standalone overlay module: it
# needs neither a catalog, ray tracing, FFTW, nor an output writer.
$(MESH_OVERLAY_TEST_TARGET): tests/test_mesh_overlay.c src/mesh_overlay.c src/mesh_overlay.h | $(TEST_OUT_DIR)
$(CC) $(CPPFLAGS) $(BUILD_CPPFLAGS) $(CFLAGS) $(BUILD_CFLAGS) $(OPENMP_FLAGS) -Isrc tests/test_mesh_overlay.c src/mesh_overlay.c $(LDLIBS) -o $@
# The FFTW-vs-spatial test is meaningful only in the CPU PSF build.
ifneq ($(CPU_FFTW_SOURCES),)
FAST_PSF_FFTW_TEST_DEP := $(FAST_PSF_FFTW_TEST_TARGET)
@@ -259,10 +299,16 @@ FAST_PSF_FFTW_TEST_DEP :=
FAST_PSF_FFTW_TEST_RUN :=
endif
test: $(CAMERA_TEST_TARGETS) $(TEST_TARGET) $(FRAME_TEST_TARGET) $(SCHWARZSCHILD_TEST_TARGET) $(ALCUBIERRE_TEST_TARGET) $(OBSERVER_TRACK_TEST_TARGET) $(CATALOG_PREFETCH_TEST_TARGET) $(FAST_PSF_FFTW_TEST_DEP) $(TONE_MAP_TEST_TARGET) $(MOVIE_OUTPUT_TEST_TARGET) $(SENSOR_BLOOM_TEST_TARGET)
test: $(CAMERA_TEST_TARGETS) $(TEST_TARGET) $(ADAPTIVE_GEODESIC_TEST_TARGET) $(ASYMPTOTIC_TEST_TARGET) $(ASYMPTOTIC_ENTRY_TEST_TARGET) $(ASYMPTOTIC_QUADRATIC_TEST_TARGET) $(ASYMPTOTIC_SCHWARZSCHILD_TEST_TARGET) $(TERMINATION_ORACLE_TEST_TARGET) $(FRAME_TEST_TARGET) $(SCHWARZSCHILD_TEST_TARGET) $(ALCUBIERRE_TEST_TARGET) $(OBSERVER_TRACK_TEST_TARGET) $(CATALOG_PREFETCH_TEST_TARGET) $(FAST_PSF_FFTW_TEST_DEP) $(TONE_MAP_TEST_TARGET) $(MOVIE_OUTPUT_TEST_TARGET) $(SENSOR_BLOOM_TEST_TARGET) $(MESH_OVERLAY_TEST_TARGET)
$(TEST_OUT_DIR)/test_observer_minkowski
$(TEST_OUT_DIR)/test_observer_schwarzschild
$(TEST_TARGET)
$(ADAPTIVE_GEODESIC_TEST_TARGET)
$(ASYMPTOTIC_TEST_TARGET)
$(ASYMPTOTIC_ENTRY_TEST_TARGET)
$(ASYMPTOTIC_QUADRATIC_TEST_TARGET)
$(ASYMPTOTIC_SCHWARZSCHILD_TEST_TARGET)
$(TERMINATION_ORACLE_TEST_TARGET)
$(FRAME_TEST_TARGET)
$(SCHWARZSCHILD_TEST_TARGET)
$(ALCUBIERRE_TEST_TARGET)
@@ -272,7 +318,12 @@ test: $(CAMERA_TEST_TARGETS) $(TEST_TARGET) $(FRAME_TEST_TARGET) $(SCHWARZSCHILD
$(TONE_MAP_TEST_TARGET)
$(MOVIE_OUTPUT_TEST_TARGET)
$(SENSOR_BLOOM_TEST_TARGET)
$(MESH_OVERLAY_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)
python3 tests/test_mesh_overlay_cli.py $(BUILD_DIR)
tone-map-test: $(TONE_MAP_TEST_TARGET)
$(TONE_MAP_TEST_TARGET)
@@ -280,6 +331,9 @@ tone-map-test: $(TONE_MAP_TEST_TARGET)
sensor-bloom-test: $(SENSOR_BLOOM_TEST_TARGET)
$(SENSOR_BLOOM_TEST_TARGET)
mesh-overlay-test: $(MESH_OVERLAY_TEST_TARGET)
$(MESH_OVERLAY_TEST_TARGET)
sensor-bloom-bench: $(SENSOR_BLOOM_BENCH_TARGET)
fast-psf-fftw-bench: $(FAST_PSF_FFTW_BENCH_TARGET)
+10 -3
View File
@@ -39,7 +39,7 @@ HIP retains parallel CPU catalog mapping and uses bounded, completion-protected
event uploads. See [HIP configuration and bounded performance checks](build.md#optional-hip-psf-acceleration).
The [Nmesh](https://github.com/nmeshsource/nmesh) numerical-spacetime backend and BBH rendering are still planned.
The current scope is black-hole capture and distant stellar backgrounds;
The current scope is black-hole shadows and distant stellar backgrounds;
local matter emission, accretion disks, and plasma are outside this stage.
See the [design document](nr_spacetime_movie_renderer_design.md) for the
architecture and development roadmap.
@@ -192,6 +192,10 @@ tone-mapped image; `--draw-mesh` additionally writes the final image-plane
triangles, so one command produces both
`output/imgs/schwarzschild_test_grid.png` (no mesh) and
`output/imgs/schwarzschild_test_grid_mesh.png` (mesh overlay).
The antialiased mesh is drawn after tone mapping: gray escape half-edges,
purple dark half-edges, yellow budget-unresolved half-edges, and red failure
half-edges, using Catppuccin Mocha defaults. Color and opacity settings are
documented in `usage.md`.
```sh
mkdir -p output/imgs
@@ -254,8 +258,11 @@ derivatives. Defaults: `--rtol 1e-10 --atol 1e-12 --stop-radius 0.001`.
Integration crosses the horizon and stops at this numerical guard before the
singularity, reporting its proper time and retaining only regular cadence samples.
The guard is not the exact singularity; reduce it and tolerances to check convergence.
The renderer's independent ray capture cutoff remains `r=1.5M`: rows inside it are
valid trajectory data but the current renderer captures those rays immediately.
The renderer no longer uses a position capture cutoff: cameras at and inside the
old `r=1.5M` guard are valid targets, and a normal dark pixel comes from the
redshift-threshold truncation `log(alpha p^0) >= 8`. Budget-exhausted and
data/integration failures are separate unresolved/incomplete outcomes and are not
silently rendered as dark.
The script reports maximum tetrad drift and rejects errors above `1e-6` rather
than silently repairing the transported frame. Run the orbit, transport and CSV
render regressions after building with `python3 tests/test_schwarzschild_camera_track.py`.
+1
View File
@@ -119,6 +119,7 @@ mkdir -p output/imgs
### 示例:叠加网格的合成测试星表
这个示例使用 `assets/sky_grid_5deg.csv` 检查 Schwarzschild 时空中的引力透镜效果与自适应网格细分。主输出是不带网格的成品图;`--draw-mesh` 会额外写出最终的像平面三角网格,因此同一次命令会同时生成 `output/imgs/schwarzschild_test_grid.png`(无网格)和 `output/imgs/schwarzschild_test_grid_mesh.png`(网格叠加)。
网格在 tone mapping 后以抗锯齿半边叠加。默认采用 Catppuccin Mocha:逃逸为灰色、暗终态为紫色、预算耗尽未决为黄色、真实失败为红色,未追踪为蓝色。颜色与透明度配置详见 `usage.md`。
```sh
mkdir -p output/imgs
@@ -0,0 +1,224 @@
# adaptive_step_bounds_2026-10-05
Long-term benchmark for calibrating the DP54 adaptive-integrator step bounds
(`min_step` / `max_step`) of the past-directed null-geodesic tracer, and the
public record of the one authorised 4K Schwarzschild `max_step` 2-vs-8 pair.
The benchmark runs the public endpoint `geodesic_trace_past()`, the production
DP core, and bounded adaptive-mesh renders against the analytic
Minkowski / Schwarzschild / Alcubierre backends. It does not modify production
sources.
## Build (required before renderer scripts)
The Python mesh/4K drivers start from an existing renderer binary. The shell
endpoint/core drivers compile their own small harnesses against production code.
```
make -j4 BUILD_TYPE=Release SPACETIME=schwarzschild backend
# binary: build/Release/schwarzschild_sky
```
## Files
| file | role |
|---|---|
| `a_public_endpoints.c` | Experiment A: step-bound scans through the public endpoint, tol=1e-12 reference, RK4 h-halving on near-critical rays. |
| `b_actual_h.c` | Experiment B: textually includes `src/geodesic.c`, drives the production `dp_advance_one` on finite trusted intervals to measure observed accepted `h`. |
| `critical_ref_check.c` | Critical-classification probe (r30/r100 inward, r2.1 outward; `theta_crit` and `theta_crit±1e-7`; tol/max_step ladder). |
| `mesh_maps.py` | Pure GRLENS v3 `load`/`compare`; no renderer, no untracked import. |
| `run_mesh.py` | 8 bounded 320x180 candidates (R100 and R2.1, `max_step` .5/2/8/32). |
| `run_mesh_reference.py` | 2 tight-tolerance references and comparisons against the 8 maps. |
| `summarize_4k.py` | Postprocess existing maps only; never invokes a renderer. |
| `run_4k_pair.py` | One-task, at-most-two-attempt 4K pair; refuses to run without `--authorize-two-4k`. |
| `record_environment.py` | Provenance (git/hashes/compiler/cpu/build/run commands); no render. |
| `failure_floor.c` | Failure-path floor diagnostic; includes `../../tests/test_geodesic_adaptive.c`. |
| `run_failure_floor.sh` | Builds/runs `failure_floor.c` with `-DGEODESIC_EVENT_TESTING`. |
| `run_limited.sh` | Builds/runs Experiment A + B; all output under `OUT_DIR`. |
| `run_critical_ref.sh` | Builds/runs `critical_ref_check.c`; all output under `OUT_DIR`. |
| `summarize.py` | `OUT_DIR` CLI arg (default `local/adaptive_step_bounds_2026-10-05`); writes `OUT_DIR/logs/summary.txt`. |
| `results/` | Tracked long-term evidence (see below). |
## Reproduction
```
AB_OUT="$PWD/local/adaptive_step_bounds_2026-10-05"
OUT="$PWD/local/adaptive_bounds_mesh"
bash benchmarks/adaptive_step_bounds_2026-10-05/run_limited.sh "$AB_OUT"
bash benchmarks/adaptive_step_bounds_2026-10-05/run_critical_ref.sh "$AB_OUT"
python3 benchmarks/adaptive_step_bounds_2026-10-05/summarize.py "$AB_OUT"
python3 benchmarks/adaptive_step_bounds_2026-10-05/run_mesh.py "$OUT"
python3 benchmarks/adaptive_step_bounds_2026-10-05/run_mesh_reference.py "$OUT"
bash benchmarks/adaptive_step_bounds_2026-10-05/run_failure_floor.sh "$OUT"
# 4K only after explicit fresh user authorization for that task:
python3 benchmarks/adaptive_step_bounds_2026-10-05/run_4k_pair.py \
--authorize-two-4k --out local/adaptive_bounds_4k
```
Defaults: A/B `OUT_DIR` is `local/adaptive_step_bounds_2026-10-05`; mesh
`OUT_DIR` is `local/adaptive_bounds_mesh`; 4K `--out` default is
`local/adaptive_bounds_4k`. All logs/CSV go to `OUT_DIR`; the tracked
benchmark directory holds only sources and `results/`. Scratch binaries go to
`/tmp/opencode/step_bounds/`.
The `run_4k_pair.py` driver refuses to run without `--authorize-two-4k`, keeps
an attempt ledger, and has no automatic retry. Authorisation does not extend to
later tasks or routine tests.
All catalog inputs are generated inline by the scripts (column header
`longitude_deg,latitude_deg,temperature_K,amplitude`, one 1e-30 star); no
external survey CSV is read.
## Results
### 1. Public-endpoint step-bound scan (Experiment A/B)
Environment and full table inputs: `results/source_environment.txt`,
`results/summary_tables.txt`; full stdout: `results/expA.log`,
`results/expB.log`. Per-ray raw CSV (not tracked) is reproduced by
`run_limited.sh`.
| backend | worst near-critical sky error vs tol=1e-12 ref | notes |
|---|---|---|
| Schwarzschild r30 | 2.82e-4 rad (`max_step`≥1); 8.67e-5 rad at 0.25 | r100 8.0e-5 / 2.6e-5; r2.1 2.87e-4 |
| Minkowski | Δn = 0, Δg/g = 0 (analytic) | crossing x/t ≤ 7.7e-11 at `max_step`≤256 |
| Alcubierre | ≤1.8e-10 rad, no class change | 8x vs 1x `initial` RHS cost 4.3x–5.7x |
- Dark terminal `L−L0` margin agrees with the reference to ≤2.1e-13 in every
config; near-critical dark stop times differ up to ~41 time units between
tol=1e-9 and tol=1e-12 (orbit-count sensitivity, reported not hidden).
- Schwarzschild floor plateau: `min_step` `1e-14 … 1e-2` give bit-identical
results and RHS; only `0.1` breaks one r1.5 ray
(`INCOMPLETE/INTEGRATION_ERROR`). No evidence to raise the default floor,
and no evidence that `1e-12` is uniquely optimal (it is never active).
- Observed accepted `h` (Experiment B, finite window `T=min(20, ref span)`):
**Schwarzschild only** — r1.5 radial 0.0921, all other sampled Schwarzschild
rays ≥0.1; Alcubierre minimum = its initial step (0.05 / 0.005 / 0.0005);
Minkowski 1.0. This is a finite-window diagnostic, **not** a full-trajectory
minimum.
- RK4 h-halving worst (r2.1, `theta_crit−1e-7`): 1.04e-5 rad.
### 2. Bounded 320x180 adaptive mesh
Numeric inputs: `results/mesh_summary.json`,
`results/mesh_reference_summary.json`. Scene: R100/FOV45/jacobian0.2 and
R2.1/outward/FOV90; DP54, hmin 1e-12, initial 0.1, tol 1e-9 (tight reference
1e-12), 8 threads, one-point dim catalog.
| scene | `max_step` | persistent vertices | RHS | wall (s) |
|---|---:|---:|---:|---:|
| R100 | 0.5 | 3158 | 16813349 | 2.071 |
| R100 | 2 | 3158 | 6030185 | 0.979 |
| R100 | 8 | 3158 | 4230835 | 0.823 |
| R100 | 32 | 3158 | 4081609 | 0.830 |
| R2.1 | 0.5 | 5496 | 20136508 | 1.974 |
| R2.1 | 2 | 5496 | 9548434 | 1.178 |
| R2.1 | 8 | 5496 | 7389760 | 0.970 |
| R2.1 | 32 | 5496 | 7197806 | 0.972 |
- 2→8 RHS reduction 29.84% (R100) / 22.61% (R2.1); 8→32 only 3.53% / 2.60%.
- Identical terminal classifications and mesh/sample counts for all four caps.
- Error vs tight reference: R100 max sky 9.216e-8 (hmax2) / 8.744e-8 (hmax8),
mutual 2-vs-8 difference 7.063e-9 rad; R2.1 max sky 1.4772e-5 rad at both,
mutual difference 8.370e-11 rad. The larger R2.1 reference error is
tolerance/conditioning sensitive and is not cured by a smaller `max_step`.
- Max log-frequency differences ≤8.51e-10 (R100) / ≤2.10e-9 (R2.1).
- Short single-run wall times include setup/cache/output; not a timing study.
### 3. One authorised 4K pair (R100, 3840x2160, FOV45)
Raw terminal output (byte-for-byte): `results/4k_hmax2.log`,
`results/4k_hmax8.log`; provenance: `results/4k_metadata.json`,
`results/4k_attempts.json`; postprocessed: `results/4k_final_summary.json`,
`results/4k_raw_summary.log`.
| metric | `max_step`=2 | `max_step`=8 |
|---|---:|---:|
| persistent vertices / triangles | 66045 / 131338 | 66045 / 131338 |
| outcomes | 62419 ESC, 3626 DARK | 62419 ESC, 3626 DARK |
| unresolved / error | 0 | 0 |
| persistent accepted | 15,768,677 | 10,183,020 |
| persistent rejected | 97,176 | 194,781 |
| persistent RHS | 130,409,139 | 91,991,144 |
| trace+refine time (s) | 10.774 | 7.805 |
| process wall (s) | 11.3893 | 8.38925 |
- Triangle payload is bit-identical; shared film identities 66045, terminal
mismatches 0; max mutual endpoint sky difference 7.9544e-7 rad, max logg
difference 3.4711e-11.
- Persistent-vertex RHS reduction −29.46%; accepted −35.42%; wall −26.34%.
- **Cost scope:** the v3 map stores final persistent vertices only. The
requested-sample count including discarded refinement probes is 116308
(> 66045), so discarded probes contribute no stored cost and these sums must
not be read as full-render executed RHS totals.
- **Timing:** one sequential pair only; no statistical timing confidence.
- PSF splat cached/direct = 0 and `--psf-min-y` discarded 1965 (hmax2) / 1954
(hmax8) events; this does not measure image error. The PNGs are dark
diagnostic backgrounds; no 2MASS catalog was used.
### 4. Critical-classification reference check
Full output: `results/critical_ref.log`, `results/critical_ref.csv` (36
endpoint traces). The exact `theta_crit` direction is a classification
separatrix; it is reported, never asserted.
- r2.1 `theta_crit`: ref tol1e-12/h0.25 vs tol1e-13/h0.125 sky difference
**2.33638 rad** — the exactly-critical escape direction is not
independently converged. `EXACT_CRITICAL_REFERENCE_NOT_CONVERGED`.
- r30 `theta_crit` (DARK) stop-time difference 13.056; r100 `theta_crit` (DARK)
12.700. At tol1e-11, h2-vs-h8 at r2.1 `theta_crit` is 5.09e-5 rad.
- Neighbours `±1e-7`: both reference tiers agree (same outcome/reason/end,
clean, positive steps) at every neighbour. Escaped-neighbour sky differences
between the layers are ~1e-7 rad (9.47e-8 r30, 2.74e-8 r100, 1.42e-7 r2.1),
consistent with the loose 1e-4 magnitude trend; DARK neighbours report
stop-time differences 1.5e-7 … 7.5e-7.
- `CRITICAL_REFERENCE_CHECK status=0` (no neighbour reference disagreement or
error endpoint).
## Decision summary
- **Schwarzschild `max_step = 8`**: supported by the far-field cost plateau
(2→8 saves ~23–30% persistent RHS; 8→32 only ~3%) together with the tight
reference and 4K 2-vs-8 comparisons. This is not claimed as globally optimal
and gives no guarantee for arbitrary near-critical/shadow directions.
- **`min_step = 1e-12`**: retained as a non-binding numerical guard; not a
measured optimum.
- **Minkowski `max_step = 16`** and **Alcubierre `max_step = 8 × initial`**:
retained as conservative caps. Their bounded accuracy checks support keeping
these values, not claiming cost optimality; larger Alcubierre caps still
reduce RHS, but have larger frequency errors in some cases.
- The R2.1 exactly-critical difference is tolerance-sensitive;
`critical_ref_check.c` confirms the exact direction is not
independently converged and the report does not treat it as certified.
## Caveats
- Analytic backends only; no time-dependent numerical-relativity metric was
exercised. Bounds and the `unit = 1` calibration are not transferable to NR.
- Experiment B samples `T = min(20, reference span)`; no full-trajectory
minimum-step claim is made.
- The 4K numbers are one sequential pair; no timing-stability confidence and
no image-error bound (inverse-lens-map Jacobian amplification is separate).
- Sky-angle differences divided by pixel angle are not inverse-lens-map image
error bounds.
- `failure_floor.c` reuses the regression invalid-metric temporal-domain
fixture; it is not a physical shadow or floor calibration.
## Verification of the selected defaults and reproduction tools
`make -B -j4 BUILD_TYPE=Debug test` passed after selecting the Schwarzschild
cap of 8 and adding CLI provenance assertions for the default bounds. Full
terminal output is retained locally at
`local/step_bounds_mesh/final_debug_tests.log` (`STEP_BOUNDS_FULL_DEBUG_EXIT=0`).
The explicit-RK4 HDR reference tests remain bit-identical. The Release
Schwarzschild binary was rebuilt with the selected default.
The self-contained limited driver was also executed with the relative output
path `local/step_bounds_reproduction`: 1500 endpoint traces and 2427 observed
finite-window steps completed, including summary generation. Small angular
differences use `atan2(|cross|,dot)` rather than rounded `acos(dot)`. Running the
4K driver without fresh explicit authorization refuses with exit code 2;
this check launches no renderer. No further 4K attempt was made.
@@ -0,0 +1,867 @@
/*
* Experiment A: DP54 step-bound scan through the *public* production
* endpoint geodesic_trace_past().
*
* Sub-commands:
* schwarzschild : r30/r100 static inward, r2.1 static outward, r1.5 free-fall
* minkowski : moving observer, escape sphere 64, analytic flat check
* alcubierre : comoving bubble-center camera, vs .3/.9, sigma 1/10/100
*
* Each sub-command computes one DP54 tol=1e-12 reference per ray, then scans
* - upper scan : max_step over a list, min_step fixed
* - min scan : min_step over a list, max_step fixed
* and compares every result against the reference (class, sky angle, g,
* dark threshold margin / stop time, cost). Raw per-ray CSV and stdout
* summaries are produced. No production source is modified.
*
* Link line (do NOT link geodesic.c twice; the backends are textually included
* below, so only the common sources are linked):
* cc -std=c11 -O2 -Isrc a_public_endpoints.c geodesic.c asymptotic.c \
* asymptotic_schwarzschild.c spacetime_common.c observer.c -lm
*/
#define _POSIX_C_SOURCE 200809L
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
/* Textually include all three analytic backends with renamed default
* constructors so one binary can exercise every provider (the files each
* define spacetime_create_default, which would collide at link time). */
#define spacetime_create_default spacetime_create_default_minkowski_local
#include "../../src/spacetime_minkowski.c"
#undef spacetime_create_default
#define spacetime_create_default spacetime_create_default_schwarzschild_local
#include "../../src/spacetime_schwarzschild.c"
#undef spacetime_create_default
#define spacetime_create_default spacetime_create_default_alcubierre_local
#include "../../src/spacetime_alcubierre.c"
#undef spacetime_create_default
#include "geodesic.h"
#include "observer.h"
#include "spacetime.h"
#define PI 3.14159265358979323846
#define MAX_CASES 16
#define MAX_DIRS 16
#define RAY_CAP 2500
static long g_rays = 0;
/* ------------------------------------------------------------------ */
/* Small helpers */
/* ------------------------------------------------------------------ */
static double dot3(const double a[3], const double b[3]) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
static double ang_delta(const double a[3], const double b[3]) {
const double na = sqrt(dot3(a, a)), nb = sqrt(dot3(b, b));
if (!(na > 0.0) || !(nb > 0.0))
return NAN;
const double ca = dot3(a, b) / (na * nb);
const double cross[3] = {a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0]};
const double sn = sqrt(dot3(cross, cross)) / (na * nb);
return atan2(sn, ca);
}
static double now_s(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec + 1e-9 * (double)ts.tv_nsec;
}
static int is_dark(int outcome) { return outcome == RAY_OUTCOME_DARK; }
static int is_escaped(int outcome) { return outcome == RAY_OUTCOME_ESCAPED; }
/* Critical local angle (static observer, M=1) for the Schwarzschild monopole.
* theta_c = asin(3 sqrt(3) sqrt(1-2/r) / r). */
static double critical_angle(double r) {
const double b = 3.0 * sqrt(3.0) * sqrt(1.0 - 2.0 / r) / r;
if (!(b < 1.0))
return PI / 2.0;
return asin(b);
}
/* ------------------------------------------------------------------ */
/* Case / result / comparison structures */
/* ------------------------------------------------------------------ */
typedef struct {
char name[32];
int prov; /* 0 schwarzschild, 1 minkowski, 2 alcubierre */
double mass, escape_radius;
double alc_vs, alc_radius, alc_sigma;
double look_ra_deg, look_dec_deg;
double position[3], velocity[3];
double initial_step, lookback;
unsigned int max_steps;
unsigned int ref_max_steps; /* budget for the tol=1e-12 reference only */
double max_step_for_min_scan; /* fixed upper during the min scan */
int n_dirs;
double dirs[MAX_DIRS][3];
double theta[MAX_DIRS]; /* NAN for non-angle direction sets */
} Case;
typedef struct {
int outcome, reason, end_id;
double stop_t, g, thr;
double n[3];
unsigned int steps, rejected;
unsigned long rhs;
double wall;
} Res;
typedef struct {
int has_ref;
int class_match;
double dn_ang, dlogg, dgrel, dstopT, dmargin;
} Cmp;
typedef struct {
long n, class_mismatch;
double max_dn_ang, max_dlogg, max_dgrel, max_dstopT, max_dmargin;
unsigned long sum_rhs, max_rhs;
unsigned int max_rejected;
double sum_wall;
long escaped, dark, unresolved, incomplete;
} Agg;
static void agg_init(Agg *a) {
memset(a, 0, sizeof *a);
a->max_dn_ang = a->max_dlogg = a->max_dgrel = a->max_dstopT =
a->max_dmargin = -1.0;
}
static void bump(double *m, double v) {
if (!isfinite(v))
return;
if (v > *m)
*m = v;
}
static void agg_add(Agg *a, const Res *r, const Cmp *c) {
++a->n;
if (c->has_ref && !c->class_match)
++a->class_mismatch;
if (c->has_ref) {
bump(&a->max_dn_ang, c->dn_ang);
bump(&a->max_dlogg, c->dlogg);
bump(&a->max_dgrel, c->dgrel);
bump(&a->max_dstopT, c->dstopT);
bump(&a->max_dmargin, c->dmargin);
}
a->sum_rhs += r->rhs;
if (r->rhs > a->max_rhs)
a->max_rhs = r->rhs;
if (r->rejected > a->max_rejected)
a->max_rejected = r->rejected;
a->sum_wall += r->wall;
if (is_dark(r->outcome))
++a->dark;
else if (is_escaped(r->outcome))
++a->escaped;
else if (r->outcome == RAY_OUTCOME_UNRESOLVED)
++a->unresolved;
else
++a->incomplete;
}
/* ------------------------------------------------------------------ */
/* Config builders */
/* ------------------------------------------------------------------ */
static GeodesicTraceConfig make_dp(double initial, double min_step,
double max_step, double tol,
unsigned int max_steps, double lookback) {
GeodesicTraceConfig c;
memset(&c, 0, sizeof c);
c.coordinate_time_step = initial;
c.max_steps = max_steps;
c.threshold.kind = THRESHOLD_LOG_ENERGY_GROWTH;
c.threshold.value = 8.0;
c.threshold.policy_version = 3;
c.stepper = GEODESIC_STEPPER_DP54;
c.atol_x = tol;
c.atol_Pi = tol;
c.atol_L = tol;
c.rtol = tol;
c.min_step = min_step;
c.max_step = max_step;
c.consecutive_rejection_limit = 32;
c.max_lookback_time = lookback;
return c;
}
static GeodesicTraceConfig make_rk4(double step, unsigned int max_steps) {
GeodesicTraceConfig c;
memset(&c, 0, sizeof c);
c.coordinate_time_step = step;
c.max_steps = max_steps;
c.threshold.kind = THRESHOLD_LOG_ENERGY_GROWTH;
c.threshold.value = 8.0;
c.threshold.policy_version = 3;
c.stepper = GEODESIC_STEPPER_RK4;
return c;
}
/* ------------------------------------------------------------------ */
/* Trace + compare */
/* ------------------------------------------------------------------ */
static Res run_ray(const SpacetimeSource *source, const ObserverState *observer,
const double dir[3], const GeodesicTraceConfig *config) {
Res r;
memset(&r, 0, sizeof r);
++g_rays;
if (g_rays > RAY_CAP) {
fprintf(stderr, "FATAL: ray cap %d exceeded\n", RAY_CAP);
exit(3);
}
const double t0 = now_s();
const RayEndpoint e = geodesic_trace_past(source, observer, dir, config);
r.wall = now_s() - t0;
r.outcome = e.outcome;
r.reason = e.reason;
r.end_id = e.end_id;
r.stop_t = e.stop_coordinate_time;
r.g = e.frequency_ratio;
r.thr = e.threshold_value;
for (int i = 0; i < 3; ++i)
r.n[i] = e.n_infinity[i];
r.steps = e.accepted_steps;
r.rejected = e.rejected_steps;
r.rhs = e.rhs_evaluations;
return r;
}
static void compare(const Res *a, const Res *ref, Cmp *c) {
memset(c, 0, sizeof *c);
c->has_ref = 1;
c->class_match = (a->outcome == ref->outcome && a->reason == ref->reason);
if (is_escaped(a->outcome) && is_escaped(ref->outcome)) {
c->dn_ang = ang_delta(a->n, ref->n);
if (a->g > 0.0 && ref->g > 0.0 && isfinite(a->g) && isfinite(ref->g)) {
c->dlogg = fabs(log(a->g) - log(ref->g));
c->dgrel = fabs(a->g / ref->g - 1.0);
} else {
c->dlogg = c->dgrel = NAN;
}
} else {
c->dn_ang = c->dlogg = c->dgrel = NAN;
}
c->dstopT = (isfinite(a->stop_t) && isfinite(ref->stop_t))
? fabs(a->stop_t - ref->stop_t)
: NAN;
c->dmargin = (is_dark(a->outcome) && is_dark(ref->outcome) &&
isfinite(a->thr) && isfinite(ref->thr))
? fabs(a->thr - ref->thr)
: NAN;
}
static void write_row(FILE *f, const char *phase, const Case *c, int di,
double upper, double min_step, double tol, const Res *r,
const Cmp *cmp) {
fprintf(f,
"%s,%s,%d,%.17g,%.6g,%.6g,%.6g,%d,%d,%u,%.17g,%u,%u,%lu,%.6g,"
"%.17g,%.17g,%.17g,%.17g,%.17g",
phase, c->name, di, c->theta[di], upper, min_step, tol, r->outcome,
r->reason, r->end_id, r->stop_t, r->steps, r->rejected, r->rhs,
r->wall, r->n[0], r->n[1], r->n[2], r->g, r->thr);
if (cmp->has_ref)
fprintf(f, ",%d,%.17g,%.17g,%.17g,%.17g,%.17g", cmp->class_match,
cmp->dn_ang, cmp->dlogg, cmp->dgrel, cmp->dstopT, cmp->dmargin);
else
fprintf(f, ",,nan,nan,nan,nan,nan");
fprintf(f, "\n");
}
static const char *ROW_HEADER =
"phase,case,dir,theta,upper,min_step,tol,outcome,reason,end_id,stop_t,"
"steps,rejected,rhs,wall_s,nx,ny,nz,g,thr,class_match,dn_ang,dlogg,dgrel,"
"dstopT,dmargin\n";
static void write_agg(FILE *f, const char *phase, const Case *c, double upper,
double min_step, double tol, const Agg *a) {
fprintf(f,
"%s,%s,%.6g,%.6g,%.6g,%ld,%ld,%.6g,%.6g,%.6g,%.6g,%.6g,%lu,%lu,%u,"
"%.4f,%ld,%ld,%ld,%ld\n",
phase, c->name, upper, min_step, tol, a->n, a->class_mismatch,
a->max_dn_ang, a->max_dlogg, a->max_dgrel, a->max_dstopT,
a->max_dmargin, a->sum_rhs, a->max_rhs, a->max_rejected, a->sum_wall,
a->escaped, a->dark, a->unresolved, a->incomplete);
}
static const char *AGG_HEADER =
"phase,case,upper,min_step,tol,n,class_mismatch,max_dn_ang,max_dlogg,"
"max_dgrel,max_dstopT,max_dmargin,sum_rhs,max_rhs,max_rejected,sum_wall,"
"escaped,dark,unresolved,incomplete\n";
/* ------------------------------------------------------------------ */
/* Case builders */
/* ------------------------------------------------------------------ */
static void dir_from_theta(double th, double n[3]) {
if (th == 0.0) {
n[0] = 1.0;
n[1] = n[2] = 0.0;
} else if (th == PI) {
n[0] = -1.0;
n[1] = n[2] = 0.0;
} else {
n[0] = cos(th);
n[1] = sin(th);
n[2] = 0.0;
}
const double nn = sqrt(dot3(n, n));
for (int i = 0; i < 3; ++i)
n[i] /= nn;
}
static void fill_static_dirs(Case *c, double r) {
const double tc = critical_angle(r);
const double th[MAX_DIRS] = {0.0, PI, PI / 2.0, tc,
tc - 1e-3, tc + 1e-3, tc - 1e-5, tc + 1e-5,
tc - 1e-7, tc + 1e-7, tc + 0.05, tc - 0.05};
c->n_dirs = 12;
for (int i = 0; i < c->n_dirs; ++i) {
c->theta[i] = th[i];
dir_from_theta(th[i], c->dirs[i]);
}
}
static void case_static(Case *c, const char *name, double r, double look_ra) {
memset(c, 0, sizeof *c);
snprintf(c->name, sizeof c->name, "%s", name);
c->prov = 0;
c->mass = 1.0;
c->escape_radius = 256.0;
c->look_ra_deg = look_ra;
c->look_dec_deg = 0.0;
c->position[0] = r;
c->position[1] = c->position[2] = 0.0;
c->velocity[0] = c->velocity[1] = c->velocity[2] = 0.0;
c->initial_step = 0.1;
c->lookback = 6553.6;
c->max_steps = 65536;
c->ref_max_steps = 65536;
c->max_step_for_min_scan = 2.0;
fill_static_dirs(c, r);
}
static void case_freefall(Case *c, const char *name, double r) {
memset(c, 0, sizeof *c);
snprintf(c->name, sizeof c->name, "%s", name);
c->prov = 0;
c->mass = 1.0;
c->escape_radius = 256.0;
c->look_ra_deg = 180.0;
c->look_dec_deg = 0.0;
c->position[0] = r;
c->position[1] = c->position[2] = 0.0;
/* Free-fall from rest at infinity, coordinate velocity dx/dt (the formula
* used by local/p2d_validation/observer_endpoints.c). */
const double y = sqrt(2.0 / r);
c->velocity[0] = -y * (1.0 + y) / (1.0 + y + y * y);
c->velocity[1] = c->velocity[2] = 0.0;
c->initial_step = 0.1;
c->lookback = 6553.6;
c->max_steps = 65536;
c->ref_max_steps = 65536;
c->max_step_for_min_scan = 2.0;
{
const double th[4] = {0.0, PI / 2.0, PI, 3.0 * PI / 4.0};
c->n_dirs = 4;
for (int i = 0; i < 4; ++i) {
c->theta[i] = th[i];
dir_from_theta(th[i], c->dirs[i]);
}
}
}
static void case_minkowski(Case *c) {
memset(c, 0, sizeof *c);
snprintf(c->name, sizeof c->name, "mink_moving");
c->prov = 1;
c->escape_radius = 64.0;
c->look_ra_deg = 0.0;
c->look_dec_deg = 0.0;
c->position[0] = 10.0;
c->position[1] = 20.0;
c->position[2] = -15.0;
c->velocity[0] = 0.3;
c->velocity[1] = 0.2;
c->velocity[2] = 0.1;
c->initial_step = 1.0;
c->lookback = 2048.0;
c->max_steps = 2048;
c->ref_max_steps = 4096;
c->max_step_for_min_scan = 16.0;
{
const double d[6][3] = {{1.0, 0.0, 0.0}, {-1.0, 0.0, 0.0},
{0.0, 1.0, 0.0}, {0.0, 0.0, 1.0},
{0.6, 0.8, 0.0}, {-0.6, 0.8, 0.0}};
c->n_dirs = 6;
for (int i = 0; i < 6; ++i) {
c->theta[i] = NAN;
for (int k = 0; k < 3; ++k)
c->dirs[i][k] = d[i][k];
}
}
}
static void case_alcubierre(Case *c, const char *name, double vs, double sigma) {
memset(c, 0, sizeof *c);
snprintf(c->name, sizeof c->name, "%s", name);
c->prov = 2;
c->alc_vs = vs;
c->alc_radius = 1.0;
c->alc_sigma = sigma;
c->look_ra_deg = 0.0;
c->look_dec_deg = 0.0;
c->position[0] = c->position[1] = c->position[2] = 0.0;
c->velocity[0] = vs; /* comoving with the bubble center */
c->velocity[1] = c->velocity[2] = 0.0;
c->initial_step = fmin(0.1, 0.05 / sigma);
c->max_steps = 100000;
c->ref_max_steps = 200000;
{
const double esc = spacetime_alcubierre_escape_radius(1.0, sigma);
c->lookback = 1.25 * 4.0 * esc / (1.0 - fabs(vs));
}
c->max_step_for_min_scan = c->initial_step * 8.0;
{
const double d[6][3] = {{1.0, 0.0, 0.0}, {-1.0, 0.0, 0.0},
{0.0, 1.0, 0.0}, {0.0, 0.0, 1.0},
{0.6, 0.8, 0.0}, {-0.6, 0.8, 0.0}};
c->n_dirs = 6;
for (int i = 0; i < 6; ++i) {
c->theta[i] = NAN;
for (int k = 0; k < 3; ++k)
c->dirs[i][k] = d[i][k];
}
}
}
/* ------------------------------------------------------------------ */
/* Source/observer construction */
/* ------------------------------------------------------------------ */
static int build_source(const Case *c, SpacetimeSource *s) {
if (c->prov == 0)
return spacetime_create_schwarzschild_ks(s, c->mass, c->escape_radius);
if (c->prov == 1)
return spacetime_create_minkowski(s, c->escape_radius);
return spacetime_create_alcubierre(s, c->alc_vs, c->alc_radius,
c->alc_sigma);
}
static int build_observer(const Case *c, const SpacetimeSource *s,
ObserverState *o) {
MetricData m;
if (spacetime_eval(s, 0.0, c->position, &m) != SPACETIME_POINT_OK)
return -1;
ObserverCamera cam;
memset(&cam, 0, sizeof cam);
cam.coordinate_time = 0.0;
for (int i = 0; i < 3; ++i) {
cam.position[i] = c->position[i];
cam.velocity[i] = c->velocity[i];
}
cam.look_ra_deg = c->look_ra_deg;
cam.look_dec_deg = c->look_dec_deg;
cam.roll_deg = 0.0;
return observer_from_coordinate_camera(&m, &cam, o, NULL) ==
OBSERVER_BUILD_OK
? 0
: -1;
}
/* ------------------------------------------------------------------ */
/* Generic scan driver */
/* ------------------------------------------------------------------ */
static void run_grid(const Case *cases, int ncases, const double *uppers,
int n_up, const double *floors, int n_fl, double tol,
const char *phase, const char *rows_path,
const char *agg_path, Res refs[][MAX_DIRS]) {
FILE *rf = fopen(rows_path, "w");
FILE *af = fopen(agg_path, "w");
if (!rf || !af) {
fprintf(stderr, "FATAL: cannot open %s / %s\n", rows_path, agg_path);
exit(2);
}
fprintf(rf, "%s", ROW_HEADER);
fprintf(af, "%s", AGG_HEADER);
for (int ci = 0; ci < ncases; ++ci) {
const Case *c = &cases[ci];
SpacetimeSource source;
ObserverState observer;
if (build_source(c, &source) || build_observer(c, &source, &observer)) {
fprintf(stderr, "FATAL: cannot build case %s\n", c->name);
exit(2);
}
for (int ui = 0; ui < n_up; ++ui) {
for (int fi = 0; fi < n_fl; ++fi) {
Agg agg;
agg_init(&agg);
for (int di = 0; di < c->n_dirs; ++di) {
const GeodesicTraceConfig cfg =
make_dp(c->initial_step, floors[fi], uppers[ui], tol, c->max_steps,
c->lookback);
const Res r = run_ray(&source, &observer, c->dirs[di], &cfg);
Cmp cmp;
compare(&r, &refs[ci][di], &cmp);
write_row(rf, phase, c, di, uppers[ui], floors[fi], tol, &r, &cmp);
agg_add(&agg, &r, &cmp);
}
write_agg(af, phase, c, uppers[ui], floors[fi], tol, &agg);
printf("SUMMARY %s %s upper=%.6g min=%.6g n=%ld mismatch=%ld "
"max_dn_ang=%.6g max_dgrel=%.6g max_dstopT=%.6g sum_rhs=%lu "
"esc=%ld dark=%ld unres=%ld inc=%ld wall=%.3fs\n",
phase, c->name, uppers[ui], floors[fi], agg.n,
agg.class_mismatch, agg.max_dn_ang, agg.max_dgrel,
agg.max_dstopT, agg.sum_rhs, agg.escaped, agg.dark,
agg.unresolved, agg.incomplete, agg.sum_wall);
fflush(stdout);
}
}
spacetime_destroy(&source);
}
fclose(rf);
fclose(af);
}
/* Compute DP tol=1e-12 reference for every direction of every case.
* upper_ref <= 0 means "use the case's own initial step" as the max_step. */
static void compute_refs(const Case *cases, int ncases, double upper_ref,
const char *path, Res refs[][MAX_DIRS]) {
FILE *f = fopen(path, "w");
if (!f) {
fprintf(stderr, "FATAL: cannot open %s\n", path);
exit(2);
}
fprintf(f, "%s", ROW_HEADER);
for (int ci = 0; ci < ncases; ++ci) {
const Case *c = &cases[ci];
SpacetimeSource source;
ObserverState observer;
if (build_source(c, &source) || build_observer(c, &source, &observer)) {
fprintf(stderr, "FATAL: cannot build case %s\n", c->name);
exit(2);
}
const double upper = (upper_ref > 0.0) ? upper_ref : c->initial_step;
for (int di = 0; di < c->n_dirs; ++di) {
const GeodesicTraceConfig cfg = make_dp(c->initial_step, 1e-12, upper,
1e-12, c->ref_max_steps,
c->lookback);
refs[ci][di] = run_ray(&source, &observer, c->dirs[di], &cfg);
Cmp none;
memset(&none, 0, sizeof none);
write_row(f, "ref", c, di, upper, 1e-12, 1e-12, &refs[ci][di], &none);
printf("REF %s dir=%d theta=%.6g outcome=%d reason=%d stop_t=%.9g "
"steps=%u rejected=%u rhs=%lu g=%.12g thr=%.12g\n",
c->name, di, c->theta[di], refs[ci][di].outcome,
refs[ci][di].reason, refs[ci][di].stop_t, refs[ci][di].steps,
refs[ci][di].rejected, refs[ci][di].rhs, refs[ci][di].g,
refs[ci][di].thr);
fflush(stdout);
}
spacetime_destroy(&source);
}
fclose(f);
}
/* ------------------------------------------------------------------ */
/* Sub-command: Schwarzschild */
/* ------------------------------------------------------------------ */
static void cmd_schwarzschild(void) {
static Case cases[MAX_CASES];
static Res refs[MAX_CASES][MAX_DIRS];
memset(refs, 0, sizeof refs);
int nc = 0;
case_static(&cases[nc++], "r30_inward", 30.0, 180.0);
case_static(&cases[nc++], "r100_inward", 100.0, 180.0);
case_static(&cases[nc++], "r2p1_outward", 2.1, 0.0);
case_freefall(&cases[nc++], "r1p5_freefall", 1.5);
compute_refs(cases, nc, 0.25, "raw/a_sch_reference.csv", refs);
static const double uppers[9] = {0.25, 0.5, 1.0, 2.0, 4.0,
8.0, 16.0, 32.0, 64.0};
static const double floors[10] = {1e-1, 1e-2, 1e-3, 1e-4, 1e-5,
1e-6, 1e-8, 1e-10, 1e-12, 1e-14};
run_grid(cases, nc, uppers, 9, (double[]){1e-12}, 1, 1e-9, "upper",
"raw/a_sch_upper.csv", "raw/a_sch_upper_summary.csv", refs);
run_grid(cases, nc, (double[]){2.0}, 1, floors, 10, 1e-9, "min",
"raw/a_sch_min.csv", "raw/a_sch_min_summary.csv", refs);
/* Sensitive-ray RK4 h-halving reference (extra independent check).
* (case index, dir index): r30 and r2.1, theta_c +- 1e-5 / +- 1e-7. */
const int sens[8][2] = {{0, 8}, {0, 9}, {0, 6}, {0, 7},
{2, 8}, {2, 9}, {2, 6}, {2, 7}};
FILE *f = fopen("raw/a_sch_rk4_sensitive.csv", "w");
if (!f) {
fprintf(stderr, "FATAL: cannot open rk4 csv\n");
exit(2);
}
fprintf(f, "%s", ROW_HEADER);
for (int k = 0; k < 8; ++k) {
const Case *c = &cases[sens[k][0]];
const int di = sens[k][1];
SpacetimeSource source;
ObserverState observer;
if (build_source(c, &source) || build_observer(c, &source, &observer)) {
fprintf(stderr, "FATAL: cannot build case %s\n", c->name);
exit(2);
}
GeodesicTraceConfig rk01 = make_rk4(0.01, 262144);
GeodesicTraceConfig rk005 = make_rk4(0.005, 262144);
Res r01 = run_ray(&source, &observer, c->dirs[di], &rk01);
Res r005 = run_ray(&source, &observer, c->dirs[di], &rk005);
Cmp hh, vsdp;
compare(&r005, &r01, &hh);
compare(&r005, &refs[sens[k][0]][di], &vsdp);
Cmp none;
memset(&none, 0, sizeof none);
write_row(f, "rk4_0.01", c, di, 0.01, 0.0, 0.0, &r01, &none);
write_row(f, "rk4_0.005", c, di, 0.005, 0.0, 0.0, &r005, &none);
printf("RK4 %s dir=%d theta=%.6g o01=%d/%d o005=%d/%d "
"hhalve_dn=%.6g hhalve_dgrel=%.6g vsdp_dn=%.6g vsdp_dgrel=%.6g "
"vsdp_class=%d\n",
c->name, di, c->theta[di], r01.outcome, r01.reason, r005.outcome,
r005.reason, hh.dn_ang, hh.dgrel, vsdp.dn_ang, vsdp.dgrel,
vsdp.class_match);
spacetime_destroy(&source);
}
fclose(f);
}
/* ------------------------------------------------------------------ */
/* Sub-command: Minkowski (analytic flat verification) */
/* ------------------------------------------------------------------ */
/* Analytic flat past escape from an observer at (t0,x0) with tetrad-frame
* direction n: coordinate photon velocity u = k_vec/k^0, crossing radius R at
* s = t-t0 < 0, sky direction n_inf = -u, frequency ratio g = 1/k^0. */
static int mink_analytic(const Case *c, const ObserverState *o,
const double n[3], double R, double *t_cross,
double *xc, double *ninf, double *g) {
double k[4];
for (int mu = 0; mu < 4; ++mu)
k[mu] = o->tetrad[0][mu];
for (int a = 0; a < 3; ++a)
for (int mu = 0; mu < 4; ++mu)
k[mu] -= n[a] * o->tetrad[a + 1][mu];
if (!(k[0] > 0.0))
return -1;
double u[3];
for (int i = 0; i < 3; ++i)
u[i] = k[i + 1] / k[0];
const double uu = dot3(u, u);
const double B = 2.0 * dot3(c->position, u);
const double C = dot3(c->position, c->position) - R * R;
const double disc = B * B - 4.0 * uu * C;
if (disc < 0.0)
return -1;
const double sq = sqrt(disc);
const double s1 = (-B + sq) / (2.0 * uu);
const double s2 = (-B - sq) / (2.0 * uu);
const double s = (s1 < 0.0) ? fmin(s1, s2) : s2;
if (!(s < 0.0))
return -1;
*t_cross = o->coordinate_time + s;
for (int i = 0; i < 3; ++i)
xc[i] = c->position[i] + u[i] * s;
const double nu = sqrt(uu);
for (int i = 0; i < 3; ++i)
ninf[i] = -u[i] / nu;
*g = 1.0 / k[0];
return 0;
}
static void cmd_minkowski(void) {
Case c;
case_minkowski(&c);
static Res refs[MAX_CASES][MAX_DIRS];
memset(refs, 0, sizeof refs);
SpacetimeSource source;
ObserverState observer;
if (build_source(&c, &source) || build_observer(&c, &source, &observer)) {
fprintf(stderr, "FATAL: cannot build minkowski case\n");
exit(2);
}
FILE *mf = fopen("raw/a_mink_analytic.csv", "w");
if (!mf) {
fprintf(stderr, "FATAL: cannot open analytic csv\n");
exit(2);
}
fprintf(mf,
"case,dir,upper,min_step,tol,outcome,stop_t,x_err,t_err,dn_ang,"
"dgrel,g_analytic,stop_t_analytic\n");
/* Reference: tol=1e-12, max_step = initial (1.0). */
const GeodesicTraceConfig refcfg =
make_dp(c.initial_step, 1e-12, c.initial_step, 1e-12, c.max_steps,
c.lookback);
for (int di = 0; di < c.n_dirs; ++di)
refs[0][di] = run_ray(&source, &observer, c.dirs[di], &refcfg);
static const double uppers[5] = {1.0, 4.0, 16.0, 64.0, 256.0};
static const double floors[3] = {1e-1, 1e-6, 1e-12};
for (int ui = 0; ui < 5; ++ui) {
for (int fi = 0; fi < 3; ++fi) {
Agg agg;
agg_init(&agg);
for (int di = 0; di < c.n_dirs; ++di) {
const GeodesicTraceConfig cfg =
make_dp(c.initial_step, floors[fi], uppers[ui], 1e-9, c.max_steps,
c.lookback);
const Res r = run_ray(&source, &observer, c.dirs[di], &cfg);
Cmp cmp;
compare(&r, &refs[0][di], &cmp);
agg_add(&agg, &r, &cmp);
double t_an, x_an[3], n_an[3], g_an;
const int ok = mink_analytic(&c, &observer, c.dirs[di], c.escape_radius,
&t_an, x_an, n_an, &g_an);
double x_err = NAN, t_err = NAN, dn_an = NAN, dg_an = NAN;
if (ok == 0 && is_escaped(r.outcome)) {
/* Crossing position is checked through the direct trace in
* a_mink_analytic_x.csv; here compare sky direction, g and time. */
t_err = fabs(r.stop_t - t_an);
dn_an = ang_delta(r.n, n_an);
if (r.g > 0.0 && g_an > 0.0)
dg_an = fabs(r.g / g_an - 1.0);
}
fprintf(mf,
"%s,%d,%.6g,%.6g,%.6g,%d,%.17g,%.17g,%.17g,%.17g,%.17g,%.17g,"
"%.17g\n",
c.name, di, uppers[ui], floors[fi], 1e-9, r.outcome, r.stop_t,
x_err, t_err, dn_an, dg_an, g_an, t_an);
}
printf("SUMMARY mink upper=%.6g min=%.6g n=%ld mismatch=%ld "
"max_dn_ang=%.6g max_dgrel=%.6g sum_rhs=%lu esc=%ld dark=%ld "
"unres=%ld inc=%ld\n",
uppers[ui], floors[fi], agg.n, agg.class_mismatch,
agg.max_dn_ang, agg.max_dgrel, agg.sum_rhs, agg.escaped, agg.dark,
agg.unresolved, agg.incomplete);
fflush(stdout);
}
}
/* Direct endpoint check for the crossing position (x) on a few configs. */
FILE *xf = fopen("raw/a_mink_analytic_x.csv", "w");
if (!xf) {
fprintf(stderr, "FATAL: cannot open analytic x csv\n");
exit(2);
}
fprintf(xf, "case,dir,upper,min_step,tol,outcome,x_err_x,x_err_y,x_err_z,"
"t_err\n");
for (int ui = 0; ui < 5; ++ui) {
for (int fi = 0; fi < 3; ++fi) {
for (int di = 0; di < c.n_dirs; ++di) {
const GeodesicTraceConfig cfg =
make_dp(c.initial_step, floors[fi], uppers[ui], 1e-9, c.max_steps,
c.lookback);
const RayEndpoint e =
geodesic_trace_past(&source, &observer, c.dirs[di], &cfg);
++g_rays;
double t_an, x_an[3], n_an[3], g_an;
const int ok = mink_analytic(&c, &observer, c.dirs[di], c.escape_radius,
&t_an, x_an, n_an, &g_an);
double ex = NAN, ey = NAN, ez = NAN, te = NAN;
if (ok == 0 && e.outcome == RAY_OUTCOME_ESCAPED) {
ex = fabs(e.final_x[0] - x_an[0]);
ey = fabs(e.final_x[1] - x_an[1]);
ez = fabs(e.final_x[2] - x_an[2]);
te = fabs(e.stop_coordinate_time - t_an);
}
fprintf(xf, "%s,%d,%.6g,%.6g,%.6g,%d,%.17g,%.17g,%.17g,%.17g\n",
c.name, di, uppers[ui], floors[fi], 1e-9, e.outcome, ex, ey, ez,
te);
}
}
}
fclose(xf);
fclose(mf);
spacetime_destroy(&source);
}
/* ------------------------------------------------------------------ */
/* Sub-command: Alcubierre */
/* ------------------------------------------------------------------ */
static void cmd_alcubierre(void) {
static Case cases[MAX_CASES];
static Res refs[MAX_CASES][MAX_DIRS];
memset(refs, 0, sizeof refs);
int nc = 0;
case_alcubierre(&cases[nc++], "alc_v3_s1", 0.3, 1.0);
case_alcubierre(&cases[nc++], "alc_v3_s10", 0.3, 10.0);
case_alcubierre(&cases[nc++], "alc_v9_s1", 0.9, 1.0);
case_alcubierre(&cases[nc++], "alc_v9_s10", 0.9, 10.0);
case_alcubierre(&cases[nc++], "alc_v9_s100", 0.9, 100.0);
compute_refs(cases, nc, -1.0, "raw/a_alc_reference.csv", refs);
/* Standard grid: upper = initial*{1,4,8,16,32,64}, floor {1e-4,1e-8,1e-12}. */
{
static const double floors[3] = {1e-4, 1e-8, 1e-12};
Case sub[4];
for (int i = 0; i < 4; ++i)
sub[i] = cases[i];
/* run_grid uses one upper array for all cases, so use per-case initial by
* calling run_grid four times. */
for (int i = 0; i < 4; ++i) {
char rp[128], ap[128];
snprintf(rp, sizeof rp, "raw/a_alc_%.31s_upper.csv", sub[i].name);
snprintf(ap, sizeof ap, "raw/a_alc_%.31s_upper_summary.csv", sub[i].name);
const double base = sub[i].initial_step;
const double up[6] = {base, 4 * base, 8 * base,
16 * base, 32 * base, 64 * base};
run_grid(&sub[i], 1, up, 6, floors, 3, 1e-9, "upper", rp, ap, &refs[i]);
}
}
/* Focused lower-bound stress: vs=.9, sigma=100, upper = initial*8 = 0.004,
* floors {1e-4,1e-5,1e-6,1e-8,1e-12,1e-14}. */
{
const Case *c = &cases[4];
const double up[1] = {c->initial_step * 8.0};
const double floors[6] = {1e-4, 1e-5, 1e-6, 1e-8, 1e-12, 1e-14};
run_grid(c, 1, up, 1, floors, 6, 1e-9, "min_stress",
"raw/a_alc_v9_s100_min.csv", "raw/a_alc_v9_s100_min_summary.csv",
&refs[4]);
}
}
/* ------------------------------------------------------------------ */
int main(int argc, char **argv) {
if (argc < 2) {
fprintf(stderr,
"usage: %s schwarzschild|minkowski|alcubierre\n",
argv[0]);
return 1;
}
if (!strcmp(argv[1], "schwarzschild"))
cmd_schwarzschild();
else if (!strcmp(argv[1], "minkowski"))
cmd_minkowski();
else if (!strcmp(argv[1], "alcubierre"))
cmd_alcubierre();
else {
fprintf(stderr, "unknown sub-command %s\n", argv[1]);
return 1;
}
printf("TOTAL_RAYS %ld\n", g_rays);
return 0;
}
@@ -0,0 +1,404 @@
/*
* Experiment B: observed accepted-step sizes of the production DP54 core.
*
* This translation unit textually includes src/geodesic.c so the real,
* production `dp_advance_one` driver and the real `State` (GeodesicRayState)
* are exercised directly on a finite trajectory interval. The production
* initialization is reproduced exactly as `geodesic_trace_past` does for an
* INSIDE route: `geodesic_initialize_past_ray_metric` at the camera event,
* then `next_step = config->coordinate_time_step` (the init already sets
* `integration_start_time`, `log_alpha_p0` and `log_alpha_p0_0`). This is the
* same initialization the public endpoint uses, not a re-implementation.
*
* For each selected ray we:
* 1. run the public endpoint with tol=1e-12 to get a trusted reference
* stop time and terminal class (physical terminal class is owned by A);
* 2. integrate the same ray with the production DP core up to
* T = min(20, ref_span) observed steps <= 2000, recording each accepted
* step magnitude, boundary-limit flag, rejection/RHS deltas and the null
* residual gamma^{ij} Pi_i Pi_j - 1.
*
* Link line (geodesic.c is textually included, so it is NOT linked; the
* analytic backends are also textually included here):
* cc -std=c11 -O2 -Isrc b_actual_h.c asymptotic.c asymptotic_schwarzschild.c \
* spacetime_common.c observer.c -lm
*/
#define _POSIX_C_SOURCE 200809L
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define spacetime_create_default spacetime_create_default_minkowski_b
#include "../../src/spacetime_minkowski.c"
#undef spacetime_create_default
#define spacetime_create_default spacetime_create_default_schwarzschild_b
#include "../../src/spacetime_schwarzschild.c"
#undef spacetime_create_default
#define spacetime_create_default spacetime_create_default_alcubierre_b
#include "../../src/spacetime_alcubierre.c"
#undef spacetime_create_default
/* The production geodesic core, textually included. Its static State,
* dp_advance_one and invert become visible to the code below. */
#include "../../src/geodesic.c"
#define PI 3.14159265358979323846
#define MAX_BCASES 12
#define MAX_BDIRS 8
#define MAX_OBS_STEPS 2000
typedef struct {
char name[32];
int prov; /* 0 sch, 1 mink, 2 alc */
double mass, esc;
double vs, radius, sigma;
double look_ra_deg, look_dec_deg;
double pos[3], vel[3];
double initial, max_step;
double ref_max_step, lookback;
unsigned int ref_max_steps;
int ndirs;
double dirs[MAX_BDIRS][3];
} BCase;
static double b_now(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec + 1e-9 * (double)ts.tv_nsec;
}
static void b_dir(double th, double n[3]) {
if (th == 0.0) {
n[0] = 1.0;
n[1] = n[2] = 0.0;
} else if (th == PI) {
n[0] = -1.0;
n[1] = n[2] = 0.0;
} else {
n[0] = cos(th);
n[1] = sin(th);
n[2] = 0.0;
}
const double nn = sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]);
for (int i = 0; i < 3; ++i)
n[i] /= nn;
}
static double b_critical_angle(double r) {
const double b = 3.0 * sqrt(3.0) * sqrt(1.0 - 2.0 / r) / r;
return (b < 1.0) ? asin(b) : PI / 2.0;
}
static GeodesicTraceConfig b_make_dp(double initial, double min_step,
double max_step, double tol,
unsigned int max_steps, double lookback) {
GeodesicTraceConfig c;
memset(&c, 0, sizeof c);
c.coordinate_time_step = initial;
c.max_steps = max_steps;
c.threshold.kind = THRESHOLD_LOG_ENERGY_GROWTH;
c.threshold.value = 8.0;
c.threshold.policy_version = 3;
c.stepper = GEODESIC_STEPPER_DP54;
c.atol_x = c.atol_Pi = c.atol_L = c.rtol = tol;
c.min_step = min_step;
c.max_step = max_step;
c.consecutive_rejection_limit = 32;
c.max_lookback_time = lookback;
return c;
}
static int b_build_source(const BCase *c, SpacetimeSource *s) {
if (c->prov == 0)
return spacetime_create_schwarzschild_ks(s, c->mass, c->esc);
if (c->prov == 1)
return spacetime_create_minkowski(s, c->esc);
return spacetime_create_alcubierre(s, c->vs, c->radius, c->sigma);
}
static int b_build_observer(const BCase *c, const SpacetimeSource *s,
ObserverState *o) {
MetricData m;
if (spacetime_eval(s, 0.0, c->pos, &m) != SPACETIME_POINT_OK)
return -1;
ObserverCamera cam;
memset(&cam, 0, sizeof cam);
for (int i = 0; i < 3; ++i) {
cam.position[i] = c->pos[i];
cam.velocity[i] = c->vel[i];
}
cam.look_ra_deg = c->look_ra_deg;
cam.look_dec_deg = c->look_dec_deg;
return observer_from_coordinate_camera(&m, &cam, o, NULL) ==
OBSERVER_BUILD_OK
? 0
: -1;
}
static void b_add_sch(BCase *c, const char *name, double r, double look,
const double *thetas, int nt) {
memset(c, 0, sizeof *c);
snprintf(c->name, sizeof c->name, "%s", name);
c->prov = 0;
c->mass = 1.0;
c->esc = 256.0;
c->look_ra_deg = look;
c->pos[0] = r;
c->initial = 0.1;
c->max_step = 2.0;
c->ref_max_step = 0.25;
c->lookback = 6553.6;
c->ref_max_steps = 65536;
c->ndirs = nt;
for (int i = 0; i < nt; ++i)
b_dir(thetas[i], c->dirs[i]);
}
static void b_add_mink(BCase *c) {
memset(c, 0, sizeof *c);
snprintf(c->name, sizeof c->name, "mink_moving");
c->prov = 1;
c->esc = 64.0;
c->look_ra_deg = 0.0;
c->pos[0] = 10.0;
c->pos[1] = 20.0;
c->pos[2] = -15.0;
c->vel[0] = 0.3;
c->vel[1] = 0.2;
c->vel[2] = 0.1;
c->initial = 1.0;
c->max_step = 16.0;
c->ref_max_step = 1.0;
c->lookback = 2048.0;
c->ref_max_steps = 4096;
c->ndirs = 2;
b_dir(0.0, c->dirs[0]);
b_dir(PI, c->dirs[1]);
}
static void b_add_alc(BCase *c, const char *name, double vs, double sigma,
const double *thetas, int nt) {
memset(c, 0, sizeof *c);
snprintf(c->name, sizeof c->name, "%s", name);
c->prov = 2;
c->vs = vs;
c->radius = 1.0;
c->sigma = sigma;
c->look_ra_deg = 0.0;
c->vel[0] = vs;
c->initial = fmin(0.1, 0.05 / sigma);
c->max_step = c->initial * 8.0;
c->ref_max_step = c->initial;
const double esc = spacetime_alcubierre_escape_radius(1.0, sigma);
c->lookback = 1.25 * 4.0 * esc / (1.0 - fabs(vs));
c->ref_max_steps = 200000;
c->ndirs = nt;
for (int i = 0; i < nt; ++i)
b_dir(thetas[i], c->dirs[i]);
}
/* gamma^{ij} Pi_i Pi_j - 1; `invert` is the production static from
* geodesic.c, so this is the exact production metric contraction. */
static double b_null_residual(const MetricData *m, const double Pi[3]) {
double g[3][3], inv[3][3];
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
g[i][j] = m->gamma[i][j];
if (invert(g, inv))
return NAN;
double v = 0.0;
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
v += inv[i][j] * Pi[i] * Pi[j];
return v - 1.0;
}
int main(void) {
static BCase cases[MAX_BCASES];
int nc = 0;
{
const double tc = b_critical_angle(30.0);
const double th[6] = {0.0, PI, tc - 1e-7, tc + 1e-7, tc - 1e-5, tc + 1e-5};
b_add_sch(&cases[nc++], "r30_inward", 30.0, 180.0, th, 6);
}
{
const double tc = b_critical_angle(100.0);
const double th[4] = {0.0, PI, tc, tc - 1e-7};
b_add_sch(&cases[nc++], "r100_inward", 100.0, 180.0, th, 4);
}
{
const double tc = b_critical_angle(2.1);
const double th[5] = {0.0, tc, tc - 1e-7, tc + 1e-7, PI};
b_add_sch(&cases[nc++], "r2p1_outward", 2.1, 0.0, th, 5);
}
{
const double th[2] = {0.0, PI};
b_add_sch(&cases[nc++], "r1p5_freefall", 1.5, 180.0, th, 2);
/* free-fall velocity is set below (b_add_sch leaves it zero). */
}
b_add_mink(&cases[nc++]);
{
const double th[2] = {0.0, PI};
b_add_alc(&cases[nc++], "alc_v3_s1", 0.3, 1.0, th, 2);
b_add_alc(&cases[nc++], "alc_v9_s10", 0.9, 10.0, th, 2);
b_add_alc(&cases[nc++], "alc_v9_s100", 0.9, 100.0, th, 2);
}
/* Free-fall coordinate velocity for r1.5 (same formula as A). */
{
BCase *c = &cases[3];
const double y = sqrt(2.0 / c->pos[0]);
c->vel[0] = -y * (1.0 + y) / (1.0 + y + y * y);
}
FILE *hf = fopen("raw/b_actual_h.csv", "w");
FILE *sf = fopen("raw/b_summary.csv", "w");
if (!hf || !sf) {
fprintf(stderr, "FATAL: cannot open b csv\n");
return 2;
}
fprintf(hf, "case,dir,step,t,h,boundary_limited,rhs_delta,reject_delta,"
"null_residual\n");
fprintf(sf, "case,dir,ref_outcome,ref_stop_t,span,target,observed_steps,"
"reached_target,terminated_reason,h_min,h_max,h_first,h_last,"
"boundary_steps,sum_rhs,sum_reject,max_reject_delta,"
"null_residual_max,final_t,wall_s\n");
long total_steps = 0;
for (int ci = 0; ci < nc; ++ci) {
BCase *c = &cases[ci];
SpacetimeSource source;
ObserverState observer;
if (b_build_source(c, &source) || b_build_observer(c, &source, &observer)) {
fprintf(stderr, "FATAL: cannot build B case %s\n", c->name);
return 2;
}
for (int di = 0; di < c->ndirs; ++di) {
/* 1. trusted reference via the public endpoint (physical terminal). */
GeodesicTraceConfig refcfg =
b_make_dp(c->initial, 1e-12, c->ref_max_step, 1e-12, c->ref_max_steps,
c->lookback);
RayEndpoint ref =
geodesic_trace_past(&source, &observer, c->dirs[di], &refcfg);
const double span =
isfinite(ref.stop_coordinate_time)
? (observer.coordinate_time - ref.stop_coordinate_time)
: 20.0;
const double finite_T = fmin(20.0, span > 0.0 ? span : 20.0);
const double target = observer.coordinate_time - finite_T;
/* 2. production-exact INSIDE-route initialization. */
MetricData metric;
if (spacetime_eval(&source, observer.coordinate_time,
observer.coordinate_position, &metric) !=
SPACETIME_POINT_OK) {
fprintf(stderr, "FATAL: camera metric %s\n", c->name);
return 2;
}
State st;
if (geodesic_initialize_past_ray_metric(&metric, &observer, c->dirs[di],
&st)) {
fprintf(stderr, "FATAL: init %s dir %d\n", c->name, di);
return 2;
}
GeodesicTraceConfig cfg =
b_make_dp(c->initial, 1e-12, c->max_step, 1e-9, 0u, 0.0);
st.next_step = cfg.coordinate_time_step;
MetricSlab *slab = NULL;
if (spacetime_load_slab(&source, st.coordinate_time, target - 1.0,
&slab)) {
fprintf(stderr, "FATAL: slab %s dir %d\n", c->name, di);
return 2;
}
double h_min = INFINITY, h_max = 0.0, h_first = NAN, h_last = NAN;
double null_max = 0.0, sum_wall = 0.0;
unsigned long sum_rhs = 0;
unsigned int sum_rej = 0, max_rej_delta = 0, boundary_steps = 0;
int reached = 0, terminated_reason = -1;
int step_index = 0;
const double start_wall = b_now();
while (st.coordinate_time > target && step_index < MAX_OBS_STEPS) {
const double before_t = st.coordinate_time;
unsigned long rhs = st.rhs_evaluations;
unsigned int rej = st.rejected_steps;
RayReason reason = RAY_REASON_INTEGRATION_ERROR;
const int rc =
dp_advance_one(slab, &cfg, &st, target, &reason, &rhs, &rej, NULL);
const unsigned long rhs_delta = rhs - st.rhs_evaluations;
const unsigned int rej_delta = rej - st.rejected_steps;
st.rhs_evaluations = rhs;
st.rejected_steps = rej;
sum_rhs += rhs_delta;
sum_rej += rej_delta;
if (rej_delta > max_rej_delta)
max_rej_delta = rej_delta;
if (rc) {
terminated_reason = (int)reason;
break;
}
const double h = before_t - st.coordinate_time;
const int boundary = (st.coordinate_time == target);
if (boundary)
++boundary_steps;
if (h < h_min)
h_min = h;
if (h > h_max)
h_max = h;
if (step_index == 0)
h_first = h;
h_last = h;
++step_index;
MetricData m;
double nr = NAN;
if (spacetime_slab_eval(slab, st.coordinate_time, st.x, &m) ==
SPACETIME_POINT_OK)
nr = b_null_residual(&m, st.Pi);
if (isfinite(nr) && fabs(nr) > null_max)
null_max = fabs(nr);
fprintf(hf, "%s,%d,%d,%.17g,%.17g,%d,%lu,%u,%.6g\n", c->name, di,
step_index, st.coordinate_time, h, boundary, rhs_delta,
rej_delta, nr);
if (st.coordinate_time <= target)
reached = 1;
}
sum_wall = b_now() - start_wall;
if (step_index >= MAX_OBS_STEPS)
reached = 0;
if (!isfinite(h_first)) {
h_first = NAN;
h_max = NAN;
}
if (!isfinite(h_min) || h_min == INFINITY)
h_min = NAN;
if (h_last < 0.0)
h_last = NAN;
fprintf(sf,
"%s,%d,%d,%.17g,%.17g,%.17g,%d,%d,%d,%.17g,%.17g,%.17g,%.17g,"
"%u,%lu,%u,%u,%.6g,%.17g,%.6g\n",
c->name, di, ref.outcome, ref.stop_coordinate_time, span, target,
step_index, reached, terminated_reason, h_min, h_max, h_first,
h_last, boundary_steps, sum_rhs, sum_rej, max_rej_delta, null_max,
st.coordinate_time, sum_wall);
printf("B %s dir=%d ref=%d refstop=%.9g span=%.6g T=%.6g target=%.6g "
"steps=%d reached=%d term=%d h=[%.6g,%.6g] first=%.6g last=%.6g "
"boundary=%u rhs=%lu rej=%u nullmax=%.3g final_t=%.9g wall=%.4fs\n",
c->name, di, ref.outcome, ref.stop_coordinate_time, span, finite_T,
target, step_index, reached, terminated_reason, h_min, h_max,
h_first, h_last, boundary_steps, sum_rhs, sum_rej, null_max,
st.coordinate_time, sum_wall);
fflush(stdout);
total_steps += step_index;
spacetime_free_slab(slab);
}
spacetime_destroy(&source);
}
fclose(hf);
fclose(sf);
printf("TOTAL_OBSERVED_STEPS %ld\n", total_steps);
return 0;
}
@@ -0,0 +1,341 @@
/*
* Standalone critical-classification reference check.
*
* Three static Schwarzschild cameras (M=1, escape sphere 256):
* r30 look 180 (inward), r100 look 180 (inward), r2.1 look 0 (outward),
* each aimed at theta_crit exactly and at theta_crit +- 1e-7, where
* theta_crit = asin(3 sqrt(3) sqrt(1 - 2/r) / r)
* is the local tetrad angle from the camera forward direction.
*
* For every direction four DP54 configs are run (initial step 0.1,
* min_step 1e-14, max_steps 65536, max_lookback_time 6553.6, reject limit 32,
* camera-relative dark threshold 8):
*
* ref_tol1e-12_h0.25 tol 1e-12, max_step 0.25
* ref_tol1e-13_h0.125 tol 1e-13, max_step 0.125
* tol1e-11_h2 tol 1e-11, max_step 2
* tol1e-11_h8 tol 1e-11, max_step 8
*
* Total 3 cameras * 3 directions * 4 configs = 36 endpoint traces.
*
* Raw per-run rows go to raw/critical_ref.csv; stdout prints, for all nine
* directions, ref_vs_tighter / h2_vs_h8 / ref_vs_h2 / ref_vs_h8 comparisons
* (outcome/reason/end, sky angle, relative g, dark stop-time and threshold
* shifts) and a NEIGHBOR_REFERENCE_CHECK that verifies the two reference
* tiers agree at the +-1e-7 neighbours. The exactly-critical direction is a
* separatrix: a class flip or a large direction/stop-time difference there is
* reported as EXACT_CRITICAL_REFERENCE_NOT_CONVERGED and is diagnostic only.
*
* Exit status: 0 when the neighbours' reference tiers agree and no neighbour
* returned an error/unresolved endpoint or a zero-step trace; 1 when a
* neighbour reference check fails; 2 on setup error. The exact-critical
* separatrix is never an assertion.
*
* Link line (the Schwarzschild backend is textually included):
* cc -std=c11 -O2 -Isrc critical_ref_check.c geodesic.c asymptotic.c \
* asymptotic_schwarzschild.c spacetime_common.c observer.c -lm
*/
#define _POSIX_C_SOURCE 200809L
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../../src/spacetime_schwarzschild.c"
#include "geodesic.h"
#include "observer.h"
#include "spacetime.h"
#define PI 3.14159265358979323846
typedef struct {
const char *name;
double tol;
double max_step;
} CfgDef;
static const CfgDef cfgs[4] = {
{"ref_tol1e-12_h0.25", 1e-12, 0.25},
{"ref_tol1e-13_h0.125", 1e-13, 0.125},
{"tol1e-11_h2", 1e-11, 2.0},
{"tol1e-11_h8", 1e-11, 8.0},
};
typedef struct {
const char *name;
double r;
double look_ra_deg;
} CamDef;
static const CamDef cams[3] = {
{"r30_inward", 30.0, 180.0},
{"r100_inward", 100.0, 180.0},
{"r2p1_outward", 2.1, 0.0},
};
typedef struct {
int outcome, reason;
unsigned int end_id;
double stop_t, g, thr;
double L, L0;
double n[3];
unsigned int steps, rejected;
unsigned long rhs;
} Res;
static double b_dot3(const double a[3], const double b[3]) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
static double b_ang_delta(const double a[3], const double b[3]) {
const double na = sqrt(b_dot3(a, a)), nb = sqrt(b_dot3(b, b));
if (!(na > 0.0) || !(nb > 0.0))
return NAN;
const double cross[3] = {a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0]};
return atan2(sqrt(b_dot3(cross, cross)) / (na * nb),
b_dot3(a, b) / (na * nb));
}
static double critical_angle(double r) {
const double b = 3.0 * sqrt(3.0) * sqrt(1.0 - 2.0 / r) / r;
return (b < 1.0) ? asin(b) : PI / 2.0;
}
static void dir_from_theta(double th, double n[3]) {
n[0] = cos(th);
n[1] = sin(th);
n[2] = 0.0;
const double nn = sqrt(b_dot3(n, n));
for (int i = 0; i < 3; ++i)
n[i] /= nn;
}
static GeodesicTraceConfig make_cfg(const CfgDef *d) {
GeodesicTraceConfig c;
memset(&c, 0, sizeof c);
c.coordinate_time_step = 0.1;
c.max_steps = 65536;
c.threshold.kind = THRESHOLD_LOG_ENERGY_GROWTH;
c.threshold.value = 8.0;
c.threshold.policy_version = 3;
c.stepper = GEODESIC_STEPPER_DP54;
c.atol_x = c.atol_Pi = c.atol_L = c.rtol = d->tol;
c.min_step = 1e-14;
c.max_step = d->max_step;
c.consecutive_rejection_limit = 32;
c.max_lookback_time = 6553.6;
return c;
}
static int build_observer(const CamDef *cam, const SpacetimeSource *s,
ObserverState *o) {
const double pos[3] = {cam->r, 0.0, 0.0};
MetricData m;
if (spacetime_eval(s, 0.0, pos, &m) != SPACETIME_POINT_OK)
return -1;
ObserverCamera camera;
memset(&camera, 0, sizeof camera);
camera.position[0] = pos[0];
camera.position[1] = pos[1];
camera.position[2] = pos[2];
camera.look_ra_deg = cam->look_ra_deg;
camera.look_dec_deg = 0.0;
return observer_from_coordinate_camera(&m, &camera, o, NULL) ==
OBSERVER_BUILD_OK
? 0
: -1;
}
static Res run_one(const SpacetimeSource *s, const ObserverState *o,
const double dir[3], const GeodesicTraceConfig *cfg) {
Res r;
memset(&r, 0, sizeof r);
const RayEndpoint e = geodesic_trace_past(s, o, dir, cfg);
r.outcome = e.outcome;
r.reason = e.reason;
r.end_id = e.end_id;
r.stop_t = e.stop_coordinate_time;
r.g = e.frequency_ratio;
r.thr = e.threshold_value;
r.L = e.final_log_alpha_p0;
r.L0 = e.final_log_alpha_p0_0;
for (int i = 0; i < 3; ++i)
r.n[i] = e.n_infinity[i];
r.steps = e.accepted_steps;
r.rejected = e.rejected_steps;
r.rhs = e.rhs_evaluations;
return r;
}
static const char *outcome_name(int o) {
switch (o) {
case RAY_OUTCOME_ESCAPED:
return "ESC";
case RAY_OUTCOME_DARK:
return "DARK";
case RAY_OUTCOME_UNRESOLVED:
return "UNRES";
default:
return "INC";
}
}
static void print_pair(const char *cam, int di, double theta, const char *pn,
const Res *a, const Res *b) {
const int same = a->outcome == b->outcome && a->reason == b->reason &&
a->end_id == b->end_id;
const int esc = a->outcome == RAY_OUTCOME_ESCAPED &&
b->outcome == RAY_OUTCOME_ESCAPED;
const double dn = esc ? b_ang_delta(a->n, b->n) : NAN;
const double dg =
(esc && a->g > 0.0 && b->g > 0.0) ? fabs(a->g / b->g - 1.0) : NAN;
const double dstop =
(isfinite(a->stop_t) && isfinite(b->stop_t)) ? a->stop_t - b->stop_t
: NAN;
const double dthr = (isfinite(a->thr) && isfinite(b->thr))
? a->thr - b->thr
: NAN;
printf("CAPAIR camera=%s dir=%d theta=%.17g pair=%s class=%s/%s same=%d "
"dn_ang=%.17g dgrel=%.17g dstop_t=%.17g dthr=%.17g end=%u/%u\n",
cam, di, theta, pn, outcome_name(a->outcome), outcome_name(b->outcome),
same, dn, dg, dstop, dthr, a->end_id, b->end_id);
}
/* A two-reference-tier agreement check for one neighbour direction. Returns
* 1 when the pair is a valid, agreeing ESC/DARK reference, 0 otherwise. */
static int neighbour_reference_check(const char *cam, int di, double theta,
const Res *a, const Res *b) {
const int class_same = a->outcome == b->outcome && a->reason == b->reason &&
a->end_id == b->end_id;
const int clean = (a->outcome == RAY_OUTCOME_ESCAPED ||
a->outcome == RAY_OUTCOME_DARK) &&
(b->outcome == RAY_OUTCOME_ESCAPED ||
b->outcome == RAY_OUTCOME_DARK);
const int stepped = a->steps > 0 && b->steps > 0;
const int esc = a->outcome == RAY_OUTCOME_ESCAPED &&
b->outcome == RAY_OUTCOME_ESCAPED;
const double dn = esc ? b_ang_delta(a->n, b->n) : NAN;
const double dg =
(esc && a->g > 0.0 && b->g > 0.0) ? fabs(a->g / b->g - 1.0) : NAN;
const double dstop =
(isfinite(a->stop_t) && isfinite(b->stop_t)) ? a->stop_t - b->stop_t
: NAN;
printf("NEIGHBOR_REFERENCE_CHECK camera=%s dir=%d theta=%.17g "
"ref_class0=%s/%d/%u ref_class1=%s/%d/%u same=%d clean=%d stepped=%d "
"dn_ang=%.17g dgrel=%.17g dstop_t=%.17g\n",
cam, di, theta, outcome_name(a->outcome), a->reason, a->end_id,
outcome_name(b->outcome), b->reason, b->end_id, class_same, clean,
stepped, dn, dg, dstop);
return class_same && clean && stepped;
}
int main(void) {
SpacetimeSource source;
if (spacetime_create_schwarzschild_ks(&source, 1.0, 256.0)) {
fprintf(stderr, "FATAL: cannot create Schwarzschild source\n");
return 2;
}
FILE *f = fopen("raw/critical_ref.csv", "w");
if (!f) {
fprintf(stderr, "FATAL: cannot open raw/critical_ref.csv\n");
return 2;
}
fprintf(f, "camera,dir,theta,cfg,outcome,reason,end_id,stop_t,steps,"
"rejected,rhs,nx,ny,nz,g,thr,L,L0\n");
Res res[3][3][4];
double thetas[3][3];
for (int ci = 0; ci < 3; ++ci) {
ObserverState observer;
if (build_observer(&cams[ci], &source, &observer)) {
fprintf(stderr, "FATAL: cannot build observer %s\n", cams[ci].name);
return 2;
}
const double tc = critical_angle(cams[ci].r);
const double th[3] = {tc - 1e-7, tc, tc + 1e-7};
for (int di = 0; di < 3; ++di) {
double dir[3];
dir_from_theta(th[di], dir);
thetas[ci][di] = th[di];
for (int fi = 0; fi < 4; ++fi) {
const GeodesicTraceConfig cfg = make_cfg(&cfgs[fi]);
const Res r = run_one(&source, &observer, dir, &cfg);
res[ci][di][fi] = r;
fprintf(f, "%s,%d,%.17g,%s,%d,%d,%u,%.17g,%u,%u,%lu,%.17g,%.17g,"
"%.17g,%.17g,%.17g,%.17g,%.17g\n",
cams[ci].name, di, th[di], cfgs[fi].name, r.outcome, r.reason,
r.end_id, r.stop_t, r.steps, r.rejected, r.rhs, r.n[0], r.n[1],
r.n[2], r.g, r.thr, r.L, r.L0);
printf("RAW camera=%s dir=%d theta=%.17g cfg=%s outcome=%d reason=%d "
"end=%u stop_t=%.17g steps=%u rejected=%u rhs=%lu g=%.17g "
"thr=%.17g L=%.17g L0=%.17g nx=%.17g ny=%.17g nz=%.17g\n",
cams[ci].name, di, th[di], cfgs[fi].name, r.outcome, r.reason,
r.end_id, r.stop_t, r.steps, r.rejected, r.rhs, r.g, r.thr, r.L,
r.L0, r.n[0], r.n[1], r.n[2]);
fflush(stdout);
}
}
}
fclose(f);
/* All nine directions: ref_vs_tighter, h2_vs_h8, ref_vs_h2, ref_vs_h8. */
for (int ci = 0; ci < 3; ++ci) {
for (int di = 0; di < 3; ++di) {
print_pair(cams[ci].name, di, thetas[ci][di], "ref_vs_tighter",
&res[ci][di][0], &res[ci][di][1]);
print_pair(cams[ci].name, di, thetas[ci][di], "h2_vs_h8",
&res[ci][di][2], &res[ci][di][3]);
print_pair(cams[ci].name, di, thetas[ci][di], "ref_vs_h2",
&res[ci][di][0], &res[ci][di][2]);
print_pair(cams[ci].name, di, thetas[ci][di], "ref_vs_h8",
&res[ci][di][0], &res[ci][di][3]);
}
}
/* Exact-critical: diagnostic only, never asserted. */
for (int ci = 0; ci < 3; ++ci) {
const Res *a = &res[ci][1][0];
const Res *b = &res[ci][1][1];
const int same = a->outcome == b->outcome && a->reason == b->reason &&
a->end_id == b->end_id;
const int esc = a->outcome == RAY_OUTCOME_ESCAPED &&
b->outcome == RAY_OUTCOME_ESCAPED;
const int dark = a->outcome == RAY_OUTCOME_DARK &&
b->outcome == RAY_OUTCOME_DARK;
const double dn = esc ? b_ang_delta(a->n, b->n) : NAN;
const double dstop =
(isfinite(a->stop_t) && isfinite(b->stop_t)) ? a->stop_t - b->stop_t
: NAN;
const int nonconverged =
!same || (esc && !(dn <= 1e-6)) || (dark && fabs(dstop) > 1e-4);
printf("EXACT_CRITICAL_DIRECTION camera=%s theta=%.17g ref0=%s/%d/%u "
"ref1=%s/%d/%u same=%d dn_ang=%.17g dstop_t=%.17g\n",
cams[ci].name, thetas[ci][1], outcome_name(a->outcome), a->reason,
a->end_id, outcome_name(b->outcome), b->reason, b->end_id, same, dn,
dstop);
if (nonconverged)
printf("EXACT_CRITICAL_REFERENCE_NOT_CONVERGED camera=%s dn_ang=%.17g "
"dstop_t=%.17g class_same=%d\n",
cams[ci].name, dn, dstop, same);
}
/* Neighbour reference check: must pass; this is the assertion-valid path. */
int fail = 0;
for (int ci = 0; ci < 3; ++ci) {
for (int di = 0; di < 3; di += 2) {
if (!neighbour_reference_check(cams[ci].name, di, thetas[ci][di],
&res[ci][di][0], &res[ci][di][1]))
fail = 1;
}
}
spacetime_destroy(&source);
printf("CRITICAL_REFERENCE_CHECK status=%d\n", fail ? 1 : 0);
printf("CRITICAL_REF_DONE\n");
return fail ? 1 : 0;
}
@@ -0,0 +1,26 @@
/* Diagnostic only: reuse the production regression's invalid-metric fixture. */
#define main adaptive_regression_main_not_called
#include "../../tests/test_geodesic_adaptive.c"
#undef main
int main(void) {
const double floors[] = {1e-2,1e-4,1e-6,1e-8,1e-10,1e-12,1e-14};
for (unsigned i=0;i<sizeof floors/sizeof *floors;++i) {
FixtureContext context={.bad_status=SPACETIME_POINT_INVALID_METRIC,
.bad_lo=-10,.bad_hi=-.05,.radius=100};
SpacetimeSource source={.context=&context,.ops=&fixture_ops};
GeodesicTraceConfig config=dp_config(1e-9,.1,10);
config.min_step=floors[i]; config.max_step=1;
const ObserverState observer=flat_observer_at((double[3]){0,0,0});
const double direction[3]={1,0,0};
RayEndpoint endpoint=geodesic_trace_past(&source,&observer,direction,&config);
printf("FAILURE_FLOOR min=%.0e outcome=%d reason=%d t=%.17g distance_to_invalid=%.17g accepted=%u rejected=%u rhs=%lu\n",
floors[i],endpoint.outcome,endpoint.reason,endpoint.stop_coordinate_time,
endpoint.stop_coordinate_time+.05,endpoint.accepted_steps,
endpoint.rejected_steps,endpoint.rhs_evaluations);
if(endpoint.outcome!=RAY_OUTCOME_INCOMPLETE ||
endpoint.stop_coordinate_time<=-.05) return 1;
}
puts("FAILURE_FLOOR_EXIT=0");
return 0;
}
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""Pure GRLENS v3 map load/compare helpers.
No renderer, no environment probing, no data generation: this module is only
imported by the benchmark scripts under
benchmarks/adaptive_step_bounds_2026-10-05/. Keeping it here means the
benchmark never imports an untracked module from local/.
"""
import collections
import hashlib
import math
import struct
import zlib
def load(path):
"""Parse one GRLENS v3 map; returns (info dict, {film_pos: vertex tuple})."""
data = path.read_bytes()
assert data[:8] == b'GRLENS\x01\x00'
assert struct.unpack_from('<I', data, 8)[0] == 3
assert struct.unpack_from('<Q', data, 32)[0] == 1
nv, nt = struct.unpack_from('<QQ', data, 200)
end = 224 + nv * 108 + nt * 32
assert end + 4 == len(data)
assert zlib.crc32(data[224:end]) == struct.unpack_from('<I', data, end)[0]
vertices = [struct.unpack_from('<9dIIIQQQ', data, 224 + i * 108)
for i in range(nv)]
costs = [sum(v[j] for v in vertices) for j in (12, 13, 14)]
result = dict(vertices=nv, triangles=nt,
outcomes=dict(collections.Counter(v[10] for v in vertices)),
reasons=dict(collections.Counter(v[11] for v in vertices)),
accepted=costs[0], rejected=costs[1], rhs=costs[2])
names = ('atol_x', 'atol_Pi', 'atol_L', 'rtol', 'min_step', 'max_step',
'max_lookback_time', 'retry_lookback_increment',
'max_total_lookback_time')
result['provenance'] = dict(zip(names, struct.unpack_from('<9d', data, 100)))
result['provenance'].update(
integrator=struct.unpack_from('<I', data, 68)[0],
initial_step=struct.unpack_from('<d', data, 88)[0],
initial_max_steps=struct.unpack_from('<I', data, 96)[0],
threshold=struct.unpack_from('<d', data, 48)[0],
retry_step_increment=struct.unpack_from('<I', data, 56)[0],
max_total_steps=struct.unpack_from('<I', data, 60)[0])
for j, name in ((12, 'accepted'), (13, 'rejected'), (14, 'rhs')):
xs = sorted(v[j] for v in vertices)
result[name + '_percentiles'] = {
str(q): xs[min(len(xs) - 1, int(q * (len(xs) - 1)))]
for q in (0, .5, .9, .99, 1)}
result['sha256'] = hashlib.sha256(data).hexdigest()
result['triangle_sha256'] = hashlib.sha256(
data[224 + nv * 108:end]).hexdigest()
by_film = {(v[0], v[1]): v for v in vertices}
result['unique_film_positions'] = len(by_film)
result['duplicate_film_positions'] = len(vertices) - len(by_film)
return result, by_film
def compare(a, b):
"""Compare two {film_pos: vertex} maps by shared film-coordinate identity."""
keys = a.keys() & b.keys()
mismatches = 0
worst_sky = worst_logg = 0.
for k in keys:
x, y = a[k], b[k]
if x[9:12] != y[9:12]:
mismatches += 1
elif x[10] == 0:
n, m = x[5:8], y[5:8]
cross = (n[1] * m[2] - n[2] * m[1], n[2] * m[0] - n[0] * m[2],
n[0] * m[1] - n[1] * m[0])
angle = math.atan2(math.sqrt(sum(v * v for v in cross)),
sum(v * w for v, w in zip(n, m)))
worst_sky = max(worst_sky, angle)
worst_logg = max(worst_logg, abs(x[8] - y[8]))
return dict(shared=len(keys), only_a=len(a) - len(keys),
only_b=len(b) - len(keys), terminal_mismatches=mismatches,
max_sky_angle=worst_sky, max_logg_difference=worst_logg)
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""Record provenance without launching a render.
Usage:
record_environment.py [OUT_DIR]
OUT_DIR defaults to <repo>/local/adaptive_bounds_mesh. Writes metadata.json in
OUT_DIR. No renderer or test is run.
"""
import hashlib
import json
from pathlib import Path
import subprocess
import sys
import time
ROOT = Path(__file__).resolve().parents[2]
OUT = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else \
ROOT / 'local/adaptive_bounds_mesh'
PATHS = ['src/geodesic.c', 'src/main.c', 'src/frame.c',
'build/Release/schwarzschild_sky']
metadata = {
'recorded_utc': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
'git_head': subprocess.check_output(
['git', 'rev-parse', 'HEAD'], cwd=ROOT, text=True).strip(),
'hashes': {p: hashlib.sha256((ROOT / p).read_bytes()).hexdigest()
for p in PATHS},
'compiler': subprocess.check_output(['cc', '--version'], text=True),
'cpu': subprocess.check_output(['lscpu'], text=True),
'scope': '8 bounded 320x180 candidates plus 2 tight references; no 4K here',
'threads': 8,
'build_command': 'make -j4 BUILD_TYPE=Release SPACETIME=schwarzschild backend',
'run_commands': [
'python3 benchmarks/adaptive_step_bounds_2026-10-05/run_mesh.py [OUT]',
'python3 benchmarks/adaptive_step_bounds_2026-10-05/run_mesh_reference.py [OUT]',
'bash benchmarks/adaptive_step_bounds_2026-10-05/run_failure_floor.sh [OUT]'],
}
OUT.mkdir(parents=True, exist_ok=True)
(OUT / 'metadata.json').write_text(json.dumps(metadata, indent=2) + '\n')
print(json.dumps(metadata, indent=2))
@@ -0,0 +1,16 @@
[
{
"upper": 2,
"command": "/home/wyj/Code/C/GR_4d_raytracing/build/Release/schwarzschild_sky --integrator dp54 --width 3840 --height 2160 --look-ra-deg 262.5 --look-dec-deg -30 --fov-deg 45 --observer-radius 100 --coarse-cell-pixels 16 --refine-max-level 4 --refine-jacobian-min .2 --catalog /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/single_dim_star.csv --exposure 1 --tone-map reinhard --psf-min-y 1e-20 --psf-relative-tail 1e-4 --ode-min-step 1e-12 --ode-max-step 2 --verbose --lens-map-output /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/r100_4k_hmax2.grlens --output /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/r100_4k_hmax2.png",
"state": "completed",
"returncode": 0,
"wall_seconds": 11.389317202148959
},
{
"upper": 8,
"command": "/home/wyj/Code/C/GR_4d_raytracing/build/Release/schwarzschild_sky --integrator dp54 --width 3840 --height 2160 --look-ra-deg 262.5 --look-dec-deg -30 --fov-deg 45 --observer-radius 100 --coarse-cell-pixels 16 --refine-max-level 4 --refine-jacobian-min .2 --catalog /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/single_dim_star.csv --exposure 1 --tone-map reinhard --psf-min-y 1e-20 --psf-relative-tail 1e-4 --ode-min-step 1e-12 --ode-max-step 8 --verbose --lens-map-output /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/r100_4k_hmax8.grlens --output /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/r100_4k_hmax8.png",
"state": "completed",
"returncode": 0,
"wall_seconds": 8.389249739935622
}
]
@@ -0,0 +1,160 @@
{
"r100_4k_hmax2": {
"vertices": 66045,
"triangles": 131338,
"outcomes": {
"0": 62419,
"1": 3626
},
"reasons": {
"0": 62419,
"1": 3626
},
"accepted": 15768677,
"rejected": 97176,
"rhs": 130409139,
"provenance": {
"atol_x": 1e-09,
"atol_Pi": 1e-09,
"atol_L": 1e-09,
"rtol": 1e-09,
"min_step": 1e-12,
"max_step": 2.0,
"max_lookback_time": 6553.6,
"retry_lookback_increment": 6553.6,
"max_total_lookback_time": 26214.4,
"integrator": 1,
"initial_step": 0.1,
"initial_max_steps": 65536,
"threshold": 8.0,
"retry_step_increment": 65536,
"max_total_steps": 262144
},
"accepted_percentiles": {
"0": 162,
"0.5": 212,
"0.9": 328,
"0.99": 404,
"1": 520
},
"rejected_percentiles": {
"0": 0,
"0.5": 1,
"0.9": 3,
"0.99": 4,
"1": 5
},
"rhs_percentiles": {
"0": 1421,
"0.5": 1799,
"0.9": 2597,
"0.99": 3129,
"1": 3941
},
"sha256": "ef7267074338192f8597b1f2a0300fd36da8e25441e064719c7ad586c481ae7a",
"triangle_sha256": "2a943902e9b1401a16bc40fc547c19077bb12e396080074393636ee30761b7b2",
"unique_film_positions": 66045,
"duplicate_film_positions": 0,
"requested_samples_including_nonpersistent_probes": 116308,
"initial_samples": 32776,
"refinement_samples_per_generation": [
32686,
7900,
13375,
24826,
4403,
261,
67,
14
],
"trace_wall_seconds": 10.774,
"wall_seconds": 11.389317202148959
},
"r100_4k_hmax8": {
"vertices": 66045,
"triangles": 131338,
"outcomes": {
"0": 62419,
"1": 3626
},
"reasons": {
"0": 62419,
"1": 3626
},
"accepted": 10183020,
"rejected": 194781,
"rhs": 91991144,
"provenance": {
"atol_x": 1e-09,
"atol_Pi": 1e-09,
"atol_L": 1e-09,
"rtol": 1e-09,
"min_step": 1e-12,
"max_step": 8.0,
"max_lookback_time": 6553.6,
"retry_lookback_increment": 6553.6,
"max_total_lookback_time": 26214.4,
"integrator": 1,
"initial_step": 0.1,
"initial_max_steps": 65536,
"threshold": 8.0,
"retry_step_increment": 65536,
"max_total_steps": 262144
},
"accepted_percentiles": {
"0": 80,
"0.5": 127,
"0.9": 259,
"0.99": 337,
"1": 432
},
"rejected_percentiles": {
"0": 0,
"0.5": 3,
"0.9": 5,
"0.99": 6,
"1": 7
},
"rhs_percentiles": {
"0": 868,
"0.5": 1204,
"0.9": 2114,
"0.99": 2646,
"1": 3318
},
"sha256": "b1897493b7fa5b88f86872e81d15ac876818a705fa0fefe8b1ee262da9de59bb",
"triangle_sha256": "2a943902e9b1401a16bc40fc547c19077bb12e396080074393636ee30761b7b2",
"unique_film_positions": 66045,
"duplicate_film_positions": 0,
"requested_samples_including_nonpersistent_probes": 116308,
"initial_samples": 32776,
"refinement_samples_per_generation": [
32686,
7900,
13375,
24826,
4403,
261,
67,
14
],
"trace_wall_seconds": 7.805000000000001,
"wall_seconds": 8.389249739935622
},
"comparison": {
"shared": 66045,
"only_a": 0,
"only_b": 0,
"terminal_mismatches": 0,
"max_sky_angle": 7.954413592847793e-07,
"max_logg_difference": 3.471110204822381e-11,
"triangle_payload_identical": true
},
"relative_reduction": {
"accepted": 0.35422483446138187,
"rhs": 0.2945958795111745,
"wall_seconds": 0.2634106513116764,
"trace_wall_seconds": 0.27557081863746047
},
"cost_scope": "accepted/rejected/RHS sums cover final persistent vertices only; nonpersistent discarded refinement probes are not in v3 map. Do not interpret them as full-render executed RHS totals."
}
@@ -0,0 +1,242 @@
OMP_NUM_THREADS=16 OMP_DYNAMIC=FALSE /home/wyj/Code/C/GR_4d_raytracing/build/Release/schwarzschild_sky --integrator dp54 --width 3840 --height 2160 --look-ra-deg 262.5 --look-dec-deg -30 --fov-deg 45 --observer-radius 100 --coarse-cell-pixels 16 --refine-max-level 4 --refine-jacobian-min .2 --catalog /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/single_dim_star.csv --exposure 1 --tone-map reinhard --psf-min-y 1e-20 --psf-relative-tail 1e-4 --ode-min-step 1e-12 --ode-max-step 2 --verbose --lens-map-output /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/r100_4k_hmax2.grlens --output /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/r100_4k_hmax2.png
Blackbody LUT: 1024 CIE 1931 2-deg 1 nm-linear XYZ nodes, T=[670.146556, 101408.88] K, loaded assets/blackbody/cie1931_2deg_xyz_1024.grbblut in 0.000040 s; payload fnv1a64=0e39867b0e70a809
Blackbody backend: lut
PSF cache ready: 64x64 phases, radius 25 px, relative tail 1e-04, tail abs 1e-06, boundary 1e-07, build 0.250 s
Frame 0: integrator=dp54 step=0.1 max_steps=65536 threshold=8 atol=(x 1e-09, Pi 1e-09, L 1e-09) rtol=1e-09 bounds=[1e-12, 2] max_rejections=32 lookback=6553.6
Frame 0: tracing 32776 initial rays from 64800 mesh triangles...
Frame 0: initial ray trace finished in 2.562 s.
Frame 0: starting adaptive ray-trace refinement (max level 4)...
Frame 0: refinement generation 0 tracing 32686 samples from 32776 vertices and 64800 triangles.
Frame 0: refinement generation 0 finished; added 3014 vertices, now 35790 vertices and 70828 triangles.
Frame 0: refinement generation 1 tracing 7900 samples from 35790 vertices and 70828 triangles.
Frame 0: refinement generation 1 finished; added 4852 vertices, now 40642 vertices and 80532 triangles.
Frame 0: refinement generation 2 tracing 13375 samples from 40642 vertices and 80532 triangles.
Frame 0: refinement generation 2 finished; added 8889 vertices, now 49531 vertices and 98310 triangles.
Frame 0: refinement generation 3 tracing 24826 samples from 49531 vertices and 98310 triangles.
Frame 0: refinement generation 3 finished; added 16055 vertices, now 65586 vertices and 130420 triangles.
Frame 0: refinement generation 4 tracing 4403 samples from 65586 vertices and 130420 triangles.
Frame 0: refinement generation 4 finished; added 389 vertices, now 65975 vertices and 131198 triangles.
Frame 0: refinement generation 5 tracing 261 samples from 65975 vertices and 131198 triangles.
Frame 0: refinement generation 5 finished; added 58 vertices, now 66033 vertices and 131314 triangles.
Frame 0: refinement generation 6 tracing 67 samples from 66033 vertices and 131314 triangles.
Frame 0: refinement generation 6 finished; added 12 vertices, now 66045 vertices and 131338 triangles.
Frame 0: refinement generation 7 tracing 14 samples from 66045 vertices and 131338 triangles.
Frame 0: refinement generation 7 finished; added 0 vertices, now 66045 vertices and 131338 triangles.
Frame 0: adaptive ray-trace refinement finished in 8.212 s; 66045 vertices, 131338 triangles.
Boundary accounting: EEE=122456 DDD=5624 EED/EDD=3258 UUD/UDD=0 U+E=0 UUU=0 error=0; approx-black=0 (0 px^2), retries=0, budget-incomplete=0.
Frame 0 trace cost: accepted=15768677 rejected=97176 rhs=130409139 over 66045 vertices (saturated=0).
Wrote lens map: /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/r100_4k_hmax2.grlens (66045 vertices, 131338 triangles)
Frame 0: traced 66045 lens vertices; starting catalog render.
Frame 0: finding and prefetching catalog tiles...
Frame 0: catalog prefetch finished (0 candidate tiles).
Frame 0: splatting 131338 lens triangles...
Frame 0: splat worker 1/16 started.
Frame 0: splat worker 1/16 reached 8 local triangles.
Frame 0: splat worker 1/16 reached 16 local triangles.
Frame 0: splat worker 1/16 reached 32 local triangles.
Frame 0: splat worker 6/16 started.
Frame 0: splat worker 8/16 started.
Frame 0: splat worker 8/16 reached 8 local triangles.
Frame 0: splat worker 8/16 reached 16 local triangles.
Frame 0: splat worker 12/16 started.
Frame 0: splat worker 13/16 started.
Frame 0: splat worker 3/16 started.
Frame 0: splat worker 6/16 reached 8 local triangles.
Frame 0: splat worker 6/16 reached 16 local triangles.
Frame 0: splat worker 9/16 started.
Frame 0: splat worker 11/16 started.
Frame 0: splat worker 7/16 started.
Frame 0: splat worker 7/16 reached 8 local triangles.
Frame 0: splat worker 13/16 reached 8 local triangles.
Frame 0: splat worker 13/16 reached 16 local triangles.
Frame 0: splat worker 13/16 reached 32 local triangles.
Frame 0: splat worker 3/16 reached 8 local triangles.
Frame 0: splat worker 3/16 reached 16 local triangles.
Frame 0: splat worker 3/16 reached 32 local triangles.
Frame 0: splat worker 15/16 started.
Frame 0: splat worker 15/16 reached 8 local triangles.
Frame 0: splat worker 15/16 reached 16 local triangles.
Frame 0: splat worker 15/16 reached 32 local triangles.
Frame 0: splat worker 10/16 started.
Frame 0: splat worker 8/16 reached 32 local triangles.
Frame 0: splat worker 9/16 reached 8 local triangles.
Frame 0: splat worker 9/16 reached 16 local triangles.
Frame 0: splat worker 12/16 reached 8 local triangles.
Frame 0: splat worker 7/16 reached 16 local triangles.
Frame 0: splat worker 14/16 started.
Frame 0: splat worker 4/16 started.
Frame 0: splat worker 13/16 reached 64 local triangles.
Frame 0: splat worker 2/16 started.
Frame 0: splat worker 5/16 started.
Frame 0: splat worker 5/16 reached 8 local triangles.
Frame 0: splat worker 5/16 reached 16 local triangles.
Frame 0: splat worker 1/16 reached 64 local triangles.
Frame 0: splat worker 16/16 started.
Frame 0: splat worker 16/16 reached 8 local triangles.
Frame 0: splat worker 16/16 reached 16 local triangles.
Frame 0: splat worker 8/16 reached 64 local triangles.
Frame 0: splat worker 11/16 reached 8 local triangles.
Frame 0: splat worker 8/16 reached 128 local triangles.
Frame 0: splat worker 12/16 reached 16 local triangles.
Frame 0: splat worker 7/16 reached 32 local triangles.
Frame 0: splat worker 14/16 reached 8 local triangles.
Frame 0: splat worker 8/16 reached 256 local triangles.
Frame 0: splat worker 13/16 reached 128 local triangles.
Frame 0: splat worker 2/16 reached 8 local triangles.
Frame 0: splat worker 2/16 reached 16 local triangles.
Frame 0: splat worker 6/16 reached 32 local triangles.
Frame 0: splat worker 5/16 reached 32 local triangles.
Frame 0: splat worker 6/16 reached 64 local triangles.
Frame 0: splat worker 1/16 reached 128 local triangles.
Frame 0: splat worker 5/16 reached 64 local triangles.
Frame 0: splat worker 10/16 reached 8 local triangles.
Frame 0: splat worker 9/16 reached 32 local triangles.
Frame 0: splat worker 6/16 reached 128 local triangles.
Frame 0: splat worker 11/16 reached 16 local triangles.
Frame 0: splat worker 5/16 reached 128 local triangles.
Frame 0: splat worker 8/16 reached 512 local triangles.
Frame 0: splat worker 7/16 reached 64 local triangles.
Frame 0: splat worker 14/16 reached 16 local triangles.
Frame 0: splat worker 4/16 reached 8 local triangles.
Frame 0: splat worker 2/16 reached 32 local triangles.
Frame 0: splat worker 15/16 reached 64 local triangles.
Frame 0: splat worker 7/16 reached 128 local triangles.
Frame 0: splat worker 13/16 reached 256 local triangles.
Frame 0: splat worker 10/16 reached 16 local triangles.
Frame 0: splat worker 5/16 reached 256 local triangles.
Frame 0: splat worker 11/16 reached 32 local triangles.
Frame 0: splat worker 9/16 reached 64 local triangles.
Frame 0: splat worker 1/16 reached 256 local triangles.
Frame 0: splat worker 11/16 reached 64 local triangles.
Frame 0: splat worker 3/16 reached 64 local triangles.
Frame 0: splat worker 14/16 reached 32 local triangles.
Frame 0: splat worker 4/16 reached 16 local triangles.
Frame 0: splat worker 6/16 reached 256 local triangles.
Frame 0: splat worker 4/16 reached 32 local triangles.
Frame 0: splat worker 14/16 reached 64 local triangles.
Frame 0: splat worker 11/16 reached 128 local triangles.
Frame 0: splat worker 3/16 reached 128 local triangles.
Frame 0: splat worker 10/16 reached 32 local triangles.
Frame 0: splat worker 4/16 reached 64 local triangles.
Frame 0: splat worker 7/16 reached 256 local triangles.
Frame 0: splat worker 10/16 reached 64 local triangles.
Frame 0: splat worker 16/16 reached 32 local triangles.
Frame 0: splat worker 4/16 reached 128 local triangles.
Frame 0: splat worker 12/16 reached 32 local triangles.
Frame 0: splat worker 5/16 reached 512 local triangles.
Frame 0: splat worker 11/16 reached 256 local triangles.
Frame 0: splat worker 16/16 reached 64 local triangles.
Frame 0: splat worker 3/16 reached 256 local triangles.
Frame 0: splat worker 12/16 reached 64 local triangles.
Frame 0: splat worker 13/16 reached 512 local triangles.
Frame 0: splat worker 15/16 reached 128 local triangles.
Frame 0: splat worker 14/16 reached 128 local triangles.
Frame 0: splat worker 9/16 reached 128 local triangles.
Frame 0: splat worker 10/16 reached 128 local triangles.
Frame 0: splat worker 2/16 reached 64 local triangles.
Frame 0: splat worker 12/16 reached 128 local triangles.
Frame 0: splat worker 16/16 reached 128 local triangles.
Frame 0: splat worker 6/16 reached 512 local triangles.
Frame 0: splat worker 4/16 reached 256 local triangles.
Frame 0: splat worker 1/16 reached 512 local triangles.
Frame 0: splat worker 8/16 reached 1024 local triangles.
Frame 0: splat worker 2/16 reached 128 local triangles.
Frame 0: splat worker 15/16 reached 256 local triangles.
Frame 0: splat worker 14/16 reached 256 local triangles.
Frame 0: splat worker 7/16 reached 512 local triangles.
Frame 0: splat worker 10/16 reached 256 local triangles.
Frame 0: splat worker 9/16 reached 256 local triangles.
Frame 0: splat worker 11/16 reached 512 local triangles.
Frame 0: splat worker 16/16 reached 256 local triangles.
Frame 0: splat worker 12/16 reached 256 local triangles.
Frame 0: splat worker 2/16 reached 256 local triangles.
Frame 0: splat worker 3/16 reached 512 local triangles.
Frame 0: splat worker 4/16 reached 512 local triangles.
Frame 0: splat worker 15/16 reached 512 local triangles.
Frame 0: splat worker 14/16 reached 512 local triangles.
Frame 0: splat worker 10/16 reached 512 local triangles.
Frame 0: splat worker 9/16 reached 512 local triangles.
Frame 0: splat worker 5/16 reached 1024 local triangles.
Frame 0: splat worker 12/16 reached 512 local triangles.
Frame 0: splat worker 16/16 reached 512 local triangles.
Frame 0: splat worker 13/16 reached 1024 local triangles.
Frame 0: splat worker 2/16 reached 512 local triangles.
Frame 0: splat worker 1/16 reached 1024 local triangles.
Frame 0: splat worker 6/16 reached 1024 local triangles.
Frame 0: splat worker 7/16 reached 1024 local triangles.
Frame 0: splat worker 11/16 reached 1024 local triangles.
Frame 0: splat worker 4/16 reached 1024 local triangles.
Frame 0: splat worker 3/16 reached 1024 local triangles.
Frame 0: splat worker 15/16 reached 1024 local triangles.
Frame 0: splat worker 14/16 reached 1024 local triangles.
Frame 0: splat worker 10/16 reached 1024 local triangles.
Frame 0: splat worker 9/16 reached 1024 local triangles.
Frame 0: splat worker 12/16 reached 1024 local triangles.
Frame 0: splat worker 16/16 reached 1024 local triangles.
Frame 0: splat worker 2/16 reached 1024 local triangles.
Frame 0: splat worker 8/16 reached 2048 local triangles.
Frame 0: splat worker 5/16 reached 2048 local triangles.
Frame 0: splat worker 13/16 reached 2048 local triangles.
Frame 0: splat worker 1/16 reached 2048 local triangles.
Frame 0: splat worker 6/16 reached 2048 local triangles.
Frame 0: splat worker 7/16 reached 2048 local triangles.
Frame 0: splat worker 11/16 reached 2048 local triangles.
Frame 0: splat worker 4/16 reached 2048 local triangles.
Frame 0: splat worker 15/16 reached 2048 local triangles.
Frame 0: splat worker 3/16 reached 2048 local triangles.
Frame 0: splat worker 14/16 reached 2048 local triangles.
Frame 0: splat worker 9/16 reached 2048 local triangles.
Frame 0: splat worker 10/16 reached 2048 local triangles.
Frame 0: splat worker 12/16 reached 2048 local triangles.
Frame 0: splat worker 2/16 reached 2048 local triangles.
Frame 0: splat worker 16/16 reached 2048 local triangles.
Frame 0: splat worker 8/16 reached 4096 local triangles.
Frame 0: splat worker 5/16 reached 4096 local triangles.
Frame 0: splat worker 13/16 reached 4096 local triangles.
Frame 0: splat worker 1/16 reached 4096 local triangles.
Frame 0: splat worker 6/16 reached 4096 local triangles.
Frame 0: splat worker 11/16 reached 4096 local triangles.
Frame 0: splat worker 4/16 reached 4096 local triangles.
Frame 0: splat worker 14/16 reached 4096 local triangles.
Frame 0: splat worker 15/16 reached 4096 local triangles.
Frame 0: splat worker 9/16 reached 4096 local triangles.
Frame 0: splat worker 10/16 reached 4096 local triangles.
Frame 0: splat worker 16/16 reached 4096 local triangles.
Frame 0: splat worker 2/16 reached 4096 local triangles.
Frame 0: splat worker 12/16 reached 4096 local triangles.
Frame 0: splat worker 3/16 reached 4096 local triangles.
Frame 0: splat worker 8/16 reached 8192 local triangles.
Frame 0: splat worker 13/16 reached 8192 local triangles.
Frame 0: splat worker 5/16 reached 8192 local triangles.
Frame 0: splat worker 12/16 reached 8192 local triangles.
Frame 0: splat worker 1/16 reached 8192 local triangles.
Frame 0: splat worker 6/16 reached 8192 local triangles.
Frame 0: splat worker 11/16 reached 8192 local triangles.
Frame 0: splat worker 14/16 reached 8192 local triangles.
Frame 0: splat worker 4/16 reached 8192 local triangles.
Frame 0: splat worker 9/16 reached 8192 local triangles.
Frame 0: splat worker 16/16 reached 8192 local triangles.
Frame 0: splat worker 10/16 reached 8192 local triangles.
Frame 0: splat worker 2/16 reached 8192 local triangles.
Frame 0: splat worker 7/16 reached 4096 local triangles.
Frame 0: splat worker 15/16 reached 8192 local triangles.
Frame 0: splat worker 6/16 finished after 8555 local triangles.
Frame 0: splat worker 16/16 finished after 8230 local triangles.
Frame 0: splat worker 15/16 finished after 8192 local triangles.
Frame 0: splat worker 11/16 finished after 8468 local triangles.
Frame 0: splat worker 10/16 finished after 8207 local triangles.
Frame 0: splat worker 5/16 finished after 8824 local triangles.
Frame 0: splat worker 4/16 finished after 8382 local triangles.
Frame 0: splat worker 3/16 finished after 8004 local triangles.
Frame 0: splat worker 8/16 finished after 9239 local triangles.
Frame 0: splat worker 1/16 finished after 8561 local triangles.
Frame 0: splat worker 12/16 finished after 8804 local triangles.
Frame 0: splat worker 14/16 finished after 8429 local triangles.
Frame 0: splat worker 7/16 finished after 4096 local triangles.
Frame 0: splat worker 2/16 finished after 8201 local triangles.
Frame 0: splat worker 9/16 finished after 8290 local triangles.
Frame 0: splat worker 13/16 finished after 8856 local triangles.
Frame 0: catalog splatting finished in 0.1 s; writing image...
Rendered 1965 images from 1 catalog stars to /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/r100_4k_hmax2.png (ok)
PSF splats: cached 0, cached wing-clipped 0, direct fallbacks 0, discarded below min-Y 1965
Warning: --psf-min-y discarded one or more PSF events.
@@ -0,0 +1,233 @@
OMP_NUM_THREADS=16 OMP_DYNAMIC=FALSE /home/wyj/Code/C/GR_4d_raytracing/build/Release/schwarzschild_sky --integrator dp54 --width 3840 --height 2160 --look-ra-deg 262.5 --look-dec-deg -30 --fov-deg 45 --observer-radius 100 --coarse-cell-pixels 16 --refine-max-level 4 --refine-jacobian-min .2 --catalog /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/single_dim_star.csv --exposure 1 --tone-map reinhard --psf-min-y 1e-20 --psf-relative-tail 1e-4 --ode-min-step 1e-12 --ode-max-step 8 --verbose --lens-map-output /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/r100_4k_hmax8.grlens --output /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/r100_4k_hmax8.png
Blackbody LUT: 1024 CIE 1931 2-deg 1 nm-linear XYZ nodes, T=[670.146556, 101408.88] K, loaded assets/blackbody/cie1931_2deg_xyz_1024.grbblut in 0.000041 s; payload fnv1a64=0e39867b0e70a809
Blackbody backend: lut
PSF cache ready: 64x64 phases, radius 25 px, relative tail 1e-04, tail abs 1e-06, boundary 1e-07, build 0.223 s
Frame 0: integrator=dp54 step=0.1 max_steps=65536 threshold=8 atol=(x 1e-09, Pi 1e-09, L 1e-09) rtol=1e-09 bounds=[1e-12, 8] max_rejections=32 lookback=6553.6
Frame 0: tracing 32776 initial rays from 64800 mesh triangles...
Frame 0: initial ray trace finished in 1.735 s.
Frame 0: starting adaptive ray-trace refinement (max level 4)...
Frame 0: refinement generation 0 tracing 32686 samples from 32776 vertices and 64800 triangles.
Frame 0: refinement generation 0 finished; added 3014 vertices, now 35790 vertices and 70828 triangles.
Frame 0: refinement generation 1 tracing 7900 samples from 35790 vertices and 70828 triangles.
Frame 0: refinement generation 1 finished; added 4852 vertices, now 40642 vertices and 80532 triangles.
Frame 0: refinement generation 2 tracing 13375 samples from 40642 vertices and 80532 triangles.
Frame 0: refinement generation 2 finished; added 8889 vertices, now 49531 vertices and 98310 triangles.
Frame 0: refinement generation 3 tracing 24826 samples from 49531 vertices and 98310 triangles.
Frame 0: refinement generation 3 finished; added 16055 vertices, now 65586 vertices and 130420 triangles.
Frame 0: refinement generation 4 tracing 4403 samples from 65586 vertices and 130420 triangles.
Frame 0: refinement generation 4 finished; added 389 vertices, now 65975 vertices and 131198 triangles.
Frame 0: refinement generation 5 tracing 261 samples from 65975 vertices and 131198 triangles.
Frame 0: refinement generation 5 finished; added 58 vertices, now 66033 vertices and 131314 triangles.
Frame 0: refinement generation 6 tracing 67 samples from 66033 vertices and 131314 triangles.
Frame 0: refinement generation 6 finished; added 12 vertices, now 66045 vertices and 131338 triangles.
Frame 0: refinement generation 7 tracing 14 samples from 66045 vertices and 131338 triangles.
Frame 0: refinement generation 7 finished; added 0 vertices, now 66045 vertices and 131338 triangles.
Frame 0: adaptive ray-trace refinement finished in 6.070 s; 66045 vertices, 131338 triangles.
Boundary accounting: EEE=122456 DDD=5624 EED/EDD=3258 UUD/UDD=0 U+E=0 UUU=0 error=0; approx-black=0 (0 px^2), retries=0, budget-incomplete=0.
Frame 0 trace cost: accepted=10183020 rejected=194781 rhs=91991144 over 66045 vertices (saturated=0).
Wrote lens map: /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/r100_4k_hmax8.grlens (66045 vertices, 131338 triangles)
Frame 0: traced 66045 lens vertices; starting catalog render.
Frame 0: finding and prefetching catalog tiles...
Frame 0: catalog prefetch finished (0 candidate tiles).
Frame 0: splatting 131338 lens triangles...
Frame 0: splat worker 1/16 started.
Frame 0: splat worker 1/16 reached 8 local triangles.
Frame 0: splat worker 1/16 reached 16 local triangles.
Frame 0: splat worker 10/16 started.
Frame 0: splat worker 10/16 reached 8 local triangles.
Frame 0: splat worker 2/16 started.
Frame 0: splat worker 2/16 reached 8 local triangles.
Frame 0: splat worker 11/16 started.
Frame 0: splat worker 12/16 started.
Frame 0: splat worker 12/16 reached 8 local triangles.
Frame 0: splat worker 12/16 reached 16 local triangles.
Frame 0: splat worker 7/16 started.
Frame 0: splat worker 16/16 started.
Frame 0: splat worker 16/16 reached 8 local triangles.
Frame 0: splat worker 16/16 reached 16 local triangles.
Frame 0: splat worker 16/16 reached 32 local triangles.
Frame 0: splat worker 16/16 reached 64 local triangles.
Frame 0: splat worker 16/16 reached 128 local triangles.
Frame 0: splat worker 16/16 reached 256 local triangles.
Frame 0: splat worker 15/16 started.
Frame 0: splat worker 8/16 started.
Frame 0: splat worker 16/16 reached 512 local triangles.
Frame 0: splat worker 8/16 reached 8 local triangles.
Frame 0: splat worker 11/16 reached 8 local triangles.
Frame 0: splat worker 10/16 reached 16 local triangles.
Frame 0: splat worker 14/16 started.
Frame 0: splat worker 5/16 started.
Frame 0: splat worker 6/16 started.
Frame 0: splat worker 6/16 reached 8 local triangles.
Frame 0: splat worker 9/16 started.
Frame 0: splat worker 7/16 reached 8 local triangles.
Frame 0: splat worker 7/16 reached 16 local triangles.
Frame 0: splat worker 16/16 reached 1024 local triangles.
Frame 0: splat worker 4/16 started.
Frame 0: splat worker 3/16 started.
Frame 0: splat worker 8/16 reached 16 local triangles.
Frame 0: splat worker 3/16 reached 8 local triangles.
Frame 0: splat worker 10/16 reached 32 local triangles.
Frame 0: splat worker 16/16 reached 2048 local triangles.
Frame 0: splat worker 5/16 reached 8 local triangles.
Frame 0: splat worker 10/16 reached 64 local triangles.
Frame 0: splat worker 5/16 reached 16 local triangles.
Frame 0: splat worker 5/16 reached 32 local triangles.
Frame 0: splat worker 10/16 reached 128 local triangles.
Frame 0: splat worker 10/16 reached 256 local triangles.
Frame 0: splat worker 10/16 reached 512 local triangles.
Frame 0: splat worker 1/16 reached 32 local triangles.
Frame 0: splat worker 6/16 reached 16 local triangles.
Frame 0: splat worker 12/16 reached 32 local triangles.
Frame 0: splat worker 1/16 reached 64 local triangles.
Frame 0: splat worker 9/16 reached 8 local triangles.
Frame 0: splat worker 2/16 reached 16 local triangles.
Frame 0: splat worker 2/16 reached 32 local triangles.
Frame 0: splat worker 2/16 reached 64 local triangles.
Frame 0: splat worker 2/16 reached 128 local triangles.
Frame 0: splat worker 10/16 reached 1024 local triangles.
Frame 0: splat worker 2/16 reached 256 local triangles.
Frame 0: splat worker 4/16 reached 8 local triangles.
Frame 0: splat worker 11/16 reached 16 local triangles.
Frame 0: splat worker 4/16 reached 16 local triangles.
Frame 0: splat worker 11/16 reached 32 local triangles.
Frame 0: splat worker 4/16 reached 32 local triangles.
Frame 0: splat worker 14/16 reached 8 local triangles.
Frame 0: splat worker 5/16 reached 64 local triangles.
Frame 0: splat worker 14/16 reached 16 local triangles.
Frame 0: splat worker 7/16 reached 32 local triangles.
Frame 0: splat worker 12/16 reached 64 local triangles.
Frame 0: splat worker 5/16 reached 128 local triangles.
Frame 0: splat worker 7/16 reached 64 local triangles.
Frame 0: splat worker 12/16 reached 128 local triangles.
Frame 0: splat worker 7/16 reached 128 local triangles.
Frame 0: splat worker 5/16 reached 256 local triangles.
Frame 0: splat worker 13/16 started.
Frame 0: splat worker 8/16 reached 32 local triangles.
Frame 0: splat worker 3/16 reached 16 local triangles.
Frame 0: splat worker 2/16 reached 512 local triangles.
Frame 0: splat worker 8/16 reached 64 local triangles.
Frame 0: splat worker 5/16 reached 512 local triangles.
Frame 0: splat worker 8/16 reached 128 local triangles.
Frame 0: splat worker 4/16 reached 64 local triangles.
Frame 0: splat worker 10/16 reached 2048 local triangles.
Frame 0: splat worker 16/16 reached 4096 local triangles.
Frame 0: splat worker 14/16 reached 32 local triangles.
Frame 0: splat worker 9/16 reached 16 local triangles.
Frame 0: splat worker 4/16 reached 128 local triangles.
Frame 0: splat worker 14/16 reached 64 local triangles.
Frame 0: splat worker 7/16 reached 256 local triangles.
Frame 0: splat worker 12/16 reached 256 local triangles.
Frame 0: splat worker 3/16 reached 32 local triangles.
Frame 0: splat worker 11/16 reached 64 local triangles.
Frame 0: splat worker 3/16 reached 64 local triangles.
Frame 0: splat worker 8/16 reached 256 local triangles.
Frame 0: splat worker 6/16 reached 32 local triangles.
Frame 0: splat worker 4/16 reached 256 local triangles.
Frame 0: splat worker 9/16 reached 32 local triangles.
Frame 0: splat worker 11/16 reached 128 local triangles.
Frame 0: splat worker 3/16 reached 128 local triangles.
Frame 0: splat worker 15/16 reached 8 local triangles.
Frame 0: splat worker 5/16 reached 1024 local triangles.
Frame 0: splat worker 15/16 reached 16 local triangles.
Frame 0: splat worker 14/16 reached 128 local triangles.
Frame 0: splat worker 9/16 reached 64 local triangles.
Frame 0: splat worker 1/16 reached 128 local triangles.
Frame 0: splat worker 7/16 reached 512 local triangles.
Frame 0: splat worker 6/16 reached 64 local triangles.
Frame 0: splat worker 15/16 reached 32 local triangles.
Frame 0: splat worker 11/16 reached 256 local triangles.
Frame 0: splat worker 3/16 reached 256 local triangles.
Frame 0: splat worker 13/16 reached 8 local triangles.
Frame 0: splat worker 15/16 reached 64 local triangles.
Frame 0: splat worker 13/16 reached 16 local triangles.
Frame 0: splat worker 9/16 reached 128 local triangles.
Frame 0: splat worker 14/16 reached 256 local triangles.
Frame 0: splat worker 2/16 reached 1024 local triangles.
Frame 0: splat worker 13/16 reached 32 local triangles.
Frame 0: splat worker 15/16 reached 128 local triangles.
Frame 0: splat worker 6/16 reached 128 local triangles.
Frame 0: splat worker 12/16 reached 512 local triangles.
Frame 0: splat worker 13/16 reached 64 local triangles.
Frame 0: splat worker 4/16 reached 512 local triangles.
Frame 0: splat worker 8/16 reached 512 local triangles.
Frame 0: splat worker 1/16 reached 256 local triangles.
Frame 0: splat worker 9/16 reached 256 local triangles.
Frame 0: splat worker 13/16 reached 128 local triangles.
Frame 0: splat worker 15/16 reached 256 local triangles.
Frame 0: splat worker 6/16 reached 256 local triangles.
Frame 0: splat worker 3/16 reached 512 local triangles.
Frame 0: splat worker 11/16 reached 512 local triangles.
Frame 0: splat worker 14/16 reached 512 local triangles.
Frame 0: splat worker 13/16 reached 256 local triangles.
Frame 0: splat worker 1/16 reached 512 local triangles.
Frame 0: splat worker 9/16 reached 512 local triangles.
Frame 0: splat worker 7/16 reached 1024 local triangles.
Frame 0: splat worker 15/16 reached 512 local triangles.
Frame 0: splat worker 6/16 reached 512 local triangles.
Frame 0: splat worker 13/16 reached 512 local triangles.
Frame 0: splat worker 4/16 reached 1024 local triangles.
Frame 0: splat worker 8/16 reached 1024 local triangles.
Frame 0: splat worker 3/16 reached 1024 local triangles.
Frame 0: splat worker 11/16 reached 1024 local triangles.
Frame 0: splat worker 14/16 reached 1024 local triangles.
Frame 0: splat worker 12/16 reached 1024 local triangles.
Frame 0: splat worker 1/16 reached 1024 local triangles.
Frame 0: splat worker 5/16 reached 2048 local triangles.
Frame 0: splat worker 15/16 reached 1024 local triangles.
Frame 0: splat worker 9/16 reached 1024 local triangles.
Frame 0: splat worker 6/16 reached 1024 local triangles.
Frame 0: splat worker 13/16 reached 1024 local triangles.
Frame 0: splat worker 7/16 reached 2048 local triangles.
Frame 0: splat worker 2/16 reached 2048 local triangles.
Frame 0: splat worker 4/16 reached 2048 local triangles.
Frame 0: splat worker 8/16 reached 2048 local triangles.
Frame 0: splat worker 10/16 reached 4096 local triangles.
Frame 0: splat worker 3/16 reached 2048 local triangles.
Frame 0: splat worker 14/16 reached 2048 local triangles.
Frame 0: splat worker 11/16 reached 2048 local triangles.
Frame 0: splat worker 1/16 reached 2048 local triangles.
Frame 0: splat worker 15/16 reached 2048 local triangles.
Frame 0: splat worker 9/16 reached 2048 local triangles.
Frame 0: splat worker 6/16 reached 2048 local triangles.
Frame 0: splat worker 13/16 reached 2048 local triangles.
Frame 0: splat worker 12/16 reached 2048 local triangles.
Frame 0: splat worker 5/16 reached 4096 local triangles.
Frame 0: splat worker 7/16 reached 4096 local triangles.
Frame 0: splat worker 4/16 reached 4096 local triangles.
Frame 0: splat worker 8/16 reached 4096 local triangles.
Frame 0: splat worker 14/16 reached 4096 local triangles.
Frame 0: splat worker 16/16 reached 8192 local triangles.
Frame 0: splat worker 3/16 reached 4096 local triangles.
Frame 0: splat worker 11/16 reached 4096 local triangles.
Frame 0: splat worker 1/16 reached 4096 local triangles.
Frame 0: splat worker 15/16 reached 4096 local triangles.
Frame 0: splat worker 9/16 reached 4096 local triangles.
Frame 0: splat worker 6/16 reached 4096 local triangles.
Frame 0: splat worker 13/16 reached 4096 local triangles.
Frame 0: splat worker 2/16 reached 4096 local triangles.
Frame 0: splat worker 12/16 reached 4096 local triangles.
Frame 0: splat worker 10/16 reached 8192 local triangles.
Frame 0: splat worker 5/16 reached 8192 local triangles.
Frame 0: splat worker 14/16 reached 8192 local triangles.
Frame 0: splat worker 7/16 reached 8192 local triangles.
Frame 0: splat worker 10/16 finished after 10255 local triangles.
Frame 0: splat worker 13/16 finished after 7729 local triangles.
Frame 0: splat worker 14/16 finished after 8310 local triangles.
Frame 0: splat worker 3/16 finished after 8102 local triangles.
Frame 0: splat worker 12/16 finished after 5601 local triangles.
Frame 0: splat worker 2/16 finished after 6132 local triangles.
Frame 0: splat worker 7/16 finished after 8294 local triangles.
Frame 0: splat worker 6/16 finished after 7791 local triangles.
Frame 0: splat worker 5/16 finished after 8901 local triangles.
Frame 0: splat worker 4/16 finished after 8159 local triangles.
Frame 0: splat worker 1/16 finished after 7930 local triangles.
Frame 0: splat worker 16/16 finished after 12209 local triangles.
Frame 0: splat worker 15/16 finished after 7871 local triangles.
Frame 0: splat worker 9/16 finished after 7811 local triangles.
Frame 0: splat worker 11/16 finished after 8075 local triangles.
Frame 0: splat worker 8/16 finished after 8168 local triangles.
Frame 0: catalog splatting finished in 0.1 s; writing image...
Rendered 1954 images from 1 catalog stars to /home/wyj/Code/C/GR_4d_raytracing/local/step_bounds_4k/r100_4k_hmax8.png (ok)
PSF splats: cached 0, cached wing-clipped 0, direct fallbacks 0, discarded below min-Y 1954
Warning: --psf-min-y discarded one or more PSF events.
@@ -0,0 +1,13 @@
{
"binary_sha256": "72ba409cd60bc72a7f0ca16f736cab6be6d4ab3f42a11d00647eeb03cc7a7e2a",
"utc": "2026-10-05T22:19:23Z",
"source_sha256": {
"src/main.c": "6cf3f6354b52b5eb029e1f3605c403c4161bb3df5d4202a47b97409318358ff9",
"src/geodesic.c": "25f24a2e1c9ce49508fed230dfb3f6d9986e92bb4a5e6347bb2356ac0ee5fffa",
"src/frame.c": "317cf2a777be139a2cab6a1b9f00094b7834fbb0382389960526ee3281ca55bf"
},
"catalog": "one negligible synthetic point; no survey catalog",
"scope": "README R100 45-degree 3840x2160 coarse16 refine4 jacobian0.2",
"threads": 16,
"max_attempts": 2
}
@@ -0,0 +1,3 @@
r100_4k_hmax2 {"vertices": 66045, "triangles": 131338, "outcomes": {"0": 62419, "1": 3626}, "reasons": {"0": 62419, "1": 3626}, "accepted": 15768677, "rejected": 97176, "rhs": 130409139, "provenance": {"atol_x": 1e-09, "atol_Pi": 1e-09, "atol_L": 1e-09, "rtol": 1e-09, "min_step": 1e-12, "max_step": 2.0, "max_lookback_time": 6553.6, "retry_lookback_increment": 6553.6, "max_total_lookback_time": 26214.4, "integrator": 1, "initial_step": 0.1, "initial_max_steps": 65536, "threshold": 8.0, "retry_step_increment": 65536, "max_total_steps": 262144}, "accepted_percentiles": {"0": 162, "0.5": 212, "0.9": 328, "0.99": 404, "1": 520}, "rejected_percentiles": {"0": 0, "0.5": 1, "0.9": 3, "0.99": 4, "1": 5}, "rhs_percentiles": {"0": 1421, "0.5": 1799, "0.9": 2597, "0.99": 3129, "1": 3941}, "sha256": "ef7267074338192f8597b1f2a0300fd36da8e25441e064719c7ad586c481ae7a", "unique_film_positions": 66045, "duplicate_film_positions": 0, "wall_seconds": 11.389317202148959}
r100_4k_hmax8 {"vertices": 66045, "triangles": 131338, "outcomes": {"0": 62419, "1": 3626}, "reasons": {"0": 62419, "1": 3626}, "accepted": 10183020, "rejected": 194781, "rhs": 91991144, "provenance": {"atol_x": 1e-09, "atol_Pi": 1e-09, "atol_L": 1e-09, "rtol": 1e-09, "min_step": 1e-12, "max_step": 8.0, "max_lookback_time": 6553.6, "retry_lookback_increment": 6553.6, "max_total_lookback_time": 26214.4, "integrator": 1, "initial_step": 0.1, "initial_max_steps": 65536, "threshold": 8.0, "retry_step_increment": 65536, "max_total_steps": 262144}, "accepted_percentiles": {"0": 80, "0.5": 127, "0.9": 259, "0.99": 337, "1": 432}, "rejected_percentiles": {"0": 0, "0.5": 3, "0.9": 5, "0.99": 6, "1": 7}, "rhs_percentiles": {"0": 868, "0.5": 1204, "0.9": 2114, "0.99": 2646, "1": 3318}, "sha256": "b1897493b7fa5b88f86872e81d15ac876818a705fa0fefe8b1ee262da9de59bb", "unique_film_positions": 66045, "duplicate_film_positions": 0, "wall_seconds": 8.389249739935622}
comparison {"shared": 66045, "only_a": 0, "only_b": 0, "terminal_mismatches": 0, "max_sky_angle": 7.954413592847793e-07, "max_logg_difference": 3.471110204822381e-11}
@@ -0,0 +1,37 @@
camera,dir,theta,cfg,outcome,reason,end_id,stop_t,steps,rejected,rhs,nx,ny,nz,g,thr,L,L0
r30_inward,0,0.16812279429621627,ref_tol1e-12_h0.25,1,1,4294967295,-142.17574796859728,2081,0,14854,0,0,0,0,8.0000000000000195,8.0658840965825682,0.065884096582549498
r30_inward,0,0.16812279429621627,ref_tol1e-13_h0.125,1,1,4294967295,-142.17574845681202,3332,0,23611,0,0,0,0,8.0000000000000249,8.0658840965825735,0.065884096582549498
r30_inward,0,0.16812279429621627,tol1e-11_h2,1,1,4294967295,-142.17574172483768,1304,0,9415,0,0,0,0,8.0000000000000266,8.0658840965825753,0.065884096582549498
r30_inward,0,0.16812279429621627,tol1e-11_h8,1,1,4294967295,-142.17574172483768,1304,0,9415,0,0,0,0,8.0000000000000266,8.0658840965825753,0.065884096582549498
r30_inward,1,0.16812289429621627,ref_tol1e-12_h0.25,1,1,4294967295,-225.85293500418322,3746,0,26509,0,0,0,0,8.0000000000000338,8.0658840955358357,0.065884095535802364
r30_inward,1,0.16812289429621627,ref_tol1e-13_h0.125,1,1,4294967295,-238.90906331559862,6388,0,45003,0,0,0,0,8.000000000000016,8.065884095535818,0.065884095535802364
r30_inward,1,0.16812289429621627,tol1e-11_h2,1,1,4294967295,-212.59582609170599,2180,0,15547,0,0,0,0,8.0000000000000071,8.0658840955358091,0.065884095535802364
r30_inward,1,0.16812289429621627,tol1e-11_h8,1,1,4294967295,-212.59582609170599,2180,0,15547,0,0,0,0,8.0000000000000071,8.0658840955358091,0.065884095535802364
r30_inward,2,0.16812299429621627,ref_tol1e-12_h0.25,0,0,0,-370.18047830118235,2790,1,19831,-0.35873499147611138,-6.5068267720987458e-16,-0.9334394495041628,1.0350983390130453,nan,-0.038385896473527227,0.065884094489054593
r30_inward,2,0.16812299429621627,ref_tol1e-13_h0.125,0,0,0,-370.1804778092,4857,1,34300,-0.35873507984449715,-6.5068265353608199e-16,-0.93343941554284182,1.035098339013504,nan,-0.038385896473972475,0.065884094489054593
r30_inward,2,0.16812299429621627,tol1e-11_h2,0,0,0,-370.18048452857704,1299,1,9387,-0.3587338729380895,-6.5068297686427377e-16,-0.93343987937458439,1.0350983390054869,nan,-0.038385896466199748,0.065884094489054593
r30_inward,2,0.16812299429621627,tol1e-11_h8,0,0,0,-370.1804845285946,1257,1,9100,-0.35873387293809145,-6.5068297686427416e-16,-0.93343987937458361,1.035098339005424,nan,-0.038385896466138665,0.065884094489054593
r100_inward,0,0.051461896376274734,ref_tol1e-12_h0.25,1,1,4294967295,-211.027405235164,2252,0,16051,0,0,0,0,8.0000000000000266,8.0199767087115159,0.01997670871149013
r100_inward,0,0.051461896376274734,ref_tol1e-13_h0.125,1,1,4294967295,-211.02740538583069,3727,0,26376,0,0,0,0,8.0000000000000142,8.0199767087115035,0.01997670871149013
r100_inward,0,0.051461896376274734,tol1e-11_h2,1,1,4294967295,-211.02740354768991,1292,0,9331,0,0,0,0,8.0000000000000533,8.0199767087115426,0.01997670871149013
r100_inward,0,0.051461896376274734,tol1e-11_h8,1,1,4294967295,-211.02740354768991,1292,0,9331,0,0,0,0,8.0000000000000533,8.0199767087115426,0.01997670871149013
r100_inward,1,0.051461996376274736,ref_tol1e-12_h0.25,1,1,4294967295,-301.07740508539132,4045,0,28602,0,0,0,0,8.0000000000000018,8.0199767086106277,0.019976708610626077
r100_inward,1,0.051461996376274736,ref_tol1e-13_h0.125,1,1,4294967295,-313.77734009633099,6986,0,49189,0,0,0,0,8.0000000000000018,8.0199767086106277,0.019976708610626077
r100_inward,1,0.051461996376274736,tol1e-11_h2,1,1,4294967295,-288.21364790845217,2256,0,16079,0,0,0,0,8.0000000000000639,8.0199767086106899,0.019976708610626077
r100_inward,1,0.051461996376274736,tol1e-11_h8,1,1,4294967295,-288.21364790845217,2256,0,16079,0,0,0,0,8.0000000000000639,8.0199767086106899,0.019976708610626077
r100_inward,2,0.051462096376274739,ref_tol1e-12_h0.25,0,0,0,-439.31355204311825,2969,0,21077,-0.99110324423183982,-3.1326762563025509e-16,-0.13309530146899301,1.0101525445517785,nan,-0.013990814384499865,0.019976708509762027
r100_inward,2,0.051462096376274739,ref_tol1e-13_h0.125,0,0,0,-439.31355190087947,5265,0,37149,-0.99110324787406912,-3.132675617927086e-16,-0.13309527434688151,1.010152544552184,nan,-0.013990814384902493,0.019976708509762027
r100_inward,2,0.051462096376274739,tol1e-11_h2,0,0,0,-439.31355372933643,1291,1,9338,-0.99110320105936889,-3.1326838231562615e-16,-0.13309562295534913,1.010152544545182,nan,-0.013990814377948021,0.019976708509762027
r100_inward,2,0.051462096376274739,tol1e-11_h8,0,0,0,-439.31355372935258,1250,1,9051,-0.991103201059369,-3.1326838231562231e-16,-0.13309562295534744,1.0101525445451216,nan,-0.013990814377887956,0.019976708509762027
r2p1_outward,0,0.57037671003940071,ref_tol1e-12_h0.25,0,0,0,-333.45070432558816,2639,1,18774,-0.30976840652656795,0,-0.95081203942629522,4.5825756949539365,nan,-1.5261506795942201,-0.42981831573151047
r2p1_outward,0,0.57037671003940071,ref_tol1e-13_h0.125,0,0,0,-333.45070506871724,4589,2,32431,-0.30976827139480229,0,-0.95081208345133916,4.5825756949557528,nan,-1.5261506795946143,-0.42981831573151047
r2p1_outward,0,0.57037671003940071,tol1e-11_h2,0,0,0,-333.45070071486469,1210,2,8778,-0.30976906608911198,0,-0.95081182454483582,4.5825756949192931,nan,-1.5261506795866686,-0.42981831573151047
r2p1_outward,0,0.57037671003940071,tol1e-11_h8,0,0,0,-333.45070071488124,1169,2,8491,-0.30976906647032454,0,-0.95081182442063883,4.5825756949190151,nan,-1.5261506795866076,-0.42981831573151047
r2p1_outward,1,0.57037681003940066,ref_tol1e-12_h0.25,0,0,0,-414.94010328784253,4263,2,30149,0.3336576345143959,0,0.94269432104487505,4.5825756949507124,nan,-1.5261506795930104,-0.42981805651739968
r2p1_outward,1,0.57037681003940066,ref_tol1e-13_h0.125,0,0,0,-427.08073320404128,7560,1,53221,-0.91087431940825703,0,-0.41268386719442363,4.5825756949554943,nan,-1.5261506795940556,-0.42981805651739968
r2p1_outward,1,0.57037681003940066,tol1e-11_h2,0,0,0,-406.06581257014602,2120,2,15141,0.88820167266587602,0,-0.45945379383953305,4.58257569487448,nan,-1.5261506795763404,-0.42981805651739968
r2p1_outward,1,0.57037681003940066,tol1e-11_h8,0,0,0,-406.06581257016262,2078,2,14854,0.88817826517728515,0,-0.4594990416384655,4.5825756948742002,nan,-1.5261506795762789,-0.42981805651739968
r2p1_outward,2,0.57037691003940061,ref_tol1e-12_h0.25,1,1,4294967295,-109.41561097431348,1950,1,13944,0,0,0,0,8.0000000000000195,7.5701822026967047,-0.42981779730331554
r2p1_outward,2,0.57037691003940061,ref_tol1e-13_h0.125,1,1,4294967295,-109.41561022532379,3098,1,21980,0,0,0,0,8.000000000000016,7.5701822026967012,-0.42981779730331554
r2p1_outward,2,0.57037691003940061,tol1e-11_h2,1,1,4294967295,-109.41561457850582,1228,1,8890,0,0,0,0,8.0000000000000195,7.5701822026967038,-0.42981779730331554
r2p1_outward,2,0.57037691003940061,tol1e-11_h8,1,1,4294967295,-109.41561457850582,1228,1,8890,0,0,0,0,8.0000000000000195,7.5701822026967038,-0.42981779730331554
1 camera dir theta cfg outcome reason end_id stop_t steps rejected rhs nx ny nz g thr L L0
2 r30_inward 0 0.16812279429621627 ref_tol1e-12_h0.25 1 1 4294967295 -142.17574796859728 2081 0 14854 0 0 0 0 8.0000000000000195 8.0658840965825682 0.065884096582549498
3 r30_inward 0 0.16812279429621627 ref_tol1e-13_h0.125 1 1 4294967295 -142.17574845681202 3332 0 23611 0 0 0 0 8.0000000000000249 8.0658840965825735 0.065884096582549498
4 r30_inward 0 0.16812279429621627 tol1e-11_h2 1 1 4294967295 -142.17574172483768 1304 0 9415 0 0 0 0 8.0000000000000266 8.0658840965825753 0.065884096582549498
5 r30_inward 0 0.16812279429621627 tol1e-11_h8 1 1 4294967295 -142.17574172483768 1304 0 9415 0 0 0 0 8.0000000000000266 8.0658840965825753 0.065884096582549498
6 r30_inward 1 0.16812289429621627 ref_tol1e-12_h0.25 1 1 4294967295 -225.85293500418322 3746 0 26509 0 0 0 0 8.0000000000000338 8.0658840955358357 0.065884095535802364
7 r30_inward 1 0.16812289429621627 ref_tol1e-13_h0.125 1 1 4294967295 -238.90906331559862 6388 0 45003 0 0 0 0 8.000000000000016 8.065884095535818 0.065884095535802364
8 r30_inward 1 0.16812289429621627 tol1e-11_h2 1 1 4294967295 -212.59582609170599 2180 0 15547 0 0 0 0 8.0000000000000071 8.0658840955358091 0.065884095535802364
9 r30_inward 1 0.16812289429621627 tol1e-11_h8 1 1 4294967295 -212.59582609170599 2180 0 15547 0 0 0 0 8.0000000000000071 8.0658840955358091 0.065884095535802364
10 r30_inward 2 0.16812299429621627 ref_tol1e-12_h0.25 0 0 0 -370.18047830118235 2790 1 19831 -0.35873499147611138 -6.5068267720987458e-16 -0.9334394495041628 1.0350983390130453 nan -0.038385896473527227 0.065884094489054593
11 r30_inward 2 0.16812299429621627 ref_tol1e-13_h0.125 0 0 0 -370.1804778092 4857 1 34300 -0.35873507984449715 -6.5068265353608199e-16 -0.93343941554284182 1.035098339013504 nan -0.038385896473972475 0.065884094489054593
12 r30_inward 2 0.16812299429621627 tol1e-11_h2 0 0 0 -370.18048452857704 1299 1 9387 -0.3587338729380895 -6.5068297686427377e-16 -0.93343987937458439 1.0350983390054869 nan -0.038385896466199748 0.065884094489054593
13 r30_inward 2 0.16812299429621627 tol1e-11_h8 0 0 0 -370.1804845285946 1257 1 9100 -0.35873387293809145 -6.5068297686427416e-16 -0.93343987937458361 1.035098339005424 nan -0.038385896466138665 0.065884094489054593
14 r100_inward 0 0.051461896376274734 ref_tol1e-12_h0.25 1 1 4294967295 -211.027405235164 2252 0 16051 0 0 0 0 8.0000000000000266 8.0199767087115159 0.01997670871149013
15 r100_inward 0 0.051461896376274734 ref_tol1e-13_h0.125 1 1 4294967295 -211.02740538583069 3727 0 26376 0 0 0 0 8.0000000000000142 8.0199767087115035 0.01997670871149013
16 r100_inward 0 0.051461896376274734 tol1e-11_h2 1 1 4294967295 -211.02740354768991 1292 0 9331 0 0 0 0 8.0000000000000533 8.0199767087115426 0.01997670871149013
17 r100_inward 0 0.051461896376274734 tol1e-11_h8 1 1 4294967295 -211.02740354768991 1292 0 9331 0 0 0 0 8.0000000000000533 8.0199767087115426 0.01997670871149013
18 r100_inward 1 0.051461996376274736 ref_tol1e-12_h0.25 1 1 4294967295 -301.07740508539132 4045 0 28602 0 0 0 0 8.0000000000000018 8.0199767086106277 0.019976708610626077
19 r100_inward 1 0.051461996376274736 ref_tol1e-13_h0.125 1 1 4294967295 -313.77734009633099 6986 0 49189 0 0 0 0 8.0000000000000018 8.0199767086106277 0.019976708610626077
20 r100_inward 1 0.051461996376274736 tol1e-11_h2 1 1 4294967295 -288.21364790845217 2256 0 16079 0 0 0 0 8.0000000000000639 8.0199767086106899 0.019976708610626077
21 r100_inward 1 0.051461996376274736 tol1e-11_h8 1 1 4294967295 -288.21364790845217 2256 0 16079 0 0 0 0 8.0000000000000639 8.0199767086106899 0.019976708610626077
22 r100_inward 2 0.051462096376274739 ref_tol1e-12_h0.25 0 0 0 -439.31355204311825 2969 0 21077 -0.99110324423183982 -3.1326762563025509e-16 -0.13309530146899301 1.0101525445517785 nan -0.013990814384499865 0.019976708509762027
23 r100_inward 2 0.051462096376274739 ref_tol1e-13_h0.125 0 0 0 -439.31355190087947 5265 0 37149 -0.99110324787406912 -3.132675617927086e-16 -0.13309527434688151 1.010152544552184 nan -0.013990814384902493 0.019976708509762027
24 r100_inward 2 0.051462096376274739 tol1e-11_h2 0 0 0 -439.31355372933643 1291 1 9338 -0.99110320105936889 -3.1326838231562615e-16 -0.13309562295534913 1.010152544545182 nan -0.013990814377948021 0.019976708509762027
25 r100_inward 2 0.051462096376274739 tol1e-11_h8 0 0 0 -439.31355372935258 1250 1 9051 -0.991103201059369 -3.1326838231562231e-16 -0.13309562295534744 1.0101525445451216 nan -0.013990814377887956 0.019976708509762027
26 r2p1_outward 0 0.57037671003940071 ref_tol1e-12_h0.25 0 0 0 -333.45070432558816 2639 1 18774 -0.30976840652656795 0 -0.95081203942629522 4.5825756949539365 nan -1.5261506795942201 -0.42981831573151047
27 r2p1_outward 0 0.57037671003940071 ref_tol1e-13_h0.125 0 0 0 -333.45070506871724 4589 2 32431 -0.30976827139480229 0 -0.95081208345133916 4.5825756949557528 nan -1.5261506795946143 -0.42981831573151047
28 r2p1_outward 0 0.57037671003940071 tol1e-11_h2 0 0 0 -333.45070071486469 1210 2 8778 -0.30976906608911198 0 -0.95081182454483582 4.5825756949192931 nan -1.5261506795866686 -0.42981831573151047
29 r2p1_outward 0 0.57037671003940071 tol1e-11_h8 0 0 0 -333.45070071488124 1169 2 8491 -0.30976906647032454 0 -0.95081182442063883 4.5825756949190151 nan -1.5261506795866076 -0.42981831573151047
30 r2p1_outward 1 0.57037681003940066 ref_tol1e-12_h0.25 0 0 0 -414.94010328784253 4263 2 30149 0.3336576345143959 0 0.94269432104487505 4.5825756949507124 nan -1.5261506795930104 -0.42981805651739968
31 r2p1_outward 1 0.57037681003940066 ref_tol1e-13_h0.125 0 0 0 -427.08073320404128 7560 1 53221 -0.91087431940825703 0 -0.41268386719442363 4.5825756949554943 nan -1.5261506795940556 -0.42981805651739968
32 r2p1_outward 1 0.57037681003940066 tol1e-11_h2 0 0 0 -406.06581257014602 2120 2 15141 0.88820167266587602 0 -0.45945379383953305 4.58257569487448 nan -1.5261506795763404 -0.42981805651739968
33 r2p1_outward 1 0.57037681003940066 tol1e-11_h8 0 0 0 -406.06581257016262 2078 2 14854 0.88817826517728515 0 -0.4594990416384655 4.5825756948742002 nan -1.5261506795762789 -0.42981805651739968
34 r2p1_outward 2 0.57037691003940061 ref_tol1e-12_h0.25 1 1 4294967295 -109.41561097431348 1950 1 13944 0 0 0 0 8.0000000000000195 7.5701822026967047 -0.42981779730331554
35 r2p1_outward 2 0.57037691003940061 ref_tol1e-13_h0.125 1 1 4294967295 -109.41561022532379 3098 1 21980 0 0 0 0 8.000000000000016 7.5701822026967012 -0.42981779730331554
36 r2p1_outward 2 0.57037691003940061 tol1e-11_h2 1 1 4294967295 -109.41561457850582 1228 1 8890 0 0 0 0 8.0000000000000195 7.5701822026967038 -0.42981779730331554
37 r2p1_outward 2 0.57037691003940061 tol1e-11_h8 1 1 4294967295 -109.41561457850582 1228 1 8890 0 0 0 0 8.0000000000000195 7.5701822026967038 -0.42981779730331554
@@ -0,0 +1,96 @@
### building critical_ref_check
+ cc -std=c11 -O2 -Wall -Wextra -Wpedantic -I/home/wyj/Code/C/GR_4d_raytracing/src /home/wyj/Code/C/GR_4d_raytracing/benchmarks/adaptive_step_bounds_2026-10-05/critical_ref_check.c /home/wyj/Code/C/GR_4d_raytracing/src/geodesic.c /home/wyj/Code/C/GR_4d_raytracing/src/asymptotic.c /home/wyj/Code/C/GR_4d_raytracing/src/asymptotic_schwarzschild.c /home/wyj/Code/C/GR_4d_raytracing/src/spacetime_common.c /home/wyj/Code/C/GR_4d_raytracing/src/observer.c -lm -o /tmp/opencode/step_bounds/critical_ref_check
+ echo '### command: critical_ref_check'
### command: critical_ref_check
+ /tmp/opencode/step_bounds/critical_ref_check
RAW camera=r30_inward dir=0 theta=0.16812279429621627 cfg=ref_tol1e-12_h0.25 outcome=1 reason=1 end=4294967295 stop_t=-142.17574796859728 steps=2081 rejected=0 rhs=14854 g=0 thr=8.0000000000000195 L=8.0658840965825682 L0=0.065884096582549498 nx=0 ny=0 nz=0
RAW camera=r30_inward dir=0 theta=0.16812279429621627 cfg=ref_tol1e-13_h0.125 outcome=1 reason=1 end=4294967295 stop_t=-142.17574845681202 steps=3332 rejected=0 rhs=23611 g=0 thr=8.0000000000000249 L=8.0658840965825735 L0=0.065884096582549498 nx=0 ny=0 nz=0
RAW camera=r30_inward dir=0 theta=0.16812279429621627 cfg=tol1e-11_h2 outcome=1 reason=1 end=4294967295 stop_t=-142.17574172483768 steps=1304 rejected=0 rhs=9415 g=0 thr=8.0000000000000266 L=8.0658840965825753 L0=0.065884096582549498 nx=0 ny=0 nz=0
RAW camera=r30_inward dir=0 theta=0.16812279429621627 cfg=tol1e-11_h8 outcome=1 reason=1 end=4294967295 stop_t=-142.17574172483768 steps=1304 rejected=0 rhs=9415 g=0 thr=8.0000000000000266 L=8.0658840965825753 L0=0.065884096582549498 nx=0 ny=0 nz=0
RAW camera=r30_inward dir=1 theta=0.16812289429621627 cfg=ref_tol1e-12_h0.25 outcome=1 reason=1 end=4294967295 stop_t=-225.85293500418322 steps=3746 rejected=0 rhs=26509 g=0 thr=8.0000000000000338 L=8.0658840955358357 L0=0.065884095535802364 nx=0 ny=0 nz=0
RAW camera=r30_inward dir=1 theta=0.16812289429621627 cfg=ref_tol1e-13_h0.125 outcome=1 reason=1 end=4294967295 stop_t=-238.90906331559862 steps=6388 rejected=0 rhs=45003 g=0 thr=8.000000000000016 L=8.065884095535818 L0=0.065884095535802364 nx=0 ny=0 nz=0
RAW camera=r30_inward dir=1 theta=0.16812289429621627 cfg=tol1e-11_h2 outcome=1 reason=1 end=4294967295 stop_t=-212.59582609170599 steps=2180 rejected=0 rhs=15547 g=0 thr=8.0000000000000071 L=8.0658840955358091 L0=0.065884095535802364 nx=0 ny=0 nz=0
RAW camera=r30_inward dir=1 theta=0.16812289429621627 cfg=tol1e-11_h8 outcome=1 reason=1 end=4294967295 stop_t=-212.59582609170599 steps=2180 rejected=0 rhs=15547 g=0 thr=8.0000000000000071 L=8.0658840955358091 L0=0.065884095535802364 nx=0 ny=0 nz=0
RAW camera=r30_inward dir=2 theta=0.16812299429621627 cfg=ref_tol1e-12_h0.25 outcome=0 reason=0 end=0 stop_t=-370.18047830118235 steps=2790 rejected=1 rhs=19831 g=1.0350983390130453 thr=nan L=-0.038385896473527227 L0=0.065884094489054593 nx=-0.35873499147611138 ny=-6.5068267720987458e-16 nz=-0.9334394495041628
RAW camera=r30_inward dir=2 theta=0.16812299429621627 cfg=ref_tol1e-13_h0.125 outcome=0 reason=0 end=0 stop_t=-370.1804778092 steps=4857 rejected=1 rhs=34300 g=1.035098339013504 thr=nan L=-0.038385896473972475 L0=0.065884094489054593 nx=-0.35873507984449715 ny=-6.5068265353608199e-16 nz=-0.93343941554284182
RAW camera=r30_inward dir=2 theta=0.16812299429621627 cfg=tol1e-11_h2 outcome=0 reason=0 end=0 stop_t=-370.18048452857704 steps=1299 rejected=1 rhs=9387 g=1.0350983390054869 thr=nan L=-0.038385896466199748 L0=0.065884094489054593 nx=-0.3587338729380895 ny=-6.5068297686427377e-16 nz=-0.93343987937458439
RAW camera=r30_inward dir=2 theta=0.16812299429621627 cfg=tol1e-11_h8 outcome=0 reason=0 end=0 stop_t=-370.1804845285946 steps=1257 rejected=1 rhs=9100 g=1.035098339005424 thr=nan L=-0.038385896466138665 L0=0.065884094489054593 nx=-0.35873387293809145 ny=-6.5068297686427416e-16 nz=-0.93343987937458361
RAW camera=r100_inward dir=0 theta=0.051461896376274734 cfg=ref_tol1e-12_h0.25 outcome=1 reason=1 end=4294967295 stop_t=-211.027405235164 steps=2252 rejected=0 rhs=16051 g=0 thr=8.0000000000000266 L=8.0199767087115159 L0=0.01997670871149013 nx=0 ny=0 nz=0
RAW camera=r100_inward dir=0 theta=0.051461896376274734 cfg=ref_tol1e-13_h0.125 outcome=1 reason=1 end=4294967295 stop_t=-211.02740538583069 steps=3727 rejected=0 rhs=26376 g=0 thr=8.0000000000000142 L=8.0199767087115035 L0=0.01997670871149013 nx=0 ny=0 nz=0
RAW camera=r100_inward dir=0 theta=0.051461896376274734 cfg=tol1e-11_h2 outcome=1 reason=1 end=4294967295 stop_t=-211.02740354768991 steps=1292 rejected=0 rhs=9331 g=0 thr=8.0000000000000533 L=8.0199767087115426 L0=0.01997670871149013 nx=0 ny=0 nz=0
RAW camera=r100_inward dir=0 theta=0.051461896376274734 cfg=tol1e-11_h8 outcome=1 reason=1 end=4294967295 stop_t=-211.02740354768991 steps=1292 rejected=0 rhs=9331 g=0 thr=8.0000000000000533 L=8.0199767087115426 L0=0.01997670871149013 nx=0 ny=0 nz=0
RAW camera=r100_inward dir=1 theta=0.051461996376274736 cfg=ref_tol1e-12_h0.25 outcome=1 reason=1 end=4294967295 stop_t=-301.07740508539132 steps=4045 rejected=0 rhs=28602 g=0 thr=8.0000000000000018 L=8.0199767086106277 L0=0.019976708610626077 nx=0 ny=0 nz=0
RAW camera=r100_inward dir=1 theta=0.051461996376274736 cfg=ref_tol1e-13_h0.125 outcome=1 reason=1 end=4294967295 stop_t=-313.77734009633099 steps=6986 rejected=0 rhs=49189 g=0 thr=8.0000000000000018 L=8.0199767086106277 L0=0.019976708610626077 nx=0 ny=0 nz=0
RAW camera=r100_inward dir=1 theta=0.051461996376274736 cfg=tol1e-11_h2 outcome=1 reason=1 end=4294967295 stop_t=-288.21364790845217 steps=2256 rejected=0 rhs=16079 g=0 thr=8.0000000000000639 L=8.0199767086106899 L0=0.019976708610626077 nx=0 ny=0 nz=0
RAW camera=r100_inward dir=1 theta=0.051461996376274736 cfg=tol1e-11_h8 outcome=1 reason=1 end=4294967295 stop_t=-288.21364790845217 steps=2256 rejected=0 rhs=16079 g=0 thr=8.0000000000000639 L=8.0199767086106899 L0=0.019976708610626077 nx=0 ny=0 nz=0
RAW camera=r100_inward dir=2 theta=0.051462096376274739 cfg=ref_tol1e-12_h0.25 outcome=0 reason=0 end=0 stop_t=-439.31355204311825 steps=2969 rejected=0 rhs=21077 g=1.0101525445517785 thr=nan L=-0.013990814384499865 L0=0.019976708509762027 nx=-0.99110324423183982 ny=-3.1326762563025509e-16 nz=-0.13309530146899301
RAW camera=r100_inward dir=2 theta=0.051462096376274739 cfg=ref_tol1e-13_h0.125 outcome=0 reason=0 end=0 stop_t=-439.31355190087947 steps=5265 rejected=0 rhs=37149 g=1.010152544552184 thr=nan L=-0.013990814384902493 L0=0.019976708509762027 nx=-0.99110324787406912 ny=-3.132675617927086e-16 nz=-0.13309527434688151
RAW camera=r100_inward dir=2 theta=0.051462096376274739 cfg=tol1e-11_h2 outcome=0 reason=0 end=0 stop_t=-439.31355372933643 steps=1291 rejected=1 rhs=9338 g=1.010152544545182 thr=nan L=-0.013990814377948021 L0=0.019976708509762027 nx=-0.99110320105936889 ny=-3.1326838231562615e-16 nz=-0.13309562295534913
RAW camera=r100_inward dir=2 theta=0.051462096376274739 cfg=tol1e-11_h8 outcome=0 reason=0 end=0 stop_t=-439.31355372935258 steps=1250 rejected=1 rhs=9051 g=1.0101525445451216 thr=nan L=-0.013990814377887956 L0=0.019976708509762027 nx=-0.991103201059369 ny=-3.1326838231562231e-16 nz=-0.13309562295534744
RAW camera=r2p1_outward dir=0 theta=0.57037671003940071 cfg=ref_tol1e-12_h0.25 outcome=0 reason=0 end=0 stop_t=-333.45070432558816 steps=2639 rejected=1 rhs=18774 g=4.5825756949539365 thr=nan L=-1.5261506795942201 L0=-0.42981831573151047 nx=-0.30976840652656795 ny=0 nz=-0.95081203942629522
RAW camera=r2p1_outward dir=0 theta=0.57037671003940071 cfg=ref_tol1e-13_h0.125 outcome=0 reason=0 end=0 stop_t=-333.45070506871724 steps=4589 rejected=2 rhs=32431 g=4.5825756949557528 thr=nan L=-1.5261506795946143 L0=-0.42981831573151047 nx=-0.30976827139480229 ny=0 nz=-0.95081208345133916
RAW camera=r2p1_outward dir=0 theta=0.57037671003940071 cfg=tol1e-11_h2 outcome=0 reason=0 end=0 stop_t=-333.45070071486469 steps=1210 rejected=2 rhs=8778 g=4.5825756949192931 thr=nan L=-1.5261506795866686 L0=-0.42981831573151047 nx=-0.30976906608911198 ny=0 nz=-0.95081182454483582
RAW camera=r2p1_outward dir=0 theta=0.57037671003940071 cfg=tol1e-11_h8 outcome=0 reason=0 end=0 stop_t=-333.45070071488124 steps=1169 rejected=2 rhs=8491 g=4.5825756949190151 thr=nan L=-1.5261506795866076 L0=-0.42981831573151047 nx=-0.30976906647032454 ny=0 nz=-0.95081182442063883
RAW camera=r2p1_outward dir=1 theta=0.57037681003940066 cfg=ref_tol1e-12_h0.25 outcome=0 reason=0 end=0 stop_t=-414.94010328784253 steps=4263 rejected=2 rhs=30149 g=4.5825756949507124 thr=nan L=-1.5261506795930104 L0=-0.42981805651739968 nx=0.3336576345143959 ny=0 nz=0.94269432104487505
RAW camera=r2p1_outward dir=1 theta=0.57037681003940066 cfg=ref_tol1e-13_h0.125 outcome=0 reason=0 end=0 stop_t=-427.08073320404128 steps=7560 rejected=1 rhs=53221 g=4.5825756949554943 thr=nan L=-1.5261506795940556 L0=-0.42981805651739968 nx=-0.91087431940825703 ny=0 nz=-0.41268386719442363
RAW camera=r2p1_outward dir=1 theta=0.57037681003940066 cfg=tol1e-11_h2 outcome=0 reason=0 end=0 stop_t=-406.06581257014602 steps=2120 rejected=2 rhs=15141 g=4.58257569487448 thr=nan L=-1.5261506795763404 L0=-0.42981805651739968 nx=0.88820167266587602 ny=0 nz=-0.45945379383953305
RAW camera=r2p1_outward dir=1 theta=0.57037681003940066 cfg=tol1e-11_h8 outcome=0 reason=0 end=0 stop_t=-406.06581257016262 steps=2078 rejected=2 rhs=14854 g=4.5825756948742002 thr=nan L=-1.5261506795762789 L0=-0.42981805651739968 nx=0.88817826517728515 ny=0 nz=-0.4594990416384655
RAW camera=r2p1_outward dir=2 theta=0.57037691003940061 cfg=ref_tol1e-12_h0.25 outcome=1 reason=1 end=4294967295 stop_t=-109.41561097431348 steps=1950 rejected=1 rhs=13944 g=0 thr=8.0000000000000195 L=7.5701822026967047 L0=-0.42981779730331554 nx=0 ny=0 nz=0
RAW camera=r2p1_outward dir=2 theta=0.57037691003940061 cfg=ref_tol1e-13_h0.125 outcome=1 reason=1 end=4294967295 stop_t=-109.41561022532379 steps=3098 rejected=1 rhs=21980 g=0 thr=8.000000000000016 L=7.5701822026967012 L0=-0.42981779730331554 nx=0 ny=0 nz=0
RAW camera=r2p1_outward dir=2 theta=0.57037691003940061 cfg=tol1e-11_h2 outcome=1 reason=1 end=4294967295 stop_t=-109.41561457850582 steps=1228 rejected=1 rhs=8890 g=0 thr=8.0000000000000195 L=7.5701822026967038 L0=-0.42981779730331554 nx=0 ny=0 nz=0
RAW camera=r2p1_outward dir=2 theta=0.57037691003940061 cfg=tol1e-11_h8 outcome=1 reason=1 end=4294967295 stop_t=-109.41561457850582 steps=1228 rejected=1 rhs=8890 g=0 thr=8.0000000000000195 L=7.5701822026967038 L0=-0.42981779730331554 nx=0 ny=0 nz=0
CAPAIR camera=r30_inward dir=0 theta=0.16812279429621627 pair=ref_vs_tighter class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=4.8821473797033832e-07 dthr=-5.3290705182007514e-15 end=4294967295/4294967295
CAPAIR camera=r30_inward dir=0 theta=0.16812279429621627 pair=h2_vs_h8 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=0 dthr=0 end=4294967295/4294967295
CAPAIR camera=r30_inward dir=0 theta=0.16812279429621627 pair=ref_vs_h2 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=-6.2437596000108897e-06 dthr=-7.1054273576010019e-15 end=4294967295/4294967295
CAPAIR camera=r30_inward dir=0 theta=0.16812279429621627 pair=ref_vs_h8 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=-6.2437596000108897e-06 dthr=-7.1054273576010019e-15 end=4294967295/4294967295
CAPAIR camera=r30_inward dir=1 theta=0.16812289429621627 pair=ref_vs_tighter class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=13.056128311415392 dthr=1.7763568394002505e-14 end=4294967295/4294967295
CAPAIR camera=r30_inward dir=1 theta=0.16812289429621627 pair=h2_vs_h8 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=0 dthr=0 end=4294967295/4294967295
CAPAIR camera=r30_inward dir=1 theta=0.16812289429621627 pair=ref_vs_h2 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=-13.257108912477236 dthr=2.6645352591003757e-14 end=4294967295/4294967295
CAPAIR camera=r30_inward dir=1 theta=0.16812289429621627 pair=ref_vs_h8 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=-13.257108912477236 dthr=2.6645352591003757e-14 end=4294967295/4294967295
CAPAIR camera=r30_inward dir=2 theta=0.16812299429621627 pair=ref_vs_tighter class=ESC/ESC same=1 dn_ang=9.4669651562462453e-08 dgrel=4.4320103143036249e-13 dstop_t=-4.9198234819414211e-07 dthr=nan end=0/0
CAPAIR camera=r30_inward dir=2 theta=0.16812299429621627 pair=h2_vs_h8 class=ESC/ESC same=1 dn_ang=2.0539125955565396e-15 dgrel=6.0618177144533547e-14 dstop_t=1.7564616427989677e-11 dthr=nan end=0/0
CAPAIR camera=r30_inward dir=2 theta=0.16812299429621627 pair=ref_vs_h2 class=ESC/ESC same=1 dn_ang=1.198297077398556e-06 dgrel=7.3021588775645796e-12 dstop_t=6.2273946923596668e-06 dthr=nan end=0/0
CAPAIR camera=r30_inward dir=2 theta=0.16812299429621627 pair=ref_vs_h8 class=ESC/ESC same=1 dn_ang=1.1982970752891323e-06 dgrel=7.3627770547091131e-12 dstop_t=6.2274122569760948e-06 dthr=nan end=0/0
CAPAIR camera=r100_inward dir=0 theta=0.051461896376274734 pair=ref_vs_tighter class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=1.5066669334373728e-07 dthr=1.2434497875801753e-14 end=4294967295/4294967295
CAPAIR camera=r100_inward dir=0 theta=0.051461896376274734 pair=h2_vs_h8 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=0 dthr=0 end=4294967295/4294967295
CAPAIR camera=r100_inward dir=0 theta=0.051461896376274734 pair=ref_vs_h2 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=-1.6874740822458989e-06 dthr=-2.6645352591003757e-14 end=4294967295/4294967295
CAPAIR camera=r100_inward dir=0 theta=0.051461896376274734 pair=ref_vs_h8 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=-1.6874740822458989e-06 dthr=-2.6645352591003757e-14 end=4294967295/4294967295
CAPAIR camera=r100_inward dir=1 theta=0.051461996376274736 pair=ref_vs_tighter class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=12.699935010939669 dthr=0 end=4294967295/4294967295
CAPAIR camera=r100_inward dir=1 theta=0.051461996376274736 pair=h2_vs_h8 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=0 dthr=0 end=4294967295/4294967295
CAPAIR camera=r100_inward dir=1 theta=0.051461996376274736 pair=ref_vs_h2 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=-12.863757176939146 dthr=-6.2172489379008766e-14 end=4294967295/4294967295
CAPAIR camera=r100_inward dir=1 theta=0.051461996376274736 pair=ref_vs_h8 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=-12.863757176939146 dthr=-6.2172489379008766e-14 end=4294967295/4294967295
CAPAIR camera=r100_inward dir=2 theta=0.051462096376274739 pair=ref_vs_tighter class=ESC/ESC same=1 dn_ang=2.7365576305005138e-08 dgrel=4.0134562340199409e-13 dstop_t=-1.4223877542463015e-07 dthr=nan end=0/0
CAPAIR camera=r100_inward dir=2 theta=0.051462096376274739 pair=h2_vs_h8 class=ESC/ESC same=1 dn_ang=1.6930901125533633e-15 dgrel=5.9729998724833422e-14 dstop_t=1.6143530956469476e-11 dthr=nan end=0/0
CAPAIR camera=r100_inward dir=2 theta=0.051462096376274739 pair=ref_vs_h2 class=ESC/ESC same=1 dn_ang=3.243722235535319e-07 dgrel=6.5301097862402457e-12 dstop_t=1.6862181837495882e-06 dthr=nan end=0/0
CAPAIR camera=r100_inward dir=2 theta=0.051462096376274739 pair=ref_vs_h8 class=ESC/ESC same=1 dn_ang=3.2437222186044184e-07 dgrel=6.5900618295700042e-12 dstop_t=1.6862343272805447e-06 dthr=nan end=0/0
CAPAIR camera=r2p1_outward dir=0 theta=0.57037671003940071 pair=ref_vs_tighter class=ESC/ESC same=1 dn_ang=1.4212247739786624e-07 dgrel=3.9634961979118088e-13 dstop_t=7.4312907827334129e-07 dthr=nan end=0/0
CAPAIR camera=r2p1_outward dir=0 theta=0.57037671003940071 pair=h2_vs_h8 class=ESC/ESC same=1 dn_ang=4.0093378617100928e-10 dgrel=6.0618177144533547e-14 dstop_t=1.6541434888495132e-11 dthr=nan end=0/0
CAPAIR camera=r2p1_outward dir=0 theta=0.57037671003940071 pair=ref_vs_h2 class=ESC/ESC same=1 dn_ang=6.9368349486792499e-07 dgrel=7.5597306192776159e-12 dstop_t=-3.6107234677729139e-06 dthr=nan end=0/0
CAPAIR camera=r2p1_outward dir=0 theta=0.57037671003940071 pair=ref_vs_h8 class=ESC/ESC same=1 dn_ang=6.9408442870960736e-07 dgrel=7.6205708410270745e-12 dstop_t=-3.6107069263380254e-06 dthr=nan end=0/0
CAPAIR camera=r2p1_outward dir=1 theta=0.57037681003940066 pair=ref_vs_tighter class=ESC/ESC same=1 dn_ang=2.3363758065620761 dgrel=1.0434986208451846e-12 dstop_t=12.140629916198748 dthr=nan end=0/0
CAPAIR camera=r2p1_outward dir=1 theta=0.57037681003940066 pair=h2_vs_h8 class=ESC/ESC same=1 dn_ang=5.0943830155578499e-05 dgrel=6.106226635438361e-14 dstop_t=1.659827830735594e-11 dthr=nan end=0/0
CAPAIR camera=r2p1_outward dir=1 theta=0.57037681003940066 pair=ref_vs_h2 class=ESC/ESC same=1 dn_ang=1.7079955663413957 dgrel=1.6635359756378421e-11 dstop_t=-8.8742907176965105 dthr=nan end=0/0
CAPAIR camera=r2p1_outward dir=1 theta=0.57037681003940066 pair=ref_vs_h8 class=ESC/ESC same=1 dn_ang=1.7080465101715514 dgrel=1.6696422022732804e-11 dstop_t=-8.8742907176799122 dthr=nan end=0/0
CAPAIR camera=r2p1_outward dir=2 theta=0.57037691003940061 pair=ref_vs_tighter class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=-7.4898969160130946e-07 dthr=3.5527136788005009e-15 end=4294967295/4294967295
CAPAIR camera=r2p1_outward dir=2 theta=0.57037691003940061 pair=h2_vs_h8 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=0 dthr=0 end=4294967295/4294967295
CAPAIR camera=r2p1_outward dir=2 theta=0.57037691003940061 pair=ref_vs_h2 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=3.6041923436869183e-06 dthr=0 end=4294967295/4294967295
CAPAIR camera=r2p1_outward dir=2 theta=0.57037691003940061 pair=ref_vs_h8 class=DARK/DARK same=1 dn_ang=nan dgrel=nan dstop_t=3.6041923436869183e-06 dthr=0 end=4294967295/4294967295
EXACT_CRITICAL_DIRECTION camera=r30_inward theta=0.16812289429621627 ref0=DARK/1/4294967295 ref1=DARK/1/4294967295 same=1 dn_ang=nan dstop_t=13.056128311415392
EXACT_CRITICAL_REFERENCE_NOT_CONVERGED camera=r30_inward dn_ang=nan dstop_t=13.056128311415392 class_same=1
EXACT_CRITICAL_DIRECTION camera=r100_inward theta=0.051461996376274736 ref0=DARK/1/4294967295 ref1=DARK/1/4294967295 same=1 dn_ang=nan dstop_t=12.699935010939669
EXACT_CRITICAL_REFERENCE_NOT_CONVERGED camera=r100_inward dn_ang=nan dstop_t=12.699935010939669 class_same=1
EXACT_CRITICAL_DIRECTION camera=r2p1_outward theta=0.57037681003940066 ref0=ESC/0/0 ref1=ESC/0/0 same=1 dn_ang=2.3363758065620761 dstop_t=12.140629916198748
EXACT_CRITICAL_REFERENCE_NOT_CONVERGED camera=r2p1_outward dn_ang=2.3363758065620761 dstop_t=12.140629916198748 class_same=1
NEIGHBOR_REFERENCE_CHECK camera=r30_inward dir=0 theta=0.16812279429621627 ref_class0=DARK/1/4294967295 ref_class1=DARK/1/4294967295 same=1 clean=1 stepped=1 dn_ang=nan dgrel=nan dstop_t=4.8821473797033832e-07
NEIGHBOR_REFERENCE_CHECK camera=r30_inward dir=2 theta=0.16812299429621627 ref_class0=ESC/0/0 ref_class1=ESC/0/0 same=1 clean=1 stepped=1 dn_ang=9.4669651562462453e-08 dgrel=4.4320103143036249e-13 dstop_t=-4.9198234819414211e-07
NEIGHBOR_REFERENCE_CHECK camera=r100_inward dir=0 theta=0.051461896376274734 ref_class0=DARK/1/4294967295 ref_class1=DARK/1/4294967295 same=1 clean=1 stepped=1 dn_ang=nan dgrel=nan dstop_t=1.5066669334373728e-07
NEIGHBOR_REFERENCE_CHECK camera=r100_inward dir=2 theta=0.051462096376274739 ref_class0=ESC/0/0 ref_class1=ESC/0/0 same=1 clean=1 stepped=1 dn_ang=2.7365576305005138e-08 dgrel=4.0134562340199409e-13 dstop_t=-1.4223877542463015e-07
NEIGHBOR_REFERENCE_CHECK camera=r2p1_outward dir=0 theta=0.57037671003940071 ref_class0=ESC/0/0 ref_class1=ESC/0/0 same=1 clean=1 stepped=1 dn_ang=1.4212247739786624e-07 dgrel=3.9634961979118088e-13 dstop_t=7.4312907827334129e-07
NEIGHBOR_REFERENCE_CHECK camera=r2p1_outward dir=2 theta=0.57037691003940061 ref_class0=DARK/1/4294967295 ref_class1=DARK/1/4294967295 same=1 clean=1 stepped=1 dn_ang=nan dgrel=nan dstop_t=-7.4898969160130946e-07
CRITICAL_REFERENCE_CHECK status=0
CRITICAL_REF_DONE
real 0m0.418s
user 0m0.402s
sys 0m0.016s
+ set +x
@@ -0,0 +1,271 @@
### command: a_public schwarzschild
+ /tmp/opencode/step_bounds/a_public schwarzschild
REF r30_inward dir=0 theta=0 outcome=1 reason=1 stop_t=-69.5728394 steps=531 rejected=0 rhs=4004 g=0 thr=8
REF r30_inward dir=1 theta=3.14159 outcome=0 reason=0 stop_t=-226 steps=905 rejected=0 rhs=6629 g=1.03509833901 thr=nan
REF r30_inward dir=2 theta=1.5708 outcome=0 reason=0 stop_t=-256.70592 steps=1028 rejected=0 rhs=7490 g=1.03509833901 thr=nan
REF r30_inward dir=3 theta=0.168123 outcome=1 reason=1 stop_t=-225.852935 steps=3746 rejected=0 rhs=26509 g=0 thr=8
REF r30_inward dir=4 theta=0.167123 outcome=1 reason=1 stop_t=-94.2779103 steps=1127 rejected=0 rhs=8176 g=0 thr=8
REF r30_inward dir=5 theta=0.169123 outcome=0 reason=0 stop_t=-322.366447 steps=1834 rejected=0 rhs=13132 g=1.03509833901 thr=nan
REF r30_inward dir=6 theta=0.168113 outcome=1 reason=1 stop_t=-118.245807 steps=1606 rejected=0 rhs=11529 g=0 thr=8
REF r30_inward dir=7 theta=0.168133 outcome=0 reason=0 stop_t=-346.252134 steps=2312 rejected=1 rhs=16485 g=1.03509833901 thr=nan
REF r30_inward dir=8 theta=0.168123 outcome=1 reason=1 stop_t=-142.175748 steps=2081 rejected=0 rhs=14854 g=0 thr=8
REF r30_inward dir=9 theta=0.168123 outcome=0 reason=0 stop_t=-370.180478 steps=2790 rejected=1 rhs=19831 g=1.03509833901 thr=nan
REF r30_inward dir=10 theta=0.218123 outcome=0 reason=0 stop_t=-302.584521 steps=1409 rejected=1 rhs=10164 g=1.03509833901 thr=nan
REF r30_inward dir=11 theta=0.118123 outcome=1 reason=1 stop_t=-73.7563113 steps=665 rejected=0 rhs=4942 g=0 thr=8
REF r100_inward dir=0 theta=0 outcome=1 reason=1 stop_t=-144.299101 steps=830 rejected=0 rhs=6097 g=0 thr=8
REF r100_inward dir=1 theta=3.14159 outcome=0 reason=0 stop_t=-156 steps=625 rejected=0 rhs=4669 g=1.01015254455 thr=nan
REF r100_inward dir=2 theta=1.5708 outcome=0 reason=0 stop_t=-237.678408 steps=952 rejected=0 rhs=6958 g=1.01015254455 thr=nan
REF r100_inward dir=3 theta=0.051462 outcome=1 reason=1 stop_t=-301.077405 steps=4045 rejected=0 rhs=28602 g=0 thr=8
REF r100_inward dir=4 theta=0.050462 outcome=1 reason=1 stop_t=-163.061409 steps=1292 rejected=0 rhs=9331 g=0 thr=8
REF r100_inward dir=5 theta=0.052462 outcome=0 reason=0 stop_t=-391.57927 steps=2014 rejected=1 rhs=14399 g=1.01015254455 thr=nan
REF r100_inward dir=6 theta=0.051452 outcome=1 reason=1 stop_t=-187.095903 steps=1773 rejected=0 rhs=12698 g=0 thr=8
REF r100_inward dir=7 theta=0.051472 outcome=0 reason=0 stop_t=-415.386875 steps=2493 rejected=0 rhs=17745 g=1.01015254455 thr=nan
REF r100_inward dir=8 theta=0.0514619 outcome=1 reason=1 stop_t=-211.027405 steps=2252 rejected=0 rhs=16051 g=0 thr=8
REF r100_inward dir=9 theta=0.0514621 outcome=0 reason=0 stop_t=-439.313552 steps=2969 rejected=0 rhs=21077 g=1.01015254455 thr=nan
REF r100_inward dir=10 theta=0.101462 outcome=0 reason=0 stop_t=-372.348056 steps=1571 rejected=0 rhs=11291 g=1.01015254455 thr=nan
REF r100_inward dir=11 theta=0.001462 outcome=1 reason=1 stop_t=-144.304446 steps=830 rejected=0 rhs=6097 g=0 thr=8
REF r2p1_outward dir=0 theta=0 outcome=0 reason=0 stop_t=-253.9 steps=1120 rejected=1 rhs=8141 g=4.58257569496 thr=nan
REF r2p1_outward dir=1 theta=3.14159 outcome=1 reason=1 stop_t=-32.2468847 steps=186 rejected=0 rhs=1589 g=0 thr=8
REF r2p1_outward dir=2 theta=1.5708 outcome=1 reason=1 stop_t=-32.7725901 steps=316 rejected=1 rhs=2506 g=0 thr=8
REF r2p1_outward dir=3 theta=0.570377 outcome=0 reason=0 stop_t=-414.940103 steps=4263 rejected=2 rhs=30149 g=4.58257569495 thr=nan
REF r2p1_outward dir=4 theta=0.569377 outcome=0 reason=0 stop_t=-285.577403 steps=1686 rejected=1 rhs=12103 g=4.58257569496 thr=nan
REF r2p1_outward dir=5 theta=0.571377 outcome=1 reason=1 stop_t=-61.5813727 steps=995 rejected=1 rhs=7259 g=0 thr=8
REF r2p1_outward dir=6 theta=0.570367 outcome=0 reason=0 stop_t=-309.521289 steps=2162 rejected=2 rhs=15442 g=4.58257569495 thr=nan
REF r2p1_outward dir=7 theta=0.570387 outcome=1 reason=1 stop_t=-85.4867841 steps=1474 rejected=1 rhs=10612 g=0 thr=8
REF r2p1_outward dir=8 theta=0.570377 outcome=0 reason=0 stop_t=-333.450704 steps=2639 rejected=1 rhs=18774 g=4.58257569495 thr=nan
REF r2p1_outward dir=9 theta=0.570377 outcome=1 reason=1 stop_t=-109.415611 steps=1950 rejected=1 rhs=13944 g=0 thr=8
REF r2p1_outward dir=10 theta=0.620377 outcome=1 reason=1 stop_t=-41.9883684 steps=585 rejected=1 rhs=4389 g=0 thr=8
REF r2p1_outward dir=11 theta=0.520377 outcome=0 reason=0 stop_t=-264.96182 steps=1303 rejected=2 rhs=9429 g=4.58257569496 thr=nan
REF r1p5_freefall dir=0 theta=0 outcome=1 reason=1 stop_t=-30.6580907 steps=231 rejected=1 rhs=1911 g=0 thr=8
REF r1p5_freefall dir=1 theta=1.5708 outcome=0 reason=0 stop_t=-255.249558 steps=1130 rejected=1 rhs=8211 g=1 thr=nan
REF r1p5_freefall dir=2 theta=3.14159 outcome=0 reason=0 stop_t=-254.5 steps=1143 rejected=1 rhs=8302 g=0.464101615138 thr=nan
REF r1p5_freefall dir=3 theta=2.35619 outcome=0 reason=0 stop_t=-254.612997 steps=1127 rejected=1 rhs=8190 g=0.550510257217 thr=nan
SUMMARY upper r30_inward upper=0.25 min=1e-12 n=12 mismatch=0 max_dn_ang=8.66705e-05 max_dgrel=2.2616e-09 max_dstopT=35.0811 sum_rhs=76510 esc=6 dark=6 unres=0 inc=0 wall=0.041s
SUMMARY upper r30_inward upper=0.5 min=1e-12 n=12 mismatch=0 max_dn_ang=0.00018656 max_dgrel=2.26833e-09 max_dstopT=39.0614 sum_rhs=49063 esc=6 dark=6 unres=0 inc=0 wall=0.027s
SUMMARY upper r30_inward upper=1 min=1e-12 n=12 mismatch=0 max_dn_ang=0.000280143 max_dgrel=2.2658e-09 max_dstopT=41.1726 sum_rhs=36974 esc=6 dark=6 unres=0 inc=0 wall=0.020s
SUMMARY upper r30_inward upper=2 min=1e-12 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.24374e-09 max_dstopT=41.2044 sum_rhs=31843 esc=6 dark=6 unres=0 inc=0 wall=0.017s
SUMMARY upper r30_inward upper=4 min=1e-12 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.24952e-09 max_dstopT=41.2044 sum_rhs=29617 esc=6 dark=6 unres=0 inc=0 wall=0.016s
SUMMARY upper r30_inward upper=8 min=1e-12 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.25767e-09 max_dstopT=41.2044 sum_rhs=28714 esc=6 dark=6 unres=0 inc=0 wall=0.016s
SUMMARY upper r30_inward upper=16 min=1e-12 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.26556e-09 max_dstopT=41.2044 sum_rhs=28455 esc=6 dark=6 unres=0 inc=0 wall=0.015s
SUMMARY upper r30_inward upper=32 min=1e-12 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.26605e-09 max_dstopT=41.2044 sum_rhs=28434 esc=6 dark=6 unres=0 inc=0 wall=0.015s
SUMMARY upper r30_inward upper=64 min=1e-12 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.26605e-09 max_dstopT=41.2044 sum_rhs=28434 esc=6 dark=6 unres=0 inc=0 wall=0.015s
SUMMARY upper r100_inward upper=0.25 min=1e-12 n=12 mismatch=0 max_dn_ang=2.60363e-05 max_dgrel=2.04765e-09 max_dstopT=35.2041 sum_rhs=93464 esc=6 dark=6 unres=0 inc=0 wall=0.050s
SUMMARY upper r100_inward upper=0.5 min=1e-12 n=12 mismatch=0 max_dn_ang=5.69583e-05 max_dgrel=2.05623e-09 max_dstopT=39.2691 sum_rhs=56595 esc=6 dark=6 unres=0 inc=0 wall=0.030s
SUMMARY upper r100_inward upper=1 min=1e-12 n=12 mismatch=0 max_dn_ang=8.34774e-05 max_dgrel=2.05088e-09 max_dstopT=41.2546 sum_rhs=39788 esc=6 dark=6 unres=0 inc=0 wall=0.022s
SUMMARY upper r100_inward upper=2 min=1e-12 n=12 mismatch=0 max_dn_ang=8.28604e-05 max_dgrel=2.0262e-09 max_dstopT=41.2161 sum_rhs=32389 esc=6 dark=6 unres=0 inc=0 wall=0.018s
SUMMARY upper r100_inward upper=4 min=1e-12 n=12 mismatch=0 max_dn_ang=8.08898e-05 max_dgrel=2.02847e-09 max_dstopT=41.091 sum_rhs=29393 esc=6 dark=6 unres=0 inc=0 wall=0.016s
SUMMARY upper r100_inward upper=8 min=1e-12 n=12 mismatch=0 max_dn_ang=8.03365e-05 max_dgrel=2.03431e-09 max_dstopT=41.0553 sum_rhs=28427 esc=6 dark=6 unres=0 inc=0 wall=0.015s
SUMMARY upper r100_inward upper=16 min=1e-12 n=12 mismatch=0 max_dn_ang=8.03365e-05 max_dgrel=2.04195e-09 max_dstopT=41.0553 sum_rhs=28189 esc=6 dark=6 unres=0 inc=0 wall=0.015s
SUMMARY upper r100_inward upper=32 min=1e-12 n=12 mismatch=0 max_dn_ang=8.03365e-05 max_dgrel=2.04297e-09 max_dstopT=41.0553 sum_rhs=28182 esc=6 dark=6 unres=0 inc=0 wall=0.015s
SUMMARY upper r100_inward upper=64 min=1e-12 n=12 mismatch=0 max_dn_ang=8.03365e-05 max_dgrel=2.04297e-09 max_dstopT=41.0553 sum_rhs=28182 esc=6 dark=6 unres=0 inc=0 wall=0.015s
SUMMARY upper r2p1_outward upper=0.25 min=1e-12 n=12 mismatch=1 max_dn_ang=0.000287223 max_dgrel=2.66126e-09 max_dstopT=263.145 sum_rhs=62258 esc=5 dark=7 unres=0 inc=0 wall=0.034s
SUMMARY upper r2p1_outward upper=0.5 min=1e-12 n=12 mismatch=1 max_dn_ang=0.000287221 max_dgrel=2.64443e-09 max_dstopT=263.145 sum_rhs=42490 esc=5 dark=7 unres=0 inc=0 wall=0.023s
SUMMARY upper r2p1_outward upper=1 min=1e-12 n=12 mismatch=1 max_dn_ang=0.000287221 max_dgrel=2.61531e-09 max_dstopT=263.145 sum_rhs=33544 esc=5 dark=7 unres=0 inc=0 wall=0.019s
SUMMARY upper r2p1_outward upper=2 min=1e-12 n=12 mismatch=1 max_dn_ang=0.000287222 max_dgrel=2.59243e-09 max_dstopT=263.145 sum_rhs=29421 esc=5 dark=7 unres=0 inc=0 wall=0.016s
SUMMARY upper r2p1_outward upper=4 min=1e-12 n=12 mismatch=1 max_dn_ang=0.000287221 max_dgrel=2.59774e-09 max_dstopT=263.145 sum_rhs=27580 esc=5 dark=7 unres=0 inc=0 wall=0.015s
SUMMARY upper r2p1_outward upper=8 min=1e-12 n=12 mismatch=1 max_dn_ang=0.000287221 max_dgrel=2.60591e-09 max_dstopT=263.145 sum_rhs=26831 esc=5 dark=7 unres=0 inc=0 wall=0.015s
SUMMARY upper r2p1_outward upper=16 min=1e-12 n=12 mismatch=1 max_dn_ang=0.000287223 max_dgrel=2.61374e-09 max_dstopT=263.145 sum_rhs=26614 esc=5 dark=7 unres=0 inc=0 wall=0.015s
SUMMARY upper r2p1_outward upper=32 min=1e-12 n=12 mismatch=1 max_dn_ang=0.000287221 max_dgrel=2.6148e-09 max_dstopT=263.145 sum_rhs=26607 esc=5 dark=7 unres=0 inc=0 wall=0.015s
SUMMARY upper r2p1_outward upper=64 min=1e-12 n=12 mismatch=1 max_dn_ang=0.000287221 max_dgrel=2.6148e-09 max_dstopT=263.145 sum_rhs=26607 esc=5 dark=7 unres=0 inc=0 wall=0.015s
SUMMARY upper r1p5_freefall upper=0.25 min=1e-12 n=4 mismatch=0 max_dn_ang=1.00222e-10 max_dgrel=3.16466e-10 max_dstopT=2.6078e-08 sum_rhs=23597 esc=3 dark=1 unres=0 inc=0 wall=0.014s
SUMMARY upper r1p5_freefall upper=0.5 min=1e-12 n=4 mismatch=0 max_dn_ang=8.87544e-11 max_dgrel=3.65847e-10 max_dstopT=3.08873e-08 sum_rhs=12747 esc=3 dark=1 unres=0 inc=0 wall=0.008s
SUMMARY upper r1p5_freefall upper=1 min=1e-12 n=4 mismatch=0 max_dn_ang=8.18746e-11 max_dgrel=3.81078e-10 max_dstopT=2.81655e-08 sum_rhs=7553 esc=3 dark=1 unres=0 inc=0 wall=0.005s
SUMMARY upper r1p5_freefall upper=2 min=1e-12 n=4 mismatch=0 max_dn_ang=8.165e-11 max_dgrel=3.7195e-10 max_dstopT=2.57141e-08 sum_rhs=5131 esc=3 dark=1 unres=0 inc=0 wall=0.003s
SUMMARY upper r1p5_freefall upper=4 min=1e-12 n=4 mismatch=0 max_dn_ang=8.1566e-11 max_dgrel=3.6318e-10 max_dstopT=2.34608e-08 sum_rhs=4039 esc=3 dark=1 unres=0 inc=0 wall=0.002s
SUMMARY upper r1p5_freefall upper=8 min=1e-12 n=4 mismatch=0 max_dn_ang=8.15091e-11 max_dgrel=3.54659e-10 max_dstopT=2.12507e-08 sum_rhs=3591 esc=3 dark=1 unres=0 inc=0 wall=0.002s
SUMMARY upper r1p5_freefall upper=16 min=1e-12 n=4 mismatch=0 max_dn_ang=8.14607e-11 max_dgrel=3.46852e-10 max_dstopT=1.92511e-08 sum_rhs=3465 esc=3 dark=1 unres=0 inc=0 wall=0.002s
SUMMARY upper r1p5_freefall upper=32 min=1e-12 n=4 mismatch=0 max_dn_ang=8.1455e-11 max_dgrel=3.45979e-10 max_dstopT=1.94793e-08 sum_rhs=3458 esc=3 dark=1 unres=0 inc=0 wall=0.002s
SUMMARY upper r1p5_freefall upper=64 min=1e-12 n=4 mismatch=0 max_dn_ang=8.1455e-11 max_dgrel=3.45979e-10 max_dstopT=1.94793e-08 sum_rhs=3458 esc=3 dark=1 unres=0 inc=0 wall=0.002s
SUMMARY min r30_inward upper=2 min=0.1 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.24374e-09 max_dstopT=41.2044 sum_rhs=31843 esc=6 dark=6 unres=0 inc=0 wall=0.017s
SUMMARY min r30_inward upper=2 min=0.01 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.24374e-09 max_dstopT=41.2044 sum_rhs=31843 esc=6 dark=6 unres=0 inc=0 wall=0.017s
SUMMARY min r30_inward upper=2 min=0.001 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.24374e-09 max_dstopT=41.2044 sum_rhs=31843 esc=6 dark=6 unres=0 inc=0 wall=0.017s
SUMMARY min r30_inward upper=2 min=0.0001 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.24374e-09 max_dstopT=41.2044 sum_rhs=31843 esc=6 dark=6 unres=0 inc=0 wall=0.017s
SUMMARY min r30_inward upper=2 min=1e-05 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.24374e-09 max_dstopT=41.2044 sum_rhs=31843 esc=6 dark=6 unres=0 inc=0 wall=0.017s
SUMMARY min r30_inward upper=2 min=1e-06 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.24374e-09 max_dstopT=41.2044 sum_rhs=31843 esc=6 dark=6 unres=0 inc=0 wall=0.017s
SUMMARY min r30_inward upper=2 min=1e-08 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.24374e-09 max_dstopT=41.2044 sum_rhs=31843 esc=6 dark=6 unres=0 inc=0 wall=0.017s
SUMMARY min r30_inward upper=2 min=1e-10 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.24374e-09 max_dstopT=41.2044 sum_rhs=31843 esc=6 dark=6 unres=0 inc=0 wall=0.017s
SUMMARY min r30_inward upper=2 min=1e-12 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.24374e-09 max_dstopT=41.2044 sum_rhs=31843 esc=6 dark=6 unres=0 inc=0 wall=0.017s
SUMMARY min r30_inward upper=2 min=1e-14 n=12 mismatch=0 max_dn_ang=0.000281866 max_dgrel=2.24374e-09 max_dstopT=41.2044 sum_rhs=31843 esc=6 dark=6 unres=0 inc=0 wall=0.017s
SUMMARY min r100_inward upper=2 min=0.1 n=12 mismatch=0 max_dn_ang=8.28604e-05 max_dgrel=2.0262e-09 max_dstopT=41.2161 sum_rhs=32389 esc=6 dark=6 unres=0 inc=0 wall=0.018s
SUMMARY min r100_inward upper=2 min=0.01 n=12 mismatch=0 max_dn_ang=8.28604e-05 max_dgrel=2.0262e-09 max_dstopT=41.2161 sum_rhs=32389 esc=6 dark=6 unres=0 inc=0 wall=0.018s
SUMMARY min r100_inward upper=2 min=0.001 n=12 mismatch=0 max_dn_ang=8.28604e-05 max_dgrel=2.0262e-09 max_dstopT=41.2161 sum_rhs=32389 esc=6 dark=6 unres=0 inc=0 wall=0.018s
SUMMARY min r100_inward upper=2 min=0.0001 n=12 mismatch=0 max_dn_ang=8.28604e-05 max_dgrel=2.0262e-09 max_dstopT=41.2161 sum_rhs=32389 esc=6 dark=6 unres=0 inc=0 wall=0.018s
SUMMARY min r100_inward upper=2 min=1e-05 n=12 mismatch=0 max_dn_ang=8.28604e-05 max_dgrel=2.0262e-09 max_dstopT=41.2161 sum_rhs=32389 esc=6 dark=6 unres=0 inc=0 wall=0.018s
SUMMARY min r100_inward upper=2 min=1e-06 n=12 mismatch=0 max_dn_ang=8.28604e-05 max_dgrel=2.0262e-09 max_dstopT=41.2161 sum_rhs=32389 esc=6 dark=6 unres=0 inc=0 wall=0.018s
SUMMARY min r100_inward upper=2 min=1e-08 n=12 mismatch=0 max_dn_ang=8.28604e-05 max_dgrel=2.0262e-09 max_dstopT=41.2161 sum_rhs=32389 esc=6 dark=6 unres=0 inc=0 wall=0.018s
SUMMARY min r100_inward upper=2 min=1e-10 n=12 mismatch=0 max_dn_ang=8.28604e-05 max_dgrel=2.0262e-09 max_dstopT=41.2161 sum_rhs=32389 esc=6 dark=6 unres=0 inc=0 wall=0.018s
SUMMARY min r100_inward upper=2 min=1e-12 n=12 mismatch=0 max_dn_ang=8.28604e-05 max_dgrel=2.0262e-09 max_dstopT=41.2161 sum_rhs=32389 esc=6 dark=6 unres=0 inc=0 wall=0.018s
SUMMARY min r100_inward upper=2 min=1e-14 n=12 mismatch=0 max_dn_ang=8.28604e-05 max_dgrel=2.0262e-09 max_dstopT=41.2161 sum_rhs=32389 esc=6 dark=6 unres=0 inc=0 wall=0.018s
SUMMARY min r2p1_outward upper=2 min=0.1 n=12 mismatch=1 max_dn_ang=0.000287222 max_dgrel=2.59243e-09 max_dstopT=263.145 sum_rhs=29421 esc=5 dark=7 unres=0 inc=0 wall=0.016s
SUMMARY min r2p1_outward upper=2 min=0.01 n=12 mismatch=1 max_dn_ang=0.000287222 max_dgrel=2.59243e-09 max_dstopT=263.145 sum_rhs=29421 esc=5 dark=7 unres=0 inc=0 wall=0.016s
SUMMARY min r2p1_outward upper=2 min=0.001 n=12 mismatch=1 max_dn_ang=0.000287222 max_dgrel=2.59243e-09 max_dstopT=263.145 sum_rhs=29421 esc=5 dark=7 unres=0 inc=0 wall=0.017s
SUMMARY min r2p1_outward upper=2 min=0.0001 n=12 mismatch=1 max_dn_ang=0.000287222 max_dgrel=2.59243e-09 max_dstopT=263.145 sum_rhs=29421 esc=5 dark=7 unres=0 inc=0 wall=0.017s
SUMMARY min r2p1_outward upper=2 min=1e-05 n=12 mismatch=1 max_dn_ang=0.000287222 max_dgrel=2.59243e-09 max_dstopT=263.145 sum_rhs=29421 esc=5 dark=7 unres=0 inc=0 wall=0.016s
SUMMARY min r2p1_outward upper=2 min=1e-06 n=12 mismatch=1 max_dn_ang=0.000287222 max_dgrel=2.59243e-09 max_dstopT=263.145 sum_rhs=29421 esc=5 dark=7 unres=0 inc=0 wall=0.016s
SUMMARY min r2p1_outward upper=2 min=1e-08 n=12 mismatch=1 max_dn_ang=0.000287222 max_dgrel=2.59243e-09 max_dstopT=263.145 sum_rhs=29421 esc=5 dark=7 unres=0 inc=0 wall=0.016s
SUMMARY min r2p1_outward upper=2 min=1e-10 n=12 mismatch=1 max_dn_ang=0.000287222 max_dgrel=2.59243e-09 max_dstopT=263.145 sum_rhs=29421 esc=5 dark=7 unres=0 inc=0 wall=0.016s
SUMMARY min r2p1_outward upper=2 min=1e-12 n=12 mismatch=1 max_dn_ang=0.000287222 max_dgrel=2.59243e-09 max_dstopT=263.145 sum_rhs=29421 esc=5 dark=7 unres=0 inc=0 wall=0.016s
SUMMARY min r2p1_outward upper=2 min=1e-14 n=12 mismatch=1 max_dn_ang=0.000287222 max_dgrel=2.59243e-09 max_dstopT=263.145 sum_rhs=29421 esc=5 dark=7 unres=0 inc=0 wall=0.016s
SUMMARY min r1p5_freefall upper=2 min=0.1 n=4 mismatch=1 max_dn_ang=8.165e-11 max_dgrel=3.7195e-10 max_dstopT=254.3 sum_rhs=3654 esc=2 dark=1 unres=0 inc=1 wall=0.002s
SUMMARY min r1p5_freefall upper=2 min=0.01 n=4 mismatch=0 max_dn_ang=8.165e-11 max_dgrel=3.7195e-10 max_dstopT=2.57141e-08 sum_rhs=5131 esc=3 dark=1 unres=0 inc=0 wall=0.003s
SUMMARY min r1p5_freefall upper=2 min=0.001 n=4 mismatch=0 max_dn_ang=8.165e-11 max_dgrel=3.7195e-10 max_dstopT=2.57141e-08 sum_rhs=5131 esc=3 dark=1 unres=0 inc=0 wall=0.003s
SUMMARY min r1p5_freefall upper=2 min=0.0001 n=4 mismatch=0 max_dn_ang=8.165e-11 max_dgrel=3.7195e-10 max_dstopT=2.57141e-08 sum_rhs=5131 esc=3 dark=1 unres=0 inc=0 wall=0.003s
SUMMARY min r1p5_freefall upper=2 min=1e-05 n=4 mismatch=0 max_dn_ang=8.165e-11 max_dgrel=3.7195e-10 max_dstopT=2.57141e-08 sum_rhs=5131 esc=3 dark=1 unres=0 inc=0 wall=0.003s
SUMMARY min r1p5_freefall upper=2 min=1e-06 n=4 mismatch=0 max_dn_ang=8.165e-11 max_dgrel=3.7195e-10 max_dstopT=2.57141e-08 sum_rhs=5131 esc=3 dark=1 unres=0 inc=0 wall=0.003s
SUMMARY min r1p5_freefall upper=2 min=1e-08 n=4 mismatch=0 max_dn_ang=8.165e-11 max_dgrel=3.7195e-10 max_dstopT=2.57141e-08 sum_rhs=5131 esc=3 dark=1 unres=0 inc=0 wall=0.003s
SUMMARY min r1p5_freefall upper=2 min=1e-10 n=4 mismatch=0 max_dn_ang=8.165e-11 max_dgrel=3.7195e-10 max_dstopT=2.57141e-08 sum_rhs=5131 esc=3 dark=1 unres=0 inc=0 wall=0.003s
SUMMARY min r1p5_freefall upper=2 min=1e-12 n=4 mismatch=0 max_dn_ang=8.165e-11 max_dgrel=3.7195e-10 max_dstopT=2.57141e-08 sum_rhs=5131 esc=3 dark=1 unres=0 inc=0 wall=0.003s
SUMMARY min r1p5_freefall upper=2 min=1e-14 n=4 mismatch=0 max_dn_ang=8.165e-11 max_dgrel=3.7195e-10 max_dstopT=2.57141e-08 sum_rhs=5131 esc=3 dark=1 unres=0 inc=0 wall=0.003s
RK4 r30_inward dir=8 theta=0.168123 o01=1/1 o005=1/1 hhalve_dn=nan hhalve_dgrel=nan vsdp_dn=nan vsdp_dgrel=nan vsdp_class=1
RK4 r30_inward dir=9 theta=0.168123 o01=0/0 o005=0/0 hhalve_dn=1.89584e-07 hhalve_dgrel=7.32747e-15 vsdp_dn=7.95167e-08 vsdp_dgrel=4.66294e-13 vsdp_class=1
RK4 r30_inward dir=6 theta=0.168113 o01=1/1 o005=1/1 hhalve_dn=nan hhalve_dgrel=nan vsdp_dn=nan vsdp_dgrel=nan vsdp_class=1
RK4 r30_inward dir=7 theta=0.168133 o01=0/0 o005=0/0 hhalve_dn=1.73965e-09 hhalve_dgrel=7.9492e-14 vsdp_dn=7.86496e-10 vsdp_dgrel=2.6934e-13 vsdp_class=1
RK4 r2p1_outward dir=8 theta=0.570377 o01=0/0 o005=0/0 hhalve_dn=1.03961e-05 hhalve_dgrel=1.66234e-12 vsdp_dn=8.49321e-07 vsdp_dgrel=5.46896e-13 vsdp_class=1
RK4 r2p1_outward dir=9 theta=0.570377 o01=1/1 o005=1/1 hhalve_dn=nan hhalve_dgrel=nan vsdp_dn=nan vsdp_dgrel=nan vsdp_class=1
RK4 r2p1_outward dir=6 theta=0.570367 o01=0/0 o005=0/0 hhalve_dn=1.03971e-07 hhalve_dgrel=1.61182e-12 vsdp_dn=8.47692e-09 vsdp_dgrel=3.02647e-13 vsdp_class=1
RK4 r2p1_outward dir=7 theta=0.570387 o01=1/1 o005=1/1 hhalve_dn=nan hhalve_dgrel=nan vsdp_dn=nan vsdp_dgrel=nan vsdp_class=1
TOTAL_RAYS 816
real 0m2.428s
user 0m2.423s
sys 0m0.004s
+ set +x
### command: a_public minkowski
+ /tmp/opencode/step_bounds/a_public minkowski
SUMMARY mink upper=1 min=0.1 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=4886 esc=6 dark=0 unres=0 inc=0
SUMMARY mink upper=1 min=1e-06 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=4886 esc=6 dark=0 unres=0 inc=0
SUMMARY mink upper=1 min=1e-12 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=4886 esc=6 dark=0 unres=0 inc=0
SUMMARY mink upper=4 min=0.1 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=2590 esc=6 dark=0 unres=0 inc=0
SUMMARY mink upper=4 min=1e-06 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=2590 esc=6 dark=0 unres=0 inc=0
SUMMARY mink upper=4 min=1e-12 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=2590 esc=6 dark=0 unres=0 inc=0
SUMMARY mink upper=16 min=0.1 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=2037 esc=6 dark=0 unres=0 inc=0
SUMMARY mink upper=16 min=1e-06 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=2037 esc=6 dark=0 unres=0 inc=0
SUMMARY mink upper=16 min=1e-12 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=2037 esc=6 dark=0 unres=0 inc=0
SUMMARY mink upper=64 min=0.1 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=1932 esc=6 dark=0 unres=0 inc=0
SUMMARY mink upper=64 min=1e-06 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=1932 esc=6 dark=0 unres=0 inc=0
SUMMARY mink upper=64 min=1e-12 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=1932 esc=6 dark=0 unres=0 inc=0
SUMMARY mink upper=256 min=0.1 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=1932 esc=6 dark=0 unres=0 inc=0
SUMMARY mink upper=256 min=1e-06 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=1932 esc=6 dark=0 unres=0 inc=0
SUMMARY mink upper=256 min=1e-12 n=6 mismatch=0 max_dn_ang=0 max_dgrel=0 sum_rhs=1932 esc=6 dark=0 unres=0 inc=0
TOTAL_RAYS 186
real 0m0.013s
user 0m0.013s
sys 0m0.000s
+ set +x
### command: a_public alcubierre
+ /tmp/opencode/step_bounds/a_public alcubierre
REF alc_v3_s1 dir=0 theta=nan outcome=0 reason=0 stop_t=-16.4338937 steps=347 rejected=1 rhs=2730 g=1.3 thr=nan
REF alc_v3_s1 dir=1 theta=nan outcome=0 reason=0 stop_t=-29.3767055 steps=597 rejected=1 rhs=4480 g=0.7 thr=nan
REF alc_v3_s1 dir=2 theta=nan outcome=0 reason=0 stop_t=-20.2190145 steps=414 rejected=0 rhs=3192 g=1 thr=nan
REF alc_v3_s1 dir=3 theta=nan outcome=0 reason=0 stop_t=-20.2190145 steps=414 rejected=0 rhs=3192 g=1 thr=nan
REF alc_v3_s1 dir=4 theta=nan outcome=0 reason=0 stop_t=-17.6897755 steps=367 rejected=0 rhs=2863 g=1.17815368048 thr=nan
REF alc_v3_s1 dir=5 theta=nan outcome=0 reason=0 stop_t=-24.329102 steps=497 rejected=1 rhs=3780 g=0.822613315483 thr=nan
REF alc_v3_s10 dir=0 theta=nan outcome=0 reason=0 stop_t=-2.53543426 steps=523 rejected=3 rhs=3976 g=1.3 thr=nan
REF alc_v3_s10 dir=1 theta=nan outcome=0 reason=0 stop_t=-3.84949982 steps=775 rejected=0 rhs=5719 g=0.7 thr=nan
REF alc_v3_s10 dir=2 theta=nan outcome=0 reason=0 stop_t=-2.94282517 steps=589 rejected=0 rhs=4417 g=1 thr=nan
REF alc_v3_s10 dir=3 theta=nan outcome=0 reason=0 stop_t=-2.94282517 steps=589 rejected=0 rhs=4417 g=1 thr=nan
REF alc_v3_s10 dir=4 theta=nan outcome=0 reason=0 stop_t=-2.67090915 steps=543 rejected=2 rhs=4109 g=1.17959135733 thr=nan
REF alc_v3_s10 dir=5 theta=nan outcome=0 reason=0 stop_t=-3.3698049 steps=677 rejected=0 rhs=5033 g=0.820579506843 thr=nan
REF alc_v9_s1 dir=0 theta=nan outcome=0 reason=0 stop_t=-11.5627491 steps=283 rejected=1 rhs=2282 g=1.9 thr=nan
REF alc_v9_s1 dir=1 theta=nan outcome=0 reason=0 stop_t=-189.149503 steps=3825 rejected=1 rhs=27069 g=0.1 thr=nan
REF alc_v9_s1 dir=2 theta=nan outcome=0 reason=0 stop_t=-16.1619146 steps=351 rejected=4 rhs=2772 g=1 thr=nan
REF alc_v9_s1 dir=3 theta=nan outcome=0 reason=0 stop_t=-16.1619146 steps=351 rejected=4 rhs=2772 g=1 thr=nan
REF alc_v9_s1 dir=4 theta=nan outcome=0 reason=0 stop_t=-13.0192142 steps=308 rejected=1 rhs=2457 g=1.5041731473 thr=nan
REF alc_v9_s1 dir=5 theta=nan outcome=0 reason=0 stop_t=-22.987334 steps=491 rejected=1 rhs=3738 g=0.54079971046 thr=nan
REF alc_v9_s10 dir=0 theta=nan outcome=0 reason=0 stop_t=-2.03742978 steps=452 rejected=5 rhs=3493 g=1.9 thr=nan
REF alc_v9_s10 dir=1 theta=nan outcome=0 reason=0 stop_t=-19.9638367 steps=4013 rejected=0 rhs=28385 g=0.1 thr=nan
REF alc_v9_s10 dir=2 theta=nan outcome=0 reason=0 stop_t=-2.62069451 steps=525 rejected=0 rhs=3969 g=1 thr=nan
REF alc_v9_s10 dir=3 theta=nan outcome=0 reason=0 stop_t=-2.62069451 steps=525 rejected=0 rhs=3969 g=1 thr=nan
REF alc_v9_s10 dir=4 theta=nan outcome=0 reason=0 stop_t=-2.20704191 steps=469 rejected=3 rhs=3598 g=1.5316539409 thr=nan
REF alc_v9_s10 dir=5 theta=nan outcome=0 reason=0 stop_t=-3.67922549 steps=742 rejected=0 rhs=5488 g=0.483265790771 thr=nan
REF alc_v9_s100 dir=0 theta=nan outcome=0 reason=0 stop_t=-1.10374298 steps=2252 rejected=5 rhs=16093 g=1.9 thr=nan
REF alc_v9_s100 dir=1 theta=nan outcome=0 reason=0 stop_t=-2.89638367 steps=5813 rejected=0 rhs=40978 g=0.1 thr=nan
REF alc_v9_s100 dir=2 theta=nan outcome=0 reason=0 stop_t=-1.18800796 steps=2377 rejected=0 rhs=16926 g=1 thr=nan
REF alc_v9_s100 dir=3 theta=nan outcome=0 reason=0 stop_t=-1.18800796 steps=2377 rejected=0 rhs=16926 g=1 thr=nan
REF alc_v9_s100 dir=4 theta=nan outcome=0 reason=0 stop_t=-1.12678339 steps=2281 rejected=2 rhs=16275 g=1.53910006629 thr=nan
REF alc_v9_s100 dir=5 theta=nan outcome=0 reason=0 stop_t=-1.36694611 steps=2740 rejected=0 rhs=19474 g=0.462933441354 thr=nan
SUMMARY upper alc_v3_s1 upper=0.05 min=0.0001 n=6 mismatch=0 max_dn_ang=7.54397e-14 max_dgrel=7.62723e-13 max_dstopT=1.98952e-13 sum_rhs=19740 esc=6 dark=0 unres=0 inc=0 wall=0.005s
SUMMARY upper alc_v3_s1 upper=0.05 min=1e-08 n=6 mismatch=0 max_dn_ang=7.54397e-14 max_dgrel=7.62723e-13 max_dstopT=1.98952e-13 sum_rhs=19740 esc=6 dark=0 unres=0 inc=0 wall=0.005s
SUMMARY upper alc_v3_s1 upper=0.05 min=1e-12 n=6 mismatch=0 max_dn_ang=7.54397e-14 max_dgrel=7.62723e-13 max_dstopT=1.98952e-13 sum_rhs=19740 esc=6 dark=0 unres=0 inc=0 wall=0.005s
SUMMARY upper alc_v3_s1 upper=0.2 min=0.0001 n=6 mismatch=0 max_dn_ang=1.2079e-10 max_dgrel=3.45157e-10 max_dstopT=4.09095e-10 sum_rhs=6475 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v3_s1 upper=0.2 min=1e-08 n=6 mismatch=0 max_dn_ang=1.2079e-10 max_dgrel=3.45157e-10 max_dstopT=4.09095e-10 sum_rhs=6475 esc=6 dark=0 unres=0 inc=0 wall=0.002s
SUMMARY upper alc_v3_s1 upper=0.2 min=1e-12 n=6 mismatch=0 max_dn_ang=1.2079e-10 max_dgrel=3.45157e-10 max_dstopT=4.09095e-10 sum_rhs=6475 esc=6 dark=0 unres=0 inc=0 wall=0.002s
SUMMARY upper alc_v3_s1 upper=0.4 min=0.0001 n=6 mismatch=0 max_dn_ang=1.02989e-10 max_dgrel=3.46442e-10 max_dstopT=3.14323e-10 sum_rhs=4613 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v3_s1 upper=0.4 min=1e-08 n=6 mismatch=0 max_dn_ang=1.02989e-10 max_dgrel=3.46442e-10 max_dstopT=3.14323e-10 sum_rhs=4613 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v3_s1 upper=0.4 min=1e-12 n=6 mismatch=0 max_dn_ang=1.02989e-10 max_dgrel=3.46442e-10 max_dstopT=3.14323e-10 sum_rhs=4613 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v3_s1 upper=0.8 min=0.0001 n=6 mismatch=0 max_dn_ang=7.88763e-11 max_dgrel=7.95433e-10 max_dstopT=7.3689e-10 sum_rhs=3794 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v3_s1 upper=0.8 min=1e-08 n=6 mismatch=0 max_dn_ang=7.88763e-11 max_dgrel=7.95433e-10 max_dstopT=7.3689e-10 sum_rhs=3794 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v3_s1 upper=0.8 min=1e-12 n=6 mismatch=0 max_dn_ang=7.88763e-11 max_dgrel=7.95433e-10 max_dstopT=7.3689e-10 sum_rhs=3794 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v3_s1 upper=1.6 min=0.0001 n=6 mismatch=0 max_dn_ang=1.23935e-10 max_dgrel=1.11826e-09 max_dstopT=1.14768e-09 sum_rhs=3430 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v3_s1 upper=1.6 min=1e-08 n=6 mismatch=0 max_dn_ang=1.23935e-10 max_dgrel=1.11826e-09 max_dstopT=1.14768e-09 sum_rhs=3430 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v3_s1 upper=1.6 min=1e-12 n=6 mismatch=0 max_dn_ang=1.23935e-10 max_dgrel=1.11826e-09 max_dstopT=1.14768e-09 sum_rhs=3430 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v3_s1 upper=3.2 min=0.0001 n=6 mismatch=0 max_dn_ang=1.54539e-10 max_dgrel=1.25996e-09 max_dstopT=1.3457e-09 sum_rhs=3290 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v3_s1 upper=3.2 min=1e-08 n=6 mismatch=0 max_dn_ang=1.54539e-10 max_dgrel=1.25996e-09 max_dstopT=1.3457e-09 sum_rhs=3290 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v3_s1 upper=3.2 min=1e-12 n=6 mismatch=0 max_dn_ang=1.54539e-10 max_dgrel=1.25996e-09 max_dstopT=1.3457e-09 sum_rhs=3290 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v3_s10 upper=0.005 min=0.0001 n=6 mismatch=0 max_dn_ang=5.82867e-15 max_dgrel=3.10862e-14 max_dstopT=4.66294e-14 sum_rhs=27412 esc=6 dark=0 unres=0 inc=0 wall=0.006s
SUMMARY upper alc_v3_s10 upper=0.005 min=1e-08 n=6 mismatch=0 max_dn_ang=5.82867e-15 max_dgrel=3.10862e-14 max_dstopT=4.66294e-14 sum_rhs=27412 esc=6 dark=0 unres=0 inc=0 wall=0.006s
SUMMARY upper alc_v3_s10 upper=0.005 min=1e-12 n=6 mismatch=0 max_dn_ang=5.82867e-15 max_dgrel=3.10862e-14 max_dstopT=4.66294e-14 sum_rhs=27412 esc=6 dark=0 unres=0 inc=0 wall=0.006s
SUMMARY upper alc_v3_s10 upper=0.02 min=0.0001 n=6 mismatch=0 max_dn_ang=6.29929e-12 max_dgrel=2.40162e-10 max_dstopT=1.39897e-11 sum_rhs=8337 esc=6 dark=0 unres=0 inc=0 wall=0.002s
SUMMARY upper alc_v3_s10 upper=0.02 min=1e-08 n=6 mismatch=0 max_dn_ang=6.29929e-12 max_dgrel=2.40162e-10 max_dstopT=1.39897e-11 sum_rhs=8337 esc=6 dark=0 unres=0 inc=0 wall=0.002s
SUMMARY upper alc_v3_s10 upper=0.02 min=1e-12 n=6 mismatch=0 max_dn_ang=6.29929e-12 max_dgrel=2.40162e-10 max_dstopT=1.39897e-11 sum_rhs=8337 esc=6 dark=0 unres=0 inc=0 wall=0.002s
SUMMARY upper alc_v3_s10 upper=0.04 min=0.0001 n=6 mismatch=0 max_dn_ang=3.28758e-11 max_dgrel=3.85361e-10 max_dstopT=6.29403e-11 sum_rhs=5621 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v3_s10 upper=0.04 min=1e-08 n=6 mismatch=0 max_dn_ang=3.28758e-11 max_dgrel=3.85361e-10 max_dstopT=6.29403e-11 sum_rhs=5621 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v3_s10 upper=0.04 min=1e-12 n=6 mismatch=0 max_dn_ang=3.28758e-11 max_dgrel=3.85361e-10 max_dstopT=6.29403e-11 sum_rhs=5621 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v3_s10 upper=0.08 min=0.0001 n=6 mismatch=0 max_dn_ang=1.04151e-11 max_dgrel=2.78111e-10 max_dstopT=8.02363e-11 sum_rhs=4648 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v3_s10 upper=0.08 min=1e-08 n=6 mismatch=0 max_dn_ang=1.04151e-11 max_dgrel=2.78111e-10 max_dstopT=8.02363e-11 sum_rhs=4648 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v3_s10 upper=0.08 min=1e-12 n=6 mismatch=0 max_dn_ang=1.04151e-11 max_dgrel=2.78111e-10 max_dstopT=8.02363e-11 sum_rhs=4648 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v3_s10 upper=0.16 min=0.0001 n=6 mismatch=0 max_dn_ang=1.13121e-11 max_dgrel=7.59968e-10 max_dstopT=9.53793e-11 sum_rhs=4270 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v3_s10 upper=0.16 min=1e-08 n=6 mismatch=0 max_dn_ang=1.13121e-11 max_dgrel=7.59968e-10 max_dstopT=9.53793e-11 sum_rhs=4270 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v3_s10 upper=0.16 min=1e-12 n=6 mismatch=0 max_dn_ang=1.13121e-11 max_dgrel=7.59968e-10 max_dstopT=9.53793e-11 sum_rhs=4270 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v3_s10 upper=0.32 min=0.0001 n=6 mismatch=0 max_dn_ang=3.13081e-11 max_dgrel=9.77839e-10 max_dstopT=1.04124e-10 sum_rhs=4116 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v3_s10 upper=0.32 min=1e-08 n=6 mismatch=0 max_dn_ang=3.13081e-11 max_dgrel=9.77839e-10 max_dstopT=1.04124e-10 sum_rhs=4116 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v3_s10 upper=0.32 min=1e-12 n=6 mismatch=0 max_dn_ang=3.13081e-11 max_dgrel=9.77839e-10 max_dstopT=1.04124e-10 sum_rhs=4116 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v9_s1 upper=0.05 min=0.0001 n=6 mismatch=0 max_dn_ang=6.63192e-13 max_dgrel=2.59086e-11 max_dstopT=1.38662e-11 sum_rhs=39452 esc=6 dark=0 unres=0 inc=0 wall=0.010s
SUMMARY upper alc_v9_s1 upper=0.05 min=1e-08 n=6 mismatch=0 max_dn_ang=6.63192e-13 max_dgrel=2.59086e-11 max_dstopT=1.38662e-11 sum_rhs=39452 esc=6 dark=0 unres=0 inc=0 wall=0.010s
SUMMARY upper alc_v9_s1 upper=0.05 min=1e-12 n=6 mismatch=0 max_dn_ang=6.63192e-13 max_dgrel=2.59086e-11 max_dstopT=1.38662e-11 sum_rhs=39452 esc=6 dark=0 unres=0 inc=0 wall=0.010s
SUMMARY upper alc_v9_s1 upper=0.2 min=0.0001 n=6 mismatch=0 max_dn_ang=1.07764e-10 max_dgrel=2.07282e-09 max_dstopT=6.80462e-10 sum_rhs=11690 esc=6 dark=0 unres=0 inc=0 wall=0.003s
SUMMARY upper alc_v9_s1 upper=0.2 min=1e-08 n=6 mismatch=0 max_dn_ang=1.07764e-10 max_dgrel=2.07282e-09 max_dstopT=6.80462e-10 sum_rhs=11690 esc=6 dark=0 unres=0 inc=0 wall=0.003s
SUMMARY upper alc_v9_s1 upper=0.2 min=1e-12 n=6 mismatch=0 max_dn_ang=1.07764e-10 max_dgrel=2.07282e-09 max_dstopT=6.80462e-10 sum_rhs=11690 esc=6 dark=0 unres=0 inc=0 wall=0.003s
SUMMARY upper alc_v9_s1 upper=0.4 min=0.0001 n=6 mismatch=0 max_dn_ang=6.25e-11 max_dgrel=2.46644e-09 max_dstopT=1.08309e-09 sum_rhs=7413 esc=6 dark=0 unres=0 inc=0 wall=0.002s
SUMMARY upper alc_v9_s1 upper=0.4 min=1e-08 n=6 mismatch=0 max_dn_ang=6.25e-11 max_dgrel=2.46644e-09 max_dstopT=1.08309e-09 sum_rhs=7413 esc=6 dark=0 unres=0 inc=0 wall=0.002s
SUMMARY upper alc_v9_s1 upper=0.4 min=1e-12 n=6 mismatch=0 max_dn_ang=6.25e-11 max_dgrel=2.46644e-09 max_dstopT=1.08309e-09 sum_rhs=7413 esc=6 dark=0 unres=0 inc=0 wall=0.002s
SUMMARY upper alc_v9_s1 upper=0.8 min=0.0001 n=6 mismatch=0 max_dn_ang=5.34924e-11 max_dgrel=2.71772e-09 max_dstopT=1.1859e-09 sum_rhs=5369 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v9_s1 upper=0.8 min=1e-08 n=6 mismatch=0 max_dn_ang=5.34924e-11 max_dgrel=2.71772e-09 max_dstopT=1.1859e-09 sum_rhs=5369 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v9_s1 upper=0.8 min=1e-12 n=6 mismatch=0 max_dn_ang=5.34924e-11 max_dgrel=2.71772e-09 max_dstopT=1.1859e-09 sum_rhs=5369 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v9_s1 upper=1.6 min=0.0001 n=6 mismatch=0 max_dn_ang=1.03537e-10 max_dgrel=2.8194e-09 max_dstopT=9.54927e-10 sum_rhs=4431 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v9_s1 upper=1.6 min=1e-08 n=6 mismatch=0 max_dn_ang=1.03537e-10 max_dgrel=2.8194e-09 max_dstopT=9.54927e-10 sum_rhs=4431 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v9_s1 upper=1.6 min=1e-12 n=6 mismatch=0 max_dn_ang=1.03537e-10 max_dgrel=2.8194e-09 max_dstopT=9.54927e-10 sum_rhs=4431 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v9_s1 upper=3.2 min=0.0001 n=6 mismatch=0 max_dn_ang=1.23927e-10 max_dgrel=2.82251e-09 max_dstopT=3.24928e-09 sum_rhs=3983 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v9_s1 upper=3.2 min=1e-08 n=6 mismatch=0 max_dn_ang=1.23927e-10 max_dgrel=2.82251e-09 max_dstopT=3.24928e-09 sum_rhs=3983 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v9_s1 upper=3.2 min=1e-12 n=6 mismatch=0 max_dn_ang=1.23927e-10 max_dgrel=2.82251e-09 max_dstopT=3.24928e-09 sum_rhs=3983 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v9_s10 upper=0.005 min=0.0001 n=6 mismatch=0 max_dn_ang=8.9373e-14 max_dgrel=1.10281e-11 max_dstopT=3.55271e-13 sum_rhs=48167 esc=6 dark=0 unres=0 inc=0 wall=0.011s
SUMMARY upper alc_v9_s10 upper=0.005 min=1e-08 n=6 mismatch=0 max_dn_ang=8.9373e-14 max_dgrel=1.10281e-11 max_dstopT=3.55271e-13 sum_rhs=48167 esc=6 dark=0 unres=0 inc=0 wall=0.011s
SUMMARY upper alc_v9_s10 upper=0.005 min=1e-12 n=6 mismatch=0 max_dn_ang=8.9373e-14 max_dgrel=1.10281e-11 max_dstopT=3.55271e-13 sum_rhs=48167 esc=6 dark=0 unres=0 inc=0 wall=0.011s
SUMMARY upper alc_v9_s10 upper=0.02 min=0.0001 n=6 mismatch=0 max_dn_ang=5.33142e-11 max_dgrel=6.90633e-10 max_dstopT=1.09151e-10 sum_rhs=13692 esc=6 dark=0 unres=0 inc=0 wall=0.003s
SUMMARY upper alc_v9_s10 upper=0.02 min=1e-08 n=6 mismatch=0 max_dn_ang=5.33142e-11 max_dgrel=6.90633e-10 max_dstopT=1.09151e-10 sum_rhs=13692 esc=6 dark=0 unres=0 inc=0 wall=0.003s
SUMMARY upper alc_v9_s10 upper=0.02 min=1e-12 n=6 mismatch=0 max_dn_ang=5.33142e-11 max_dgrel=6.90633e-10 max_dstopT=1.09151e-10 sum_rhs=13692 esc=6 dark=0 unres=0 inc=0 wall=0.003s
SUMMARY upper alc_v9_s10 upper=0.04 min=0.0001 n=6 mismatch=0 max_dn_ang=1.75204e-10 max_dgrel=9.74943e-10 max_dstopT=3.64615e-10 sum_rhs=8484 esc=6 dark=0 unres=0 inc=0 wall=0.002s
SUMMARY upper alc_v9_s10 upper=0.04 min=1e-08 n=6 mismatch=0 max_dn_ang=1.75204e-10 max_dgrel=9.74943e-10 max_dstopT=3.64615e-10 sum_rhs=8484 esc=6 dark=0 unres=0 inc=0 wall=0.002s
SUMMARY upper alc_v9_s10 upper=0.04 min=1e-12 n=6 mismatch=0 max_dn_ang=1.75204e-10 max_dgrel=9.74943e-10 max_dstopT=3.64615e-10 sum_rhs=8484 esc=6 dark=0 unres=0 inc=0 wall=0.002s
SUMMARY upper alc_v9_s10 upper=0.08 min=0.0001 n=6 mismatch=0 max_dn_ang=1.1957e-10 max_dgrel=1.45708e-09 max_dstopT=2.69508e-10 sum_rhs=6251 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v9_s10 upper=0.08 min=1e-08 n=6 mismatch=0 max_dn_ang=1.1957e-10 max_dgrel=1.45708e-09 max_dstopT=2.69508e-10 sum_rhs=6251 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v9_s10 upper=0.08 min=1e-12 n=6 mismatch=0 max_dn_ang=1.1957e-10 max_dgrel=1.45708e-09 max_dstopT=2.69508e-10 sum_rhs=6251 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v9_s10 upper=0.16 min=0.0001 n=6 mismatch=0 max_dn_ang=1.72733e-11 max_dgrel=1.07679e-09 max_dstopT=1.26013e-10 sum_rhs=5278 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v9_s10 upper=0.16 min=1e-08 n=6 mismatch=0 max_dn_ang=1.72733e-11 max_dgrel=1.07679e-09 max_dstopT=1.26013e-10 sum_rhs=5278 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v9_s10 upper=0.16 min=1e-12 n=6 mismatch=0 max_dn_ang=1.72733e-11 max_dgrel=1.07679e-09 max_dstopT=1.26013e-10 sum_rhs=5278 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v9_s10 upper=0.32 min=0.0001 n=6 mismatch=0 max_dn_ang=5.49931e-11 max_dgrel=8.67977e-10 max_dstopT=2.55501e-10 sum_rhs=4872 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v9_s10 upper=0.32 min=1e-08 n=6 mismatch=0 max_dn_ang=5.49931e-11 max_dgrel=8.67977e-10 max_dstopT=2.55501e-10 sum_rhs=4872 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY upper alc_v9_s10 upper=0.32 min=1e-12 n=6 mismatch=0 max_dn_ang=5.49931e-11 max_dgrel=8.67977e-10 max_dstopT=2.55501e-10 sum_rhs=4872 esc=6 dark=0 unres=0 inc=0 wall=0.001s
SUMMARY min_stress alc_v9_s100 upper=0.004 min=0.0001 n=6 mismatch=0 max_dn_ang=1.69451e-11 max_dgrel=1.33161e-09 max_dstopT=1.05733e-11 sum_rhs=18095 esc=6 dark=0 unres=0 inc=0 wall=0.004s
SUMMARY min_stress alc_v9_s100 upper=0.004 min=1e-05 n=6 mismatch=0 max_dn_ang=1.69451e-11 max_dgrel=1.33161e-09 max_dstopT=1.05733e-11 sum_rhs=18095 esc=6 dark=0 unres=0 inc=0 wall=0.004s
SUMMARY min_stress alc_v9_s100 upper=0.004 min=1e-06 n=6 mismatch=0 max_dn_ang=1.69451e-11 max_dgrel=1.33161e-09 max_dstopT=1.05733e-11 sum_rhs=18095 esc=6 dark=0 unres=0 inc=0 wall=0.004s
SUMMARY min_stress alc_v9_s100 upper=0.004 min=1e-08 n=6 mismatch=0 max_dn_ang=1.69451e-11 max_dgrel=1.33161e-09 max_dstopT=1.05733e-11 sum_rhs=18095 esc=6 dark=0 unres=0 inc=0 wall=0.004s
SUMMARY min_stress alc_v9_s100 upper=0.004 min=1e-12 n=6 mismatch=0 max_dn_ang=1.69451e-11 max_dgrel=1.33161e-09 max_dstopT=1.05733e-11 sum_rhs=18095 esc=6 dark=0 unres=0 inc=0 wall=0.004s
SUMMARY min_stress alc_v9_s100 upper=0.004 min=1e-14 n=6 mismatch=0 max_dn_ang=1.69451e-11 max_dgrel=1.33161e-09 max_dstopT=1.05733e-11 sum_rhs=18095 esc=6 dark=0 unres=0 inc=0 wall=0.004s
TOTAL_RAYS 498
real 0m0.264s
user 0m0.254s
sys 0m0.010s
+ set +x
@@ -0,0 +1,33 @@
### command: b_actual
+ /tmp/opencode/step_bounds/b_actual
B r30_inward dir=0 ref=1 refstop=-69.5728394 span=69.5728 T=20 target=-20 steps=17 reached=1 term=-1 h=[0.1,1.72575] first=0.1 last=0.839319 boundary=1 rhs=119 rej=0 nullmax=3.01e-12 final_t=-20 wall=0.0001s
B r30_inward dir=1 ref=0 refstop=-226 span=226 T=20 target=-20 steps=13 reached=1 term=-1 h=[0.1,2] first=0.1 last=0.123959 boundary=1 rhs=91 rej=0 nullmax=8.69e-12 final_t=-20 wall=0.0001s
B r30_inward dir=2 ref=1 refstop=-142.175748 span=142.176 T=20 target=-20 steps=20 reached=1 term=-1 h=[0.1,1.40832] first=0.1 last=0.637699 boundary=1 rhs=140 rej=0 nullmax=1.53e-11 final_t=-20 wall=0.0001s
B r30_inward dir=3 ref=0 refstop=-370.180478 span=370.18 T=20 target=-20 steps=20 reached=1 term=-1 h=[0.1,1.40832] first=0.1 last=0.6377 boundary=1 rhs=140 rej=0 nullmax=1.53e-11 final_t=-20 wall=0.0001s
B r30_inward dir=4 ref=1 refstop=-118.245807 span=118.246 T=20 target=-20 steps=20 reached=1 term=-1 h=[0.1,1.40833] first=0.1 last=0.637634 boundary=1 rhs=140 rej=0 nullmax=1.53e-11 final_t=-20 wall=0.0001s
B r30_inward dir=5 ref=0 refstop=-346.252134 span=346.252 T=20 target=-20 steps=20 reached=1 term=-1 h=[0.1,1.40831] first=0.1 last=0.637765 boundary=1 rhs=140 rej=0 nullmax=1.53e-11 final_t=-20 wall=0.0001s
B r100_inward dir=0 ref=1 refstop=-144.299101 span=144.299 T=20 target=-20 steps=12 reached=1 term=-1 h=[0.1,2] first=0.1 last=1.4 boundary=1 rhs=84 rej=0 nullmax=1.45e-14 final_t=-20 wall=0.0001s
B r100_inward dir=1 ref=0 refstop=-156 span=156 T=20 target=-20 steps=12 reached=1 term=-1 h=[0.1,2] first=0.1 last=1.4 boundary=1 rhs=84 rej=0 nullmax=7.66e-15 final_t=-20 wall=0.0001s
B r100_inward dir=2 ref=1 refstop=-301.077405 span=301.077 T=20 target=-20 steps=12 reached=1 term=-1 h=[0.1,2] first=0.1 last=1.4 boundary=1 rhs=84 rej=0 nullmax=1.4e-14 final_t=-20 wall=0.0001s
B r100_inward dir=3 ref=1 refstop=-211.027405 span=211.027 T=20 target=-20 steps=12 reached=1 term=-1 h=[0.1,2] first=0.1 last=1.4 boundary=1 rhs=84 rej=0 nullmax=1.32e-14 final_t=-20 wall=0.0001s
B r2p1_outward dir=0 ref=0 refstop=-253.9 span=253.9 T=20 target=-20 steps=46 reached=1 term=-1 h=[0.1,1.0708] first=0.1 last=1.06594 boundary=1 rhs=322 rej=0 nullmax=6.27e-11 final_t=-20 wall=0.0002s
B r2p1_outward dir=1 ref=0 refstop=-414.940103 span=414.94 T=20 target=-20 steps=105 reached=1 term=-1 h=[0.1,0.211533] first=0.1 last=0.192781 boundary=1 rhs=735 rej=0 nullmax=4.7e-11 final_t=-20 wall=0.0005s
B r2p1_outward dir=2 ref=0 refstop=-333.450704 span=333.451 T=20 target=-20 steps=105 reached=1 term=-1 h=[0.1,0.211533] first=0.1 last=0.192739 boundary=1 rhs=735 rej=0 nullmax=4.7e-11 final_t=-20 wall=0.0005s
B r2p1_outward dir=3 ref=1 refstop=-109.415611 span=109.416 T=20 target=-20 steps=105 reached=1 term=-1 h=[0.1,0.211532] first=0.1 last=0.192823 boundary=1 rhs=735 rej=0 nullmax=4.7e-11 final_t=-20 wall=0.0005s
B r2p1_outward dir=4 ref=1 refstop=-32.2468847 span=32.2469 T=20 target=-20 steps=37 reached=1 term=-1 h=[0.1,0.899361] first=0.1 last=0.332712 boundary=1 rhs=259 rej=0 nullmax=2.99e-11 final_t=-20 wall=0.0002s
B r1p5_freefall dir=0 ref=1 refstop=-30.6580907 span=30.6581 T=20 target=-20 steps=49 reached=1 term=-1 h=[0.1,0.695968] first=0.1 last=0.106174 boundary=1 rhs=343 rej=0 nullmax=4.94e-10 final_t=-20 wall=0.0002s
B r1p5_freefall dir=1 ref=0 refstop=-254.5 span=254.5 T=20 target=-20 steps=52 reached=1 term=-1 h=[0.0920967,1.08419] first=0.1 last=0.22147 boundary=1 rhs=364 rej=0 nullmax=4.03e-11 final_t=-20 wall=0.0002s
B mink_moving dir=0 ref=0 refstop=-51.2640254 span=51.264 T=20 target=-20 steps=3 reached=1 term=-1 h=[1,14] first=1 last=14 boundary=1 rhs=21 rej=0 nullmax=2.22e-16 final_t=-20 wall=0.0000s
B mink_moving dir=1 ref=0 refstop=-71.5846741 span=71.5847 T=20 target=-20 steps=3 reached=1 term=-1 h=[1,14] first=1 last=14 boundary=1 rhs=21 rej=0 nullmax=0 final_t=-20 wall=0.0000s
B alc_v3_s1 dir=0 ref=0 refstop=-16.4338937 span=16.4339 T=16.4339 target=-16.4339 steps=56 reached=1 term=-1 h=[0.05,0.4] first=0.05 last=0.203538 boundary=1 rhs=406 rej=2 nullmax=0 final_t=-16.4338937 wall=0.0001s
B alc_v3_s1 dir=1 ref=0 refstop=-29.3767055 span=29.3767 T=20 target=-20 steps=62 reached=1 term=-1 h=[0.05,0.4] first=0.05 last=0.27784 boundary=1 rhs=441 rej=1 nullmax=0 final_t=-20 wall=0.0001s
B alc_v9_s10 dir=0 ref=0 refstop=-2.03742978 span=2.03743 T=2.03743 target=-2.03743 steps=77 reached=1 term=-1 h=[0.005,0.04] first=0.005 last=0.0388523 boundary=1 rhs=588 rej=7 nullmax=0 final_t=-2.03742978 wall=0.0001s
B alc_v9_s10 dir=1 ref=0 refstop=-19.9638367 span=19.9638 T=19.9638 target=-19.9638 steps=521 reached=1 term=-1 h=[0.005,0.04] first=0.005 last=0.0365282 boundary=1 rhs=3682 rej=5 nullmax=2.22e-16 final_t=-19.9638367 wall=0.0009s
B alc_v9_s100 dir=0 ref=0 refstop=-1.10374298 span=1.10374 T=1.10374 target=-1.10374 steps=302 reached=1 term=-1 h=[0.0005,0.004] first=0.0005 last=0.00388798 boundary=1 rhs=2163 rej=7 nullmax=0 final_t=-1.10374298 wall=0.0005s
B alc_v9_s100 dir=1 ref=0 refstop=-2.89638367 span=2.89638 T=2.89638 target=-2.89638 steps=746 reached=1 term=-1 h=[0.0005,0.004] first=0.0005 last=0.00365282 boundary=1 rhs=5257 rej=5 nullmax=2.22e-16 final_t=-2.89638367 wall=0.0012s
TOTAL_OBSERVED_STEPS 2427
real 0m0.152s
user 0m0.145s
sys 0m0.006s
+ set +x
@@ -0,0 +1,142 @@
{
"r100_tight_reference": {
"vertices": 3158,
"triangles": 6188,
"outcomes": {
"0": 2906,
"1": 252
},
"reasons": {
"0": 2906,
"1": 252
},
"accepted": 2908050,
"rejected": 1963,
"rhs": 21294763,
"accepted_percentiles": {
"0": 659,
"0.5": 885,
"0.9": 1166,
"0.99": 1410,
"1": 1609
},
"rejected_percentiles": {
"0": 0,
"0.5": 0,
"0.9": 2,
"0.99": 3,
"1": 4
},
"rhs_percentiles": {
"0": 4900,
"0.5": 6496,
"0.9": 8463,
"0.99": 10171,
"1": 11564
},
"sha256": "a4f7def4212a7dae2b6290fd8635a5f1b087e11b0f8043e60e48fec71ecdb6e4",
"wall_seconds": 2.6278538939077407
},
"r100_ref_vs_0.5": {
"shared": 3158,
"only_a": 0,
"only_b": 0,
"terminal_mismatches": 0,
"max_sky_angle": 6.236425088081268e-08,
"max_logg_difference": 8.506182824080666e-10
},
"r100_ref_vs_2": {
"shared": 3158,
"only_a": 0,
"only_b": 0,
"terminal_mismatches": 0,
"max_sky_angle": 9.216337364737666e-08,
"max_logg_difference": 8.199086565935376e-10
},
"r100_ref_vs_8": {
"shared": 3158,
"only_a": 0,
"only_b": 0,
"terminal_mismatches": 0,
"max_sky_angle": 8.743723551600822e-08,
"max_logg_difference": 8.257244662329688e-10
},
"r100_ref_vs_32": {
"shared": 3158,
"only_a": 0,
"only_b": 0,
"terminal_mismatches": 0,
"max_sky_angle": 8.743704837242435e-08,
"max_logg_difference": 8.34318713074933e-10
},
"r2p1_tight_reference": {
"vertices": 5496,
"triangles": 10816,
"outcomes": {
"1": 1336,
"0": 4160
},
"reasons": {
"1": 1336,
"0": 4160
},
"accepted": 4515098,
"rejected": 8672,
"rhs": 33269502,
"accepted_percentiles": {
"0": 419,
"0.5": 805,
"0.9": 1034,
"0.99": 1253,
"1": 1860
},
"rejected_percentiles": {
"0": 1,
"0.5": 2,
"0.9": 2,
"0.99": 3,
"1": 3
},
"rhs_percentiles": {
"0": 3227,
"0.5": 5936,
"0.9": 7546,
"0.99": 9072,
"1": 13328
},
"sha256": "a81aac81cff2891627e21c22998ceb51d65ef779bb013922cadeb97140274cba",
"wall_seconds": 3.180392162874341
},
"r2p1_ref_vs_0.5": {
"shared": 5496,
"only_a": 0,
"only_b": 0,
"terminal_mismatches": 0,
"max_sky_angle": 1.4772143877471049e-05,
"max_logg_difference": 2.098608620215714e-09
},
"r2p1_ref_vs_2": {
"shared": 5496,
"only_a": 0,
"only_b": 0,
"terminal_mismatches": 0,
"max_sky_angle": 1.4772101910544689e-05,
"max_logg_difference": 2.0455042104572385e-09
},
"r2p1_ref_vs_8": {
"shared": 5496,
"only_a": 0,
"only_b": 0,
"terminal_mismatches": 0,
"max_sky_angle": 1.4772018211627154e-05,
"max_logg_difference": 2.059625359152051e-09
},
"r2p1_ref_vs_32": {
"shared": 5496,
"only_a": 0,
"only_b": 0,
"terminal_mismatches": 0,
"max_sky_angle": 1.4772020763985666e-05,
"max_logg_difference": 2.068350379857975e-09
}
}
@@ -0,0 +1,354 @@
{
"r100_hmax0.5": {
"vertices": 3158,
"triangles": 6188,
"outcomes": {
"0": 2906,
"1": 252
},
"reasons": {
"0": 2906,
"1": 252
},
"accepted": 2269662,
"rejected": 177,
"rhs": 16813349,
"accepted_percentiles": {
"0": 312,
"0.5": 736,
"0.9": 786,
"0.99": 848,
"1": 899
},
"rejected_percentiles": {
"0": 0,
"0.5": 0,
"0.9": 0,
"0.99": 1,
"1": 1
},
"rhs_percentiles": {
"0": 2471,
"0.5": 5446,
"0.9": 5803,
"0.99": 6223,
"1": 6587
},
"sha256": "787386a026203d846debf25a6a4f431a131b06bd3ab464f75f3e17a1d379992a",
"wall_seconds": 2.0714269301388413
},
"r100_hmax2": {
"vertices": 3158,
"triangles": 6188,
"outcomes": {
"0": 2906,
"1": 252
},
"reasons": {
"0": 2906,
"1": 252
},
"accepted": 723677,
"rejected": 5690,
"rhs": 6030185,
"accepted_percentiles": {
"0": 165,
"0.5": 220,
"0.9": 290,
"0.99": 348,
"1": 397
},
"rejected_percentiles": {
"0": 0,
"0.5": 2,
"0.9": 4,
"0.99": 5,
"1": 5
},
"rhs_percentiles": {
"0": 1442,
"0.5": 1855,
"0.9": 2324,
"0.99": 2737,
"1": 3073
},
"sha256": "187c3cbba6a18d88608cd3859da94d88b58ce9d65e0d800d84b3254a6b139151",
"wall_seconds": 0.9786226029973477
},
"r100_hmax8": {
"vertices": 3158,
"triangles": 6188,
"outcomes": {
"0": 2906,
"1": 252
},
"reasons": {
"0": 2906,
"1": 252
},
"accepted": 463004,
"rejected": 9336,
"rhs": 4230835,
"accepted_percentiles": {
"0": 80,
"0.5": 133,
"0.9": 225,
"0.99": 286,
"1": 357
},
"rejected_percentiles": {
"0": 0,
"0.5": 3,
"0.9": 5,
"0.99": 6,
"1": 7
},
"rhs_percentiles": {
"0": 868,
"0.5": 1253,
"0.9": 1869,
"0.99": 2289,
"1": 2786
},
"sha256": "4c7e9d401f2af237129a6a677ca94ed1ee41472594db57852a08647b1dc34aad",
"wall_seconds": 0.8228706018999219
},
"r100_hmax32": {
"vertices": 3158,
"triangles": 6188,
"outcomes": {
"0": 2906,
"1": 252
},
"reasons": {
"0": 2906,
"1": 252
},
"accepted": 441750,
"rejected": 9336,
"rhs": 4081609,
"accepted_percentiles": {
"0": 72,
"0.5": 126,
"0.9": 222,
"0.99": 285,
"1": 357
},
"rejected_percentiles": {
"0": 0,
"0.5": 3,
"0.9": 5,
"0.99": 6,
"1": 7
},
"rhs_percentiles": {
"0": 812,
"0.5": 1204,
"0.9": 1848,
"0.99": 2282,
"1": 2786
},
"sha256": "3742efd56893bec9e62a01668915ef7dda613d6504b2057043eb97c9b38334f3",
"wall_seconds": 0.8300854018889368
},
"r100_2_vs_0.5": {
"shared": 3158,
"only_a": 0,
"only_b": 0,
"terminal_mismatches": 0,
"max_sky_angle": 3.077090601306577e-08,
"max_logg_difference": 5.28674309191457e-11
},
"r100_2_vs_8": {
"shared": 3158,
"only_a": 0,
"only_b": 0,
"terminal_mismatches": 0,
"max_sky_angle": 7.063214322895989e-09,
"max_logg_difference": 3.2702011237839557e-11
},
"r100_2_vs_32": {
"shared": 3158,
"only_a": 0,
"only_b": 0,
"terminal_mismatches": 0,
"max_sky_angle": 7.063404202987903e-09,
"max_logg_difference": 2.477072019724247e-11
},
"r2p1_hmax0.5": {
"vertices": 5496,
"triangles": 10816,
"outcomes": {
"1": 1336,
"0": 4160
},
"reasons": {
"1": 1336,
"0": 4160
},
"accepted": 2646842,
"rejected": 830,
"rhs": 20136508,
"accepted_percentiles": {
"0": 116,
"0.5": 552,
"0.9": 618,
"0.99": 672,
"1": 823
},
"rejected_percentiles": {
"0": 0,
"0.5": 0,
"0.9": 1,
"0.99": 1,
"1": 2
},
"rhs_percentiles": {
"0": 1099,
"0.5": 4158,
"0.9": 4620,
"0.99": 4991,
"1": 6055
},
"sha256": "3a77a1e7dee3cd18d3ab8a408826aba932f912dc88749989e0c307013d6e7e81",
"wall_seconds": 1.973590083885938
},
"r2p1_hmax2": {
"vertices": 5496,
"triangles": 10816,
"outcomes": {
"1": 1336,
"0": 4160
},
"reasons": {
"1": 1336,
"0": 4160
},
"accepted": 1131104,
"rejected": 4004,
"rhs": 9548434,
"accepted_percentiles": {
"0": 109,
"0.5": 202,
"0.9": 258,
"0.99": 311,
"1": 460
},
"rejected_percentiles": {
"0": 0,
"0.5": 1,
"0.9": 1,
"0.99": 2,
"1": 2
},
"rhs_percentiles": {
"0": 1050,
"0.5": 1708,
"0.9": 2100,
"0.99": 2478,
"1": 3521
},
"sha256": "899331ea41d1612cf66bc7664b50e866f4f3bc7309e9c206440f801238ca455c",
"wall_seconds": 1.177934248931706
},
"r2p1_hmax8": {
"vertices": 5496,
"triangles": 10816,
"outcomes": {
"1": 1336,
"0": 4160
},
"reasons": {
"1": 1336,
"0": 4160
},
"accepted": 822680,
"rejected": 4004,
"rhs": 7389760,
"accepted_percentiles": {
"0": 87,
"0.5": 137,
"0.9": 212,
"0.99": 265,
"1": 386
},
"rejected_percentiles": {
"0": 0,
"0.5": 1,
"0.9": 1,
"0.99": 2,
"1": 2
},
"rhs_percentiles": {
"0": 910,
"0.5": 1260,
"0.9": 1778,
"0.99": 2142,
"1": 3003
},
"sha256": "49edfbe9dfb2948d3472e6ef6287bed103332758953756a7e4089316caa0b1b6",
"wall_seconds": 0.9704615350347012
},
"r2p1_hmax32": {
"vertices": 5496,
"triangles": 10816,
"outcomes": {
"1": 1336,
"0": 4160
},
"reasons": {
"1": 1336,
"0": 4160
},
"accepted": 795354,
"rejected": 4004,
"rhs": 7197806,
"accepted_percentiles": {
"0": 81,
"0.5": 132,
"0.9": 210,
"0.99": 261,
"1": 379
},
"rejected_percentiles": {
"0": 0,
"0.5": 1,
"0.9": 1,
"0.99": 2,
"1": 2
},
"rhs_percentiles": {
"0": 861,
"0.5": 1218,
"0.9": 1757,
"0.99": 2128,
"1": 2954
},
"sha256": "74bbb1e969644d49b7fdf5a6a7a5930ca8b962ec91628cd91feeb9df039100e7",
"wall_seconds": 0.9720155198592693
},
"r2p1_2_vs_0.5": {
"shared": 5496,
"only_a": 0,
"only_b": 0,
"terminal_mismatches": 0,
"max_sky_angle": 1.046601929593161e-10,
"max_logg_difference": 6.900746640781108e-11
},
"r2p1_2_vs_8": {
"shared": 5496,
"only_a": 0,
"only_b": 0,
"terminal_mismatches": 0,
"max_sky_angle": 8.369894549651799e-11,
"max_logg_difference": 1.9082291302652266e-11
},
"r2p1_2_vs_32": {
"shared": 5496,
"only_a": 0,
"only_b": 0,
"terminal_mismatches": 0,
"max_sky_angle": 8.114658658759588e-11,
"max_logg_difference": 2.823297151621773e-11
}
}
@@ -0,0 +1,54 @@
# Experiment A/B environment and source identity
date_utc: 2026-10-05T22:15:50Z
host: Linux ATRI-gentoo 6.18.48-gentoo #1 SMP PREEMPT_DYNAMIC Mon Aug 31 13:33:24 EDT 2026 x86_64 12th Gen Intel(R) Core(TM) i7-12700K GenuineIntel GNU/Linux
nproc: 16
cc:
cc (Gentoo 16.2.1_p20260926 p1) 16.2.1 20260926
Copyright (C) 2026 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
git_head: c894fdb11a0845558fdb67b71a9bd019a2d94ed4
git_status_short:
M Makefile
M mk/reference_images.mk
M nr_spacetime_movie_renderer_design.md
M src/asymptotic.c
M src/frame.c
M src/frame.h
M src/geodesic.c
M src/geodesic.h
M src/lens_map.c
M src/lens_map.h
M src/main.c
M src/ray.c
M src/ray.h
M tests/test_asymptotic.c
M tests/test_camera_cli.py
M tests/test_frame.c
M usage.md
?? nmesh_spacetime_output_format.md
?? tests/test_adaptive_cli.py
?? tests/test_geodesic_adaptive.c
# sha256 of the sources consumed by the harnesses
25f24a2e1c9ce49508fed230dfb3f6d9986e92bb4a5e6347bb2356ac0ee5fffa src/geodesic.c
1e43b95346f484050354631e8682f9793e618cbbd1de6338bd15e092216ae6fb src/geodesic.h
e6c0616461507adb1141ca3caa7e76f30e2497c4d53acfef092597fb60431a4a src/asymptotic.c
db029ef837f12800bbf81fc74e7bab6eb9f18e42a53ceb5f205867d8a329321d src/asymptotic.h
1792a17f432ceee32ee9c802486ecf9d64d96307c249e0d5cfa563ef4a0fefd5 src/asymptotic_schwarzschild.c
cc9ad0b04e11bd08bcf238901125e76a26ec332cd7fe72871e7c46fb7a322aec src/asymptotic_schwarzschild.h
ea0b5838bc56fbfeedec9110cd20653b892127bfc863db91a3a8cabaad4c520b src/asymptotic_gl48.h
359066d78a10f9589a220124147cdbefaff8f97028a9fdd2923b474349599752 src/spacetime.h
4c7ae6e96fe57e2a2ae2491db924bad56af59abfcc305bbb6ab2609f3b405e0d src/spacetime_common.c
690045ed715a951335dc7d654258e0f3b802e0bf6e2815ceb4858fa99cd372d7 src/spacetime_minkowski.c
e5b5935d16e24b07cedb25d184e8f7029075aec9496f54c23cb1061971914a72 src/spacetime_schwarzschild.c
75956034303fe3bab9324ecb87533e054458e35a26132a3e885233acad785767 src/spacetime_alcubierre.c
559f626983c68e06d3223d8381085a0b5ac422529a1ea656f4e24b1b3a50f51a src/observer.c
d761aabfbc277b0989bd20302578302d91f8253a9d900943de62ab3edd3f145b src/observer.h
# sha256 of the harnesses and this script
853018775c335180199aa3a0046445a5f795f83d18de0ff6a664a6368d835dbd a_public_endpoints.c
10a6558815e3ff5fa114aa8ca6dfe3c7db6e860ecb9f7cab20c59145a662d3d2 b_actual_h.c
1cfd93b0c9f187b4c45d169fbeb8c6fea7f02c3317ac24a32691723b0086548c run_experiment.sh
@@ -0,0 +1,318 @@
# Experiment A/B summary tables
## A Schwarzschild references (DP tol=1e-12, max_step=0.25)
| case | dir | theta | outcome | reason | stop_t | steps | rej | rhs | g | thr |
|---|---|---|---|---|---|---|---|---|---|---|
| r30_inward | 0 | 0 | DARK | REDSHIFT | -69.57 | 531 | 0 | 4004 | 0 | 8 |
| r30_inward | 1 | 3.142 | ESC | NONE | -226 | 905 | 0 | 6629 | 1.035 | nan |
| r30_inward | 2 | 1.571 | ESC | NONE | -256.7 | 1028 | 0 | 7490 | 1.035 | nan |
| r30_inward | 3 | 0.1681 | DARK | REDSHIFT | -225.9 | 3746 | 0 | 26509 | 0 | 8 |
| r30_inward | 4 | 0.1671 | DARK | REDSHIFT | -94.28 | 1127 | 0 | 8176 | 0 | 8 |
| r30_inward | 5 | 0.1691 | ESC | NONE | -322.4 | 1834 | 0 | 13132 | 1.035 | nan |
| r30_inward | 6 | 0.1681 | DARK | REDSHIFT | -118.2 | 1606 | 0 | 11529 | 0 | 8 |
| r30_inward | 7 | 0.1681 | ESC | NONE | -346.3 | 2312 | 1 | 16485 | 1.035 | nan |
| r30_inward | 8 | 0.1681 | DARK | REDSHIFT | -142.2 | 2081 | 0 | 14854 | 0 | 8 |
| r30_inward | 9 | 0.1681 | ESC | NONE | -370.2 | 2790 | 1 | 19831 | 1.035 | nan |
| r30_inward | 10 | 0.2181 | ESC | NONE | -302.6 | 1409 | 1 | 10164 | 1.035 | nan |
| r30_inward | 11 | 0.1181 | DARK | REDSHIFT | -73.76 | 665 | 0 | 4942 | 0 | 8 |
| r100_inward | 0 | 0 | DARK | REDSHIFT | -144.3 | 830 | 0 | 6097 | 0 | 8 |
| r100_inward | 1 | 3.142 | ESC | NONE | -156 | 625 | 0 | 4669 | 1.01 | nan |
| r100_inward | 2 | 1.571 | ESC | NONE | -237.7 | 952 | 0 | 6958 | 1.01 | nan |
| r100_inward | 3 | 0.05146 | DARK | REDSHIFT | -301.1 | 4045 | 0 | 28602 | 0 | 8 |
| r100_inward | 4 | 0.05046 | DARK | REDSHIFT | -163.1 | 1292 | 0 | 9331 | 0 | 8 |
| r100_inward | 5 | 0.05246 | ESC | NONE | -391.6 | 2014 | 1 | 14399 | 1.01 | nan |
| r100_inward | 6 | 0.05145 | DARK | REDSHIFT | -187.1 | 1773 | 0 | 12698 | 0 | 8 |
| r100_inward | 7 | 0.05147 | ESC | NONE | -415.4 | 2493 | 0 | 17745 | 1.01 | nan |
| r100_inward | 8 | 0.05146 | DARK | REDSHIFT | -211 | 2252 | 0 | 16051 | 0 | 8 |
| r100_inward | 9 | 0.05146 | ESC | NONE | -439.3 | 2969 | 0 | 21077 | 1.01 | nan |
| r100_inward | 10 | 0.1015 | ESC | NONE | -372.3 | 1571 | 0 | 11291 | 1.01 | nan |
| r100_inward | 11 | 0.001462 | DARK | REDSHIFT | -144.3 | 830 | 0 | 6097 | 0 | 8 |
| r2p1_outward | 0 | 0 | ESC | NONE | -253.9 | 1120 | 1 | 8141 | 4.583 | nan |
| r2p1_outward | 1 | 3.142 | DARK | REDSHIFT | -32.25 | 186 | 0 | 1589 | 0 | 8 |
| r2p1_outward | 2 | 1.571 | DARK | REDSHIFT | -32.77 | 316 | 1 | 2506 | 0 | 8 |
| r2p1_outward | 3 | 0.5704 | ESC | NONE | -414.9 | 4263 | 2 | 30149 | 4.583 | nan |
| r2p1_outward | 4 | 0.5694 | ESC | NONE | -285.6 | 1686 | 1 | 12103 | 4.583 | nan |
| r2p1_outward | 5 | 0.5714 | DARK | REDSHIFT | -61.58 | 995 | 1 | 7259 | 0 | 8 |
| r2p1_outward | 6 | 0.5704 | ESC | NONE | -309.5 | 2162 | 2 | 15442 | 4.583 | nan |
| r2p1_outward | 7 | 0.5704 | DARK | REDSHIFT | -85.49 | 1474 | 1 | 10612 | 0 | 8 |
| r2p1_outward | 8 | 0.5704 | ESC | NONE | -333.5 | 2639 | 1 | 18774 | 4.583 | nan |
| r2p1_outward | 9 | 0.5704 | DARK | REDSHIFT | -109.4 | 1950 | 1 | 13944 | 0 | 8 |
| r2p1_outward | 10 | 0.6204 | DARK | REDSHIFT | -41.99 | 585 | 1 | 4389 | 0 | 8 |
| r2p1_outward | 11 | 0.5204 | ESC | NONE | -265 | 1303 | 2 | 9429 | 4.583 | nan |
| r1p5_freefall | 0 | 0 | DARK | REDSHIFT | -30.66 | 231 | 1 | 1911 | 0 | 8 |
| r1p5_freefall | 1 | 1.571 | ESC | NONE | -255.2 | 1130 | 1 | 8211 | 1 | nan |
| r1p5_freefall | 2 | 3.142 | ESC | NONE | -254.5 | 1143 | 1 | 8302 | 0.4641 | nan |
| r1p5_freefall | 3 | 2.356 | ESC | NONE | -254.6 | 1127 | 1 | 8190 | 0.5505 | nan |
## A Schwarzschild upper scan (min_step=1e-12, tol=1e-9)
| case | upper | mismatch | max_dn_ang | max_dgrel | max_dstopT | sum_rhs | max_rej | ESC | DARK | UNRES | INC |
|---|---|---|---|---|---|---|---|---|---|---|---|
| r100_inward | 0.25 | 0 | 2.604e-05 | 2.048e-09 | 35.2 | 93464 | 0 | 6 | 6 | 0 | 0 |
| r100_inward | 0.5 | 0 | 5.696e-05 | 2.056e-09 | 39.27 | 56595 | 0 | 6 | 6 | 0 | 0 |
| r100_inward | 1 | 0 | 8.348e-05 | 2.051e-09 | 41.25 | 39788 | 1 | 6 | 6 | 0 | 0 |
| r100_inward | 2 | 0 | 8.286e-05 | 2.026e-09 | 41.22 | 32389 | 1 | 6 | 6 | 0 | 0 |
| r100_inward | 4 | 0 | 8.089e-05 | 2.028e-09 | 41.09 | 29393 | 2 | 6 | 6 | 0 | 0 |
| r100_inward | 8 | 0 | 8.034e-05 | 2.034e-09 | 41.06 | 28427 | 3 | 6 | 6 | 0 | 0 |
| r100_inward | 16 | 0 | 8.034e-05 | 2.042e-09 | 41.06 | 28189 | 3 | 6 | 6 | 0 | 0 |
| r100_inward | 32 | 0 | 8.034e-05 | 2.043e-09 | 41.06 | 28182 | 3 | 6 | 6 | 0 | 0 |
| r100_inward | 64 | 0 | 8.034e-05 | 2.043e-09 | 41.06 | 28182 | 3 | 6 | 6 | 0 | 0 |
| r1p5_freefall | 0.25 | 0 | 1.002e-10 | 3.165e-10 | 2.608e-08 | 23597 | 0 | 3 | 1 | 0 | 0 |
| r1p5_freefall | 0.5 | 0 | 8.875e-11 | 3.658e-10 | 3.089e-08 | 12747 | 0 | 3 | 1 | 0 | 0 |
| r1p5_freefall | 1 | 0 | 8.187e-11 | 3.811e-10 | 2.817e-08 | 7553 | 0 | 3 | 1 | 0 | 0 |
| r1p5_freefall | 2 | 0 | 8.165e-11 | 3.719e-10 | 2.571e-08 | 5131 | 0 | 3 | 1 | 0 | 0 |
| r1p5_freefall | 4 | 0 | 8.157e-11 | 3.632e-10 | 2.346e-08 | 4039 | 0 | 3 | 1 | 0 | 0 |
| r1p5_freefall | 8 | 0 | 8.151e-11 | 3.547e-10 | 2.125e-08 | 3591 | 0 | 3 | 1 | 0 | 0 |
| r1p5_freefall | 16 | 0 | 8.146e-11 | 3.469e-10 | 1.925e-08 | 3465 | 0 | 3 | 1 | 0 | 0 |
| r1p5_freefall | 32 | 0 | 8.146e-11 | 3.46e-10 | 1.948e-08 | 3458 | 0 | 3 | 1 | 0 | 0 |
| r1p5_freefall | 64 | 0 | 8.146e-11 | 3.46e-10 | 1.948e-08 | 3458 | 0 | 3 | 1 | 0 | 0 |
| r2p1_outward | 0.25 | 1 | 0.0002872 | 2.661e-09 | 263.1 | 62258 | 0 | 5 | 7 | 0 | 0 |
| r2p1_outward | 0.5 | 1 | 0.0002872 | 2.644e-09 | 263.1 | 42490 | 0 | 5 | 7 | 0 | 0 |
| r2p1_outward | 1 | 1 | 0.0002872 | 2.615e-09 | 263.1 | 33544 | 1 | 5 | 7 | 0 | 0 |
| r2p1_outward | 2 | 1 | 0.0002872 | 2.592e-09 | 263.1 | 29421 | 1 | 5 | 7 | 0 | 0 |
| r2p1_outward | 4 | 1 | 0.0002872 | 2.598e-09 | 263.1 | 27580 | 1 | 5 | 7 | 0 | 0 |
| r2p1_outward | 8 | 1 | 0.0002872 | 2.606e-09 | 263.1 | 26831 | 1 | 5 | 7 | 0 | 0 |
| r2p1_outward | 16 | 1 | 0.0002872 | 2.614e-09 | 263.1 | 26614 | 1 | 5 | 7 | 0 | 0 |
| r2p1_outward | 32 | 1 | 0.0002872 | 2.615e-09 | 263.1 | 26607 | 1 | 5 | 7 | 0 | 0 |
| r2p1_outward | 64 | 1 | 0.0002872 | 2.615e-09 | 263.1 | 26607 | 1 | 5 | 7 | 0 | 0 |
| r30_inward | 0.25 | 0 | 8.667e-05 | 2.262e-09 | 35.08 | 76510 | 0 | 6 | 6 | 0 | 0 |
| r30_inward | 0.5 | 0 | 0.0001866 | 2.268e-09 | 39.06 | 49063 | 1 | 6 | 6 | 0 | 0 |
| r30_inward | 1 | 0 | 0.0002801 | 2.266e-09 | 41.17 | 36974 | 2 | 6 | 6 | 0 | 0 |
| r30_inward | 2 | 0 | 0.0002819 | 2.244e-09 | 41.2 | 31843 | 2 | 6 | 6 | 0 | 0 |
| r30_inward | 4 | 0 | 0.0002819 | 2.25e-09 | 41.2 | 29617 | 2 | 6 | 6 | 0 | 0 |
| r30_inward | 8 | 0 | 0.0002819 | 2.258e-09 | 41.2 | 28714 | 2 | 6 | 6 | 0 | 0 |
| r30_inward | 16 | 0 | 0.0002819 | 2.266e-09 | 41.2 | 28455 | 2 | 6 | 6 | 0 | 0 |
| r30_inward | 32 | 0 | 0.0002819 | 2.266e-09 | 41.2 | 28434 | 2 | 6 | 6 | 0 | 0 |
| r30_inward | 64 | 0 | 0.0002819 | 2.266e-09 | 41.2 | 28434 | 2 | 6 | 6 | 0 | 0 |
## A Schwarzschild min scan (max_step=2, tol=1e-9)
| case | min_step | mismatch | max_dn_ang | max_dgrel | max_dstopT | sum_rhs | max_rej | ESC | DARK | UNRES | INC |
|---|---|---|---|---|---|---|---|---|---|---|---|
| r100_inward | 1e-14 | 0 | 8.286e-05 | 2.026e-09 | 41.22 | 32389 | 1 | 6 | 6 | 0 | 0 |
| r100_inward | 1e-12 | 0 | 8.286e-05 | 2.026e-09 | 41.22 | 32389 | 1 | 6 | 6 | 0 | 0 |
| r100_inward | 1e-10 | 0 | 8.286e-05 | 2.026e-09 | 41.22 | 32389 | 1 | 6 | 6 | 0 | 0 |
| r100_inward | 1e-08 | 0 | 8.286e-05 | 2.026e-09 | 41.22 | 32389 | 1 | 6 | 6 | 0 | 0 |
| r100_inward | 1e-06 | 0 | 8.286e-05 | 2.026e-09 | 41.22 | 32389 | 1 | 6 | 6 | 0 | 0 |
| r100_inward | 1e-05 | 0 | 8.286e-05 | 2.026e-09 | 41.22 | 32389 | 1 | 6 | 6 | 0 | 0 |
| r100_inward | 0.0001 | 0 | 8.286e-05 | 2.026e-09 | 41.22 | 32389 | 1 | 6 | 6 | 0 | 0 |
| r100_inward | 0.001 | 0 | 8.286e-05 | 2.026e-09 | 41.22 | 32389 | 1 | 6 | 6 | 0 | 0 |
| r100_inward | 0.01 | 0 | 8.286e-05 | 2.026e-09 | 41.22 | 32389 | 1 | 6 | 6 | 0 | 0 |
| r100_inward | 0.1 | 0 | 8.286e-05 | 2.026e-09 | 41.22 | 32389 | 1 | 6 | 6 | 0 | 0 |
| r1p5_freefall | 1e-14 | 0 | 8.165e-11 | 3.719e-10 | 2.571e-08 | 5131 | 0 | 3 | 1 | 0 | 0 |
| r1p5_freefall | 1e-12 | 0 | 8.165e-11 | 3.719e-10 | 2.571e-08 | 5131 | 0 | 3 | 1 | 0 | 0 |
| r1p5_freefall | 1e-10 | 0 | 8.165e-11 | 3.719e-10 | 2.571e-08 | 5131 | 0 | 3 | 1 | 0 | 0 |
| r1p5_freefall | 1e-08 | 0 | 8.165e-11 | 3.719e-10 | 2.571e-08 | 5131 | 0 | 3 | 1 | 0 | 0 |
| r1p5_freefall | 1e-06 | 0 | 8.165e-11 | 3.719e-10 | 2.571e-08 | 5131 | 0 | 3 | 1 | 0 | 0 |
| r1p5_freefall | 1e-05 | 0 | 8.165e-11 | 3.719e-10 | 2.571e-08 | 5131 | 0 | 3 | 1 | 0 | 0 |
| r1p5_freefall | 0.0001 | 0 | 8.165e-11 | 3.719e-10 | 2.571e-08 | 5131 | 0 | 3 | 1 | 0 | 0 |
| r1p5_freefall | 0.001 | 0 | 8.165e-11 | 3.719e-10 | 2.571e-08 | 5131 | 0 | 3 | 1 | 0 | 0 |
| r1p5_freefall | 0.01 | 0 | 8.165e-11 | 3.719e-10 | 2.571e-08 | 5131 | 0 | 3 | 1 | 0 | 0 |
| r1p5_freefall | 0.1 | 1 | 8.165e-11 | 3.719e-10 | 254.3 | 3654 | 0 | 2 | 1 | 0 | 1 |
| r2p1_outward | 1e-14 | 1 | 0.0002872 | 2.592e-09 | 263.1 | 29421 | 1 | 5 | 7 | 0 | 0 |
| r2p1_outward | 1e-12 | 1 | 0.0002872 | 2.592e-09 | 263.1 | 29421 | 1 | 5 | 7 | 0 | 0 |
| r2p1_outward | 1e-10 | 1 | 0.0002872 | 2.592e-09 | 263.1 | 29421 | 1 | 5 | 7 | 0 | 0 |
| r2p1_outward | 1e-08 | 1 | 0.0002872 | 2.592e-09 | 263.1 | 29421 | 1 | 5 | 7 | 0 | 0 |
| r2p1_outward | 1e-06 | 1 | 0.0002872 | 2.592e-09 | 263.1 | 29421 | 1 | 5 | 7 | 0 | 0 |
| r2p1_outward | 1e-05 | 1 | 0.0002872 | 2.592e-09 | 263.1 | 29421 | 1 | 5 | 7 | 0 | 0 |
| r2p1_outward | 0.0001 | 1 | 0.0002872 | 2.592e-09 | 263.1 | 29421 | 1 | 5 | 7 | 0 | 0 |
| r2p1_outward | 0.001 | 1 | 0.0002872 | 2.592e-09 | 263.1 | 29421 | 1 | 5 | 7 | 0 | 0 |
| r2p1_outward | 0.01 | 1 | 0.0002872 | 2.592e-09 | 263.1 | 29421 | 1 | 5 | 7 | 0 | 0 |
| r2p1_outward | 0.1 | 1 | 0.0002872 | 2.592e-09 | 263.1 | 29421 | 1 | 5 | 7 | 0 | 0 |
| r30_inward | 1e-14 | 0 | 0.0002819 | 2.244e-09 | 41.2 | 31843 | 2 | 6 | 6 | 0 | 0 |
| r30_inward | 1e-12 | 0 | 0.0002819 | 2.244e-09 | 41.2 | 31843 | 2 | 6 | 6 | 0 | 0 |
| r30_inward | 1e-10 | 0 | 0.0002819 | 2.244e-09 | 41.2 | 31843 | 2 | 6 | 6 | 0 | 0 |
| r30_inward | 1e-08 | 0 | 0.0002819 | 2.244e-09 | 41.2 | 31843 | 2 | 6 | 6 | 0 | 0 |
| r30_inward | 1e-06 | 0 | 0.0002819 | 2.244e-09 | 41.2 | 31843 | 2 | 6 | 6 | 0 | 0 |
| r30_inward | 1e-05 | 0 | 0.0002819 | 2.244e-09 | 41.2 | 31843 | 2 | 6 | 6 | 0 | 0 |
| r30_inward | 0.0001 | 0 | 0.0002819 | 2.244e-09 | 41.2 | 31843 | 2 | 6 | 6 | 0 | 0 |
| r30_inward | 0.001 | 0 | 0.0002819 | 2.244e-09 | 41.2 | 31843 | 2 | 6 | 6 | 0 | 0 |
| r30_inward | 0.01 | 0 | 0.0002819 | 2.244e-09 | 41.2 | 31843 | 2 | 6 | 6 | 0 | 0 |
| r30_inward | 0.1 | 0 | 0.0002819 | 2.244e-09 | 41.2 | 31843 | 2 | 6 | 6 | 0 | 0 |
## A Schwarzschild sensitive RK4 .01 vs .005
| case | dir | theta | out.01 | out.005 | hhalve_dn_ang | |dg| | rhs.01 | rhs.005 |
|---|---|---|---|---|---|---|---|---|
| r2p1_outward | 6 | 0.5704 | ESC | ESC | 1.032e-07 | 7.386e-12 | 124072 | 247880 |
| r2p1_outward | 7 | 0.5704 | DARK | DARK | nan | 0 | 34196 | 68392 |
| r2p1_outward | 8 | 0.5704 | ESC | ESC | 1.04e-05 | 7.618e-12 | 133644 | 267024 |
| r2p1_outward | 9 | 0.5704 | DARK | DARK | nan | 0 | 43768 | 87536 |
| r30_inward | 6 | 0.1681 | DARK | DARK | nan | 0 | 47300 | 94600 |
| r30_inward | 7 | 0.1681 | ESC | ESC | 0 | 8.238e-14 | 138764 | 277264 |
| r30_inward | 8 | 0.1681 | DARK | DARK | nan | 0 | 56872 | 113744 |
| r30_inward | 9 | 0.1681 | ESC | ESC | 1.891e-07 | 7.55e-15 | 148336 | 296408 |
## A Minkowski flat analytic check (n_inf and t)
| upper | min_step | n | max_dn_ang | max_dgrel | max_t_err |
|---|---|---|---|---|---|
| 1 | 1e-12 | 6 | 0 | 0 | 5.258e-13 |
| 1 | 1e-06 | 6 | 0 | 0 | 5.258e-13 |
| 1 | 0.1 | 6 | 0 | 0 | 5.258e-13 |
| 4 | 1e-12 | 6 | 0 | 0 | 1.776e-12 |
| 4 | 1e-06 | 6 | 0 | 0 | 1.776e-12 |
| 4 | 0.1 | 6 | 0 | 0 | 1.776e-12 |
| 16 | 1e-12 | 6 | 0 | 0 | 1.3e-11 |
| 16 | 1e-06 | 6 | 0 | 0 | 1.3e-11 |
| 16 | 0.1 | 6 | 0 | 0 | 1.3e-11 |
| 64 | 1e-12 | 6 | 0 | 0 | 3.894e-11 |
| 64 | 1e-06 | 6 | 0 | 0 | 3.894e-11 |
| 64 | 0.1 | 6 | 0 | 0 | 3.894e-11 |
| 256 | 1e-12 | 6 | 0 | 0 | 7.661e-11 |
| 256 | 1e-06 | 6 | 0 | 0 | 7.661e-11 |
| 256 | 0.1 | 6 | 0 | 0 | 7.661e-11 |
## A Minkowski flat analytic check (crossing x and t)
| upper | min_step | n | max_x_err | max_t_err |
|---|---|---|---|---|
| 1 | 1e-12 | 6 | 4.832e-13 | 5.258e-13 |
| 1 | 1e-06 | 6 | 4.832e-13 | 5.258e-13 |
| 1 | 0.1 | 6 | 4.832e-13 | 5.258e-13 |
| 4 | 1e-12 | 6 | 1.72e-12 | 1.776e-12 |
| 4 | 1e-06 | 6 | 1.72e-12 | 1.776e-12 |
| 4 | 0.1 | 6 | 1.72e-12 | 1.776e-12 |
| 16 | 1e-12 | 6 | 1.268e-11 | 1.3e-11 |
| 16 | 1e-06 | 6 | 1.268e-11 | 1.3e-11 |
| 16 | 0.1 | 6 | 1.268e-11 | 1.3e-11 |
| 64 | 1e-12 | 6 | 3.506e-11 | 3.894e-11 |
| 64 | 1e-06 | 6 | 3.506e-11 | 3.894e-11 |
| 64 | 0.1 | 6 | 3.506e-11 | 3.894e-11 |
| 256 | 1e-12 | 6 | 7.061e-11 | 7.661e-11 |
| 256 | 1e-06 | 6 | 7.061e-11 | 7.661e-11 |
| 256 | 0.1 | 6 | 7.061e-11 | 7.661e-11 |
## A Alcubierre alc_v3_s1 upper scan
| case | upper | mismatch | max_dn_ang | max_dgrel | max_dstopT | sum_rhs | max_rej | ESC | DARK | UNRES | INC |
|---|---|---|---|---|---|---|---|---|---|---|---|
| alc_v3_s1 | 0.05 | 0 | 7.544e-14 | 7.627e-13 | 1.99e-13 | 19740 | 0 | 6 | 0 | 0 | 0 |
| alc_v3_s1 | 0.05 | 0 | 7.544e-14 | 7.627e-13 | 1.99e-13 | 19740 | 0 | 6 | 0 | 0 | 0 |
| alc_v3_s1 | 0.05 | 0 | 7.544e-14 | 7.627e-13 | 1.99e-13 | 19740 | 0 | 6 | 0 | 0 | 0 |
| alc_v3_s1 | 0.2 | 0 | 1.208e-10 | 3.452e-10 | 4.091e-10 | 6475 | 2 | 6 | 0 | 0 | 0 |
| alc_v3_s1 | 0.2 | 0 | 1.208e-10 | 3.452e-10 | 4.091e-10 | 6475 | 2 | 6 | 0 | 0 | 0 |
| alc_v3_s1 | 0.2 | 0 | 1.208e-10 | 3.452e-10 | 4.091e-10 | 6475 | 2 | 6 | 0 | 0 | 0 |
| alc_v3_s1 | 0.4 | 0 | 1.03e-10 | 3.464e-10 | 3.143e-10 | 4613 | 2 | 6 | 0 | 0 | 0 |
| alc_v3_s1 | 0.4 | 0 | 1.03e-10 | 3.464e-10 | 3.143e-10 | 4613 | 2 | 6 | 0 | 0 | 0 |
| alc_v3_s1 | 0.4 | 0 | 1.03e-10 | 3.464e-10 | 3.143e-10 | 4613 | 2 | 6 | 0 | 0 | 0 |
| alc_v3_s1 | 0.8 | 0 | 7.888e-11 | 7.954e-10 | 7.369e-10 | 3794 | 2 | 6 | 0 | 0 | 0 |
| alc_v3_s1 | 0.8 | 0 | 7.888e-11 | 7.954e-10 | 7.369e-10 | 3794 | 2 | 6 | 0 | 0 | 0 |
| alc_v3_s1 | 0.8 | 0 | 7.888e-11 | 7.954e-10 | 7.369e-10 | 3794 | 2 | 6 | 0 | 0 | 0 |
| alc_v3_s1 | 1.6 | 0 | 1.239e-10 | 1.118e-09 | 1.148e-09 | 3430 | 2 | 6 | 0 | 0 | 0 |
| alc_v3_s1 | 1.6 | 0 | 1.239e-10 | 1.118e-09 | 1.148e-09 | 3430 | 2 | 6 | 0 | 0 | 0 |
| alc_v3_s1 | 1.6 | 0 | 1.239e-10 | 1.118e-09 | 1.148e-09 | 3430 | 2 | 6 | 0 | 0 | 0 |
| alc_v3_s1 | 3.2 | 0 | 1.545e-10 | 1.26e-09 | 1.346e-09 | 3290 | 2 | 6 | 0 | 0 | 0 |
| alc_v3_s1 | 3.2 | 0 | 1.545e-10 | 1.26e-09 | 1.346e-09 | 3290 | 2 | 6 | 0 | 0 | 0 |
| alc_v3_s1 | 3.2 | 0 | 1.545e-10 | 1.26e-09 | 1.346e-09 | 3290 | 2 | 6 | 0 | 0 | 0 |
## A Alcubierre alc_v3_s10 upper scan
| case | upper | mismatch | max_dn_ang | max_dgrel | max_dstopT | sum_rhs | max_rej | ESC | DARK | UNRES | INC |
|---|---|---|---|---|---|---|---|---|---|---|---|
| alc_v3_s10 | 0.005 | 0 | 5.829e-15 | 3.109e-14 | 4.663e-14 | 27412 | 0 | 6 | 0 | 0 | 0 |
| alc_v3_s10 | 0.005 | 0 | 5.829e-15 | 3.109e-14 | 4.663e-14 | 27412 | 0 | 6 | 0 | 0 | 0 |
| alc_v3_s10 | 0.005 | 0 | 5.829e-15 | 3.109e-14 | 4.663e-14 | 27412 | 0 | 6 | 0 | 0 | 0 |
| alc_v3_s10 | 0.02 | 0 | 6.299e-12 | 2.402e-10 | 1.399e-11 | 8337 | 4 | 6 | 0 | 0 | 0 |
| alc_v3_s10 | 0.02 | 0 | 6.299e-12 | 2.402e-10 | 1.399e-11 | 8337 | 4 | 6 | 0 | 0 | 0 |
| alc_v3_s10 | 0.02 | 0 | 6.299e-12 | 2.402e-10 | 1.399e-11 | 8337 | 4 | 6 | 0 | 0 | 0 |
| alc_v3_s10 | 0.04 | 0 | 3.288e-11 | 3.854e-10 | 6.294e-11 | 5621 | 6 | 6 | 0 | 0 | 0 |
| alc_v3_s10 | 0.04 | 0 | 3.288e-11 | 3.854e-10 | 6.294e-11 | 5621 | 6 | 6 | 0 | 0 | 0 |
| alc_v3_s10 | 0.04 | 0 | 3.288e-11 | 3.854e-10 | 6.294e-11 | 5621 | 6 | 6 | 0 | 0 | 0 |
| alc_v3_s10 | 0.08 | 0 | 1.042e-11 | 2.781e-10 | 8.024e-11 | 4648 | 12 | 6 | 0 | 0 | 0 |
| alc_v3_s10 | 0.08 | 0 | 1.042e-11 | 2.781e-10 | 8.024e-11 | 4648 | 12 | 6 | 0 | 0 | 0 |
| alc_v3_s10 | 0.08 | 0 | 1.042e-11 | 2.781e-10 | 8.024e-11 | 4648 | 12 | 6 | 0 | 0 | 0 |
| alc_v3_s10 | 0.16 | 0 | 1.131e-11 | 7.6e-10 | 9.538e-11 | 4270 | 13 | 6 | 0 | 0 | 0 |
| alc_v3_s10 | 0.16 | 0 | 1.131e-11 | 7.6e-10 | 9.538e-11 | 4270 | 13 | 6 | 0 | 0 | 0 |
| alc_v3_s10 | 0.16 | 0 | 1.131e-11 | 7.6e-10 | 9.538e-11 | 4270 | 13 | 6 | 0 | 0 | 0 |
| alc_v3_s10 | 0.32 | 0 | 3.131e-11 | 9.778e-10 | 1.041e-10 | 4116 | 13 | 6 | 0 | 0 | 0 |
| alc_v3_s10 | 0.32 | 0 | 3.131e-11 | 9.778e-10 | 1.041e-10 | 4116 | 13 | 6 | 0 | 0 | 0 |
| alc_v3_s10 | 0.32 | 0 | 3.131e-11 | 9.778e-10 | 1.041e-10 | 4116 | 13 | 6 | 0 | 0 | 0 |
## A Alcubierre alc_v9_s1 upper scan
| case | upper | mismatch | max_dn_ang | max_dgrel | max_dstopT | sum_rhs | max_rej | ESC | DARK | UNRES | INC |
|---|---|---|---|---|---|---|---|---|---|---|---|
| alc_v9_s1 | 0.05 | 0 | 6.632e-13 | 2.591e-11 | 1.387e-11 | 39452 | 0 | 6 | 0 | 0 | 0 |
| alc_v9_s1 | 0.05 | 0 | 6.632e-13 | 2.591e-11 | 1.387e-11 | 39452 | 0 | 6 | 0 | 0 | 0 |
| alc_v9_s1 | 0.05 | 0 | 6.632e-13 | 2.591e-11 | 1.387e-11 | 39452 | 0 | 6 | 0 | 0 | 0 |
| alc_v9_s1 | 0.2 | 0 | 1.078e-10 | 2.073e-09 | 6.805e-10 | 11690 | 3 | 6 | 0 | 0 | 0 |
| alc_v9_s1 | 0.2 | 0 | 1.078e-10 | 2.073e-09 | 6.805e-10 | 11690 | 3 | 6 | 0 | 0 | 0 |
| alc_v9_s1 | 0.2 | 0 | 1.078e-10 | 2.073e-09 | 6.805e-10 | 11690 | 3 | 6 | 0 | 0 | 0 |
| alc_v9_s1 | 0.4 | 0 | 6.25e-11 | 2.466e-09 | 1.083e-09 | 7413 | 3 | 6 | 0 | 0 | 0 |
| alc_v9_s1 | 0.4 | 0 | 6.25e-11 | 2.466e-09 | 1.083e-09 | 7413 | 3 | 6 | 0 | 0 | 0 |
| alc_v9_s1 | 0.4 | 0 | 6.25e-11 | 2.466e-09 | 1.083e-09 | 7413 | 3 | 6 | 0 | 0 | 0 |
| alc_v9_s1 | 0.8 | 0 | 5.349e-11 | 2.718e-09 | 1.186e-09 | 5369 | 3 | 6 | 0 | 0 | 0 |
| alc_v9_s1 | 0.8 | 0 | 5.349e-11 | 2.718e-09 | 1.186e-09 | 5369 | 3 | 6 | 0 | 0 | 0 |
| alc_v9_s1 | 0.8 | 0 | 5.349e-11 | 2.718e-09 | 1.186e-09 | 5369 | 3 | 6 | 0 | 0 | 0 |
| alc_v9_s1 | 1.6 | 0 | 1.035e-10 | 2.819e-09 | 9.549e-10 | 4431 | 3 | 6 | 0 | 0 | 0 |
| alc_v9_s1 | 1.6 | 0 | 1.035e-10 | 2.819e-09 | 9.549e-10 | 4431 | 3 | 6 | 0 | 0 | 0 |
| alc_v9_s1 | 1.6 | 0 | 1.035e-10 | 2.819e-09 | 9.549e-10 | 4431 | 3 | 6 | 0 | 0 | 0 |
| alc_v9_s1 | 3.2 | 0 | 1.239e-10 | 2.823e-09 | 3.249e-09 | 3983 | 3 | 6 | 0 | 0 | 0 |
| alc_v9_s1 | 3.2 | 0 | 1.239e-10 | 2.823e-09 | 3.249e-09 | 3983 | 3 | 6 | 0 | 0 | 0 |
| alc_v9_s1 | 3.2 | 0 | 1.239e-10 | 2.823e-09 | 3.249e-09 | 3983 | 3 | 6 | 0 | 0 | 0 |
## A Alcubierre alc_v9_s10 upper scan
| case | upper | mismatch | max_dn_ang | max_dgrel | max_dstopT | sum_rhs | max_rej | ESC | DARK | UNRES | INC |
|---|---|---|---|---|---|---|---|---|---|---|---|
| alc_v9_s10 | 0.005 | 0 | 8.937e-14 | 1.103e-11 | 3.553e-13 | 48167 | 0 | 6 | 0 | 0 | 0 |
| alc_v9_s10 | 0.005 | 0 | 8.937e-14 | 1.103e-11 | 3.553e-13 | 48167 | 0 | 6 | 0 | 0 | 0 |
| alc_v9_s10 | 0.005 | 0 | 8.937e-14 | 1.103e-11 | 3.553e-13 | 48167 | 0 | 6 | 0 | 0 | 0 |
| alc_v9_s10 | 0.02 | 0 | 5.331e-11 | 6.906e-10 | 1.092e-10 | 13692 | 6 | 6 | 0 | 0 | 0 |
| alc_v9_s10 | 0.02 | 0 | 5.331e-11 | 6.906e-10 | 1.092e-10 | 13692 | 6 | 6 | 0 | 0 | 0 |
| alc_v9_s10 | 0.02 | 0 | 5.331e-11 | 6.906e-10 | 1.092e-10 | 13692 | 6 | 6 | 0 | 0 | 0 |
| alc_v9_s10 | 0.04 | 0 | 1.752e-10 | 9.749e-10 | 3.646e-10 | 8484 | 7 | 6 | 0 | 0 | 0 |
| alc_v9_s10 | 0.04 | 0 | 1.752e-10 | 9.749e-10 | 3.646e-10 | 8484 | 7 | 6 | 0 | 0 | 0 |
| alc_v9_s10 | 0.04 | 0 | 1.752e-10 | 9.749e-10 | 3.646e-10 | 8484 | 7 | 6 | 0 | 0 | 0 |
| alc_v9_s10 | 0.08 | 0 | 1.196e-10 | 1.457e-09 | 2.695e-10 | 6251 | 12 | 6 | 0 | 0 | 0 |
| alc_v9_s10 | 0.08 | 0 | 1.196e-10 | 1.457e-09 | 2.695e-10 | 6251 | 12 | 6 | 0 | 0 | 0 |
| alc_v9_s10 | 0.08 | 0 | 1.196e-10 | 1.457e-09 | 2.695e-10 | 6251 | 12 | 6 | 0 | 0 | 0 |
| alc_v9_s10 | 0.16 | 0 | 1.727e-11 | 1.077e-09 | 1.26e-10 | 5278 | 15 | 6 | 0 | 0 | 0 |
| alc_v9_s10 | 0.16 | 0 | 1.727e-11 | 1.077e-09 | 1.26e-10 | 5278 | 15 | 6 | 0 | 0 | 0 |
| alc_v9_s10 | 0.16 | 0 | 1.727e-11 | 1.077e-09 | 1.26e-10 | 5278 | 15 | 6 | 0 | 0 | 0 |
| alc_v9_s10 | 0.32 | 0 | 5.499e-11 | 8.68e-10 | 2.555e-10 | 4872 | 14 | 6 | 0 | 0 | 0 |
| alc_v9_s10 | 0.32 | 0 | 5.499e-11 | 8.68e-10 | 2.555e-10 | 4872 | 14 | 6 | 0 | 0 | 0 |
| alc_v9_s10 | 0.32 | 0 | 5.499e-11 | 8.68e-10 | 2.555e-10 | 4872 | 14 | 6 | 0 | 0 | 0 |
## A Alcubierre v9 s100 lower-bound stress (upper=0.004)
| case | min_step | mismatch | max_dn_ang | max_dgrel | max_dstopT | sum_rhs | max_rej | ESC | DARK | UNRES | INC |
|---|---|---|---|---|---|---|---|---|---|---|---|
| alc_v9_s100 | 1e-14 | 0 | 1.695e-11 | 1.332e-09 | 1.057e-11 | 18095 | 7 | 6 | 0 | 0 | 0 |
| alc_v9_s100 | 1e-12 | 0 | 1.695e-11 | 1.332e-09 | 1.057e-11 | 18095 | 7 | 6 | 0 | 0 | 0 |
| alc_v9_s100 | 1e-08 | 0 | 1.695e-11 | 1.332e-09 | 1.057e-11 | 18095 | 7 | 6 | 0 | 0 | 0 |
| alc_v9_s100 | 1e-06 | 0 | 1.695e-11 | 1.332e-09 | 1.057e-11 | 18095 | 7 | 6 | 0 | 0 | 0 |
| alc_v9_s100 | 1e-05 | 0 | 1.695e-11 | 1.332e-09 | 1.057e-11 | 18095 | 7 | 6 | 0 | 0 | 0 |
| alc_v9_s100 | 0.0001 | 0 | 1.695e-11 | 1.332e-09 | 1.057e-11 | 18095 | 7 | 6 | 0 | 0 | 0 |
## B observed accepted step h (finite T=min(20, ref span))
| case | dir | ref | span | target | steps | reached | term | h_min | h_max | h_first | bnd | rhs | rej | null_max |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| r30_inward | 0 | DARK | 69.57 | -20 | 17 | 1 | -1 | 0.1 | 1.726 | 0.1 | 1 | 119 | 0 | 3.009e-12 |
| r30_inward | 1 | ESC | 226 | -20 | 13 | 1 | -1 | 0.1 | 2 | 0.1 | 1 | 91 | 0 | 8.69e-12 |
| r30_inward | 2 | DARK | 142.2 | -20 | 20 | 1 | -1 | 0.1 | 1.408 | 0.1 | 1 | 140 | 0 | 1.526e-11 |
| r30_inward | 3 | ESC | 370.2 | -20 | 20 | 1 | -1 | 0.1 | 1.408 | 0.1 | 1 | 140 | 0 | 1.526e-11 |
| r30_inward | 4 | DARK | 118.2 | -20 | 20 | 1 | -1 | 0.1 | 1.408 | 0.1 | 1 | 140 | 0 | 1.526e-11 |
| r30_inward | 5 | ESC | 346.3 | -20 | 20 | 1 | -1 | 0.1 | 1.408 | 0.1 | 1 | 140 | 0 | 1.526e-11 |
| r100_inward | 0 | DARK | 144.3 | -20 | 12 | 1 | -1 | 0.1 | 2 | 0.1 | 1 | 84 | 0 | 1.454e-14 |
| r100_inward | 1 | ESC | 156 | -20 | 12 | 1 | -1 | 0.1 | 2 | 0.1 | 1 | 84 | 0 | 7.661e-15 |
| r100_inward | 2 | DARK | 301.1 | -20 | 12 | 1 | -1 | 0.1 | 2 | 0.1 | 1 | 84 | 0 | 1.399e-14 |
| r100_inward | 3 | DARK | 211 | -20 | 12 | 1 | -1 | 0.1 | 2 | 0.1 | 1 | 84 | 0 | 1.321e-14 |
| r2p1_outward | 0 | ESC | 253.9 | -20 | 46 | 1 | -1 | 0.1 | 1.071 | 0.1 | 1 | 322 | 0 | 6.274e-11 |
| r2p1_outward | 1 | ESC | 414.9 | -20 | 105 | 1 | -1 | 0.1 | 0.2115 | 0.1 | 1 | 735 | 0 | 4.698e-11 |
| r2p1_outward | 2 | ESC | 333.5 | -20 | 105 | 1 | -1 | 0.1 | 0.2115 | 0.1 | 1 | 735 | 0 | 4.698e-11 |
| r2p1_outward | 3 | DARK | 109.4 | -20 | 105 | 1 | -1 | 0.1 | 0.2115 | 0.1 | 1 | 735 | 0 | 4.698e-11 |
| r2p1_outward | 4 | DARK | 32.25 | -20 | 37 | 1 | -1 | 0.1 | 0.8994 | 0.1 | 1 | 259 | 0 | 2.991e-11 |
| r1p5_freefall | 0 | DARK | 30.66 | -20 | 49 | 1 | -1 | 0.1 | 0.696 | 0.1 | 1 | 343 | 0 | 4.938e-10 |
| r1p5_freefall | 1 | ESC | 254.5 | -20 | 52 | 1 | -1 | 0.0921 | 1.084 | 0.1 | 1 | 364 | 0 | 4.034e-11 |
| mink_moving | 0 | ESC | 51.26 | -20 | 3 | 1 | -1 | 1 | 14 | 1 | 1 | 21 | 0 | 2.22e-16 |
| mink_moving | 1 | ESC | 71.58 | -20 | 3 | 1 | -1 | 1 | 14 | 1 | 1 | 21 | 0 | 0 |
| alc_v3_s1 | 0 | ESC | 16.43 | -16.43 | 56 | 1 | -1 | 0.05 | 0.4 | 0.05 | 1 | 406 | 2 | 0 |
| alc_v3_s1 | 1 | ESC | 29.38 | -20 | 62 | 1 | -1 | 0.05 | 0.4 | 0.05 | 1 | 441 | 1 | 0 |
| alc_v9_s10 | 0 | ESC | 2.037 | -2.037 | 77 | 1 | -1 | 0.005 | 0.04 | 0.005 | 1 | 588 | 7 | 0 |
| alc_v9_s10 | 1 | ESC | 19.96 | -19.96 | 521 | 1 | -1 | 0.005 | 0.04 | 0.005 | 1 | 3682 | 5 | 2.22e-16 |
| alc_v9_s100 | 0 | ESC | 1.104 | -1.104 | 302 | 1 | -1 | 0.0005 | 0.004 | 0.0005 | 1 | 2163 | 7 | 0 |
| alc_v9_s100 | 1 | ESC | 2.896 | -2.896 | 746 | 1 | -1 | 0.0005 | 0.004 | 0.0005 | 1 | 5257 | 5 | 2.22e-16 |
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""One-task, at-most-two-attempt 4K comparison (R100, 3840x2160, FOV45).
This driver does NOT run as part of routine verification. It refuses to do
anything unless ``--authorize-two-4k`` is passed explicitly, and an attempt
ledger makes the task non-repeatable. Any future execution requires a fresh,
explicit user authorization for that task; authorization never extends to later
tasks or routine tests.
Usage:
run_4k_pair.py --authorize-two-4k [--out DIR]
OUT defaults to <repo>/local/adaptive_bounds_4k (distinct from any older
one-off ledger). The renderer binary is expected at
build/Release/schwarzschild_sky (see README for the make command).
"""
import argparse
import hashlib
import json
import os
from pathlib import Path
import shlex
import subprocess
import sys
import time
from mesh_maps import load, compare
ROOT = Path(__file__).resolve().parents[2]
ENV = dict(os.environ, OMP_NUM_THREADS='16', OMP_DYNAMIC='FALSE')
BUILD_CMD = 'make -j4 BUILD_TYPE=Release SPACETIME=schwarzschild backend'
REFUSAL = (
'REFUSING TO RUN: the 4K hmax 2-vs-8 pair is a one-task, at-most-two-attempt '
'experiment. Pass --authorize-two-4k only after an explicit fresh user '
'authorization for this task. It is not a routine verification target.')
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument('--authorize-two-4k', action='store_true',
help='explicit authorization for the two-attempt 4K pair')
ap.add_argument('--out', default=str(ROOT / 'local/adaptive_bounds_4k'),
help='output directory (default: local/adaptive_bounds_4k)')
args = ap.parse_args()
if not args.authorize_two_4k:
print(REFUSAL, file=sys.stderr)
return 2
out = Path(args.out).resolve()
binary = ROOT / 'build/Release/schwarzschild_sky'
if not binary.exists():
print(f'missing {binary}; build it first:\n {BUILD_CMD}',
file=sys.stderr)
return 2
out.mkdir(parents=True, exist_ok=True)
ledger = out / 'attempts.json'
attempts = json.loads(ledger.read_text()) if ledger.exists() else []
if attempts:
print('This one-task pair has already been attempted; no reruns '
'authorized.', file=sys.stderr)
return 3
catalog = out / 'single_dim_star.csv'
catalog.write_text('longitude_deg,latitude_deg,temperature_K,amplitude\n'
'262.5,-30,6000,1e-30\n')
with (out / 'cpu.txt').open('w') as f:
subprocess.run(['lscpu'], stdout=f, check=True)
metadata = {
'binary_sha256': hashlib.sha256(binary.read_bytes()).hexdigest(),
'utc': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
'source_sha256': {
p: hashlib.sha256((ROOT / p).read_bytes()).hexdigest()
for p in ('src/main.c', 'src/geodesic.c', 'src/frame.c')},
'catalog': 'one negligible synthetic point; no survey catalog',
'scope': 'README R100 45-degree 3840x2160 coarse16 refine4 jacobian0.2',
'threads': 16, 'max_attempts': 2,
'authorization': 'explicit --authorize-two-4k for this task only'}
(out / 'metadata.json').write_text(json.dumps(metadata, indent=2) + '\n')
maps = {}
results = {}
for upper in (2, 8):
name = f'r100_4k_hmax{upper}'
cmd = [str(binary), '--integrator', 'dp54', '--width', '3840',
'--height', '2160', '--look-ra-deg', '262.5',
'--look-dec-deg', '-30', '--fov-deg', '45',
'--observer-radius', '100', '--coarse-cell-pixels', '16',
'--refine-max-level', '4', '--refine-jacobian-min', '.2',
'--catalog', str(catalog), '--exposure', '1',
'--tone-map', 'reinhard', '--psf-min-y', '1e-20',
'--psf-relative-tail', '1e-4', '--ode-min-step', '1e-12',
'--ode-max-step', str(upper), '--verbose',
'--lens-map-output', str(out / (name + '.grlens')),
'--output', str(out / (name + '.png'))]
attempts.append({'upper': upper, 'command': shlex.join(cmd),
'state': 'attempted'})
ledger.write_text(json.dumps(attempts, indent=2) + '\n')
start = time.perf_counter()
with (out / (name + '.log')).open('w') as log:
log.write('OMP_NUM_THREADS=16 OMP_DYNAMIC=FALSE ' +
shlex.join(cmd) + '\n')
log.flush()
result = subprocess.run(cmd, cwd=ROOT, env=ENV, stdout=log,
stderr=subprocess.STDOUT, timeout=900)
attempts[-1].update(returncode=result.returncode,
wall_seconds=time.perf_counter() - start,
state='completed')
ledger.write_text(json.dumps(attempts, indent=2) + '\n')
if result.returncode:
raise SystemExit(f'4K attempt failed: {name}; no automatic retry.')
info, maps[upper] = load(out / (name + '.grlens'))
info['wall_seconds'] = attempts[-1]['wall_seconds']
results[name] = info
print(name, json.dumps(info), flush=True)
results['comparison'] = compare(maps[2], maps[8])
p, q = (results[f'r100_4k_hmax{h}']['provenance'] for h in (2, 8))
assert p['max_step'] == 2 and q['max_step'] == 8
assert {k: v for k, v in p.items() if k != 'max_step'} == \
{k: v for k, v in q.items() if k != 'max_step'}
print('comparison', json.dumps(results['comparison']), flush=True)
(out / 'summary.json').write_text(json.dumps(results, indent=2) + '\n')
return 0
if __name__ == '__main__':
sys.exit(main())
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Run the standalone critical-classification reference check
# (benchmarks/adaptive_step_bounds_2026-10-05/critical_ref_check.c).
#
# benchmarks/adaptive_step_bounds_2026-10-05/run_critical_ref.sh [OUT_DIR]
#
# OUT_DIR defaults to <repo>/local/adaptive_step_bounds_2026-10-05. All output
# stays under OUT_DIR; the tracked benchmark directory holds only the sources.
# This is a diagnostic probe (see the C file header); it makes no pass/fail
# claim about the exactly-critical physical outcome.
set -euo pipefail
SOURCE=$(cd "$(dirname "$0")" && pwd)
root=$(cd "$SOURCE/../.." && pwd)
OUT=${1:-$root/local/adaptive_step_bounds_2026-10-05}
BIN=/tmp/opencode/step_bounds
mkdir -p "$OUT/raw" "$OUT/logs" "$BIN"
cd "$OUT"
OUT=$PWD
{
echo "### building critical_ref_check"
set -x
cc -std=c11 -O2 -Wall -Wextra -Wpedantic -I"$root/src" \
"$SOURCE/critical_ref_check.c" "$root/src/geodesic.c" \
"$root/src/asymptotic.c" "$root/src/asymptotic_schwarzschild.c" \
"$root/src/spacetime_common.c" "$root/src/observer.c" -lm \
-o "$BIN/critical_ref_check"
echo "### command: critical_ref_check"
time "$BIN/critical_ref_check"
set +x
} > logs/critical_ref.log 2>&1
cat logs/critical_ref.log
echo "outputs under $OUT"
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# Build and run the failure-path floor diagnostic.
#
# benchmarks/adaptive_step_bounds_2026-10-05/run_failure_floor.sh [OUT_DIR]
#
# OUT_DIR defaults to <repo>/local/adaptive_bounds_mesh. All output stays
# under OUT_DIR; the tracked benchmark directory holds only the sources.
#
# failure_floor.c includes ../../tests/test_geodesic_adaptive.c (depth-2 path
# valid from this directory) and therefore requires -DGEODESIC_EVENT_TESTING
# and the production RayPool source src/ray.c. The two analytic backends are
# textually included by the regression file, so they are not linked here.
set -euo pipefail
SOURCE=$(cd "$(dirname "$0")" && pwd)
root=$(cd "$SOURCE/../.." && pwd)
OUT=${1:-$root/local/adaptive_bounds_mesh}
BIN=/tmp/opencode/step_bounds
mkdir -p "$OUT/logs" "$BIN"
cd "$OUT"
{
set -x
cc -std=c11 -O2 -Wall -Wextra -Wpedantic -fopenmp -DGEODESIC_EVENT_TESTING \
-I"$root/src" \
"$SOURCE/failure_floor.c" "$root/src/geodesic.c" "$root/src/ray.c" \
"$root/src/spacetime_common.c" "$root/src/observer.c" \
"$root/src/asymptotic.c" "$root/src/asymptotic_schwarzschild.c" -lm \
-o "$BIN/failure_floor"
"$BIN/failure_floor"
set +x
} > logs/failure_floor.log 2>&1
cat logs/failure_floor.log
echo "outputs under $OUT"
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
# Long-term reproduction driver for the adaptive step-bound scan (Experiment A/B).
#
# benchmarks/adaptive_step_bounds_2026-10-05/run_limited.sh [OUT_DIR]
#
# OUT_DIR defaults to <repo>/local/adaptive_step_bounds_2026-10-05. All build
# and run output — env/hash record, build log, stdout logs and per-ray CSV —
# is written under OUT_DIR, never into this tracked benchmark directory.
# Scratch binaries go to /tmp/opencode/step_bounds.
set -euo pipefail
SOURCE=$(cd "$(dirname "$0")" && pwd)
root=$(cd "$SOURCE/../.." && pwd)
OUT=${1:-$root/local/adaptive_step_bounds_2026-10-05}
BIN=/tmp/opencode/step_bounds
mkdir -p "$OUT/raw" "$OUT/logs" "$BIN"
cd "$OUT"
OUT=$PWD
SRC_COMMON="$root/src/geodesic.c $root/src/asymptotic.c \
$root/src/asymptotic_schwarzschild.c $root/src/spacetime_common.c \
$root/src/observer.c"
# --- environment + source identity (full shell output) --------------------
{
echo "# adaptive_step_bounds_2026-10-05 environment and source identity"
echo "date_utc: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "host: $(uname -a)"
echo "nproc: $(nproc)"
echo "cc:"; cc --version
echo
echo "git_head: $(git -C "$root" rev-parse HEAD 2>/dev/null || echo none)"
echo "git_status_short:"; git -C "$root" status --short 2>/dev/null || true
echo
echo "# sha256 of the production sources consumed by the harnesses"
sha256sum $SRC_COMMON
echo
echo "# sha256 of the benchmark harnesses"
sha256sum "$SOURCE/a_public_endpoints.c" "$SOURCE/b_actual_h.c" \
"$SOURCE/summarize.py" "$SOURCE/run_limited.sh"
} > logs/env.txt 2>&1
cat logs/env.txt
# --- build ----------------------------------------------------------------
{
echo "### building a_public_endpoints"
set -x
cc -std=c11 -O2 -Wall -Wextra -Wpedantic -I"$root/src" \
"$SOURCE/a_public_endpoints.c" $SRC_COMMON -lm -o "$BIN/a_public"
echo "### building b_actual_h"
cc -std=c11 -O2 -Wall -Wextra -Wpedantic -I"$root/src" \
"$SOURCE/b_actual_h.c" "$root/src/asymptotic.c" \
"$root/src/asymptotic_schwarzschild.c" "$root/src/spacetime_common.c" \
"$root/src/observer.c" -lm -o "$BIN/b_actual"
set +x
} > logs/build.log 2>&1
cat logs/build.log
# --- Experiment A ---------------------------------------------------------
{
echo "### command: a_public schwarzschild"
set -x; time "$BIN/a_public" schwarzschild; set +x
echo "### command: a_public minkowski"
set -x; time "$BIN/a_public" minkowski; set +x
echo "### command: a_public alcubierre"
set -x; time "$BIN/a_public" alcubierre; set +x
} > logs/expA.log 2>&1
grep -E '^(SUMMARY|TOTAL_RAYS|real|user)' logs/expA.log | tail -20
# --- Experiment B ---------------------------------------------------------
{
echo "### command: b_actual"
set -x; time "$BIN/b_actual"; set +x
} > logs/expB.log 2>&1
cat logs/expB.log
# --- summary tables -------------------------------------------------------
python3 "$SOURCE/summarize.py" "$OUT"
echo
echo "rays A = $(grep '^TOTAL_RAYS' logs/expA.log | awk '{s+=$2} END{print s}')"
echo "observed steps B = $(grep '^TOTAL_OBSERVED_STEPS' logs/expB.log | awk '{print $2}')"
echo "outputs under $OUT"
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""Bounded 320x180 adaptive-mesh candidates (8 cases; no 4K, no references).
Usage:
run_mesh.py [OUT_DIR]
OUT_DIR defaults to <repo>/local/adaptive_bounds_mesh. The renderer binary is
expected to already exist at build/Release/schwarzschild_sky; build it with
(make command is also in README.md):
make -j4 BUILD_TYPE=Release SPACETIME=schwarzschild backend
No survey catalog is used: the script writes a one-point synthetic dim catalog
with the required column header (longitude_deg,latitude_deg,temperature_K,
amplitude) and a 1e-30 amplitude star. No external CSV is read.
"""
import json
import os
from pathlib import Path
import shlex
import subprocess
import sys
import time
from mesh_maps import load, compare # tracked sibling module (no local/ import)
ROOT = Path(__file__).resolve().parents[2]
OUT = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else \
ROOT / 'local/adaptive_bounds_mesh'
BIN = ROOT / 'build/Release/schwarzschild_sky'
BUILD_CMD = 'make -j4 BUILD_TYPE=Release SPACETIME=schwarzschild backend'
ENV = dict(os.environ, OMP_NUM_THREADS='8', OMP_DYNAMIC='FALSE')
def scene_args(scene):
if scene == 'r100':
return ['--observer-radius', '100', '--look-ra-deg', '262.5',
'--look-dec-deg', '-30', '--fov-deg', '45',
'--refine-jacobian-min', '.2']
return ['--observer-position', '2.1', '0', '0', '--observer-velocity',
'0', '0', '0', '--look-ra-deg', '0', '--look-dec-deg', '0',
'--fov-deg', '90']
def main():
if not BIN.exists():
raise SystemExit(f'missing {BIN}; build it first:\n {BUILD_CMD}')
OUT.mkdir(parents=True, exist_ok=True)
catalog = OUT / 'single_dim_star.csv'
catalog.write_text('longitude_deg,latitude_deg,temperature_K,amplitude\n'
'262.5,-30,6000,1e-30\n')
results = {}
for scene in ('r100', 'r2p1'):
meshes = {}
for upper in (.5, 2, 8, 32):
name = f'{scene}_hmax{upper:g}'
cmd = [str(BIN), '--integrator', 'dp54',
'--width', '320', '--height', '180',
'--coarse-cell-pixels', '8', '--refine-max-level', '3',
'--catalog', str(catalog), '--exposure', '1',
'--psf-min-y', '1e-20', '--psf-relative-tail', '1e-4',
'--ode-min-step', '1e-12', '--ode-max-step', str(upper),
'--verbose', '--lens-map-output', str(OUT / (name + '.grlens')),
'--output', str(OUT / (name + '.png')), *scene_args(scene)]
start = time.perf_counter()
with (OUT / (name + '.log')).open('w') as log:
log.write('OMP_NUM_THREADS=8 OMP_DYNAMIC=FALSE ' +
shlex.join(cmd) + '\n')
log.flush()
subprocess.run(cmd, cwd=ROOT, env=ENV, stdout=log,
stderr=subprocess.STDOUT, check=True, timeout=180)
info, meshes[upper] = load(OUT / (name + '.grlens'))
info['wall_seconds'] = time.perf_counter() - start
results[name] = info
print(name, json.dumps(info), flush=True)
for upper in (.5, 8, 32):
comparison = compare(meshes[2], meshes[upper])
results[f'{scene}_2_vs_{upper:g}'] = comparison
print(f'{scene} 2 vs {upper:g}', json.dumps(comparison), flush=True)
(OUT / 'summary.json').write_text(json.dumps(results, indent=2) + '\n')
if __name__ == '__main__':
main()
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""Two bounded tight-tolerance mesh references and comparisons (no 4K).
Usage:
run_mesh_reference.py [OUT_DIR]
OUT_DIR defaults to <repo>/local/adaptive_bounds_mesh and must already contain
the eight ``<scene>_hmax<h>.grlens`` maps from ``run_mesh.py``. The renderer
binary is expected at build/Release/schwarzschild_sky (see README for the make
command). Same one-point synthetic dim catalog, no external CSV.
"""
import json
import os
from pathlib import Path
import shlex
import subprocess
import sys
import time
from mesh_maps import load, compare
ROOT = Path(__file__).resolve().parents[2]
OUT = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else \
ROOT / 'local/adaptive_bounds_mesh'
BIN = ROOT / 'build/Release/schwarzschild_sky'
BUILD_CMD = 'make -j4 BUILD_TYPE=Release SPACETIME=schwarzschild backend'
ENV = dict(os.environ, OMP_NUM_THREADS='8', OMP_DYNAMIC='FALSE')
def scene_args(scene):
if scene == 'r100':
return ['--observer-radius', '100', '--look-ra-deg', '262.5',
'--look-dec-deg', '-30', '--fov-deg', '45',
'--refine-jacobian-min', '.2']
return ['--observer-position', '2.1', '0', '0', '--observer-velocity',
'0', '0', '0', '--look-ra-deg', '0', '--look-dec-deg', '0',
'--fov-deg', '90']
def main():
if not BIN.exists():
raise SystemExit(f'missing {BIN}; build it first:\n {BUILD_CMD}')
OUT.mkdir(parents=True, exist_ok=True)
catalog = OUT / 'single_dim_star.csv'
if not catalog.exists():
catalog.write_text('longitude_deg,latitude_deg,temperature_K,amplitude\n'
'262.5,-30,6000,1e-30\n')
results = {}
for scene in ('r100', 'r2p1'):
name = scene + '_tight_reference'
cmd = [str(BIN), '--integrator', 'dp54', '--width', '320',
'--height', '180', '--coarse-cell-pixels', '8',
'--refine-max-level', '3', '--catalog', str(catalog),
'--exposure', '1', '--psf-min-y', '1e-20',
'--psf-relative-tail', '1e-4', '--ode-min-step', '1e-12',
'--ode-max-step', '.5', '--ode-rtol', '1e-12',
'--ode-atol-x', '1e-12', '--ode-atol-pi', '1e-12',
'--ode-atol-l', '1e-12', '--verbose',
'--lens-map-output', str(OUT / (name + '.grlens')),
'--output', str(OUT / (name + '.png')), *scene_args(scene)]
start = time.perf_counter()
with (OUT / (name + '.log')).open('w') as log:
log.write('OMP_NUM_THREADS=8 OMP_DYNAMIC=FALSE ' +
shlex.join(cmd) + '\n')
log.flush()
subprocess.run(cmd, cwd=ROOT, env=ENV, stdout=log,
stderr=subprocess.STDOUT, check=True, timeout=180)
info, ref = load(OUT / (name + '.grlens'))
info['wall_seconds'] = time.perf_counter() - start
results[name] = info
print(name, json.dumps(info), flush=True)
for upper in (.5, 2, 8, 32):
_, vertices = load(OUT / f'{scene}_hmax{upper:g}.grlens')
result = compare(ref, vertices)
results[f'{scene}_ref_vs_{upper:g}'] = result
print(scene, 'ref vs', upper, json.dumps(result), flush=True)
(OUT / 'reference_summary.json').write_text(
json.dumps(results, indent=2) + '\n')
if __name__ == '__main__':
main()
@@ -0,0 +1,261 @@
#!/usr/bin/env python3
"""Summarise the adaptive-step-bounds benchmark raw CSV logs.
Usage:
summarize.py [OUT_DIR]
Reads ``OUT_DIR/raw/*.csv`` (written by ``run_limited.sh``) and writes
``OUT_DIR/logs/summary.txt``. It has no dependency on any untracked script or
on older local data.
Defaults: OUT_DIR = <repo>/local/adaptive_step_bounds_2026-10-05, derived from
this file's location (benchmarks/adaptive_step_bounds_2026-10-05/).
"""
import csv
import math
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(HERE))
DEFAULT_OUT = os.path.join(ROOT, "local", "adaptive_step_bounds_2026-10-05")
OUTCOME = {0: "ESC", 1: "DARK", 2: "UNRES", 3: "INC"}
REASON = {
0: "NONE",
1: "REDSHIFT",
2: "BUDGET",
3: "TIMERANGE",
4: "DOMAIN",
5: "INVMETRIC",
6: "INTEGERR",
7: "UNSUPPORTED",
8: "PROTOCOL",
9: "IO",
}
def fnum(v):
if v is None or v == "" or v == "nan":
return math.nan
return float(v)
def fmt(v):
if isinstance(v, float):
if math.isnan(v):
return "nan"
if v == 0:
return "0"
return f"{v:.4g}"
return str(v)
def table(title, header, recs):
lines = [f"## {title}", ""]
lines.append("| " + " | ".join(header) + " |")
lines.append("|" + "|".join(["---"] * len(header)) + "|")
for r in recs:
lines.append("| " + " | ".join(fmt(x) for x in r) + " |")
lines.append("")
return lines
def ang_between(a, b):
va = [fnum(a["nx"]), fnum(a["ny"]), fnum(a["nz"])]
vb = [fnum(b["nx"]), fnum(b["ny"]), fnum(b["nz"])]
na = math.sqrt(sum(x * x for x in va))
nb = math.sqrt(sum(x * x for x in vb))
if not (na > 0 and nb > 0):
return math.nan
cross = (va[1]*vb[2]-va[2]*vb[1],
va[2]*vb[0]-va[0]*vb[2],
va[0]*vb[1]-va[1]*vb[0])
dot = sum(x * y for x, y in zip(va, vb))
return math.atan2(math.sqrt(sum(x*x for x in cross)), dot)
class Raw:
def __init__(self, out_dir):
self.raw = os.path.join(out_dir, "raw")
self.logs = os.path.join(out_dir, "logs")
def rows(self, name):
with open(os.path.join(self.raw, name)) as f:
return list(csv.DictReader(f))
def summarise_agg(raw, name, title, keycol):
recs = raw.rows(name)
out = []
for r in recs:
out.append((r["case"], float(r[keycol]), int(r["class_mismatch"]),
fnum(r["max_dn_ang"]), fnum(r["max_dgrel"]),
fnum(r["max_dstopT"]), int(r["sum_rhs"]),
int(r["max_rejected"]), int(r["escaped"]), int(r["dark"]),
int(r["unresolved"]), int(r["incomplete"])))
out.sort(key=lambda x: (x[0], x[1]))
return table(title,
["case", keycol, "mismatch", "max_dn_ang", "max_dgrel",
"max_dstopT", "sum_rhs", "max_rej", "ESC", "DARK", "UNRES",
"INC"], out)
def build(raw):
lines = ["# Adaptive-step-bounds benchmark summary tables", ""]
ref = raw.rows("a_sch_reference.csv")
recs = []
for r in ref:
recs.append((r["case"], int(r["dir"]), fnum(r["theta"]),
OUTCOME[int(r["outcome"])], REASON[int(r["reason"])],
fnum(r["stop_t"]), int(r["steps"]), int(r["rejected"]),
int(r["rhs"]), fnum(r["g"]), fnum(r["thr"])))
lines += table("A Schwarzschild references (DP tol=1e-12, max_step=0.25)",
["case", "dir", "theta", "outcome", "reason", "stop_t",
"steps", "rej", "rhs", "g", "thr"], recs)
lines += summarise_agg(raw, "a_sch_upper_summary.csv",
"A Schwarzschild upper scan (min_step=1e-12, "
"tol=1e-9)", "upper")
lines += summarise_agg(raw, "a_sch_min_summary.csv",
"A Schwarzschild min scan (max_step=2, tol=1e-9)",
"min_step")
rk = raw.rows("a_sch_rk4_sensitive.csv")
groups = {}
for r in rk:
groups.setdefault((r["case"], r["dir"]), {})[r["phase"]] = r
recs = []
for (case, d), gg in sorted(groups.items()):
a = gg.get("rk4_0.01")
b = gg.get("rk4_0.005")
if not a or not b:
continue
dn = math.nan if a["outcome"] != b["outcome"] else ang_between(a, b)
dg = (math.nan if a["outcome"] != b["outcome"]
else abs(fnum(a["g"]) - fnum(b["g"])))
recs.append((case, d, fnum(a["theta"]), OUTCOME[int(a["outcome"])],
OUTCOME[int(b["outcome"])], dn, dg, int(a["rhs"]),
int(b["rhs"])))
lines += table("A Schwarzschild sensitive RK4 .01 vs .005",
["case", "dir", "theta", "out.01", "out.005",
"hhalve_dn_ang", "|dg|", "rhs.01", "rhs.005"], recs)
mk = raw.rows("a_mink_analytic.csv")
mkx = raw.rows("a_mink_analytic_x.csv")
groups = {}
for r in mk:
groups.setdefault((r["upper"], r["min_step"]), []).append(r)
recs = []
for (u, f), g in sorted(groups.items(),
key=lambda kv: (float(kv[0][0]), float(kv[0][1]))):
recs.append((u, f, len(g), max(fnum(r["dn_ang"]) for r in g),
max(fnum(r["dgrel"]) for r in g),
max(fnum(r["t_err"]) for r in g)))
lines += table("A Minkowski flat analytic check (n_inf and t)",
["upper", "min_step", "n", "max_dn_ang", "max_dgrel",
"max_t_err"], recs)
groups = {}
for r in mkx:
groups.setdefault((r["upper"], r["min_step"]), []).append(r)
recs = []
for (u, f), g in sorted(groups.items(),
key=lambda kv: (float(kv[0][0]), float(kv[0][1]))):
maxx = max(max(fnum(r["x_err_x"]), fnum(r["x_err_y"]),
fnum(r["x_err_z"])) for r in g)
recs.append((u, f, len(g), maxx, max(fnum(r["t_err"]) for r in g)))
lines += table("A Minkowski flat analytic check (crossing x and t)",
["upper", "min_step", "n", "max_x_err", "max_t_err"], recs)
for case in ["alc_v3_s1", "alc_v3_s10", "alc_v9_s1", "alc_v9_s10"]:
lines += summarise_agg(raw, f"a_alc_{case}_upper_summary.csv",
f"A Alcubierre {case} upper scan", "upper")
lines += summarise_agg(raw, "a_alc_v9_s100_min_summary.csv",
"A Alcubierre v9 s100 lower-bound stress "
"(upper=0.004)", "min_step")
b = raw.rows("b_summary.csv")
recs = []
for r in b:
recs.append((r["case"], int(r["dir"]),
OUTCOME[int(r["ref_outcome"])], fnum(r["span"]),
fnum(r["target"]), int(r["observed_steps"]),
int(r["reached_target"]),
REASON.get(int(r["terminated_reason"]),
r["terminated_reason"]),
fnum(r["h_min"]), fnum(r["h_max"]), fnum(r["h_first"]),
int(r["boundary_steps"]), int(r["sum_rhs"]),
int(r["sum_reject"]), fnum(r["null_residual_max"])))
lines += table("B observed accepted step h (finite T=min(20, ref span))",
["case", "dir", "ref", "span", "target", "steps", "reached",
"term", "h_min", "h_max", "h_first", "bnd", "rhs", "rej",
"null_max"], recs)
if os.path.exists(os.path.join(raw.raw, "critical_ref.csv")):
lines += critical_section(raw)
return lines
def critical_section(raw):
rows = raw.rows("critical_ref.csv")
key = {}
for r in rows:
key[(r["camera"], int(r["dir"]), r["cfg"])] = r
lines = table("critical reference check (raw rows)",
["camera", "dir", "theta", "cfg", "outcome", "reason",
"end_id", "stop_t", "steps", "rejected", "rhs", "g", "thr",
"L", "L0"],
[(r["camera"], int(r["dir"]), fnum(r["theta"]), r["cfg"],
OUTCOME[int(r["outcome"])], REASON[int(r["reason"])],
r["end_id"], fnum(r["stop_t"]), int(r["steps"]),
int(r["rejected"]), int(r["rhs"]), fnum(r["g"]),
fnum(r["thr"]), fnum(r.get("L")), fnum(r.get("L0")))
for r in rows])
# pair comparisons
pairs = [("ref_tol1e-12_h0.25", "ref_tol1e-13_h0.125"),
("tol1e-11_h2", "tol1e-11_h8"),
("ref_tol1e-12_h0.25", "tol1e-11_h2"),
("ref_tol1e-12_h0.25", "tol1e-11_h8")]
for a_cfg, b_cfg in pairs:
recs = []
for (cam, di, cfg), a in sorted(key.items()):
if cfg != a_cfg:
continue
b = key.get((cam, di, b_cfg))
if not b:
continue
same = a["outcome"] == b["outcome"]
dn = ang_between(a, b) if same else math.nan
dg = (math.nan if not same else
abs(fnum(a["g"]) / fnum(b["g"]) - 1.0)
if fnum(a["g"]) > 0 and fnum(b["g"]) > 0 else math.nan)
dstop = (fnum(a["stop_t"]) - fnum(b["stop_t"])
if fnum(a["stop_t"]) == fnum(a["stop_t"]) and
fnum(b["stop_t"]) == fnum(b["stop_t"]) else math.nan)
dmar = (fnum(a["thr"]) - fnum(b["thr"])
if fnum(a["thr"]) == fnum(a["thr"]) and
fnum(b["thr"]) == fnum(b["thr"]) else math.nan)
recs.append((cam, di, fnum(a["theta"]),
OUTCOME[int(a["outcome"])], OUTCOME[int(b["outcome"])],
same, dn, dg, dstop, dmar))
lines += table(f"critical pair {a_cfg} vs {b_cfg}",
["camera", "dir", "theta", "a", "b", "same",
"dn_ang", "dgrel", "dstop_t", "dthr"], recs)
return lines
def main(argv):
out = argv[1] if len(argv) > 1 else DEFAULT_OUT
raw = Raw(out)
os.makedirs(raw.logs, exist_ok=True)
lines = build(raw)
path = os.path.join(raw.logs, "summary.txt")
with open(path, "w") as f:
f.write("\n".join(lines) + "\n")
print(f"wrote {path} ({len(lines)} lines)")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Postprocess existing 4K/limited maps only; never invokes a renderer.
Usage:
summarize_4k.py [OUT_DIR]
OUT_DIR defaults to <repo>/local/adaptive_bounds_4k and must contain
``r100_4k_hmax2.grlens``, ``r100_4k_hmax8.grlens`` and ``attempts.json``.
Writes summary.json in OUT_DIR.
Cost scope note: accepted/rejected/RHS sums cover the final persistent map
vertices only. Nonpersistent discarded refinement probes are not stored in the
v3 map, so these sums are not full-render executed RHS totals.
"""
import json
import re
from pathlib import Path
import sys
from mesh_maps import load, compare
ROOT = Path(__file__).resolve().parents[2]
OUT = Path(sys.argv[1]).resolve() if len(sys.argv) > 1 else \
ROOT / 'local/adaptive_bounds_4k'
def main():
summaries = {}
meshes = {}
for upper in (2, 8):
name = f'r100_4k_hmax{upper}'
summaries[name], meshes[upper] = load(OUT / (name + '.grlens'))
text = (OUT / (name + '.log')).read_text()
initial = int(re.search(r'tracing (\d+) initial rays', text).group(1))
generations = [int(n) for n in re.findall(
r'refinement generation \d+ tracing (\d+) samples', text)]
summaries[name]['requested_samples_including_nonpersistent_probes'] = \
initial + sum(generations)
summaries[name]['initial_samples'] = initial
summaries[name]['refinement_samples_per_generation'] = generations
summaries[name]['trace_wall_seconds'] = sum(
float(x) for x in re.findall(
r'(?:initial ray trace|adaptive ray-trace refinement) '
r'finished in ([\d.]+) s', text))
ledger = OUT / 'attempts.json'
if ledger.exists():
for entry in json.loads(ledger.read_text()):
if entry.get('state') == 'completed':
summaries[f'r100_4k_hmax{entry["upper"]}']['wall_seconds'] = \
entry['wall_seconds']
summaries['comparison'] = compare(meshes[2], meshes[8])
a, b = (summaries[f'r100_4k_hmax{upper}'] for upper in (2, 8))
summaries['relative_reduction'] = {
key: 1 - b[key] / a[key]
for key in ('accepted', 'rhs', 'wall_seconds', 'trace_wall_seconds')}
summaries['comparison']['triangle_payload_identical'] = \
a['triangle_sha256'] == b['triangle_sha256']
summaries['cost_scope'] = (
'accepted/rejected/RHS sums cover final persistent vertices only; '
'nonpersistent discarded refinement probes are not in the v3 map. '
'Do not interpret them as full-render executed RHS totals.')
(OUT / 'summary.json').write_text(json.dumps(summaries, indent=2) + '\n')
print(json.dumps(summaries, indent=2))
if __name__ == '__main__':
main()
+232
View File
@@ -0,0 +1,232 @@
# quadratic_precision — stable entry quadratic: precision vs. cost
Self-contained, reproducible benchmark comparing four arithmetic realisations of
the **same** asymptotic-entry algebra. Nothing here modifies production code,
the `Makefile`, or git state.
## Question
The production camera pre-route solves the relative-distance quadratic
```
F(s) = |d + q s|^2 - (R0 - rr s)^2 = a s^2 + b s + c
d = x_cur - c_frame, q = w_frame + v_frame
```
in `long double`, with a stable root formula, an ordinary discriminant
`b*b - 4*a*c` and entry slope, a common power-of-two scaling, an
uncertainty band, geometric validation and a numerical fallback. Which matters
for accuracy and which for speed?
Four variants share the *entire* rest of the module (scaling, uncertainty band,
validation, fallback, dispatch) and are generated from the current
`src/asymptotic.c` by rewriting exactly one lexical region:
| variant | coefficients | type | discriminant / slope |
|---------------------|--------------|-------------|----------------------|
| `ld_plain` | as production| `long double` | plain `b*b - 4*a*c`, plain slope |
| `ld_fma` | as production| `long double` | compensated `fmal` (experimental arm) |
| `double_fma` | ordinary `+=`| `double` | compensated `fma` |
| `double_fma_coeff` | `fma` dots, `fma` products | `double` | compensated `fma` |
`double_fma` is the control that isolates *type* precision from *FMA*; the two
double variants isolate *coefficient accumulation*.
## Generation and build
`build.py` locates the kernel region between two lexical anchors in the current
working-tree `src/asymptotic.c`:
* start: the comment `/* Long-double coefficients of the relative-distance
quadratic`
* end: the forward declaration `static void minkowski_route_entry(...)`
It asserts each anchor and the presence of `entry_quadratic_coeffs`,
`entry_solve`, `entry_discriminant` exactly once, then emits four full-module
copies under `<output>/generated/`. Every other line (dispatch, fallback,
`asymptotic_route_camera`, Schwarzschild path, …) is copied verbatim. If a
future edit moves an anchor or changes the region, generation **fails** rather
than silently measuring the wrong code.
`ld_plain` retains the production kernel verbatim; `ld_fma` rewrites only the
discriminant body and the entry slope. Double
variants replace `long double`→`double`, `fmal`/`fabsl`/`fmaxl`/`frexpl`/
`scalbnl`/`sqrtl`/`copysignl`→their double forms, `LDBL_*`→`DBL_*`, and strip
`L` suffixes from literals. A guard rejects any remaining `long double`
promotion, `L` literal or `*l`/`fmal` call in the double kernel section, so no
double expression can be re-promoted to `long double`. `double_fma_coeff`
additionally changes the coefficient accumulation to `fma`.
The probe `#include`s the generated module, so the private static
`entry_quadratic_coeffs`/`entry_solve` and the public `asymptotic_route_camera`
that are exercised are the **actual generated code**, not an independent copy.
Compile flags, identical for all four variants:
```
-std=c11 -march=native -O2 -DNDEBUG -ffp-contract=off -fopenmp
```
`-ffp-contract=off` prevents implicit FMA contraction from silently changing
the `ld_plain` arm; explicit `fma`/`fmal` still lower as written.
The probe links a minimal production subset (`geodesic.c`,
`asymptotic_entry.c`, `asymptotic_schwarzschild.c`, `spacetime_common.c`,
`spacetime_minkowski.c`) plus libm. No FFTW, PNG, catalog or observer-track
data are needed.
## Inputs
All fixtures are frozen as IEEE-754 hex literals (`cases.json` and the
generated C header are bit-identical), including the two production grazing
rows reproduced from the Alcubierre pre-route (`x=(0,-24,0)`, `centre=2t`,
`v=2`, `R=5`, canonical `w` hex values). Families:
* **curated / adversarial**: head-on hit/miss, clear miss, grazing
`y=nextafter(R,0)`, exact tangent, near-boundary, `1e10 + 0.5, R=1`
cancellation, large-`t` `centre=0.1t`, exact linear `a==0` (inward and
initially outward), translated-origin cancellation.
* **fixed**: axes and three oblique frames, radii
`1e-100…1e100`, `D/R ∈ {1+ulp,2,10,100,512,1024,1e4,1e8,1e10}`, impact
ratios `{0,.5,.99,1-1e-6,nextafter(1,0),1,nextafter(1,∞),1+1e-6,1.1}`.
The `512`/`1024` ratios sample the crossover where long-double coefficient
rounding meets the `128 ε_D R²` geometry tolerance (`≈512`) and the double
crossover (`≈11`), separating fallback rates from the all-UNCERTAIN `1e10`
regime.
* **moving**: `v ∈ {0,.1,2,10}` along a transverse direction.
* **growing**: `rr = -1` and `nextafter(-1,∓∞)`, **inward and
initially-outward** photons (`w = ∓tow`), axis and oblique.
* **shrinking**: `rr>0`, labelled/domain-checked algebra (radius would go
negative on the open past; roots outside `R0 - rr·s > 0` are non-physical).
* **route**: `rr = 0` plus **`rr<0` growing inward/outward** families
(positive radius on the whole past, `valid_t_min=-1e300`) across all three
direction frames, driven through the public `asymptotic_route_camera`. The
oblique-2 growing-outward family reproduces the double kernel false-MISS
cases, where a kernel MISS bypasses the fallback and the route escapes
directly; the route reference uses the actual normalised canonical `w`, so
normalisation effects are explicit.
Exact counts are emitted by the run (and in `summary.json`); they are not
hard-coded here.
## Reference oracle
Inputs are exact, so they are represented as `fractions.Fraction` (Python
stdlib). `d`, `q`, `a`, `b`, `c`, the discriminant and polynomial residuals
are **exact rationals**; only `sqrt` uses `decimal` (`--precision`, default
160). A custom hex parser handles both `%a` (double) and `%La` (long double)
without `float.fromhex`, which would silently drop a 64-bit long-double mantissa
to 53 bits. Reference classification mirrors production semantics: `c<0` is
INSIDE (not an entry failure), `c==0` uses the boundary slope, `a==0` is the
exact linear branch, a real double root/tangent is MISS.
Route references are built from the **actual canonical state the probe printed**
(`canonx*`, `canonw*`) joined with the callback samples at `t0`, so the 1-ulp
normalisation of `w` and both callback models (`centre = c0 + v(t-t0)` and the
production `centre = v t`) are handled explicitly.
## Metrics
Kernel:
* status counts (ENTRY/MISS/UNCERTAIN) vs the reference, **false MISS**
(reference ENTER, kernel MISS) and **false candidate** (reference MISS,
kernel ENTRY);
* root error in ULP of the returned double root and relative error, for the
common reference-ENTER set and the intersection where *all* variants returned
ENTRY (so more UNCERTAIN cannot look better by selection);
* polynomial residual at the candidate root using the actual printed
coefficients, the exact ideal polynomial, and the nearest-double-rounded
reference root as a baseline;
* `a` exact-zero vs kernel-zero, `a` sign mismatches, `a` collapse/spurious
non-zero, and conditioning labels (`near_linear`, `late`, `ill_conditioned`).
Outward/growing and near-linear cases are ill-conditioned; a rounded-zero `a`
can turn a very late true entry into a false MISS, so these are reported
separately and are not treated as a universal precision claim.
Route:
* kind/status vs reference, false MISS, false candidate, camera-INSIDE
mismatches, total `INVALID/ENTRY_UNCONFIRMED` outcomes independent of reference
classification (a conservative refusal to confirm is not a false escape);
* the **public kernel classification at the actual canonical inputs**
(`kern_status` in the route CSV), so a reference ENTER / kernel MISS / route
ESCAPED (`kernel_false_miss_escaped`) is visible and is not confused with
canonical normalisation;
* fast-path vs fallback (`entry_fallback_evaluations`), `F/tol` for accepted
entries, and Pi/`L_camera` preservation.
The reference enforces `R0 - rr·s > 0` for quadratic roots; a positive root
outside the physical radius domain is reported as `domain_clipped` and is not
counted as a physical ENTER/false-MISS. A 160-vs-240-digit consistency check is
run for every curated and near-linear/late/domain-clipped id and must report the
same status, discriminant sign and nearest-double root.
Timing:
* kernel: coefficient assembly + `entry_solve`, serial, `noinline` +
`volatile` sink, and an inline-asm opaque loop index so GCC cannot hoist the
pure kernel out of the repetition loop (identical for all variants). A
linearity check runs the kernel at 1× and 2× calls and reports the ratio
(expect ≈2) to prove per-call execution.
* route: public pre-route, OpenMP `static` schedule + reductions, with
route-kind and fallback counts captured so a timing gain cannot come from
silently escaping more rays. `--threads` defaults to 4.
Codegen evidence is collected with `objdump`/`nm`: for the double variants
`entry_solve` contains hardware `vfmadd` and zero x87; for `ld_fma` it contains
x87 plus `fmal` PLT relocations (extended-precision FMA is a libm software
routine on x86-64).
## Run
```sh
python3 benchmarks/quadratic_precision/run.py \
--output-dir /tmp/opencode/quadratic-comparison
```
Useful overrides: `--rounds N`, `--threads T`, `--precision P`,
`--kernel-target-calls N`, `--route-target-calls N`, `--skip-build` (reuse the
last build), `--repo PATH`, `--cc CC`.
Artifacts (all under `--output-dir`):
```
generated/{ld_plain,ld_fma,double_fma,double_fma_coeff}.c
cases/quadratic_cases.h, cases/cases.json
environment.json, build_manifest.json
raw/kernel_<v>.csv, raw/route_<v>.csv
raw/kernel_metrics_<v>.csv, raw/route_metrics_<v>.csv
raw/curated_kernel.csv, raw/curated_route.csv
raw/microbench_<v>_r*.json, raw/routebench_<v>_r*.json
raw/fma_verify.json, raw/{linearity,curated}*
summary.json
logs/{build_commands,accuracy_*,microbench_*,routebench_*}.log
```
`summary.json` is the machine-readable aggregate; the script also prints the
coverage counts, per-variant ULP distributions, the curated case table, codegen
evidence and median ns/call.
## Interpretation caveats
* FMA improves the rounding of one product only. It **cannot** restore the
information already lost in the double inputs and in the coefficient sums
(`d = x - c`, `q = w + v`, and the `qq`/`dq`/`dd` accumulations). The data
should be read stage by stage.
* A kernel UNCERTAIN is not a failure: the shared route fallback may confirm the
entry. A kernel **MISS bypasses the fallback entirely**, so growing/outward
false-MISS families are exercised through dedicated `rr<0` route fixtures and
reported both as the raw kernel decision (`kern_status`) and the route
outcome. This benchmark does **not** claim that the fallback rescues a raw
kernel MISS on the same case; it only observes that more fallback is used to
confirm inaccurate candidates.
* The double uncertainty band differs from the long-double one by the precision
term: production uses `64·(DBL_EPSILON + LDBL_EPSILON)·scale` while the
double variants use `64·(DBL_EPSILON + DBL_EPSILON)·scale`; since
`LDBL_EPSILON ≪ DBL_EPSILON` this is roughly a factor of two in the band
width, which only matters exactly at the UNCERTAIN/MISS border.
* Number/scale results are machine- and compiler-specific; `environment.json`
records the source hash, compiler, flags and platform.
* This benchmark does not establish a universal guarantee, only the sampled
behaviour on the frozen fixture set.
+489
View File
@@ -0,0 +1,489 @@
#!/usr/bin/env python3
"""Generate the four precision variants of the asymptotic entry kernel and build
one self-contained probe executable per variant.
This module never edits the production tree. It reads the *current working
tree* ``src/asymptotic.c``, extracts the "entry kernel" region delimited by two
lexical anchors, emits four generated full-module copies under the scratch
directory, and compiles a probe that textually includes exactly one copy so the
private static ``entry_solve``/``entry_quadratic_coeffs`` are exercised as the
real generated code (not an independent toy copy).
Variants (all share the same stable-entry algebra, scaling, uncertainty policy,
validation and fallback; only the arithmetic under test changes):
ld_plain long double coefficients/solver, ordinary b*b-4ac and slope
ld_fma reconstructed experimental compensated fma disc/slope
double_fma double type/solver with compensated fma disc/slope,
ordinary coefficient accumulation (type-precision control)
double_fma_coeff double like double_fma plus fma coefficient accumulation
Only the kernel region is rewritten. Every other production dispatch/fallback
path is copied verbatim.
"""
from __future__ import annotations
import hashlib
import json
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
# ---------------------------------------------------------------------------
# Fixed compiler configuration. Identical for all four variants so the
# comparison is not confounded by flags. ``-ffp-contract=off`` disables the
# implicit FMA contraction GCC enables at -O2 for this target, so the ld_plain
# arm cannot silently gain extra precision; explicit fma()/fmal() calls still
# lower to hardware FMA.
# ---------------------------------------------------------------------------
BASE_FLAGS = [
"-std=c11",
"-march=native",
"-O2",
"-DNDEBUG",
"-ffp-contract=off",
"-fopenmp",
]
VARIANTS = ("ld_plain", "ld_fma", "double_fma", "double_fma_coeff")
# Common production translation units needed by the probe. Chosen as the
# minimal dependency closure of the entry kernel plus the public pre-route:
# geodesic initialisation, the numerical entry localizer, the Schwarzschild
# exterior referenced by the full module, the dispatch wrapper and the
# Minkowski provider. main/asymptotic/frame/catalog/PSF/FFTW are excluded.
COMMON_SOURCES = (
"src/geodesic.c",
"src/asymptotic_entry.c",
"src/asymptotic_schwarzschild.c",
"src/spacetime_common.c",
"src/spacetime_minkowski.c",
)
START_ANCHOR = "/* Long-double coefficients of the relative-distance quadratic"
FWD_PREFIX = "static void minkowski_route_entry("
END_ANCHOR = (
"static void minkowski_route_entry(const SpacetimeAsymptoticEnd *end, "
"double t0,"
)
class BuildError(RuntimeError):
pass
def _replace_once(text: str, old: str, new: str, what: str) -> str:
count = text.count(old)
if count != 1:
raise BuildError(f"expected exactly one occurrence of {what}, found {count}")
return text.replace(old, new, 1)
def locate_kernel(lines: list[str]) -> tuple[int, int]:
"""Return [start, end) line indices of the rewritten kernel region.
start is the 'Long-double coefficients' comment; end is the forward
declaration of ``minkowski_route_entry`` (preserved verbatim).
"""
starts = [i for i, l in enumerate(lines) if l.startswith(START_ANCHOR)]
if len(starts) != 1:
raise BuildError(
f"kernel start anchor must appear exactly once, found {len(starts)}"
)
start = starts[0]
ends = [
i
for i, l in enumerate(lines)
if i > start and l.startswith(FWD_PREFIX)
]
if not ends:
raise BuildError("kernel end anchor (minkowski_route_entry) not found")
end = ends[0]
if not lines[end].startswith(END_ANCHOR):
raise BuildError(
"kernel end anchor does not match the expected signature; "
"production source changed"
)
region = "".join(lines[start:end])
for needle in (
"typedef struct {",
"static EntryQuadratic entry_quadratic_coeffs(",
"static EntrySolveResult entry_solve(",
"static long double entry_discriminant(",
"EntryQuadratic;",
):
if region.count(needle) != 1:
raise BuildError(
f"kernel region must contain exactly one '{needle}', "
f"found {region.count(needle)}"
)
# The region must not contain the forward declaration or any later route
# code: those must be byte-for-byte preserved.
if "minkowski_preroute" in region or "AsymptoticStatus asymptotic_route" in region:
raise BuildError("kernel region overran into preserved route code")
return start, end
def transform_ld_plain(kernel: str) -> str:
"""Convert a compensated experimental kernel back to plain long double."""
old_disc = (
" const long double four_a = 4.0L * k->a;\n"
" const long double ac = four_a * k->c;\n"
" const long double ac_error = fmal(four_a, k->c, -ac);\n"
" *scale = k->b * k->b + fabsl(ac);\n"
" return fmal(k->b, k->b, -ac) - ac_error;\n"
)
new_disc = (
" const long double four_a = 4.0L * k->a;\n"
" const long double ac = four_a * k->c;\n"
" *scale = k->b * k->b + fabsl(ac);\n"
" return k->b * k->b - ac;\n"
)
out = _replace_once(kernel, old_disc, new_disc, "ld_plain discriminant body")
old_slope = "const long double slope = fmal(2.0L * k->a, s, k->b);"
new_slope = "const long double slope = 2.0L * k->a * s + k->b;"
out = _replace_once(out, old_slope, new_slope, "ld_plain slope")
return out
def _assert_double_clean(t: str) -> None:
"""The rewritten double kernel must contain no long-double promotion."""
forbidden = ("long double", "fmal(", "fabsl(", "fmaxl(", "frexpl(",
"scalbnl(", "sqrtl(", "copysignl(", "LDBL_")
for tok in forbidden:
if tok in t:
raise BuildError(f"double kernel still contains '{tok}'")
if re.search(r"(?<=[0-9])L(?![A-Za-z0-9_])", t):
raise BuildError("double kernel still contains an L-suffixed literal")
def transform_to_double(kernel: str) -> str:
"""Convert the kernel from long double to double arithmetic.
Explicit fmal->fma, *l->*, LDBL_*->DBL_*, long double->double and strips the
L suffix from the (few) decimal literals. Explicit fma calls are retained
(double_fma keeps compensated discriminant/slope).
"""
t = kernel
for old, new in (
("fmal(", "fma("),
("fabsl(", "fabs("),
("fmaxl(", "fmax("),
("frexpl(", "frexp("),
("scalbnl(", "scalbn("),
("sqrtl(", "sqrt("),
("copysignl(", "copysign("),
):
t = t.replace(old, new)
t = t.replace("LDBL_MIN", "DBL_MIN")
t = t.replace("LDBL_EPSILON", "DBL_EPSILON")
t = t.replace("long double", "double")
# Strip L suffixes from decimal literals; otherwise 4.0L/2.0L would
# re-promote the double expression and confound type accuracy/timing.
t = re.sub(r"(?<=[0-9])L(?![A-Za-z0-9_])", "", t)
_assert_double_clean(t)
return t
def transform_double_fma_coeff(kernel: str) -> str:
"""double variants plus fma coefficient accumulation and products."""
dbl = transform_to_double(kernel)
old_block = (
" double qq = 0.0, dot_dq = 0.0, dot_dd = 0.0;\n"
" for (int i = 0; i < 3; ++i) {\n"
" qq += q[i] * q[i];\n"
" dot_dq += d[i] * q[i];\n"
" dot_dd += d[i] * d[i];\n"
" }\n"
" const double R0_ld = (double)R0;\n"
" const double rr_ld = (double)rr;\n"
" EntryQuadratic k;\n"
" k.a = qq - rr_ld * rr_ld;\n"
" k.b = 2.0 * (dot_dq + R0_ld * rr_ld);\n"
" k.c = dot_dd - R0_ld * R0_ld;\n"
)
new_block = (
" double qq = 0.0, dot_dq = 0.0, dot_dd = 0.0;\n"
" for (int i = 0; i < 3; ++i) {\n"
" qq = fma(q[i], q[i], qq);\n"
" dot_dq = fma(d[i], q[i], dot_dq);\n"
" dot_dd = fma(d[i], d[i], dot_dd);\n"
" }\n"
" const double R0_ld = (double)R0;\n"
" const double rr_ld = (double)rr;\n"
" EntryQuadratic k;\n"
" k.a = fma(-rr_ld, rr_ld, qq);\n"
" k.b = 2.0 * fma(R0_ld, rr_ld, dot_dq);\n"
" k.c = fma(-R0_ld, R0_ld, dot_dd);\n"
)
return _replace_once(dbl, old_block, new_block, "double_fma_coeff block")
def generate_variants(repo: Path, out_dir: Path) -> dict:
"""Read current src/asymptotic.c and emit the four variant modules."""
src_path = repo / "src" / "asymptotic.c"
text = src_path.read_text()
lines = text.splitlines(keepends=True)
start, end = locate_kernel(lines)
kernel = "".join(lines[start:end])
prefix = "".join(lines[:start])
suffix = "".join(lines[end:])
sha = hashlib.sha256(text.encode()).hexdigest()
# Production now uses plain long-double arithmetic. Reconstruct the former
# compensated arm so the original four-way comparison remains reproducible.
plain_disc = (
" const long double four_a = 4.0L * k->a;\n"
" const long double ac = four_a * k->c;\n"
" *scale = k->b * k->b + fabsl(ac);\n"
" return k->b * k->b - ac;\n"
)
fused_disc = plain_disc.replace(
" *scale =", " const long double ac_error = fmal(four_a, k->c, -ac);\n *scale ="
).replace("return k->b * k->b - ac;", "return fmal(k->b, k->b, -ac) - ac_error;")
fused = _replace_once(kernel, plain_disc, fused_disc, "ld_fma discriminant body")
fused = _replace_once(
fused, "const long double slope = 2.0L * k->a * s + k->b;",
"const long double slope = fmal(2.0L * k->a, s, k->b);", "ld_fma slope"
)
kernels = {
"ld_plain": kernel,
"ld_fma": fused,
"double_fma": transform_to_double(fused),
"double_fma_coeff": transform_double_fma_coeff(fused),
}
gen_dir = out_dir / "generated"
gen_dir.mkdir(parents=True, exist_ok=True)
paths = {}
metas = {}
for name in VARIANTS:
body = kernels[name]
banner = (
f"/* GENERATED FILE - do not edit.\n"
f" * variant: {name}\n"
f" * source: src/asymptotic.c sha256={sha}\n"
f" * region lines [{start + 1}, {end}] rewritten; all other code verbatim.\n"
f" */\n"
)
out = banner + prefix + body + suffix
path = gen_dir / f"{name}.c"
path.write_text(out)
paths[name] = path
metas[name] = {
"path": str(path),
"sha256": hashlib.sha256(out.encode()).hexdigest(),
"kernel_sha256": hashlib.sha256(body.encode()).hexdigest(),
}
return {
"source": str(src_path),
"source_sha256": sha,
"kernel_line_range": [start + 1, end],
"variants": metas,
}
def cc_version(cc: str) -> str:
try:
out = subprocess.run(
[cc, "--version"], capture_output=True, text=True, check=False
)
return (out.stdout or out.stderr).splitlines()[0] if out.stdout or out.stderr else ""
except OSError:
return ""
def environment(repo: Path, cc: str, threads: int) -> dict:
src_path = repo / "src" / "asymptotic.c"
env = {
"repo": str(repo),
"cc": cc,
"cc_version": cc_version(cc),
"base_flags": list(BASE_FLAGS),
"threads": threads,
"source_sha256": hashlib.sha256(src_path.read_bytes()).hexdigest(),
"git_head": _git(repo, "rev-parse", "HEAD"),
"git_status_short": _git(repo, "status", "--short"),
"uname": os.uname().sysname + " " + os.uname().release + " " + os.uname().machine,
"cpu_model": _cpu_model(),
}
return env
def _git(repo: Path, *args: str) -> str:
try:
out = subprocess.run(
["git", "-C", str(repo), *args], capture_output=True, text=True, check=False
)
return out.stdout.strip()
except OSError:
return ""
def _cpu_model() -> str:
try:
for line in Path("/proc/cpuinfo").read_text().splitlines():
if line.startswith("model name"):
return line.split(":", 1)[1].strip()
except OSError:
pass
return "unknown"
def compile_common(repo: Path, out_dir: Path, cc: str, flags: list[str]) -> list[Path]:
obj_dir = out_dir / "build" / "common"
obj_dir.mkdir(parents=True, exist_ok=True)
objs = []
for rel in COMMON_SOURCES:
src = repo / rel
obj = obj_dir / (Path(rel).stem + ".o")
cmd = [cc, *flags, f"-I{repo / 'src'}", "-c", str(src), "-o", str(obj)]
_run(cmd, out_dir, f"compile common {rel}")
objs.append(obj)
return objs
def compile_probe(
repo: Path,
out_dir: Path,
cc: str,
flags: list[str],
variant: str,
probe_src: Path,
cases_dir: Path,
common_objs: list[Path],
) -> Path:
"""Compile the probe (which #includes the generated variant) and link."""
exe_dir = out_dir / "build" / "bin"
exe_dir.mkdir(parents=True, exist_ok=True)
obj = out_dir / "build" / f"probe_{variant}.o"
variant_src = out_dir / "generated" / f"{variant}.c"
cmd = [
cc,
*flags,
f"-I{repo / 'src'}",
f"-I{cases_dir}",
f'-DPROBE_VARIANT_SOURCE="{variant_src}"',
f'-DPROBE_VARIANT_NAME="{variant}"',
"-c",
str(probe_src),
"-o",
str(obj),
]
_run(cmd, out_dir, f"compile probe {variant}")
exe = exe_dir / f"probe_{variant}"
link = [cc, *flags, str(obj), *[str(o) for o in common_objs], "-lm", "-o", str(exe)]
_run(link, out_dir, f"link probe {variant}")
return exe
def _run(cmd: list[str], out_dir: Path, what: str) -> None:
log_dir = out_dir / "logs"
log_dir.mkdir(parents=True, exist_ok=True)
with open(log_dir / "build_commands.log", "a") as fh:
fh.write(what + "\n$ " + " ".join(cmd) + "\n")
try:
proc = subprocess.run(cmd, capture_output=True, text=True, check=False)
except OSError as exc:
raise BuildError(f"{what}: {exc}") from exc
with open(log_dir / "build_commands.log", "a") as fh:
fh.write(f"exit={proc.returncode}\n")
if proc.stdout:
fh.write(proc.stdout)
if proc.stderr:
fh.write(proc.stderr)
if proc.returncode != 0:
raise BuildError(
f"{what} failed (exit {proc.returncode}); see logs/build_commands.log\n"
+ (proc.stderr or "")[-4000:]
)
def build_all(repo: Path, out_dir: Path, cc: str, flags: list[str], threads: int) -> dict:
out_dir.mkdir(parents=True, exist_ok=True)
(out_dir / "logs").mkdir(parents=True, exist_ok=True)
variant_info = generate_variants(repo, out_dir)
common = compile_common(repo, out_dir, cc, flags)
probe_src = repo / "benchmarks" / "quadratic_precision" / "fixtures" / "probe.c"
cases_dir = out_dir / "cases"
exes = {}
for name in VARIANTS:
exes[name] = compile_probe(
repo, out_dir, cc, flags, name, probe_src, cases_dir, common
)
result = {"variants": variant_info["variants"],
"source_sha256": variant_info["source_sha256"],
"base_flags": list(flags),
"dependencies": dependency_hashes(repo),
"compiler": {"command": cc, "version": cc_version(cc)},
"probe_sha256": _file_sha(probe_src),
"cases_header_sha256": _file_sha(cases_dir / "quadratic_cases.h")}
result["executables"] = {k: str(v) for k, v in exes.items()}
result["common_objects"] = [str(o) for o in common]
(out_dir / "build_manifest.json").write_text(json.dumps(result, indent=2) + "\n")
return result
def _file_sha(path: Path):
if not Path(path).exists():
return None
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
def dependency_hashes(repo: Path):
# Include all project headers conservatively, including transitive includes.
paths = {repo / rel for rel in COMMON_SOURCES}
paths.update((repo / "src").rglob("*.h"))
return {str(p.relative_to(repo)): _file_sha(p) for p in sorted(paths)}
def verify_manifest(repo: Path, out_dir: Path, flags: list[str], cc: str):
"""Check that an existing build matches the current source, flags and
fixtures. Returns (manifest, list_of_mismatch_reasons). Regenerates the
variant sources as a side effect so their hashes can be compared."""
manifest = json.loads((out_dir / "build_manifest.json").read_text())
reasons = []
if manifest.get("dependencies") != dependency_hashes(repo):
reasons.append("dependencies")
if manifest.get("compiler") != {"command": cc, "version": cc_version(cc)}:
reasons.append("compiler")
current = generate_variants(repo, out_dir)
if manifest.get("source_sha256") != current["source_sha256"]:
reasons.append("source_sha256")
if list(manifest.get("base_flags", [])) != list(flags):
reasons.append("base_flags")
for name, meta in current["variants"].items():
if manifest.get("variants", {}).get(name, {}).get("sha256") != meta["sha256"]:
reasons.append(f"variant:{name}")
probe_src = repo / "benchmarks" / "quadratic_precision" / "fixtures" / "probe.c"
if manifest.get("probe_sha256") != _file_sha(probe_src):
reasons.append("probe_sha256")
cases_header = out_dir / "cases" / "quadratic_cases.h"
if cases_header.exists() and manifest.get("cases_header_sha256") != _file_sha(cases_header):
reasons.append("cases_header_sha256")
for name, exe in manifest.get("executables", {}).items():
if not Path(exe).exists():
reasons.append(f"missing_exe:{name}")
return manifest, reasons
if __name__ == "__main__":
import argparse
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--repo", default=str(Path(__file__).resolve().parents[2]))
ap.add_argument("--output-dir", default="/tmp/opencode/quadratic-comparison")
ap.add_argument("--cc", default=os.environ.get("CC", "cc"))
ap.add_argument("--threads", type=int, default=4)
args = ap.parse_args()
repo = Path(args.repo).resolve()
out = Path(args.output_dir).resolve()
try:
info = build_all(repo, out, args.cc, BASE_FLAGS, args.threads)
except BuildError as exc:
print(f"build failed: {exc}", file=sys.stderr)
sys.exit(1)
print(json.dumps(info, indent=2))
+441
View File
@@ -0,0 +1,441 @@
#!/usr/bin/env python3
"""Deterministic fixture generation for the quadratic precision benchmark.
All values are frozen as IEEE-754 hex literals in the generated C header and as
hex strings in cases.json, so the C probe and the Python oracle see bit-for-bit
identical inputs. No external catalog/observer/slab data is required.
Case families (kernel):
curated explicit adversarial / production fixtures
fixed fixed sphere, axis and oblique directions
moving constant sphere velocity (radial/transverse), inward photons
growing rr < 0 (radius grows along the past parameter), both inward
(toward-center) and initially-outward photons, axis + oblique
shrinking rr > 0, kernel only (radius would go negative on the far past)
cancellation translated/far-origin style input cancellation
Route cases use only rr == 0 (positive radius on the whole open past segment)
plus the two production grazing rows.
"""
from __future__ import annotations
import json
import math
from pathlib import Path
# Radii spanning subnormal-adjacent to huge scales.
RADII = [1e-100, 1e-10, 1.0, 1e10, 1e100]
DIST = [
math.nextafter(1.0, math.inf),
2.0,
10.0,
100.0,
512.0,
1024.0,
1e4,
1e8,
1e10,
]
IMPACTS = [
0.0,
0.5,
0.99,
1.0 - 1e-6,
math.nextafter(1.0, 0.0),
1.0,
math.nextafter(1.0, math.inf),
1.0 + 1e-6,
1.1,
]
VELS = [0.0, 0.1, 2.0, 10.0]
RR_GROW = [
-1.0,
math.nextafter(-1.0, -math.inf),
math.nextafter(-1.0, math.inf),
-0.999999,
]
def norm3(v):
n = math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2])
return [v[0] / n, v[1] / n, v[2] / n]
def cross(a, b):
return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0]]
# Fixed direction frames: (tow, perp). `tow` points from the sphere centre to
# the camera; `w = -tow` is the inward (toward-centre) past direction.
DIRS_AXIS = ([1.0, 0.0, 0.0], [0.0, 1.0, 0.0])
DIRS_OBLIQUE1 = (
norm3([1.0, 2.0, 3.0]),
norm3(cross(norm3([1.0, 2.0, 3.0]), [0.0, 0.0, 1.0])),
)
DIRS_OBLIQUE2 = (
norm3([-2.0, 1.0, 0.7]),
norm3(cross(norm3([-2.0, 1.0, 0.7]), [0.0, 1.0, 0.0])),
)
DIRS = [DIRS_AXIS, DIRS_OBLIQUE1, DIRS_OBLIQUE2]
def _case(cid, category, x, c, w, v, R0, rr):
return {
"id": cid,
"category": category,
"x": list(x),
"c": list(c),
"w": list(w),
"v": list(v),
"R0": R0,
"rr": rr,
}
def curated_kernel_cases():
"""Explicit adversarial and production-reproduction fixtures."""
cases = []
t63 = float.fromhex("0x1.fa8f5c28f5c29p+3")
t64 = float.fromhex("0x1.fb17e4b17e4b1p+3")
w63 = [-float.fromhex("0x1.d4afba4704cap-2"),
float.fromhex("0x1.c7378f8e872d1p-1"),
float.fromhex("0x1.15bad4e30e8ddp-8")]
w64 = [-float.fromhex("0x1.f98ae1a782104p-2"),
float.fromhex("0x1.bd3bb364ac492p-1"),
float.fromhex("0x1.102d2a1c6ac74p-7")]
# Production Alcubierre grazing rows: x=(0,-24,0), centre=2t, v=2, R=5.
cases.append(_case(0, "real63", [0.0, -24.0, 0.0],
[2.0 * t63, 0.0, 0.0], w63, [2.0, 0.0, 0.0], 5.0, 0.0))
cases.append(_case(1, "real64", [0.0, -24.0, 0.0],
[2.0 * t64, 0.0, 0.0], w64, [2.0, 0.0, 0.0], 5.0, 0.0))
D = 10.0
R = 5.0
cases += [
_case(2, "headon_hit", [D, 0.0, 0.0], [0.0] * 3, [-1.0, 0.0, 0.0],
[0.0] * 3, R, 0.0),
_case(3, "headon_miss", [D, 0.0, 0.0], [0.0] * 3, [1.0, 0.0, 0.0],
[0.0] * 3, R, 0.0),
_case(4, "clear_miss", [D, 6.0, 0.0], [0.0] * 3, [-1.0, 0.0, 0.0],
[0.0] * 3, R, 0.0),
_case(5, "grazing_in", [D, math.nextafter(R, 0.0), 0.0], [0.0] * 3,
[-1.0, 0.0, 0.0], [0.0] * 3, R, 0.0),
_case(6, "exact_tangent", [D, R, 0.0], [0.0] * 3, [-1.0, 0.0, 0.0],
[0.0] * 3, R, 0.0),
_case(7, "grazing_out", [D, math.nextafter(R, math.inf), 0.0],
[0.0] * 3, [-1.0, 0.0, 0.0], [0.0] * 3, R, 0.0),
_case(8, "near_boundary_hit",
[math.nextafter(R, math.inf), 0.0, 0.0], [0.0] * 3,
[-1.0, 0.0, 0.0], [0.0] * 3, R, 0.0),
# 1e10 + 0.5, R=1: c loses the transverse term, b*b-4ac rounds to 0.
_case(9, "cancel_1e10_05", [1e10, 0.5, 0.0], [0.0] * 3,
[-1.0, 0.0, 0.0], [0.0] * 3, 1.0, 0.0),
_case(10, "cancel_1e10_1", [1e10, 1.0, 0.0], [0.0] * 3,
[-1.0, 0.0, 0.0], [0.0] * 3, 1.0, 0.0),
]
# Large-t positive reconstructed minimum (tests/test_asymptotic.c).
camera_x = float.fromhex("0x1.6bcc41e901908p+46")
t0 = 1e15
vx = 0.1
cases.append(_case(11, "large_t_01t", [camera_x, 0.0, 0.0],
[vx * t0, 0.0, 0.0], [-1.0, 0.0, 0.0], [vx, 0.0, 0.0],
10.0, 0.0))
# Exact linear growing sphere (a == 0), inward.
cases.append(_case(12, "linear_grow_in", [100.0, 0.0, 0.0], [0.0] * 3,
[-1.0, 0.0, 0.0], [0.0] * 3, 10.0, -1.0))
# Exact linear, initially outward: constant gap, honest miss.
cases.append(_case(13, "linear_grow_out", [100.0, 0.0, 0.0], [0.0] * 3,
[1.0, 0.0, 0.0], [0.0] * 3, 10.0, -1.0))
# Near-linear oblique, inward vs initially outward, rr just around -1.
tow = DIRS_OBLIQUE1[0]
perp = DIRS_OBLIQUE1[1]
for rr in RR_GROW:
for sign, tag in ((-1.0, "in"), (1.0, "out")):
w = [sign * tow[i] for i in range(3)]
x = [50.0 * tow[i] + 0.25 * perp[i] for i in range(3)]
cases.append(_case(len(cases), f"nearlin_{tag}", x, [0.0] * 3, w,
[0.0] * 3, 4.0, rr))
# Translated-origin style cancellation: d = x - c with x = c + small.
base = 1e10
cbase = [base, -base, base * 0.5]
xb = [cbase[0] + 10.0, cbase[1] + 0.5, cbase[2] + 0.0]
cases.append(_case(len(cases), "translated_origin", xb, cbase,
[-1.0, 0.0, 0.0], [0.0] * 3, 1.0, 0.0))
return cases
def _broad_fixed(cid):
for di, (tow, perp) in enumerate(DIRS):
for R in RADII:
for dr in DIST:
for ir in IMPACTS:
D = dr * R
b = ir * R
x = [D * tow[i] + b * perp[i] for i in range(3)]
w = [-tow[i] for i in range(3)]
yield _case(cid, f"fixed_d{di}", x, [0.0] * 3, w, [0.0] * 3,
R, 0.0)
cid += 1
return cid
def _broad_moving(cid):
for (tow, perp) in DIRS:
for R in (1e-10, 1.0, 1e10):
for dr in (2.0, 100.0, 1e4):
for ir in (0.0, 0.99, 1.0, 1.1):
for vel in VELS:
D = dr * R
b = ir * R
x = [D * tow[i] + b * perp[i] for i in range(3)]
w = [-tow[i] for i in range(3)]
v = [vel * perp[i] for i in range(3)]
yield _case(cid, "moving", x, [0.0] * 3, w, v, R, 0.0)
cid += 1
return cid
def _broad_growing(cid):
for di, (tow, perp) in enumerate(DIRS):
for sign, tag in ((-1.0, "in"), (1.0, "out")):
for rr in RR_GROW:
for dr in (2.0, 10.0, 100.0, 1e4, 1e8):
for ir in (0.0, 0.5, 0.99, 1.0, 1.1):
D = dr * 4.0
b = ir * 4.0
x = [D * tow[i] + b * perp[i] for i in range(3)]
w = [sign * tow[i] for i in range(3)]
yield _case(cid, f"growing_{tag}", x, [0.0] * 3, w,
[0.0] * 3, 4.0, rr)
cid += 1
return cid
def _broad_shrinking(cid):
for (tow, perp) in (DIRS_AXIS, DIRS_OBLIQUE1):
for dr in (2.0, 10.0, 100.0):
for ir in (0.0, 0.99, 1.0):
D = dr * 10.0
b = ir * 10.0
x = [D * tow[i] + b * perp[i] for i in range(3)]
w = [-tow[i] for i in range(3)]
yield _case(cid, "shrinking", x, [0.0] * 3, w, [0.0] * 3, 10.0,
0.1)
cid += 1
return cid
def _materialize(gen, cases):
for c in gen:
cases.append(c)
def _build_all():
cases = curated_kernel_cases()
cid = 1000
for gen in (_broad_fixed, _broad_moving, _broad_growing, _broad_shrinking):
gen_cases = []
_materialize(gen(cid), gen_cases)
if gen_cases:
cid = gen_cases[-1]["id"] + 1
cases.extend(gen_cases)
return cases
# ---------------------------------------------------------------------------
def _route(cid, category, t0, obs, direction, c0, v, R0, rr, model=0,
valid_t_min=-1e300):
return {
"id": cid,
"category": category,
"t0": t0,
"obs": list(obs),
"dir": list(direction),
"c0": list(c0),
"v": list(v),
"R0": R0,
"rr": rr,
"valid_t_min": valid_t_min,
"model": model,
}
def route_cases():
cases = []
t63 = float.fromhex("0x1.fa8f5c28f5c29p+3")
t64 = float.fromhex("0x1.fb17e4b17e4b1p+3")
w63 = [-float.fromhex("0x1.d4afba4704cap-2"),
float.fromhex("0x1.c7378f8e872d1p-1"),
float.fromhex("0x1.15bad4e30e8ddp-8")]
w64 = [-float.fromhex("0x1.f98ae1a782104p-2"),
float.fromhex("0x1.bd3bb364ac492p-1"),
float.fromhex("0x1.102d2a1c6ac74p-7")]
# Production rows reproduced through the Alcubierre-style callback (model 1).
cases.append(_route(0, "real63", t63, [0.0, -24.0, 0.0], w63,
[2.0 * t63, 0.0, 0.0], [2.0, 0.0, 0.0], 5.0, 0.0,
model=1))
cases.append(_route(1, "real64", t64, [0.0, -24.0, 0.0], w64,
[2.0 * t64, 0.0, 0.0], [2.0, 0.0, 0.0], 5.0, 0.0,
model=1))
# Same rows through the input-stable callback (model 0).
cases.append(_route(2, "real63_stable", t63, [0.0, -24.0, 0.0], w63,
[2.0 * t63, 0.0, 0.0], [2.0, 0.0, 0.0], 5.0, 0.0,
model=0))
cases.append(_route(3, "real64_stable", t64, [0.0, -24.0, 0.0], w64,
[2.0 * t64, 0.0, 0.0], [2.0, 0.0, 0.0], 5.0, 0.0,
model=0))
R = 5.0
cases += [
_route(10, "headon_hit", 0.0, [10.0, 0.0, 0.0], [-1.0, 0.0, 0.0],
[0.0] * 3, [0.0] * 3, R, 0.0),
_route(11, "headon_miss", 0.0, [10.0, 0.0, 0.0], [1.0, 0.0, 0.0],
[0.0] * 3, [0.0] * 3, R, 0.0),
_route(12, "clear_miss", 0.0, [10.0, 6.0, 0.0], [-1.0, 0.0, 0.0],
[0.0] * 3, [0.0] * 3, R, 0.0),
_route(13, "grazing_in", 0.0, [10.0, math.nextafter(R, 0.0), 0.0],
[-1.0, 0.0, 0.0], [0.0] * 3, [0.0] * 3, R, 0.0),
_route(14, "exact_tangent", 0.0, [10.0, R, 0.0], [-1.0, 0.0, 0.0],
[0.0] * 3, [0.0] * 3, R, 0.0),
_route(15, "grazing_out", 0.0,
[10.0, math.nextafter(R, math.inf), 0.0], [-1.0, 0.0, 0.0],
[0.0] * 3, [0.0] * 3, R, 0.0),
_route(16, "camera_inside", 0.0, [2.0, 0.0, 0.0], [1.0, 0.0, 0.0],
[0.0] * 3, [0.0] * 3, R, 0.0),
_route(17, "near_boundary_hit", 0.0,
[math.nextafter(R, math.inf), 0.0, 0.0], [-1.0, 0.0, 0.0],
[0.0] * 3, [0.0] * 3, R, 0.0),
]
# Broad fixed-sphere subset through the public route.
cid = 100
for (tow, perp) in (DIRS_AXIS, DIRS_OBLIQUE1):
for R0 in (1e-10, 1.0, 1e10):
for dr in (2.0, 10.0, 100.0, 512.0, 1024.0, 1e4):
for ir in (0.0, 0.5, 0.99, 1.0, 1.1):
D = dr * R0
b = ir * R0
obs = [D * tow[i] + b * perp[i] for i in range(3)]
direction = [-tow[i] for i in range(3)]
cases.append(_route(cid, f"route_fixed_d{dr:g}", 0.0, obs,
direction, [0.0] * 3, [0.0] * 3, R0,
0.0))
cid += 1
# Moving spheres (v = 2 tow), model 0.
for ir in (0.0, 0.99, 1.1):
cases.append(_route(cid, "route_moving", 0.0, [100.0, 0.0, 0.0],
[-1.0, 0.0, 0.0], [0.0] * 3, [2.0, 0.0, 0.0], 5.0,
0.0))
cid += 1
# Growing worldtubes (rr <= 0, positive radius on the whole past), inward
# and initially-outward photons, axis + oblique. These mirror the
# growing_out kernel false-MISS family and exercise the public route where
# a kernel MISS bypasses the fallback entirely. valid_t_min is far below
# any sampled time so no artificial history clip is introduced.
R0 = 4.0
for (tow, perp) in DIRS:
for sign, tag in ((-1.0, "in"), (1.0, "out")):
for rr in RR_GROW:
for dr in (2.0, 10.0, 100.0, 512.0, 1024.0, 1e4):
for ir in (0.0, 1.0, 1.1):
D = dr * R0
b = ir * R0
obs = [D * tow[i] + b * perp[i] for i in range(3)]
direction = [sign * tow[i] for i in range(3)]
cases.append(_route(cid, f"route_grow_{tag}_d{dr:g}",
0.0, obs, direction, [0.0] * 3,
[0.0] * 3, R0, rr))
cid += 1
return cases
# ---------------------------------------------------------------------------
def _hex(x):
return float(x).hex()
def _fmt(v):
return _hex(v)
def write_header(kernel, route, out_path: Path):
lines = []
lines.append("/* Generated by benchmarks/quadratic_precision/cases.py. */\n")
lines.append("#ifndef QUADRATIC_CASES_H\n#define QUADRATIC_CASES_H\n")
lines.append("typedef struct {\n int id;\n const char *category;\n"
" double x[3], c[3], w[3], v[3];\n double R0, rr;\n"
"} QuadKernelCase;\n\n")
lines.append("typedef struct {\n int id;\n const char *category;\n"
" double t0;\n double obs[3];\n double dir[3];\n"
" double c0[3];\n double v[3];\n double R0, rr;\n"
" double valid_t_min;\n int model;\n} QuadRouteCase;\n\n")
lines.append("static const QuadKernelCase quad_kernel_cases[] = {\n")
for c in kernel:
lines.append(
" {.id=%d,.category=\"%s\","
".x={%s,%s,%s},.c={%s,%s,%s},.w={%s,%s,%s},.v={%s,%s,%s},"
".R0=%s,.rr=%s},\n"
% (c["id"], c["category"], *[_fmt(z) for z in c["x"]],
*[_fmt(z) for z in c["c"]], *[_fmt(z) for z in c["w"]],
*[_fmt(z) for z in c["v"]], _fmt(c["R0"]), _fmt(c["rr"])))
lines.append("};\n")
lines.append("static const int quad_kernel_case_count = %d;\n\n"
% len(kernel))
lines.append("static const QuadRouteCase quad_route_cases[] = {\n")
for c in route:
lines.append(
" {.id=%d,.category=\"%s\",.t0=%s,"
".obs={%s,%s,%s},.dir={%s,%s,%s},.c0={%s,%s,%s},.v={%s,%s,%s},"
".R0=%s,.rr=%s,.valid_t_min=%s,.model=%d},\n"
% (c["id"], c["category"], _fmt(c["t0"]),
*[_fmt(z) for z in c["obs"]], *[_fmt(z) for z in c["dir"]],
*[_fmt(z) for z in c["c0"]], *[_fmt(z) for z in c["v"]],
_fmt(c["R0"]), _fmt(c["rr"]), _fmt(c["valid_t_min"]),
c["model"]))
lines.append("};\n")
lines.append("static const int quad_route_case_count = %d;\n"
% len(route))
lines.append("#endif\n")
out_path.write_text("".join(lines))
def write_json(kernel, route, out_path: Path):
def enc(c):
d = {}
for k, v in c.items():
if isinstance(v, float):
d[k] = v.hex()
elif isinstance(v, list):
d[k] = [z.hex() for z in v]
else:
d[k] = v
return d
out_path.write_text(
json.dumps(
{"deterministic": True, "kernel": [enc(c) for c in kernel],
"route": [enc(c) for c in route]},
indent=1,
)
+ "\n")
def build():
kernel = _build_all()
route = route_cases()
return kernel, route
if __name__ == "__main__":
k, r = build()
print(f"kernel cases: {len(k)}, route cases: {len(r)}")
cats = {}
for c in k:
cats[c["category"]] = cats.get(c["category"], 0) + 1
for key in sorted(cats):
print(f" {key}: {cats[key]}")
rcats = {}
for c in r:
rcats[c["category"]] = rcats.get(c["category"], 0) + 1
print("route categories:")
for key in sorted(rcats):
print(f" {key}: {rcats[key]}")
@@ -0,0 +1,614 @@
/* Quadratic precision benchmark probe.
*
* Compiled once per generated variant, with
* -DPROBE_VARIANT_SOURCE=".../ld_fma.c" -DPROBE_VARIANT_NAME="ld_fma"
* The probe #includes the generated full module, so the private static
* entry_quadratic_coeffs / entry_solve and the public asymptotic_route_camera
* are the *actual generated* code. No production file is modified.
*
* Modes:
* accuracy -- run every kernel case through the generated kernel and every
* route case through the generated public pre-route; emit CSVs.
* microbench -- timed repeated kernel assembly + entry_solve (serial).
* routebench -- timed repeated public pre-route (OpenMP, static schedule).
*/
#include PROBE_VARIANT_SOURCE
#include <float.h>
#include <math.h>
#include <omp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "quadratic_cases.h"
#ifndef PROBE_VARIANT_NAME
#define PROBE_VARIANT_NAME "unknown"
#endif
/* ------------------------------------------------------------------ */
/* Exact-value coefficient printing: long double uses %La (full mantissa),
* double uses %a. _Generic selects the right printer without knowing the
* generated EntryQuadratic field type. */
static void coeff_ld(FILE *f, long double v) { fprintf(f, "%La", v); }
static void coeff_d(FILE *f, double v) { fprintf(f, "%a", v); }
#define PRINT_COEFF(f, v) \
_Generic((v), long double : coeff_ld, double : coeff_d)((f), (v))
/* ------------------------------------------------------------------ */
/* Fixture source: flat identity metric, one Minkowski end whose worldtube is
* a constant-velocity (optionally linearly growing/shrinking) sphere.
*
* model 0 (input-stable): center = c0 + v*(t - t0), R = R0 + rr*(t - t0)
* model 1 (production-style): center = v*t, R = R0 + rr*t
*
* model 0 evaluates exactly at the camera: center(t0) == c0, R(t0) == R0, so
* the kernel inputs are the frozen case values. model 1 mirrors the
* Alcubierre-style callback (used only with rr == 0). */
typedef struct {
double c0[3];
double v[3];
double R0;
double rr;
double t0;
double valid_t_min;
int model;
} FixtureContext;
static double fixture_center(const FixtureContext *ctx, int i, double t) {
if (ctx->model == 0)
return ctx->c0[i] + ctx->v[i] * (t - ctx->t0);
return ctx->v[i] * t;
}
static double fixture_radius(const FixtureContext *ctx, double t) {
if (ctx->model == 0)
return ctx->R0 + ctx->rr * (t - ctx->t0);
return ctx->R0 + ctx->rr * t;
}
static SpacetimePointStatus fixture_eval(const SpacetimeSource *source,
double t, const double x[3],
MetricData *metric) {
(void)source;
(void)t;
(void)x;
*metric = (MetricData){.alpha = 1.0,
.gamma = {{1.0, 0.0, 0.0},
{0.0, 1.0, 0.0},
{0.0, 0.0, 1.0}}};
return SPACETIME_POINT_OK;
}
static SpacetimeRayStatus fixture_classify(const SpacetimeSource *source,
double t, const double x[3]) {
(void)source;
(void)t;
(void)x;
return SPACETIME_RAY_ACTIVE;
}
static size_t fixture_end_count(const SpacetimeSource *source) {
(void)source;
return 1;
}
static int fixture_end(const SpacetimeSource *source, size_t index,
SpacetimeAsymptoticEnd *out) {
const FixtureContext *ctx = source->context;
if (index != 0)
return -1;
*out = (SpacetimeAsymptoticEnd){
.end_id = 0,
.exterior_kind = ASYMPTOTIC_EXTERIOR_MINKOWSKI,
.mass = 0.0,
.frame_origin = {0.0, 0.0, 0.0},
.frame_axes = {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}};
return 0;
}
static int fixture_worldtube(const SpacetimeSource *source,
SpacetimeEndId end_id, double t,
SpacetimeEscapeWorldtubeSample *out) {
const FixtureContext *ctx = source->context;
if (end_id != 0)
return -1;
if (!isfinite(t) || t < ctx->valid_t_min) {
*out = (SpacetimeEscapeWorldtubeSample){.valid = 0};
return 0;
}
*out = (SpacetimeEscapeWorldtubeSample){
.center = {fixture_center(ctx, 0, t), fixture_center(ctx, 1, t),
fixture_center(ctx, 2, t)},
.velocity = {ctx->v[0], ctx->v[1], ctx->v[2]},
.radius = fixture_radius(ctx, t),
.radius_rate = ctx->rr,
.velocity_constant = 1,
.valid = 1};
return 0;
}
static double fixture_next_segment(const SpacetimeSource *source,
SpacetimeEndId end_id, double t) {
(void)source;
(void)end_id;
(void)t;
return NAN;
}
static void fixture_destroy(SpacetimeSource *source) {
source->context = NULL;
source->ops = NULL;
}
static const SpacetimeOps fixture_ops = {
.eval = fixture_eval,
.classify = fixture_classify,
.asymptotic_end_count = fixture_end_count,
.asymptotic_end = fixture_end,
.escape_worldtube_sample = fixture_worldtube,
.escape_worldtube_next_segment = fixture_next_segment,
.destroy = fixture_destroy,
};
/* ------------------------------------------------------------------ */
static void print_environment(void) {
fprintf(stdout,
"{\"variant\":\"%s\",\"sizeof_long_double\":%zu,\"LDBL_MANT_DIG\":%d,"
"\"DBL_MANT_DIG\":%d,\"LDBL_MAX_EXP\":%d,\"hardware_threads\":%d,"
"\"omp_max_threads\":%d}\n",
PROBE_VARIANT_NAME, sizeof(long double), LDBL_MANT_DIG, DBL_MANT_DIG,
LDBL_MAX_EXP, omp_get_num_procs(), omp_get_max_threads());
}
/* ------------------------------------------------------------------ */
/* accuracy mode */
static void mode_accuracy(const char *kernel_out, const char *route_out) {
FILE *kf = fopen(kernel_out, "w");
if (kf == NULL) {
fprintf(stderr, "cannot open %s\n", kernel_out);
exit(2);
}
fprintf(kf,
"id,category,status,sigma,x0,x1,x2,c0,c1,c2,w0,w1,w2,v0,v1,v2,R0,rr,"
"a,b,c\n");
for (int i = 0; i < quad_kernel_case_count; ++i) {
const QuadKernelCase *c = &quad_kernel_cases[i];
EntryQuadratic k = entry_quadratic_coeffs(c->x, c->c, c->w, c->v, c->R0,
c->rr);
double s = -1.0;
EntrySolveResult r = entry_solve(&k, &s);
fprintf(kf, "%d,%s,%d,%a", c->id, c->category, (int)r, s);
for (int j = 0; j < 3; ++j)
fprintf(kf, ",%a", c->x[j]);
for (int j = 0; j < 3; ++j)
fprintf(kf, ",%a", c->c[j]);
for (int j = 0; j < 3; ++j)
fprintf(kf, ",%a", c->w[j]);
for (int j = 0; j < 3; ++j)
fprintf(kf, ",%a", c->v[j]);
fprintf(kf, ",%a,%a,", c->R0, c->rr);
PRINT_COEFF(kf, k.a);
fputc(',', kf);
PRINT_COEFF(kf, k.b);
fputc(',', kf);
PRINT_COEFF(kf, k.c);
fputc('\n', kf);
}
fclose(kf);
FILE *rf = fopen(route_out, "w");
if (rf == NULL) {
fprintf(stderr, "cannot open %s\n", route_out);
exit(2);
}
fprintf(rf,
"id,category,status,kind,failure_reason,fallback_evals,pi_match,"
"lcam_match,F,tol,why,x0,x1,x2,Pi0,Pi1,Pi2,ninf0,ninf1,ninf2,"
"canon_t,canonx0,canonx1,canonx2,canonw0,canonw1,canonw2,"
"cbx0,cbx1,cbx2,cbr,cbok,ct00,ct01,ct02,rt0,cb0ok,"
"kern_ok,kern_status,kern_sigma,kern_a,kern_b,kern_c,"
"logcamera,logentry,"
"t0,oc0,oc1,oc2,d0,d1,d2,fc0,fc1,fc2,v0,v1,v2,"
"R0,rr,model,reason_name\n");
for (int i = 0; i < quad_route_case_count; ++i) {
const QuadRouteCase *c = &quad_route_cases[i];
FixtureContext ctx = {.c0 = {c->c0[0], c->c0[1], c->c0[2]},
.v = {c->v[0], c->v[1], c->v[2]},
.R0 = c->R0,
.rr = c->rr,
.t0 = c->t0,
.valid_t_min = c->valid_t_min,
.model = c->model};
SpacetimeSource source = {.ops = &fixture_ops, .context = &ctx};
ObserverState obs = {0};
obs.coordinate_time = c->t0;
for (int j = 0; j < 3; ++j)
obs.coordinate_position[j] = c->obs[j];
obs.tetrad[0][0] = 1.0;
obs.tetrad[1][1] = 1.0;
obs.tetrad[2][2] = 1.0;
obs.tetrad[3][3] = 1.0;
int canon_ok = 0;
AsymptoticPhotonState canon = {0};
GeodesicRayState st = {0};
{
MetricData metric;
if (spacetime_eval(&source, c->t0, obs.coordinate_position, &metric) ==
SPACETIME_POINT_OK &&
geodesic_initialize_past_ray_metric(&metric, &obs, c->dir, &st) == 0 &&
asymptotic_canonical_from_backend(&source, 0, &metric, c->t0, st.x,
st.Pi, st.log_alpha_p0,
&canon) == 0)
canon_ok = 1;
}
AsymptoticRoute route;
AsymptoticStatus status =
asymptotic_route_camera(&source, &obs, c->dir, &route);
int pi_match = 1, lcam_match = 1;
if (canon_ok && status == ASYMPTOTIC_OK &&
route.kind == ASYMPTOTIC_ROUTE_ENTRY) {
for (int j = 0; j < 3; ++j)
if (route.Pi[j] != st.Pi[j])
pi_match = 0;
if (route.log_alpha_p0_camera != st.log_alpha_p0)
lcam_match = 0;
}
double F = NAN, tol = NAN;
RayReason why = RAY_REASON_NONE;
if (status == ASYMPTOTIC_OK && route.kind == ASYMPTOTIC_ROUTE_ENTRY)
asymptotic_entry_geometry(&source, route.end_id, route.activate_t,
route.x, &F, &tol, &why);
SpacetimeEscapeWorldtubeSample cb = {0};
int cbok = 0;
{
const double tt = (status == ASYMPTOTIC_OK &&
route.kind == ASYMPTOTIC_ROUTE_ENTRY)
? route.activate_t
: c->t0;
if (spacetime_escape_worldtube_sample(&source, route.end_id, tt, &cb) ==
0 &&
cb.valid)
cbok = 1;
}
/* Worldtube sample at the segment start used by the quadratic kernel. */
SpacetimeEscapeWorldtubeSample cb0 = {0};
int cb0ok = 0;
if (spacetime_escape_worldtube_sample(&source, route.end_id, c->t0,
&cb0) == 0 &&
cb0.valid)
cb0ok = 1;
/* Reproduce the first-segment public kernel classification at the actual
* canonical inputs, so a reference ENTER / kernel MISS / route ESCAPED is
* directly visible and not confused with canonical normalisation. */
int kern_ok = 0;
int kern_status = -1;
double kern_sigma = -1.0;
EntryQuadratic kk = {0};
SpacetimeAsymptoticEnd end_desc;
if (canon_ok && cb0ok &&
spacetime_asymptotic_end(&source, 0, &end_desc) == 0) {
double c_frame[3], v_frame[3];
backend_position_to_frame(&end_desc, cb0.center, c_frame);
backend_vector_to_frame(&end_desc, cb0.velocity, v_frame);
kk = entry_quadratic_coeffs(canon.x, c_frame, canon.w, v_frame,
cb0.radius, cb0.radius_rate);
kern_status = (int)entry_solve(&kk, &kern_sigma);
kern_ok = 1;
}
fprintf(rf, "%d,%s,%d,%d,%d,%u,%d,%d,%a,%a,%d", c->id, c->category,
(int)status, (int)route.kind, (int)route.failure_reason,
route.entry_fallback_evaluations, pi_match, lcam_match, F, tol,
(int)why);
for (int j = 0; j < 3; ++j)
fprintf(rf, ",%a", route.x[j]);
for (int j = 0; j < 3; ++j)
fprintf(rf, ",%a", route.Pi[j]);
for (int j = 0; j < 3; ++j)
fprintf(rf, ",%a", route.n_infinity[j]);
fprintf(rf, ",%a", canon.t);
for (int j = 0; j < 3; ++j)
fprintf(rf, ",%a", canon.x[j]);
for (int j = 0; j < 3; ++j)
fprintf(rf, ",%a", canon.w[j]);
for (int j = 0; j < 3; ++j)
fprintf(rf, ",%a", cb.center[j]);
fprintf(rf, ",%a,%d", cb.radius, cbok);
for (int j = 0; j < 3; ++j)
fprintf(rf, ",%a", cb0.center[j]);
fprintf(rf, ",%a,%d", cb0.radius, cb0ok);
fprintf(rf, ",%d,%d,%a,", kern_ok, kern_status, kern_sigma);
PRINT_COEFF(rf, kk.a);
fputc(',', rf);
PRINT_COEFF(rf, kk.b);
fputc(',', rf);
PRINT_COEFF(rf, kk.c);
fprintf(rf, ",%a,%a", route.log_alpha_p0_camera, route.log_alpha_p0);
fprintf(rf, ",%a", c->t0);
for (int j = 0; j < 3; ++j)
fprintf(rf, ",%a", c->obs[j]);
for (int j = 0; j < 3; ++j)
fprintf(rf, ",%a", c->dir[j]);
for (int j = 0; j < 3; ++j)
fprintf(rf, ",%a", c->c0[j]);
for (int j = 0; j < 3; ++j)
fprintf(rf, ",%a", c->v[j]);
fprintf(rf, ",%a,%a,%d,%s\n", c->R0, c->rr, c->model,
ray_reason_name(route.failure_reason));
}
fclose(rf);
}
/* ------------------------------------------------------------------ */
/* microbench mode: coefficient assembly + entry_solve, serial. */
static volatile double g_kernel_sink;
static __attribute__((noinline)) double kernel_batch(const QuadKernelCase *cs,
int n, long reps) {
double acc = 0.0;
for (long r = 0; r < reps; ++r) {
for (int i = 0; i < n; ++i) {
/* Force the index through an opaque register so the compiler cannot
* hoist the pure coefficient solve out of the repetition loop or prove
* the loaded fixture invariant. Identical for every variant. */
int idx = i;
__asm__ __volatile__("" : "+r"(idx) : : "memory");
const QuadKernelCase *c = cs + idx;
EntryQuadratic k = entry_quadratic_coeffs(c->x, c->c, c->w, c->v,
c->R0, c->rr);
double s = 0.0;
acc += (double)entry_solve(&k, &s) + s * 1e-300;
}
}
return acc;
}
static void mode_microbench(long target_calls, const char *out) {
const int n = quad_kernel_case_count;
long reps = target_calls / n;
if (reps < 1)
reps = 1;
/* Warm-up outside the timed region. */
g_kernel_sink += kernel_batch(quad_kernel_cases, n, 1);
const double t0 = omp_get_wtime();
const double acc = kernel_batch(quad_kernel_cases, n, reps);
const double t1 = omp_get_wtime();
g_kernel_sink += acc;
const long calls = (long)n * reps;
const double seconds = t1 - t0;
FILE *f = fopen(out, "w");
if (f == NULL) {
fprintf(stderr, "cannot open %s\n", out);
exit(2);
}
fprintf(f,
"{\"variant\":\"%s\",\"mode\":\"microbench\",\"cases\":%d,"
"\"reps\":%ld,\"calls\":%ld,\"seconds\":%.9f,\"ns_per_call\":%.6f,"
"\"sink\":%.17g}\n",
PROBE_VARIANT_NAME, n, reps, calls, seconds,
seconds * 1e9 / (double)calls, g_kernel_sink);
fclose(f);
fprintf(stdout, "microbench %s: %ld calls in %.6f s (%.2f ns/call)\n",
PROBE_VARIANT_NAME, calls, seconds, seconds * 1e9 / (double)calls);
}
/* ------------------------------------------------------------------ */
/* routebench mode: public asymptotic_route_camera, OpenMP static. */
typedef struct {
FixtureContext *ctxs;
SpacetimeSource *srcs;
ObserverState *obss;
const QuadRouteCase *rcs;
int n;
} Preloaded;
typedef struct {
long entry, escaped, inside, time_exhausted, invalid, unsupported, other;
long fallback_sum;
} RouteCounts;
static volatile double g_route_sink;
static __attribute__((noinline)) void route_batch(const Preloaded *p, long reps,
int threads, double *seconds,
RouteCounts *counts) {
const int n = p->n;
long entry = 0, escaped = 0, inside = 0, texh = 0, inv = 0, unsup = 0,
other = 0, fallback = 0;
const long total = (long)n * reps;
const double t0 = omp_get_wtime();
#pragma omp parallel num_threads(threads) reduction(+ : entry, escaped, inside, texh, inv, unsup, other, fallback)
{
#pragma omp for schedule(static)
for (long k = 0; k < total; ++k) {
long kk = k;
__asm__ __volatile__("" : "+r"(kk) : : "memory");
const int i = (int)(kk % n);
AsymptoticRoute route;
const AsymptoticStatus st = asymptotic_route_camera(
&p->srcs[i], &p->obss[i], p->rcs[i].dir, &route);
fallback += (long)route.entry_fallback_evaluations;
if (st == ASYMPTOTIC_OK) {
switch (route.kind) {
case ASYMPTOTIC_ROUTE_ENTRY:
++entry;
break;
case ASYMPTOTIC_ROUTE_ESCAPED:
++escaped;
break;
case ASYMPTOTIC_ROUTE_INSIDE:
++inside;
break;
case ASYMPTOTIC_ROUTE_TIME_RANGE_EXHAUSTED:
++texh;
break;
default:
++other;
break;
}
} else if (st == ASYMPTOTIC_UNSUPPORTED) {
++unsup;
} else {
++inv;
}
}
}
*seconds = omp_get_wtime() - t0;
counts->entry = entry;
counts->escaped = escaped;
counts->inside = inside;
counts->time_exhausted = texh;
counts->invalid = inv;
counts->unsupported = unsup;
counts->other = other;
counts->fallback_sum = fallback;
}
static void mode_routebench(long target_calls, int threads, double max_seconds,
const char *out) {
const int n = quad_route_case_count;
Preloaded p;
p.n = n;
p.rcs = quad_route_cases;
p.ctxs = malloc(sizeof(FixtureContext) * (size_t)n);
p.srcs = malloc(sizeof(SpacetimeSource) * (size_t)n);
p.obss = malloc(sizeof(ObserverState) * (size_t)n);
if (!p.ctxs || !p.srcs || !p.obss) {
fprintf(stderr, "allocation failure\n");
exit(2);
}
for (int i = 0; i < n; ++i) {
const QuadRouteCase *c = &quad_route_cases[i];
p.ctxs[i] = (FixtureContext){.c0 = {c->c0[0], c->c0[1], c->c0[2]},
.v = {c->v[0], c->v[1], c->v[2]},
.R0 = c->R0,
.rr = c->rr,
.t0 = c->t0,
.valid_t_min = c->valid_t_min,
.model = c->model};
p.srcs[i] = (SpacetimeSource){.ops = &fixture_ops, .context = &p.ctxs[i]};
p.obss[i] = (ObserverState){0};
p.obss[i].coordinate_time = c->t0;
for (int j = 0; j < 3; ++j)
p.obss[i].coordinate_position[j] = c->obs[j];
p.obss[i].tetrad[0][0] = 1.0;
p.obss[i].tetrad[1][1] = 1.0;
p.obss[i].tetrad[2][2] = 1.0;
p.obss[i].tetrad[3][3] = 1.0;
}
/* Calibrate with one pass, then size reps to the call/time budgets. */
double cal_seconds = 0.0;
RouteCounts cal_counts;
route_batch(&p, 1, threads, &cal_seconds, &cal_counts);
const double per_call = cal_seconds / (double)n;
long reps = target_calls / n;
if (reps < 1)
reps = 1;
if (per_call > 0.0) {
const long by_time = (long)(max_seconds / (per_call * (double)n));
if (by_time < 1)
reps = 1;
else if (reps > by_time)
reps = by_time;
}
double seconds = 0.0;
RouteCounts counts;
route_batch(&p, reps, threads, &seconds, &counts);
g_route_sink += (double)counts.entry + (double)counts.escaped;
FILE *f = fopen(out, "w");
if (f == NULL) {
fprintf(stderr, "cannot open %s\n", out);
exit(2);
}
fprintf(f,
"{\"variant\":\"%s\",\"mode\":\"routebench\",\"cases\":%d,"
"\"reps\":%ld,\"calls\":%ld,\"threads\":%d,\"cal_seconds\":%.9f,"
"\"seconds\":%.9f,\"ns_per_call\":%.6f,\"entry\":%ld,\"escaped\":%ld,"
"\"inside\":%ld,\"time_exhausted\":%ld,\"invalid\":%ld,"
"\"unsupported\":%ld,\"other\":%ld,\"fallback_sum\":%ld}\n",
PROBE_VARIANT_NAME, n, reps, (long)n * reps, threads, cal_seconds,
seconds, seconds * 1e9 / (double)((long)n * reps), counts.entry,
counts.escaped, counts.inside, counts.time_exhausted, counts.invalid,
counts.unsupported, counts.other, counts.fallback_sum);
fclose(f);
fprintf(stdout,
"routebench %s: %ld calls in %.6f s on %d threads (%.2f ns/call)\n",
PROBE_VARIANT_NAME, (long)n * reps, seconds, threads,
seconds * 1e9 / (double)((long)n * reps));
free(p.ctxs);
free(p.srcs);
free(p.obss);
}
/* ------------------------------------------------------------------ */
static void usage(const char *argv0) {
fprintf(stderr,
"usage:\n"
" %s accuracy --kernel-out K.csv --route-out R.csv\n"
" %s microbench --out F.json [--target-calls N]\n"
" %s routebench --out F.json [--target-calls N] [--threads T]"
" [--max-seconds S]\n",
argv0, argv0, argv0);
}
static const char *arg_value(int argc, char **argv, const char *flag) {
for (int i = 1; i + 1 < argc; ++i)
if (strcmp(argv[i], flag) == 0)
return argv[i + 1];
return NULL;
}
int main(int argc, char **argv) {
print_environment();
if (argc < 2) {
usage(argv[0]);
return 1;
}
if (strcmp(argv[1], "accuracy") == 0) {
const char *k = arg_value(argc, argv, "--kernel-out");
const char *r = arg_value(argc, argv, "--route-out");
if (!k || !r) {
usage(argv[0]);
return 1;
}
mode_accuracy(k, r);
return 0;
}
if (strcmp(argv[1], "microbench") == 0) {
const char *out = arg_value(argc, argv, "--out");
const char *tc = arg_value(argc, argv, "--target-calls");
if (!out) {
usage(argv[0]);
return 1;
}
mode_microbench(tc ? atol(tc) : 10000000L, out);
return 0;
}
if (strcmp(argv[1], "routebench") == 0) {
const char *out = arg_value(argc, argv, "--out");
const char *tc = arg_value(argc, argv, "--target-calls");
const char *th = arg_value(argc, argv, "--threads");
const char *ms = arg_value(argc, argv, "--max-seconds");
if (!out) {
usage(argv[0]);
return 1;
}
mode_routebench(tc ? atol(tc) : 1000000L, th ? atoi(th) : 4,
ms ? atof(ms) : 20.0, out);
return 0;
}
usage(argv[0]);
return 1;
}
+571
View File
@@ -0,0 +1,571 @@
#!/usr/bin/env python3
"""High-precision reference for the quadratic entry benchmark.
Inputs are exact IEEE-754 values, so they are represented exactly as
``fractions.Fraction``. The relative-distance quadratic coefficients, the
discriminant and the polynomial residuals are therefore *exact* rationals; only
the square root needs ``decimal`` (160 significant digits by default). This
avoids ``float.fromhex`` (which would silently drop a long-double coefficient to
53 bits) and keeps the sign of a near-zero discriminant exact.
Reference classification mirrors the production contract:
* c < 0 -> camera already INSIDE (never an entry failure)
* c == 0 -> boundary slope b decides; b < 0 enters at once, b == 0 with a < 0
enters later, otherwise no crossing
* c > 0 -> smallest positive root with inward slope; a missing/tangent root
is MISS. ``a == 0`` is the exact linear branch.
"""
from __future__ import annotations
import csv
import json
import math
from decimal import Decimal, localcontext
from fractions import Fraction
DEFAULT_PREC = 160
def parse_hex(s: str) -> Fraction:
"""Exact rational value of a C ``%a`` / ``%La`` hex float literal."""
s = s.strip()
low = s.lower()
if "nan" in low:
raise ValueError("NaN input")
if "inf" in low:
return Fraction(0) # only used for flags; never present in fixtures
neg = False
if s and s[0] in "+-":
neg = s[0] == "-"
s = s[1:]
if s[:2].lower() == "0x":
s = s[2:]
mant, _, exp_s = s.partition("p")
if not exp_s:
mant, _, exp_s = s.partition("P")
exp = int(exp_s) if exp_s else 0
ip, _, fp = mant.partition(".")
digits = (ip + fp) or "0"
val = Fraction(int(digits, 16), 1)
shift = exp - 4 * len(fp)
if shift >= 0:
val *= Fraction(2) ** shift
else:
val /= Fraction(2) ** (-shift)
return -val if neg else val
def frac_to_dec(fr: Fraction, prec: int = DEFAULT_PREC) -> Decimal:
with localcontext() as ctx:
ctx.prec = prec
return Decimal(fr.numerator) / Decimal(fr.denominator)
def hex_to_dec(s: str, prec: int = DEFAULT_PREC) -> Decimal:
return frac_to_dec(parse_hex(s), prec)
def classify(a: Fraction, b: Fraction, c: Fraction, prec: int,
R0: Fraction | None = None, rr: Fraction | None = None):
"""Return (status, root_decimal_or_None, info).
``R0``/``rr`` (radius at the segment start and dR/dt) enforce the physical
positive-radius domain R(s) = R0 - rr*s > 0 along the past parameter. A
positive root outside that domain is not a physical entry: it is reported
as MISS with ``info['domain_clipped']`` set (shrinking worldtubes).
"""
info = {"a_zero": a == 0, "disc_sign": 0, "domain_clipped": False}
def domain_ok(root: Fraction) -> bool:
if R0 is None:
return True
return (R0 - (rr if rr is not None else Fraction(0)) * root) > 0
if c < 0:
return "INSIDE", None, info
if c == 0:
if b < 0:
return "ENTER", Decimal(0), info
if b == 0:
if a < 0:
return "ENTER", Decimal(0), info
return "MISS", None, info
if a < 0:
root = -b / a
if root > 0 and domain_ok(root):
return "ENTER", frac_to_dec(root, prec), info
if root > 0:
info["domain_clipped"] = True
return "MISS", None, info
return "MISS", None, info
if a == 0:
if b < 0:
root = -c / b
if root > 0:
if domain_ok(root):
return "ENTER", frac_to_dec(root, prec), info
info["domain_clipped"] = True
return "MISS", None, info
disc = b * b - 4 * a * c
info["disc_sign"] = (disc > 0) - (disc < 0)
if disc < 0:
return "MISS", None, info
if disc == 0:
return "MISS", None, info # tangency is not a crossing
with localcontext() as ctx:
ctx.prec = prec
sd = frac_to_dec(disc, prec).sqrt()
ad = frac_to_dec(a, prec)
bd = frac_to_dec(b, prec)
r1 = (-bd - sd) / (2 * ad)
r2 = (-bd + sd) / (2 * ad)
pos = sorted(r for r in (r1, r2) if r > 0)
for r in pos:
if 2 * ad * r + bd < 0: # inward (outside -> inside) slope
rfr = _dec_to_frac_snapshot(r)
if domain_ok(rfr):
return "ENTER", r, info
info["domain_clipped"] = True
return "MISS", None, info
def _dec_to_frac_snapshot(d: Decimal) -> Fraction:
return Fraction(d)
def coeffs(x, c, w, v, R0, rr):
d = [x[i] - c[i] for i in range(3)]
q = [w[i] + v[i] for i in range(3)]
qq = sum(q[i] * q[i] for i in range(3))
dq = sum(d[i] * q[i] for i in range(3))
dd = sum(d[i] * d[i] for i in range(3))
a = qq - rr * rr
b = 2 * (dq + R0 * rr)
cq = dd - R0 * R0
return a, b, cq, qq
def load_cases(path):
data = json.loads(open(path).read())
kernel = {}
for c in data["kernel"]:
kernel[c["id"]] = {
"category": c["category"],
"x": [parse_hex(z) for z in c["x"]],
"c": [parse_hex(z) for z in c["c"]],
"w": [parse_hex(z) for z in c["w"]],
"v": [parse_hex(z) for z in c["v"]],
"R0": parse_hex(c["R0"]),
"rr": parse_hex(c["rr"]),
}
route = {c["id"]: c for c in data["route"]}
return kernel, route
def kernel_reference(case, prec):
a, b, cq, qq = coeffs(case["x"], case["c"], case["w"], case["v"],
case["R0"], case["rr"])
status, root, info = classify(a, b, cq, prec, case["R0"], case["rr"])
dd = sum(xi * xi for xi in
(case["x"][i] - case["c"][i] for i in range(3)))
return {
"status": status,
"root": root,
"a": a,
"b": b,
"c": cq,
"qq": qq,
"dd": dd,
"R0": case["R0"],
"rr": case["rr"],
"a_zero": info["a_zero"],
"disc_sign": info["disc_sign"],
"domain_clipped": info["domain_clipped"],
}
def ulp_dec(cand: float) -> Decimal:
return frac_to_dec(Fraction(math.ulp(cand)))
def dec_of_float(f: float) -> Decimal:
return frac_to_dec(Fraction(f), 300)
def poly_resid(a: Fraction, b: Fraction, c: Fraction, s: Fraction) -> Fraction:
return a * s * s + b * s + c
def poly_scale(a: Fraction, b: Fraction, c: Fraction, s: Fraction) -> Fraction:
return abs(a) * s * s + abs(b) * s + abs(c)
def analyze_kernel_variant(rows, refs, prec):
"""Return per-case records for one variant's kernel CSV."""
out = []
for r in rows:
cid = int(r["id"])
ref = refs[cid]
status = int(r["status"])
cand = float.fromhex(r["sigma"]) if r["sigma"] not in ("", "nan") else float("nan")
cand_fr = parse_hex(r["sigma"]) if r["sigma"] not in ("", "nan") else None
ak = parse_hex(r["a"])
bk = parse_hex(r["b"])
ck = parse_hex(r["c"])
rec = {
"id": cid,
"category": r["category"],
"variant_status": status,
"ref_status": ref["status"],
"a_zero_ref": ref["a_zero"],
"a_zero_kernel": ak == 0,
"a_sign_ref": _sign(ref["a"]),
"a_sign_kernel": _sign(ak),
"disc_sign_ref": ref["disc_sign"],
"late": False,
"near_linear": False,
"near_boundary": False,
"ill_conditioned": False,
"domain_clipped": ref["domain_clipped"],
"root_err_ulps": None,
"root_rel_err": None,
"resid_kernel_rel": None,
"resid_ideal_rel": None,
"baseline_resid_rel": None,
}
# conditioning flags
if ref["status"] == "ENTER" and ref["root"] is not None:
s = ref["root"]
scale = max(abs(ref["R0"]), Fraction(1))
rec["late"] = bool(s > frac_to_dec(scale, prec) * (10 ** 6))
rec["near_linear"] = abs(ref["a"]) <= Fraction(1, 10**10) * max(
ref["qq"], ref["rr"] * ref["rr"], Fraction(1)
)
r0sq = ref["R0"] * ref["R0"]
dd = ref["dd"]
rec["near_boundary"] = bool(
dd + r0sq != 0
and abs(ref["c"]) <= Fraction(1, 10**8) * (dd + r0sq)
)
rec["ill_conditioned"] = bool(
rec["near_linear"] or rec["late"] or rec["near_boundary"]
or r["category"].startswith("growing_out")
or r["category"] == "shrinking"
)
# root-level comparison
if status == 1 and ref["status"] == "ENTER" and ref["root"] is not None and cand_fr is not None:
err = abs(frac_to_dec(cand_fr, prec) - ref["root"])
u = ulp_dec(cand)
if u > 0:
rec["root_err_ulps"] = float(err / u)
if ref["root"] != 0:
rec["root_rel_err"] = float(err / abs(ref["root"]))
rec["resid_kernel_rel"] = _rel(
poly_resid(ak, bk, ck, cand_fr), ak, bk, ck, cand_fr
)
rec["resid_ideal_rel"] = _rel(
poly_resid(ref["a"], ref["b"], ref["c"], cand_fr),
ref["a"], ref["b"], ref["c"], cand_fr,
)
try:
nearest = float(ref["root"])
nfr = Fraction(nearest)
rec["baseline_resid_rel"] = _rel(
poly_resid(ref["a"], ref["b"], ref["c"], nfr),
ref["a"], ref["b"], ref["c"], nfr,
)
except (OverflowError, ValueError):
rec["baseline_resid_rel"] = None
out.append(rec)
return out
def _sign(fr: Fraction) -> int:
return (fr > 0) - (fr < 0)
def _rel(resid: Fraction, a, b, c, s) -> float:
scale = poly_scale(a, b, c, s)
if scale == 0:
return 0.0
return float(abs(resid) / scale)
def route_reference(row, prec):
x = [parse_hex(row[f"canonx{i}"]) for i in range(3)]
w = [parse_hex(row[f"canonw{i}"]) for i in range(3)]
v = [parse_hex(row[f"v{i}"]) for i in range(3)]
center = [parse_hex(row[f"ct0{i}"]) for i in range(3)]
rr = parse_hex(row["rr"])
R0 = parse_hex(row["rt0"]) if row["rt0"] not in ("", "nan") else parse_hex(row["R0"])
a, b, cq, qq = coeffs(x, center, w, v, R0, rr)
status, root, info = classify(a, b, cq, prec, R0, rr)
return status, root, a, b, cq, qq, info["domain_clipped"]
STATUS_NAME = {-1: "INVALID", 0: "MISS", 1: "ENTRY", 2: "UNCERTAIN"}
KIND_NAME = {
0: "INSIDE",
1: "ENTRY",
2: "ESCAPED",
3: "TIME_RANGE_EXHAUSTED",
4: "INVALID",
}
def analyze_route_variant(rows, prec, dbl_eps=2.220446049250313e-16):
out = []
for r in rows:
(ref_status, ref_root, a, b, cq, qq,
ref_domain_clipped) = route_reference(r, prec)
status = int(r["status"])
kind = int(r["kind"])
F = _maybe_dec(r["F"])
tol = _maybe_dec(r["tol"])
kern_ok = r.get("kern_ok", "0") == "1"
kern_status = int(r["kern_status"]) if kern_ok else None
rec = {
"id": int(r["id"]),
"category": r["category"],
"ref_status": ref_status,
"ref_domain_clipped": ref_domain_clipped,
"status": status,
"kind": kind,
"kind_name": KIND_NAME.get(kind, "?"),
"failure_reason": int(r["failure_reason"]),
"fallback": int(r["fallback_evals"]),
"pi_match": int(r["pi_match"]),
"lcam_match": int(r["lcam_match"]),
"kern_status": kern_status if kern_ok else "",
"kern_sigma": r.get("kern_sigma", ""),
"F_over_tol": None,
"inside_mismatch": False,
"false_miss": False,
"false_candidate": False,
"unconfirmed": kind == 4 and r["reason_name"] == "ENTRY_UNCONFIRMED",
"kernel_false_miss": False,
"kernel_false_miss_escaped": False,
}
if ref_status == "INSIDE":
if kind != 0:
rec["inside_mismatch"] = True
elif ref_status == "ENTER":
if kind == 2:
rec["false_miss"] = True
elif ref_status == "MISS":
if kind == 1:
rec["false_candidate"] = True
if ref_status == "ENTER" and kern_ok and kern_status == 0:
rec["kernel_false_miss"] = True
if kind == 2:
rec["kernel_false_miss_escaped"] = True
if F is not None and tol is not None and tol > 0:
rec["F_over_tol"] = float(F / tol)
out.append(rec)
return out
def _maybe_dec(s):
if s in ("", "nan"):
return None
low = s.lower()
if "inf" in low:
return None
return hex_to_dec(s, 300)
def summarize_kernel(records_by_variant, refs):
"""Aggregate counts, ULP distributions, and common-ENTRY intersections."""
variants = list(records_by_variant.keys())
by_id = {v: {} for v in variants}
for v in variants:
for rec in records_by_variant[v]:
by_id[v][rec["id"]] = rec
ids = sorted(refs.keys())
summary = {"variants": {}}
for v in variants:
recs = by_id[v]
counts = {
"enter": 0, "miss": 0, "uncertain": 0, "false_miss": 0,
"false_uncertain": 0, "false_candidate": 0,
"a_zero_ref": 0, "a_zero_kernel": 0, "a_sign_mismatch": 0,
"a_zero_collapse": 0, "a_spurious_nonzero": 0,
"domain_clipped_candidate": 0,
"ill_conditioned_enter": 0,
"near_boundary_enter": 0,
"domain_clipped_cases": 0,
"late_ref_enter": 0,
}
ulps = []
rels = []
for cid in ids:
rec = recs[cid]
st = rec["variant_status"]
if st == 1:
counts["enter"] += 1
elif st == 0:
counts["miss"] += 1
elif st == 2:
counts["uncertain"] += 1
if rec["ref_status"] == "ENTER" and st == 0:
counts["false_miss"] += 1
if rec["ref_status"] == "ENTER" and st == 2:
counts["false_uncertain"] += 1
if rec["ref_status"] == "MISS" and st == 1:
if rec["domain_clipped"]:
counts["domain_clipped_candidate"] += 1
else:
counts["false_candidate"] += 1
if rec["a_zero_ref"]:
counts["a_zero_ref"] += 1
if rec["a_zero_kernel"]:
counts["a_zero_kernel"] += 1
if not rec["a_zero_ref"] and rec["a_zero_kernel"]:
counts["a_zero_collapse"] += 1
if rec["a_zero_ref"] and not rec["a_zero_kernel"]:
counts["a_spurious_nonzero"] += 1
if rec["ill_conditioned"] and rec["ref_status"] == "ENTER":
counts["ill_conditioned_enter"] += 1
if rec["near_boundary"] and rec["ref_status"] == "ENTER":
counts["near_boundary_enter"] += 1
if rec["domain_clipped"]:
counts["domain_clipped_cases"] += 1
if (rec["a_sign_ref"] != 0 and rec["a_sign_kernel"] != 0
and rec["a_sign_ref"] != rec["a_sign_kernel"]):
counts["a_sign_mismatch"] += 1
if rec["late"]:
counts["late_ref_enter"] += 1
if rec["root_err_ulps"] is not None:
ulps.append(rec["root_err_ulps"])
if rec["root_rel_err"] is not None:
rels.append(rec["root_rel_err"])
summary["variants"][v] = {
"counts": counts,
"root_ulp_all_ref_enter": _dist(ulps),
"root_rel_all_ref_enter": _dist(rels),
}
# Common reference-ENTER subset that every variant solved as ENTRY.
common = []
for cid in ids:
if refs[cid]["status"] != "ENTER":
continue
if all(by_id[v][cid]["variant_status"] == 1 for v in variants):
common.append(cid)
summary["common_enter_ids"] = len(common)
for v in variants:
vals = []
vals_nonlate = []
rels_nonlate = []
for cid in common:
rec = by_id[v][cid]
e = rec["root_err_ulps"]
if e is not None:
vals.append(e)
if not rec["ill_conditioned"]:
vals_nonlate.append(e)
if rec["root_rel_err"] is not None:
rels_nonlate.append(rec["root_rel_err"])
summary["variants"][v]["root_ulp_common_enter"] = _dist(vals)
summary["variants"][v]["root_ulp_common_enter_wellcond"] = _dist(vals_nonlate)
summary["variants"][v]["root_rel_common_enter_wellcond"] = _dist(rels_nonlate)
return summary
def _dist(vals):
if not vals:
return {"n": 0}
vs = sorted(vals)
n = len(vs)
def pct(p):
idx = min(n - 1, max(0, int(math.ceil(p * n)) - 1))
return vs[idx]
return {
"n": n,
"min": vs[0],
"median": pct(0.5),
"p95": pct(0.95),
"max": vs[-1],
}
def summarize_route(records_by_variant):
summary = {"variants": {}}
for v, recs in records_by_variant.items():
counts = {
"inside_ok": 0, "inside_mismatch": 0, "false_miss": 0,
"false_candidate": 0, "unconfirmed": 0, "fallback_used": 0,
"accepted_entry": 0, "F_over_tol_gt1": 0, "pi_mismatch": 0,
"lcam_mismatch": 0, "kernel_false_miss": 0,
"kernel_false_miss_escaped": 0, "ref_domain_clipped": 0,
}
fmax = 0.0
by_cat = {}
for rec in recs:
cat = by_cat.setdefault(rec["category"], {"cases": 0, "fallback": 0,
"entry": 0, "escaped": 0})
cat["cases"] += 1
if rec["kind"] == 0 and rec["ref_status"] == "INSIDE":
counts["inside_ok"] += 1
if rec["inside_mismatch"]:
counts["inside_mismatch"] += 1
if rec["false_miss"]:
counts["false_miss"] += 1
if rec["false_candidate"]:
counts["false_candidate"] += 1
if rec["unconfirmed"]:
counts["unconfirmed"] += 1
if rec["kernel_false_miss"]:
counts["kernel_false_miss"] += 1
if rec["kernel_false_miss_escaped"]:
counts["kernel_false_miss_escaped"] += 1
if rec["ref_domain_clipped"]:
counts["ref_domain_clipped"] += 1
if rec["fallback"] > 0:
counts["fallback_used"] += 1
cat["fallback"] += 1
if rec["kind"] == 1:
counts["accepted_entry"] += 1
cat["entry"] += 1
if rec["F_over_tol"] is not None:
fmax = max(fmax, rec["F_over_tol"])
if rec["F_over_tol"] > 1.0:
counts["F_over_tol_gt1"] += 1
if rec["kind"] == 2:
cat["escaped"] += 1
if not rec["pi_match"]:
counts["pi_mismatch"] += 1
if not rec["lcam_match"]:
counts["lcam_mismatch"] += 1
summary["variants"][v] = {"counts": counts, "max_F_over_tol": fmax,
"by_category": by_cat}
return summary
def precision_consistency(cases, ids, prec_a, prec_b):
"""Compare reference classification and nearest-double root at two Decimal
precisions. Coefficients/discriminant are exact rationals, so only the
square-root precision can differ. Returns a list of mismatches."""
mismatches = []
for cid in sorted(ids):
ra = kernel_reference(cases[cid], prec_a)
rb = kernel_reference(cases[cid], prec_b)
reason = None
if ra["status"] != rb["status"]:
reason = "status"
elif ra["disc_sign"] != rb["disc_sign"]:
reason = "disc_sign"
elif ra["status"] == "ENTER":
try:
fa = float(ra["root"])
fb = float(rb["root"])
except (OverflowError, ValueError):
reason = "root_unrepresentable"
else:
if not (fa == fb or (math.isnan(fa) and math.isnan(fb))):
reason = "nearest_double_root"
if reason is not None:
mismatches.append({"id": cid, "reason": reason,
"status_a": ra["status"], "status_b": rb["status"]})
return mismatches
+458
View File
@@ -0,0 +1,458 @@
#!/usr/bin/env python3
"""End-to-end driver for the quadratic precision/performance benchmark.
Self-contained: reads the current working-tree ``src/asymptotic.c``, generates
and builds four variants, runs the accuracy probe, evaluates the exact-decimal
reference, and times the kernel and the public pre-route. All raw artifacts
are written under ``--output-dir`` (default /tmp/opencode/quadratic-comparison).
No production source, Makefile or git state is modified.
"""
from __future__ import annotations
import argparse
import csv
import json
import os
import re
import subprocess
import sys
# Do not leave __pycache__ inside the repository benchmark directory: the
# imported local modules (build/cases/oracle) must not be cached here.
sys.dont_write_bytecode = True
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
import build as buildmod # noqa: E402
import cases as casesmod # noqa: E402
import oracle # noqa: E402
VARIANTS = list(buildmod.VARIANTS)
def run_cmd(cmd, log_path: Path, env=None):
with open(log_path, "a") as fh:
fh.write("$ " + " ".join(str(c) for c in cmd) + "\n")
proc = subprocess.run(cmd, capture_output=True, text=True, check=False, env=env)
with open(log_path, "a") as fh:
if proc.stdout:
fh.write(proc.stdout)
if proc.stderr:
fh.write(proc.stderr)
fh.write(f"exit={proc.returncode}\n")
return proc
def read_csv(path: Path):
with open(path, newline="") as fh:
return list(csv.DictReader(fh))
def run_timing(cmd, outp: Path, log_path: Path):
# Never ingest output left by a previous invocation, even after a failure.
outp.unlink(missing_ok=True)
proc = run_cmd(cmd, log_path)
if proc.returncode != 0 or not outp.is_file():
raise RuntimeError(f"timing failed or produced no fresh output: {log_path}")
return json.loads(outp.read_text())
def write_metrics_csv(path: Path, records, columns):
with open(path, "w", newline="") as fh:
w = csv.DictWriter(fh, fieldnames=columns, extrasaction="ignore")
w.writeheader()
for r in records:
w.writerow(r)
def write_curated(path: Path, rows, columns):
with open(path, "w", newline="") as fh:
w = csv.DictWriter(fh, fieldnames=columns, extrasaction="ignore")
w.writeheader()
for r in rows:
w.writerow(r)
def build_curated_kernel(kernel_records):
ref_ids = [r["id"] for r in kernel_records[VARIANTS[0]] if r["id"] < 1000]
by = {v: {r["id"]: r for r in kernel_records[v]} for v in VARIANTS}
rows = []
for cid in sorted(ref_ids):
row = {"id": cid, "category": by[VARIANTS[0]][cid]["category"],
"ref": by[VARIANTS[0]][cid]["ref_status"]}
for v in VARIANTS:
row[f"{v}_status"] = by[v][cid]["variant_status"]
row[f"{v}_ulp"] = by[v][cid]["root_err_ulps"]
row[f"{v}_rel"] = by[v][cid]["root_rel_err"]
rows.append(row)
return rows
def build_curated_route(route_records):
ref_ids = [r["id"] for r in route_records[VARIANTS[0]] if r["id"] < 100]
by = {v: {r["id"]: r for r in route_records[v]} for v in VARIANTS}
rows = []
for cid in sorted(ref_ids):
row = {"id": cid, "category": by[VARIANTS[0]][cid]["category"],
"ref": by[VARIANTS[0]][cid]["ref_status"]}
for v in VARIANTS:
row[f"{v}_kind"] = by[v][cid]["kind"]
row[f"{v}_fallback"] = by[v][cid]["fallback"]
row[f"{v}_F_over_tol"] = by[v][cid]["F_over_tol"]
rows.append(row)
return rows
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--repo", default=str(HERE.parents[1]))
ap.add_argument("--output-dir", default="/tmp/opencode/quadratic-comparison")
ap.add_argument("--cc", default=os.environ.get("CC", "cc"))
ap.add_argument("--threads", type=int, default=4)
ap.add_argument("--precision", type=int, default=160)
ap.add_argument("--rounds", type=int, default=3)
ap.add_argument("--kernel-target-calls", type=int, default=10_000_000)
ap.add_argument("--route-target-calls", type=int, default=1_000_000)
ap.add_argument("--skip-build", action="store_true")
args = ap.parse_args()
repo = Path(args.repo).resolve()
out = Path(args.output_dir).resolve()
(out / "raw").mkdir(parents=True, exist_ok=True)
(out / "cases").mkdir(parents=True, exist_ok=True)
(out / "logs").mkdir(parents=True, exist_ok=True)
import time as _time
t_start = _time.time()
phase_times = {}
# ---------------------------------------------------------------- cases
t0 = _time.time()
kernel_cases, route_cases = casesmod.build()
casesmod.write_header(kernel_cases, route_cases, out / "cases" / "quadratic_cases.h")
casesmod.write_json(kernel_cases, route_cases, out / "cases" / "cases.json")
phase_times["case_gen"] = _time.time() - t0
print(f"cases: kernel={len(kernel_cases)} route={len(route_cases)}")
# ------------------------------------------------------------ environment
env_info = buildmod.environment(repo, args.cc, args.threads)
env_info["python_version"] = sys.version.split()[0]
env_info["invocation"] = " ".join([sys.executable, *sys.argv])
(out / "environment.json").write_text(json.dumps(env_info, indent=2) + "\n")
(out / "raw" / "invocation.txt").write_text(
" ".join([sys.executable, *sys.argv]) + "\n"
)
print(f"source sha256: {env_info['source_sha256'][:16]} cc: {env_info['cc_version']}")
# ---------------------------------------------------------------- build
t0 = _time.time()
if not args.skip_build:
info = buildmod.build_all(repo, out, args.cc, buildmod.BASE_FLAGS, args.threads)
exes = {k: Path(v) for k, v in info["executables"].items()}
else:
manifest, reasons = buildmod.verify_manifest(repo, out, buildmod.BASE_FLAGS, args.cc)
if reasons:
print("refusing --skip-build: existing build does not match the "
f"current source/flags/fixtures ({', '.join(reasons)}); "
"rerun without --skip-build", file=sys.stderr)
sys.exit(1)
exes = {k: Path(v) for k, v in manifest["executables"].items()}
phase_times["build"] = _time.time() - t0
for name, exe in exes.items():
if not exe.exists():
print(f"missing executable for {name}: {exe}", file=sys.stderr)
sys.exit(1)
# -------------------------------------------------------------- accuracy
t0 = _time.time()
env_log = out / "logs" / "probe_environment.log"
env_log.write_text("")
for name in VARIANTS:
kout = out / "raw" / f"kernel_{name}.csv"
rout = out / "raw" / f"route_{name}.csv"
proc = run_cmd(
[str(exes[name]), "accuracy", "--kernel-out", str(kout),
"--route-out", str(rout)],
out / "logs" / f"accuracy_{name}.log",
)
with open(env_log, "a") as fh:
fh.write(proc.stdout)
if proc.returncode != 0:
print(f"accuracy probe failed for {name}", file=sys.stderr)
sys.exit(1)
phase_times["accuracy"] = _time.time() - t0
# ---------------------------------------------------------------- oracle
t0 = _time.time()
kernel_ref_cases, route_cases_json = oracle.load_cases(out / "cases" / "cases.json")
refs = {
cid: oracle.kernel_reference(case, args.precision)
for cid, case in kernel_ref_cases.items()
}
kernel_records = {}
route_records = {}
for name in VARIANTS:
krows = read_csv(out / "raw" / f"kernel_{name}.csv")
kernel_records[name] = oracle.analyze_kernel_variant(krows, refs, args.precision)
rrows = read_csv(out / "raw" / f"route_{name}.csv")
route_records[name] = oracle.analyze_route_variant(rrows, args.precision)
kcols = ["id", "category", "ref_status", "variant_status", "root_err_ulps",
"root_rel_err",
"resid_kernel_rel", "resid_ideal_rel", "baseline_resid_rel",
"a_zero_ref", "a_zero_kernel", "a_sign_ref", "a_sign_kernel",
"disc_sign_ref", "near_linear", "near_boundary",
"ill_conditioned", "domain_clipped", "late"]
for name in VARIANTS:
write_metrics_csv(out / "raw" / f"kernel_metrics_{name}.csv",
kernel_records[name], kcols)
rcols = ["id", "category", "ref_status", "ref_domain_clipped", "status",
"kind", "kind_name", "failure_reason", "fallback", "pi_match",
"lcam_match", "kern_status", "kern_sigma", "F_over_tol",
"inside_mismatch", "false_miss", "false_candidate", "unconfirmed",
"kernel_false_miss", "kernel_false_miss_escaped"]
for name in VARIANTS:
write_metrics_csv(out / "raw" / f"route_metrics_{name}.csv",
route_records[name], rcols)
ksummary = oracle.summarize_kernel(kernel_records, refs)
rsummary = oracle.summarize_route(route_records)
consistency_ids = {
rec["id"] for rec in kernel_records[VARIANTS[0]]
if rec["id"] < 1000 or rec["near_linear"] or rec["late"]
or rec["near_boundary"] or rec["domain_clipped"]
}
precision_mismatches = oracle.precision_consistency(
kernel_ref_cases, consistency_ids, args.precision, 240
)
curated_kernel = build_curated_kernel(kernel_records)
curated_route = build_curated_route(route_records)
write_curated(out / "raw" / "curated_kernel.csv", curated_kernel,
["id", "category", "ref"] + [f"{v}_{f}" for v in VARIANTS
for f in ("status", "ulp", "rel")])
write_curated(out / "raw" / "curated_route.csv", curated_route,
["id", "category", "ref"] + [f"{v}_{f}" for v in VARIANTS
for f in ("kind", "fallback",
"F_over_tol")])
# ------------------------------------------------------------- hardware FMA
phase_times["oracle"] = _time.time() - t0
fma_info = verify_fma(out, args.threads)
# ---------------------------------------------------------------- timing
t0 = _time.time()
timings = timed_runs(exes, out, args, kernel_target_calls=args.kernel_target_calls,
route_target_calls=args.route_target_calls)
lin_target = max(100_000, args.kernel_target_calls // 20)
timings["linearity"] = linearity_check(exes, out, lin_target)
phase_times["timing"] = _time.time() - t0
# ---------------------------------------------------------------- summary
summary = {
"source_sha256": env_info["source_sha256"],
"flags": buildmod.BASE_FLAGS,
"precision": args.precision,
"precision_consistency": {
"ids_checked": len(consistency_ids),
"mismatches": precision_mismatches,
},
"threads_timing": args.threads,
"cases": {"kernel": len(kernel_cases), "route": len(route_cases)},
"kernel_accuracy": ksummary,
"route_accuracy": rsummary,
"curated_kernel": curated_kernel,
"fma": fma_info,
"timing": timings,
"phase_seconds": phase_times,
"total_seconds": _time.time() - t_start,
}
(out / "summary.json").write_text(json.dumps(summary, indent=2) + "\n")
print_summary(summary)
print(f"\nartifacts under {out}")
def verify_fma(out: Path, threads: int):
info = {"objects": {}}
for name in VARIANTS:
obj = out / "build" / f"probe_{name}.o"
if not obj.exists():
continue
proc = subprocess.run(["objdump", "-d", str(obj)], capture_output=True,
text=True, check=False)
n = sum(1 for line in proc.stdout.splitlines()
if "vfmadd" in line or "vfmsub" in line or "fmadd" in line)
nm = subprocess.run(["nm", "-u", str(obj)], capture_output=True,
text=True, check=False)
undef = sorted({tok for line in nm.stdout.splitlines()
for tok in line.split()
if tok.startswith("fma")})
kd = subprocess.run(["objdump", "-dr", str(obj)], capture_output=True,
text=True, check=False)
kb_lines = []
inside = False
for line in kd.stdout.splitlines():
if re.match(r"^[0-9a-f]+ <entry_solve", line):
inside = True
kb_lines.append(line)
continue
if inside:
if re.match(r"^[0-9a-f]+ <", line):
break
kb_lines.append(line)
kb = "\n".join(kb_lines)
x87 = sum(1 for line in kb.splitlines()
if re.search(r"\b(fld|fstp|fmul|fadd|fsub|fdiv|fcom)\b", line))
vfma = sum(1 for line in kb.splitlines()
if "vfmadd" in line or "vfmsub" in line)
calls_fmal = sum(1 for line in kb.splitlines() if "fmal" in line)
info["objects"][name] = {
"fma_instructions": n,
"lowered_hardware": n > 0,
"undefined_fma_symbols": undef,
"entry_solve_x87": x87,
"entry_solve_vfmadd": vfma,
"entry_solve_fmal_calls": calls_fmal,
}
print(f"arith verify {name}: fused={n} undef={undef} "
f"entry_solve[x87={x87} vfmadd={vfma} fmal_calls={calls_fmal}]")
(out / "raw" / "fma_verify.json").write_text(json.dumps(info, indent=2) + "\n")
return info
def linearity_check(exes, out: Path, target_calls: int):
"""Confirm the microbench time scales with the call count (no hoisting)."""
res = {}
for name in ("ld_fma", "double_fma"):
secs = []
for mult in (1, 2):
outp = out / "raw" / f"linearity_{name}_{mult}.json"
d = run_timing([str(exes[name]), "microbench", "--out", str(outp),
"--target-calls", str(target_calls * mult)],
outp, out / "logs" / f"linearity_{name}_{mult}.log")
secs.append(d["seconds"])
res[name] = {"t1": secs[0], "t2": secs[1],
"ratio": secs[1] / secs[0] if secs[0] > 0 else None}
return res
def timed_runs(exes, out: Path, args, kernel_target_calls, route_target_calls):
schedules = []
fwd = list(VARIANTS)
schedules.append(fwd)
schedules.append(list(reversed(fwd)))
for i in range(2, args.rounds):
schedules.append(fwd if i % 2 == 0 else list(reversed(fwd)))
schedules = schedules[: args.rounds]
results = {"microbench": [], "routebench": []}
def order(seq):
return [exes[n] for n in seq]
for rnd, seq in enumerate(schedules):
for name in seq:
outp = out / "raw" / f"microbench_{name}_r{rnd}.json"
d = run_timing([str(exes[name]), "microbench", "--out", str(outp),
"--target-calls", str(kernel_target_calls)],
outp, out / "logs" / f"microbench_{name}_r{rnd}.log")
d["round"] = rnd
results["microbench"].append(d)
for rnd, seq in enumerate(schedules):
for name in seq:
outp = out / "raw" / f"routebench_{name}_r{rnd}.json"
d = run_timing([str(exes[name]), "routebench", "--out", str(outp),
"--target-calls", str(route_target_calls),
"--threads", str(args.threads), "--max-seconds", "20"],
outp, out / "logs" / f"routebench_{name}_r{rnd}.log")
d["round"] = rnd
results["routebench"].append(d)
return results
def print_summary(summary):
print("\n=== kernel accuracy (per variant) ===")
print(f"{'variant':<18}{'ENTER':>7}{'MISS':>7}{'UNC':>6}{'falseMiss':>10}"
f"{'falseCand':>10}{'domClipCand':>12}{'a0ref':>7}{'a0k':>6}{'asign':>6}"
f"{'illcond':>8}{'nearBnd':>8}{'medULP':>10}{'p95ULP':>11}{'maxULP':>11}")
for v, s in summary["kernel_accuracy"]["variants"].items():
c = s["counts"]
d = s["root_ulp_all_ref_enter"]
print(f"{v:<18}{c['enter']:>7}{c['miss']:>7}{c['uncertain']:>6}"
f"{c['false_miss']:>10}{c['false_candidate']:>10}"
f"{c['domain_clipped_candidate']:>12}"
f"{c['a_zero_ref']:>7}{c['a_zero_kernel']:>6}"
f"{c['a_sign_mismatch']:>6}{c['ill_conditioned_enter']:>8}"
f"{c['near_boundary_enter']:>8}"
f"{d.get('median', float('nan')):>10.4g}{d.get('p95', float('nan')):>11.4g}"
f"{d.get('max', float('nan')):>11.4g}")
print("well-conditioned common-ENTER ULP (excludes late / near-linear /"
" growing-out / shrinking):")
for v, s in summary["kernel_accuracy"]["variants"].items():
d = s["root_ulp_common_enter_wellcond"]
r = s["root_rel_common_enter_wellcond"]
print(f" {v:<18}n={d.get('n',0):>5} median={d.get('median', float('nan')):>9.3g}"
f" p95={d.get('p95', float('nan')):>9.3g}"
f" max={d.get('max', float('nan')):>9.3g}"
f" |rel err median={r.get('median', float('nan')):>9.3g}"
f" p95={r.get('p95', float('nan')):>9.3g}")
print(f"common reference-ENTER ids solved ENTRY by all variants: "
f"{summary['kernel_accuracy']['common_enter_ids']}")
print("\n=== curated kernel cases (status/ULP) ===")
print(f"{'id':>4} {'category':<20} {'ref':<6} "
+ " ".join(f"{v:>16}" for v in VARIANTS))
for row in summary.get("curated_kernel", []):
cells = []
for v in VARIANTS:
st = row.get(f"{v}_status")
u = row.get(f"{v}_ulp")
cells.append(f"{st}/{float(u):.3g}" if u not in (None, "") else f"{st}/-")
print(f"{row['id']:>4} {row['category']:<20} {row.get('ref',''):<6} "
+ " ".join(f"{c:>16}" for c in cells))
print("\n=== route accuracy (per variant) ===")
print(f"{'variant':<18}{'insideOK':>9}{'insideBad':>10}{'falseMiss':>10}"
f"{'falseCand':>10}{'unconf':>8}{'kernFM':>8}{'kernFMesc':>10}"
f"{'fallback':>9}{'F>tol':>7}{'maxF/tol':>10}")
for v, s in summary["route_accuracy"]["variants"].items():
c = s["counts"]
print(f"{v:<18}{c['inside_ok']:>9}{c['inside_mismatch']:>10}"
f"{c['false_miss']:>10}{c['false_candidate']:>10}"
f"{c['unconfirmed']:>8}{c['kernel_false_miss']:>8}"
f"{c['kernel_false_miss_escaped']:>10}"
f"{c['fallback_used']:>9}"
f"{c['F_over_tol_gt1']:>7}{s['max_F_over_tol']:>10.3g}")
pc = summary.get("precision_consistency", {})
print(f"reference precision {summary['precision']} vs 240: checked "
f"{pc.get('ids_checked')} curated/near-linear/late ids, "
f"mismatches={len(pc.get('mismatches', []))}")
print("\nroute fallback / entry / escaped by category "
"(ld_plain | ld_fma | double_fma):")
ra = summary["route_accuracy"]["variants"]
cats = sorted({c for s in ra.values() for c in s.get("by_category", {})})
for cat in cats:
cells = []
for v in ("ld_plain", "ld_fma", "double_fma"):
d = ra[v].get("by_category", {}).get(cat, {})
cells.append(f"fb={d.get('fallback',0)} e={d.get('entry',0)} "
f"x={d.get('escaped',0)}")
print(f" {cat:<20} " + " | ".join(cells))
print("\n=== timing (median ns/call over rounds) ===")
for mode in ("microbench", "routebench"):
per = {}
for rec in summary["timing"][mode]:
per.setdefault(rec["variant"], []).append(rec["ns_per_call"])
for v, vals in per.items():
vals.sort()
med = vals[len(vals) // 2]
print(f"{mode:<12}{v:<18}median={med:>10.2f} ns/call "
f"rounds={len(vals)}")
lin = summary["timing"].get("linearity", {})
for v, d in lin.items():
print(f"linearity {v:<18}t1={d['t1']:.4f}s t2={d['t2']:.4f}s "
f"ratio={d['ratio']:.3f} (expect ~2)")
if __name__ == "__main__":
main()
+4 -4
View File
@@ -35,11 +35,11 @@ make -j
With no explicit `SPACETIME` setting, this builds all supported spacetimes:
| Executable | Spacetime |
| --- | --- |
| `build/Release/minkowski_sky` | Flat Minkowski spacetime |
| Executable | Spacetime |
| --------------------------------- | --------------------------------------------------------- |
| `build/Release/minkowski_sky` | Flat Minkowski spacetime |
| `build/Release/schwarzschild_sky` | Analytic Schwarzschild in ingoing Kerr–Schild coordinates |
| `build/Release/alcubierre_sky` | Analytic moving Alcubierre warp bubble, `x_s(t)=v_s t` (no capture) |
| `build/Release/alcubierre_sky` | Analytic moving Alcubierre warp bubble, `x_s(t)=v_s t` |
To build only one:
+3 -3
View File
@@ -1,5 +1,5 @@
REFERENCE_DIR := tests/data/psf_event_sink_reference
REFERENCE_TMP_DIR := /tmp/gr_psf_event_sink_reference
REFERENCE_TMP_DIR := /tmp/opencode/gr_psf_event_sink_reference
FLOATDIFF_SCRIPT := scripts/fits_floatdiff.py
.PHONY: test-reference-images
@@ -9,8 +9,8 @@ test: test-reference-images
test-reference-images: $(FLOATDIFF_SCRIPT) $(REFERENCE_DIR)/minkowski_ra1_dec1_640x360_HDR.fits $(REFERENCE_DIR)/schwarzschild_ra1_dec1_fov60_640x360_HDR.fits
$(MAKE) SPACETIME=minkowski ENABLE_HDR=1 backend
mkdir -p $(REFERENCE_TMP_DIR)
OMP_NUM_THREADS=16 $(BUILD_DIR)/minkowski_sky --catalog assets/sky_grid_5deg.csv --output $(REFERENCE_TMP_DIR)/minkowski_ra1_dec1_640x360.$(IMAGE_EXT) --hdr-output --width 640 --height 360 --fov-deg 30 --look-ra-deg 1 --look-dec-deg 1 --exposure 0.1 --observer-radius 30 --observer-velocity 0 0 0 --psf-fwhm-pixels 2.7 --psf-moffat-beta 4.5 --max-magnification 1e300 --max-cache-psf-flux 1 --psf-relative-tail 1e-8 --psf-min-y 0 --coarse-cell-pixels 16 --refine-max-level 0 --refine-angle-abs-deg 0.001 --refine-angle-rel 0.1 --refine-jacobian-min 1e-3 --refine-min-edge-pixels 0.5 --refine-min-area-pixels2 0.25 --catalog-load-workers 4
OMP_NUM_THREADS=16 $(BUILD_DIR)/minkowski_sky --integrator rk4 --catalog assets/sky_grid_5deg.csv --output $(REFERENCE_TMP_DIR)/minkowski_ra1_dec1_640x360.$(IMAGE_EXT) --hdr-output --width 640 --height 360 --fov-deg 30 --look-ra-deg 1 --look-dec-deg 1 --exposure 0.1 --observer-radius 30 --observer-velocity 0 0 0 --psf-fwhm-pixels 2.7 --psf-moffat-beta 4.5 --max-magnification 1e300 --max-cache-psf-flux 1 --psf-relative-tail 1e-8 --psf-min-y 0 --coarse-cell-pixels 16 --refine-max-level 0 --refine-angle-abs-deg 0.001 --refine-angle-rel 0.1 --refine-jacobian-min 1e-3 --refine-min-edge-pixels 0.5 --refine-min-area-pixels2 0.25 --catalog-load-workers 4
python3 $(FLOATDIFF_SCRIPT) $(REFERENCE_DIR)/minkowski_ra1_dec1_640x360_HDR.fits $(REFERENCE_TMP_DIR)/minkowski_ra1_dec1_640x360_HDR.fits
$(MAKE) SPACETIME=schwarzschild ENABLE_HDR=1 backend
OMP_NUM_THREADS=16 $(BUILD_DIR)/schwarzschild_sky --catalog assets/sky_grid_5deg.csv --output $(REFERENCE_TMP_DIR)/schwarzschild_ra1_dec1_fov60_640x360.$(IMAGE_EXT) --hdr-output --width 640 --height 360 --fov-deg 60 --look-ra-deg 1 --look-dec-deg 1 --exposure 0.1 --observer-radius 30 --observer-velocity 0 0 0 --psf-fwhm-pixels 2.7 --psf-moffat-beta 4.5 --max-magnification 1e300 --max-cache-psf-flux 1 --psf-relative-tail 1e-8 --psf-min-y 0 --coarse-cell-pixels 16 --refine-max-level 0 --refine-angle-abs-deg 0.001 --refine-angle-rel 0.1 --refine-jacobian-min 1e-3 --refine-min-edge-pixels 0.5 --refine-min-area-pixels2 0.25 --catalog-load-workers 4
OMP_NUM_THREADS=16 $(BUILD_DIR)/schwarzschild_sky --integrator rk4 --catalog assets/sky_grid_5deg.csv --output $(REFERENCE_TMP_DIR)/schwarzschild_ra1_dec1_fov60_640x360.$(IMAGE_EXT) --hdr-output --width 640 --height 360 --fov-deg 60 --look-ra-deg 1 --look-dec-deg 1 --exposure 0.1 --observer-radius 30 --observer-velocity 0 0 0 --psf-fwhm-pixels 2.7 --psf-moffat-beta 4.5 --max-magnification 1e300 --max-cache-psf-flux 1 --psf-relative-tail 1e-8 --psf-min-y 0 --coarse-cell-pixels 16 --refine-max-level 0 --refine-angle-abs-deg 0.001 --refine-angle-rel 0.1 --refine-jacobian-min 1e-3 --refine-min-edge-pixels 0.5 --refine-min-area-pixels2 0.25 --catalog-load-workers 4
python3 $(FLOATDIFF_SCRIPT) $(REFERENCE_DIR)/schwarzschild_ra1_dec1_fov60_640x360_HDR.fits $(REFERENCE_TMP_DIR)/schwarzschild_ra1_dec1_fov60_640x360_HDR.fits
+539 -38
View File
@@ -14,10 +14,9 @@
- 让平直时空、解析时空、数值时空在同一渲染框架中作为可替换 backend;
- 最终能够“看到”每次 NR 代码实际跑出来的时空,而不是只看 waveform 或标量诊断。
第一阶段不考虑物质辐射、吸积盘、流体、等离子体等局域发射源。每条 ray 的终点暂时只有两类:
1. 被黑洞捕获;
2. 到达无穷远天球。
第一阶段不考虑物质辐射、吸积盘、流体、等离子体等局域发射源。每条 ray 的正常终点
为:逃逸到某个无穷远天球,或达到红移暗阈值。预算耗尽与数据/积分失败是单独的
`UNRESOLVED` / `INCOMPLETE` 类别,不与物理暗终态混同(见 §18)。
---
@@ -79,7 +78,7 @@ C++ 并不是当前项目的必要条件。需要的抽象主要可以通过:
4. 将这些 rays 组成一个全局 `RayPool`;
5. 从视频结束时刻向过去,按 time slab 顺序加载数值时空;
6. 在每个 slab 内,把所有 active rays 一起推进到 slab 左边界;
7. ray 若到达无穷远或进入黑洞,则立即终止;
7. ray 若在某个渐近端逃逸、达到红移暗阈值、耗尽预算或遇到数据/积分失败,则按类别终止;
8. 一轮 ray tracing 完成后,把 endpoint 数据回填到各帧 image mesh;
9. 根据局部 lens mapping 误差判断哪些 image-plane triangles 需要进一步细分;
10. 生成下一批新增 rays;
@@ -116,7 +115,7 @@ Spacetime time-slab stream
│
▼
ray endpoint:
n∞, frequency shift, captured/escaped
n∞, frequency shift, end/outcome/reason
│
├──────────► next refinement pass
│
@@ -222,8 +221,8 @@ F^{-1}:\ \hat n_\infty \to (x,y)_\text{image}
每个 image-plane triangle 的三个顶点都保存:
- image-plane 坐标 `(x,y)`;
- ray 是否 escaped/captured;
- 若 escaped:无穷远方向 `n_inf`;
- ray 的终态类别 `ESCAPED`/`DARK`/`UNRESOLVED`/`INCOMPLETE` 及其 reason;
- 若 escaped:无穷远方向 `n_inf` 与所属 end;
- frequency shift / redshift accumulator。
示意:
@@ -235,7 +234,9 @@ typedef struct {
double n_inf[3];
double log_g;
uint8_t ray_status;
uint8_t outcome; /* ESCAPED / DARK / UNRESOLVED / INCOMPLETE */
uint8_t reason; /* REDSHIFT_LIMIT / BUDGET_EXHAUSTED / ... */
uint32_t end_id;
} LensVertex;
typedef struct {
@@ -380,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$。
@@ -417,8 +420,9 @@ $e'_3=-\sin\rho\,e_2+\cos\rho\,e_3$ 定义。
observer 构造只接收当地 metric 和已补全的参数,不加载 slab、不分类 ray。
调用方在昂贵的 catalog/PSF 初始化前验证相机及 backend 数据域。
现有 Schwarzschild cutoff 为 $r=1.5M$;相机必须在 cutoff 外,但允许在视界内。
此功能不改变捕获 cutoff 或向过去追踪的高红移终止条件。
相机合法性只由 metric 可用性、四速度 timelike、时间定向和 tetrad 正交归一决定;
视界内、旧 cutoff 内的相机都是正常渲染目标,位置本身不决定 ray 终态。
向过去追踪的高红移阈值截断仍正常生效(见 §18)。
单张相机参数与轨迹输入、lens-map 导入互斥;导入仍跳过 metric 与 observer 初始化。
验证包括 tetrad 正交归一和 null 初始化、平直时空平移不变性与解析光行差/多普勒、
@@ -708,27 +712,513 @@ slab 边界需要少量 overlapping temporal ghost slices。
---
# 18. 黑洞捕获判据
# 18. 黑洞终止与暗终态
目标使用 moving-puncture BBH,而不是 excision。
目标使用 moving-puncture BBH,而不是 excision。正常终态**不使用** horizon 内位置
cutoff、AH-calibrated puncture 小球或 armed/re-entry 状态机判定物理捕获。过去向光线
围绕渐近端逃逸、能量阈值截断和经可靠识别的渐近轨道组织;达到红移阈值后停止、渲染
为黑,是已确定需求。
production renderer 不希望依赖每次 NR run 都开启昂贵的 AH finder。
计划:
1. 用低分辨率 single-BH / BBH calibration run 开 AH finder;
2. 测量 horizon 相对于 puncture 的最小 coordinate radius;
3. 选择明显保守、始终位于 AH 内部的 puncture-centered cutoff;
4. 正式 renderer 只根据 puncture trajectory 做判断。
形式:
形式(相机相对局域能量增长,对全部 backend 统一;具体阈值通过小型
oracle/convergence test 标定,不宣称由论文给定):
\[
|\mathbf x-\mathbf x_p(t)|<r_\text{cut}
\Rightarrow \text{captured}
L-L_0=\ln\!\frac{\alpha p^0}{(\alpha p^0)_0}\ge L_\text{dark}
\Rightarrow \text{DARK (redshift limit)}
\]
未来 BBH merger 后若仅用两个 puncture-centered 小球导致大量 doomed rays 继续积分,可以再加入 common-horizon-derived termination 优化。
`L_0` 是相机事件的参考值,对全部 spacetime backend 生效,并随 ray 状态跨 slab 与
retry 传递;减去 `L_0` 只改变判据参考,不重置光子能量或频率比 `g`。必须区分 `L`
(Eulerian 能量对数)、`ln(p^0)=L-ln(alpha)` 和真正连接源端得到的
`g=E_camera/E_source`。当前默认 `L_dark=8`,由 CLI 参数覆盖,不宣称为论文值。
终态分为四类(详见 §18A 与终点协议):
- `ESCAPED`:成功完成某个 end 的外推,必须带有效 `end_id`、`n_infinity`、`g`;
- `DARK`:无天空贡献的暗终态,当前主要为红移阈值截断 `REDSHIFT_LIMIT`;不同 dark
reason 不制造 mesh seam;
- `UNRESOLVED/BUDGET_EXHAUSTED`:轨迹仍可信但计算预算用尽,可重试;
- `INCOMPLETE/FAILED`:历史耗尽、域外、invalid metric、I/O、积分误差不可控、
unsupported chart 或 protocol error;不得伪装成 dark。
数值失败、单次 metric eval failure 或单个超阈值 trial step 均不得推断为物理 capture;
阈值只检查可信的初始或 accepted 状态。`ASYMPTOTIC_TRAPPED` 与 `SINGULARITY` 仅在存在
可靠 backend 判据及明确源边界条件时启用。有限几何分辨率导致的 shadow 略偏大与未解析
高阶像尾部由三角形近似处理,并保留 triangle provenance 与面积统计;这属于渲染近似,
不是物理捕获。跨 chart、跨 region 或穿越视界本身不是暗终态;moving-puncture trumpet
不解释成可穿越的第二个宇宙。production renderer 不假定每次 NR run 都运行昂贵的 AH
finder,也不依赖 capture sidecar。
解析 Schwarzschild 的 policy version 3 使用相机相对局域能量增长
\[
A_0 = L - L_0 \ge L_\text{dark}.
\]
`L_0` 是该 ray 积分起点的参考值,随 ray 状态跨 slab 与 retry 携带,不在每次
检查时用即时状态重算,也不重置光子能量或 frequency ratio。Eulerian 观者测得
的能量为 `e^L`,故 `A_0 = ln(E_euler/E_euler,0)`;一个常数相机 boost 在减法中
抵消,因此大 boost 或内部相机不会仅因初始 `L` 大而被判暗。这正是与视界无限
红移对应的局域相对量。对静止时空,理论上也可用 Killing 相对量
`A_K = L - ln|E_K|`,但沿精确光线 `A_K - A_0 = -ln|alpha_0 - beta_0.Pi_0|`
只是初始常数,不能仅凭守恒证明其优于 `A_0`;该 stationary 参考作为独立对照保留
在 `test_termination_oracle.c`,不进入生产判据,也不移植到动态 NR。阈值
`L_dark=8` 仍是待标定参数;生产条件不含绝对 `L` 或 backend applicability
guard。
所有 midpoint probes 同样属于完成性检查范围。失败/未决 probe 保存为 off-mesh
witness;未决 witness 合并到下一轮续追请求,完成 witness 可复用。重试回填使相关
叶子决策失效,不能借用其他叶子的 `evaluated` 标记跳过新出现的边界。最大层数、
最长边和面积共用同一停止判据;近似标黑统计报告实际最大边长、最大面积及层数停止
数量。Replay 使用文件保存的几何策略进行同样的完成性检查,诊断覆盖必须报告
`INCOMPLETE`,不得绕过发布 gate。conformity/几何限制取消全部请求边的 triangle
必须显式 settle(记录 `evaluated`),不得每代重复请求同一组 probes;被取消而
几何仍允许的 UUD/UDD 结算为 budget-incomplete,达到停止尺度的结算为近似标黑。
witness 提升为正式 midpoint 时原地复用同一 vertex id,只保留一份连续状态;只有
真正 off-mesh 的未决 witness 独立重试。lens-map 每帧保存累计 `retry_requests`,
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 状态,也不把未重试的旧失败当作新失败重复计数。
`RayReason` 的诊断粒度是追加式扩展:原 coarse reason 的数值 0..9 冻结不变,
更细的失败原因一律追加在 `RAY_REASON_IO_ERROR` 之后,`RAY_REASON_COUNT` 只作
计数/未知 sentinel 且不写入 wire。lens-map 的二进制布局(v2/v3)与字段偏移不
改变,新 reader 读旧值原样、读追加值按新名解释,并拒绝 `>= RAY_REASON_COUNT`;
旧 reader 因 coarse 校验仍会安全拒绝它不认识的新值。每个细原因通过纯函数
`ray_reason_category()` 归入既有 coarse 类别(PROTOCOL_ERROR、
INTEGRATION_ERROR、UNSUPPORTED、IO_ERROR 等),供只需要粗分类的调用方使用。
该扩展只细化失败状态的命名与上报,不改变任何物理终态判据、终止分类或 retry 规则。
---
# 18A. 渐近外区、escape worldtube 与 endpoint 协议
本节冻结“到达 escape 区域就终止”这一旧行为被替换后的职责边界,是该协议的权威
约定与唯一长期记录。
## 18A.1 问题
旧判定只看光线当前位置是否在某个 escape 半径之外,不看传播方向。因此当相机
本身位于 escape 球外时,所有光线在初始化后立即被判为逃逸,连本应进入强场区的
光线也不会积分。旧实现还把“外推到无穷远”简化为“在 escape 球处删除”,频移因此
带有 $O(M/R)$ 的误差。
## 18A.2 ray 生命周期三段
1. **相机位于 escape worldtube 外**:由公共渐近外区模块判断光线是否会与
worldtube 相交。
- 相交:把光线外推到第一次由外向内穿越,并从该 entry event 开始交给 backend
内区积分;
- 不相交:直接把光线外推到对应无穷远天球,写 `ESCAPED` endpoint。
2. **光线在 backend 内区积分**:不再因为“当前位置处于 escape 区域”立即终止;
只有沿 ray 的过去传播方向发生**有方向的 inside -> outside 穿越**时才进入外区
收尾。
3. **穿越后**:公共渐近外区模块把有限半径处的 canonical photon state 推到无穷远,
得到 `n_infinity` 和 `frequency_ratio`。
一个 backend 可以声明多个渐近远端;当前实现只暴露一个 `end_id`,但 endpoint 与
ray 状态中不得把“整个时空只有一个无穷远”写死。
## 18A.3 职责边界
backend 负责声明:
- `end_id` 及其稳定编号;
- 外区模型种类:`MINKOWSKI` 或 `SCHWARZSCHILD_MONOPOLE`;
- 从该远端看见的渐近质量 `mass`(允许为零);
- 渐近参考系的 origin 与空间基(在 backend 坐标中表达);
- 给定 coordinate time 的 escape worldtube 球心 `center`、速度 `velocity`、
半径 `radius`、半径变化率 `radius_rate`;
- worldtube 描述有效的时间区间与运动分段边界;
- backend 内一点属于哪个候选 end 的 outer region,或当前无法分类。
backend **不**负责:球外传播、entry/miss 判定、无穷远方向、pre-route 或 endpoint
写回。
公共渐近模块负责:
- 将 backend photon state 与统一 canonical state 双向转换;
- 球外相机的 entry/miss 判定;
- 对 miss 光线直接生成 infinity endpoint;
- 把 entry 光线传播到 worldtube 的第一次由外向内穿越;
- 把内区积分产生的由内向外穿越传播到无穷远;
- $M=0$ 使用精确 Minkowski 几何;$M>0$ 固定同心球使用内建 monopole 近似;
- 返回明确状态码,而不是用 NaN 或任意 fallback 掩盖适用域错误。
geodesic/ray 生命周期层负责:
- 初始化时调用 pre-route;
- 保存 entry event,并在 slab sweep 到达 entry time 时激活内区积分;
- 每个 accepted ODE step 后检测有方向的 crossing 并局部化第一次根;
- 调用公共外区模块完成 endpoint。
## 18A.4 canonical photon state 与时间约定
canonical state 至少包含:coordinate time $t$、渐近参考系中的位置、传播方向与
能量/频移所需的 photon momentum 信息、以及 `end_id`。
renderer 沿过去方向积分。文档中使用的空间单位方向唯一约定为**过去传播方向**
$\mathbf w$:令 $s=t_\text{camera}-t\ge 0$,则局部轨迹满足
$\mathbf x(s)=\mathbf x_0+s\,\mathbf w+\dots$。`n_infinity` 是光线在无穷远处的
来向,即 $\mathbf w$ 在 $s\to\infty$ 的极限,因此与旧实现中
`normalize(-gamma^{ij} Pi_j)` 的符号约定一致。
在静止时空(Minkowski 与 Kerr–Schild Schwarzschild)中,沿测地线守恒的 photon
能量为
\[
E_\infty=-p_t=\alpha p^0\left(\alpha-\beta^i\Pi_i\right),
\]
其中 $p_i$(即 `Pi` 的协变版本)满足 $p_i=\alpha p^0\,\Pi_i$。相机归一化取
$E_\text{camera}=1$,故
\[
g=\frac{E_\text{camera}}{E_\infty}=\frac{1}{\alpha p^0(\alpha-\beta^i\Pi_i)}.
\]
旧实现对 $M>0$ 在 escape 球处直接返回 $\exp(-\log(\alpha p^0))$,是上式在
$\beta^i\Pi_i\to0,\alpha\to1$ 下的近似。
## 18A.5 worldtube 与穿越方向
球面 worldtube:
\[
F(t,\mathbf x)=|\mathbf x-\mathbf c(t)|^2-R(t)^2.
\]
$F>0$ 外、$F<0$ 内、$F=0$ 边界。边界点($F=0$)必须结合过去传播方向的斜率
$\mathrm dF/\mathrm ds$ 分类:$\mathrm dF/\mathrm ds<0$ 视为即将进入、$\ge0$ 视为
向外或切触;因此相机恰在 $F=0$ 且 past-inward 才按 INSIDE 处理,past-outward
与 tangent 都按外层 route 处理。inside -> outside crossing 要求 $\mathrm dF/
\mathrm ds>0$ 的严格符号变化($F_{\rm before}\le0$ 且 $F_{\rm after}>0$);
仅有 $F_{\rm after}=0$ 的单点切触不算 crossing,需等下一步是否真正到 $F>0$。
必须区分方向:
- camera pre-route 的 entry 是沿过去传播方向第一次 outside -> inside;
- 内区 escape 是沿过去传播方向第一次 inside -> outside;
- 某次采样发现 $F\ge0$ 不能独立构成 escape。
对步进端点接近零、切触和跨越 motion-segment 边界,使用显式容差和有界 root
localization;不得用固定位置 epsilon 把 tangent 误判成 crossing。
## 18A.6 $M=0$ 外区
渐近惯性系中为解析直线传播。固定球用 ray-sphere 二次方程取沿过去传播方向最早
的合法根;匀速移动球在分段内把球心写成 $\mathbf c(t)=\mathbf c(t_0)+\mathbf v(t-t_0)$,
令 $\mathbf d=\mathbf x_0-\mathbf c(t_0)$、$\mathbf q=\mathbf w+\mathbf v$,entry 满足
$|\mathbf d+s\mathbf q|^2=R^2$(半径线性变化时右端为 $(R_0-R_\text{rate}s)^2$)。
任意加速球的未来接口使用分段 bracketed root driver;若 backend 历史在判定完成前
结束,返回 `TIME_RANGE_EXHAUSTED`,不得武断判为 miss。
$M=0$ 的 finish 是平凡的:$\mathbf n_\infty=\mathbf w$、
$g=\exp(-\log(\alpha p^0))$(flat 中守恒)。
## 18A.7 $M>0$ Schwarzschild-like 外区
首版严格限制:球心固定、escape 球与 monopole 同心、$R/M\ge64$、$M>0$、相机与
worldtube 位于该外区。不满足则返回明确的 unsupported/domain 状态;不静默退回
Minkowski,也不把一般移动 Schwarzschild 球解释成瞬时静态球。
无量纲量 $\rho=r/M$、$\beta=b/M$,
\[
Q(\rho,\beta)=1-\beta^2\frac{1-2/\rho}{\rho^2}.
\]
escape 球处切触阈值 $\beta_R=\rho_R/\sqrt{1-2/\rho_R}$。entry/miss 的拓扑分类优先
使用解析阈值和方向信息,不由低精度查表决定。
角度 primitive 为过去传播方向从半径 $\rho$ 到无穷远扫过的单调外向方位角
\[
\Phi(\rho,\beta)=\int_\rho^\infty
\frac{\beta}{\rho'^2\sqrt{Q(\rho',\beta)}}\,\mathrm d\rho'
=\int_0^{1/\rho}\frac{\beta\,\mathrm du}{\sqrt{1-\beta^2u^2+2\beta^2u^3}}.
\]
turning radius 满足 $\beta^2=\rho_\text{turn}^3/(\rho_\text{turn}-2)$。守恒的
impact parameter 与角动量满足
\[
\beta=\frac{|x\times\Pi|}{\alpha-\beta^i\Pi_i},\qquad
\mathbf N=\widehat{x\times\Pi},
\]
无穷远方向由 $\hat{\mathbf r}=x/|x|$ 绕 $\mathbf N$ 旋转 $\Phi(\rho,\beta)$ 得到。
turning map 与 Schwarzschild coordinate-time transfer 使用离线验证过的有界
residual、Chebyshev 表或解析主项;运行期不得建表。
若 time-transfer 无法在声明域内满足误差标准,则保留 $M=0$ 实现和接口,不把未经
验证的时间公式写入生产代码,也不得降低验收标准。
## 18A.8 nmesh outer-shell 约定(仅约定,不实现)
- 最外层必须是有明确六个面的 cubed-sphere shell;
- outer boundary 在渐近 frame 中是固定中心、固定半径球面;
- 提供 $R$、对应 end 的质量和 frame metadata;
- Schwarzschild monopole 模式要求 $R/M\ge64$;$64M$ 只是拒绝更靠内 junction
的硬下限。nmesh 有 AMR,生产数据应把 outer shell 放到尽可能大的半径,建议以
至少接近解析 backend 当前的 $256M$ 为目标;
- DG element 边界本来允许场跳变,故 worldtube junction 不要求两侧 metric
pointwise 连续;
- 穿越时匹配 boundary local tetrad 中的 photon direction/energy,并用分辨率与
outer-radius convergence test 验证,而不是强行匹配坐标分量。
## 18A.9 失败语义
遇到以下情况必须返回明确状态并上报,不得 fallback:
- worldtube 历史不足,无法判断 first entry;
- Schwarzschild 外区不是固定同心球;
- $R/M<64$;
- canonical state 无法保持 null constraint 或 round-trip 精度;
- time coordinate 约定不明确;
- 高精度 reference evaluator 在域内不收敛;
- 表在 seam 或 grazing 区域超过误差限。
## 18A.10 Backend 能力与路由约束
- 公共接口以 end descriptor、escape worldtube sample、canonical photon state 与
endpoint `end_id` 分离远端声明、几何事件、状态转接与终态来源;
- $M=0$ 固定球与匀速移动球的 camera pre-route、directed inside -> outside
crossing 与 `PENDING_ENTRY` 生命周期采用同一几何约定,适用于 Minkowski 与
Alcubierre 的平直外区;
- 单帧与 movie 必须共用同一 pre-route/生命周期实现;
- $M>0$ 固定同心 Schwarzschild monopole 外区以**解析 Carlson 椭圆积分**实现
(见 18A.11),接入解析 Kerr–Schild backend;相机位于 escape 球外的
pre-route、entry 传播、directed crossing 与 infinity endpoint 均可用;
- worldtube 的运动能力必须显式声明。分段匀速、线性半径变化可在各段内求首次
crossing,并在段边界重新采样;任意加速运动在没有可信区间界时必须报告
`UNSUPPORTED`,不得把离散采样没有发现根解释为正常 miss。扩展加速运动能力需要
同时提供可信的段内界或专用首次 crossing 解法。
- `end_id` 贯通 endpoint、`LensVertex`、`terminal_mismatch` 与
`discrete_jacobian`,避免跨 end 插值;lens-map 保存
`end_id`。不同物理 region 的 reachable-end 路由及 catalog 绑定仍需独立定义,
不能把能够保存 end 标识解释为已支持完整解析延拓。
- 生命周期为三态:`NO_ENDS`(才允许 legacy)、`DIRECTED_READY`、
`PROTOCOL_ERROR`(descriptor 读取失败或不支持的 exterior,显式
`INVALID/UNSUPPORTED`,绝不退回 legacy)。pre-route 也先校验所有 descriptor
与 exterior kind,再判定 worldtube 内外,因此“声明了不受支持 exterior 而相机
恰在球内”不会被静默接受。
- 内区积分时若 worldtube sample 变为 `valid == 0`(历史耗尽),作为一等
terminal reason 返回 `INCOMPLETE/TIME_RANGE_EXHAUSTED`、保留 `end_id`,并由
`RayPool` 记为 `RAY_POOL_TERMINATED`,与 pre-route 的耗尽语义一致。内区 crossing
localizer 定位过程中任何 sample 失败都直接传播具体状态,不返回一个正常 entry。
- Minkowski entry quadratic 用稳定根公式($q=-\tfrac12(b+\mathrm{copysign}
(\sqrt\Delta,b))$,取最小正根);$c=0$(相机在边界)时按 $b=\mathrm dF/
\mathrm ds$ 分类。worldtube sample 必须有限且 $R>0$,否则 `INVALID`。
Alcubierre 等平直外区共用此解法;相对位置/速度、系数、判别式和根采用
`long double` 中间量,最后才转换为 backend 的 double 状态。某些平台的
`long double` 不提供额外有效位,因此精度提升不能替代入口验证。
送入根公式前,以系数最大绝对值的二进制指数共同缩放 $a,b,c$(最大值进入
$[1/2,1)$),避免判别式乘积溢出;不改变传播参数及根,也不覆盖系数构造前
已发生的误差。非有限系数或缩放后非零系数落入 subnormal 范围,转入不确定
路径而非当作退化线性式。判别式 $b^2-4ac$ 和入口斜率采用普通 `long double`
运算,不使用显式 FMA;精度/成本对照见 `benchmarks/quadratic_precision/`。
本机软件 `fmal` 成本明显,测试未显示足以抵偿成本的精度收益;不确定带、
几何验证及后备定位仍保留,不能把此样本结论当作全参数可靠性保证。
- **入口快路径与后备定位分离**:解析相交只提供候选入口;采用前须在实际 backend
坐标、实际入口时间重新检查 worldtube 残差。通过原有几何舍入容差的候选沿快路径
激活;未通过的候选沿同一外区 geodesic,从原相机事件重新求值并用确定在外/
严格在内的区间定位首次入口。公共 localizer 不依赖具体 metric backend,
各已支持的外区提供轨迹求值与首次入口 bracket;不能把离散采样无交点当作 miss。
miss 判定必须排除真实入口;擦边或求根病态造成的数值不确定性不能作为
`ESCAPED` 的依据,允许保守地生成候选入口并进行后备验证。
浮点判别式等于零不构成精确擦边证明;重建最小点的单次正残差也不构成
miss 证明(大时间/坐标的舍入会移动该最小点)。精确擦边可由独立几何证据
排除入口,例如固定半径段内某个不变坐标的精确距离已不小于球半径;
无法获得这种证据且无法构造可信 bracket 时仍返回 `ENTRY_UNCONFIRMED`。
后备路径返回 bracket 收敛后的可表示内侧轨迹状态,不做径向位置投影,也不放宽
原 worldtube 检查容差。未能确认首次入口须报告 `INCOMPLETE/ENTRY_UNCONFIRMED`,
callback、历史及几何错误仍传播各自具体 reason,不能改写为 capture 或 escape。
定位在 pre-route 中完成,不在 slab sweep 中倒回相机时间;不重置相机 `L0`,
内区 accepted-step/lookback 预算仍从最终激活事件起算。成功的后备入口由
`entry_fallback_evaluations` 记录 localizer 的轨迹求值次数(不含构造 bracket
的探测),不混入内区 RHS 计数;临时状态由
调用者独占,不引入共享可变缓存。此标记不改变 lens-map 二进制布局。
- **构造期验证优先**:`spacetime_create_*()` 成功即承诺该 source 已可安全光追。
每个 constructor 在安装 ops/context 后调用公共 `spacetime_source_finalize()`:
检查 ops/context 完整、`end_id` 唯一且非 `NONE`、exterior kind 受支持、
mass/frame 有限。**完整 worldtube 历史(所有 segment 边界、中心/半径连续性、
全程 $R>0$)由 backend constructor 负责**,普通解析 backend 在参数校验中完成;
失败时 constructor 销毁 context 并返回错误,不存在半构造可用的 source。
- **motion-segment 有效域**:候选二次根必须满足 $0\le s\le s_{\rm segment}$;
本段无有效根时推进到下一段边界(用 `nextafter(boundary,-∞)` 进入下一段)重新
求根;只有最后一个向过去开放的恒速段无根时才判 `ESCAPED`;segment 预算耗尽
属于内部失败,返回 `INVALID`。因为 constructor 已保证全程 $R>0$,正常
routing 不再做 segment-collapse 判定;但根处仍保留一次
$R_0-\dot R\,\sigma>0$ 检查(一次乘减),用于防御绕过 constructor 的 backend,
避免负半径伪 entry。
- **运行期防御**:所有 backend(含 Schwarzschild)都经公共
`worldtube_sample()` 读取,并保留一个便宜的 callback trust-boundary 检查
`isfinite(radius) && radius > 0`:callback 失败 → `INVALID`、`valid=0` →
`TIME_RANGE_EXHAUSTED`、NaN/非正半径 → `INVALID`。这是防止第三方 backend 或
测试绕过 constructor 的安全网,不再承担正常配置验证;containment 循环原样
传播这些状态并保留 `end_id`。
- turning radius 用 bracketed bisection + Newton polishing。相机紧贴大球时的
entry 精度受 $|d|^2-R^2$ 输入条件数限制,由 entry-time 预算覆盖。
- generic accelerated driver 的初始条件同样用 $F<0$,或 $F=0$ 且
$\mathrm dF/\mathrm ds<0$ 才计为 entry;搜索只用严格 $F<0$ 的采样点确认
entry,单点 $F=0$ 切触不算。
- 终态契约已升级为两组枚举:渲染类别 `ESCAPED/DARK/UNRESOLVED/INCOMPLETE`
与诊断 reason。位置 cutoff 与 `SPACETIME_RAY_CAPTURED` 已删除;正常暗终态为
相机相对局域能量增长 `L - L0 >= L_dark`(默认 `L_dark=8`,可用
`--dark-threshold` 覆盖,对全部 spacetime backend 统一生效)。计算配额耗尽
返回可重试的 `UNRESOLVED/BUDGET_EXHAUSTED`,
保留最后可信连续状态;数据/积分/历史耗尽返回具体 `INCOMPLETE` reason。
- `eval`/`eval_slab` 返回 `SpacetimePointStatus`(时间不足、域外、invalid
metric、内部错误);observer 合法性只由 metric 可用性、timelike 四速度、
时间定向和 tetrad 正交归一决定,不再调用位置分类。
- 三角形决策实现 $E/D/U$ 账目:`UUU` 与含逃逸顶点的未决组合强制追加预算重试;
`UUD/UDD` 在几何停止尺度处近似标黑并记录 image-plane 面积与 triangle
provenance;含 `INCOMPLETE` 的三角形计为错误,不参与近似标黑。总资源上限
耗尽而未决时报告 incomplete,诊断可用 `--allow-incomplete` 覆盖。
- lens-map 文件格式升级为 v2:显式存储 `end_id/outcome/reason`、triangle
`approx_black` 以及 policy/retry/几何阈值 provenance;v1 文件被明确拒绝。
## 18A.11 $M>0$ 解析外区:闭式约化与验证
阶段 C 采用的不是 Chebyshev 表,而是把外区积分闭式约化到 Carlson 对称积分,
并在 double 下验证。记 $\rho=r/M$、$u=1/\rho$,$P(u)=1-\beta^2u^2+2\beta^2u^3$。
- **角度 primitive.** 对三次 $P$ 的实根分支用实 Legendre 形式
\[
\Phi=\frac{\sqrt2}{\sqrt{C-A}}\left[F(\varphi(u),\kappa)
-F(\varphi(0),\kappa)\right],\qquad
\sin^2\varphi=\frac{u-A}{B-A},\quad \kappa^2=\frac{B-A}{C-A},
\]
$A<B<C$ 为三个实根;对一实根加共轭复根的分支用
$\Phi=|S|/\sqrt2$、
$S=2[R_F(-e_i)-R_F(u-e_i)]$。$F(\varphi,\kappa)$ 经
$F=\sin\varphi\,R_F(\cos^2\varphi,1-\kappa^2\sin^2\varphi,1)$ 求值。
近双根时 Cardano 根用 Newton 抛光,以保证 grazing 处 $\beta_R$ 附近精度。
- **turning radius.** 用同一三次的较小正实根,safeguarded Newton/bisection。
- **KS 时间传递.** 使用 Cartesian ingoing Kerr–Schild 时间
$t_{\rm KS}=t_S+2M\ln(r/2M-1)$,故
\[
\frac{dt_{\rm KS}}{dr}=\frac{\sigma}{f\sqrt Q}+\frac{2M}{rf},\qquad
f=1-\frac{2M}{r},\quad Q=1-\frac{\beta^2f}{r^2}.
\]
解析拆出平直主项与对数项后,剩余第三类积分在 $u$ 变量下化为有界积分
$\beta^2/(\sqrt P(1+\sqrt P))$,其中 $\int du/(1+\sqrt P)$ 用 48 点
Gauss–Legendre(端点平方根奇性用 $u=u_R-(u_R-u_c) t^2$ 消去)计算。没有运行期
建表,也不需要 2 MiB 系数预算。
- **频率.** 直接用守恒量 $E=-p_t=\alpha p^0(\alpha-\beta^i\Pi_i)$,
$g=1/E$,不建表。
- **验证与误差标准.** 外推误差按渲染器总误差预算定,不追求接近机器精度:
默认 mesh refinement 阈值约 $1.75\times10^{-5}\,\mathrm{rad}$,内区 ODE 固定
步长 $0.1M$,因此外区链路的验收标准取
- 最终 $n_\infty$ 角误差 $\le10^{-8}\,\mathrm{rad}$(60°/4K 约 $4\times
10^{-5}$ pixel);
- entry time $|\delta t|\le10^{-7}M+10^{-11}|\Delta t|$;
- frequency ratio 相对误差 $\le10^{-10}$;
- turning equation residual $|Q|\le10^{-11}$;
- moving-sphere crossing 用尺度化 residual(约几十 ulp),不对近切触强求统一
forward error。
实测远优于该标准:mpmath 45–80 位 oracle 对 15000 个随机
$(R/M\in[64,5000],\ \beta)$ 角度点最大绝对误差 $1.9\times10^{-14}\,\mathrm{rad}$
(无 NaN);`tests/test_asymptotic_schwarzschild.c` 固化少量 60 位 reference
常数(radial、复根、三实根、grazing、large-radius、short-interval、
very-large-camera)作为回归,并确定性覆盖 inward-hit / inward-miss / outward
与 motion-segment / history-exhausted。三实根分支用实 Legendre + 实数
$R_F$,共轭复根分支用 principal $R_F$。
---
# 18B. 自适应测地线积分
积分器负责轨迹误差控制、拒步与数值失败诊断,不承担物理终态分类。步长、计算配额与
回溯时间是独立配置;改变步长或容差不得隐式改变可追踪的时间范围。
- 生产默认采用 DP54;固定 RK4 保留为显式数值对照。CLI 默认相对容差与 `Pi/L`
绝对容差为 `1e-9`,位置绝对容差为 `1e-9` 倍 backend 长度单位(解析
Schwarzschild 为 `M`,Alcubierre 为 bubble 半径,Minkowski 为坐标单位长度)。
初始步长、最大步长及时间配额按 backend 的长度/时间单位配置,所有参数均可显式
覆盖并写入 provenance。容差控制的是局部 ODE 误差,不承诺临界光线最终方向的
全局误差界;生产精度应以独立容差与几何尺度收敛确定。
- CLI 步长下限为 `max(1e-12,16*DBL_EPSILON)` 倍 backend 长度单位(`c=1`),
是不绑定正常轨迹的数值保护,不是已测得的物理最小时间尺度。上限在解析
Schwarzschild 为 `8M`,Minkowski 为 `16` 个坐标时间单位,Alcubierre 为
初始默认步长的八倍。上限用于限制 trial 区间,不替代误差与首次事件控制;
near-critical 光线应独立收紧容差,不能只靠减小上限保证分类与方向收敛。
参数扫描与复现输入见 `benchmarks/adaptive_step_bounds_2026-10-05/`;解析 backend
的测量不外推为 NR/DG 数据的默认步长标定。
- `GeodesicTraceConfig.stepper` 区分固定 RK4 与 Dormand–Prince 5(4)。DP54 的
`atol_x/atol_Pi/atol_L/rtol`、`min_step/max_step`、
`consecutive_rejection_limit`、`max_lookback_time` 全部显式;缺省、非正、非有限或
`min_step > max_step` 直接返回 protocol error,不偷偷填默认值。初始 trial step
复用 `coordinate_time_step`。
- DP54 采用标准 Dormand–Prince 5(4)7M tableau,接受 5 阶解,误差用
`h*sum((B5-B4)*k)` 直接计算;位置误差尺度
`atol_x + rtol*max(|dx|,|h*k1.x|)`,`Pi` 用 `atol_Pi + rtol*max(|Pi_before|,|Pi_candidate|)`,
`L` 用 `atol_L + rtol*max(1,|dL|)`;控制器 safety `0.9`、factor clamp `[0.2,5]`、
exponent `1/5`,无 FSAL(每次 trial 7 次 RHS),拒步不提交状态。
- 自适应控制状态属于各条 ray:保存 `integration_start_time`(activation/entry 时间,
非相机能量参考 `L0`)、`next_step`、累计 rejected/RHS 计数与 `previous_rejected`;
`steps` 表示 accepted 步数。状态须跨 slab 与预算重试保留;endpoint 保存最后可信
轨迹及控制/成本状态,使续追无需从相机重放,也不重置 `L0`。
- 控制/成本状态在 scheduler 各层按值传递:batch 路径的 `RayPool` SoA 保存同一组
per-ray 字段,slab activation 只重置新 ray 的 accepted 步数,绝不重置 adaptive
control state;frame/movie 的续追请求以完整 resume state 重建(frame 与 movie
共用同一 helper,禁止逐字段手抄或漏字段)。endpoint 记录本次实际授予的 time
budget,供 retry 层独立累计。off-mesh witness 原地提升必须整体复制该状态,
重试回填保留连续状态与成本计数。
- 计算配额与回溯时间是两条独立、各自饱和的 retry 预算:step 增量与 time 增量互不
替代,只有"实际耗尽且仍可增长"的 quota 才允许发起 retry;任一 blocking quota 已
达硬上限时不得仅凭另一 quota 产生无效重试,直接按 budget-incomplete 处理。两个
quota 的实际配置总 grant 必须与已 spent 数量分开记录,retry 采样取实际 grant,
不得由已花费步数反推授权。"可增长"指新 quota 必须打开严格更大的可表示区间:整数
步长严格增加,time 左边界 `start - quota` 严格前移;因大 time origin 或增量过小
而 round 到同一区间的增量不算有效增长,不得发起永远无法推进的 retry。两者都到上限
后 UUU 仍为 budget-incomplete,UUD/UDD 在几何停止尺度可按 finite-resolution 近似
标黑。自动补齐的 time 重试增量由 `max_lookback_time` 指定,总上限为其四倍
(对有限数值范围饱和);这是可覆盖的 resource quota policy,不是物理终态条件。
- 每步对 slab 左边界与显式回溯预算左边界截断;DP54 不以
`coordinate_time_step * max_steps` 定义时间范围。回溯预算耗尽而轨迹仍可信
返回 `UNRESOLVED/BUDGET_EXHAUSTED`;真实的 slab data time failure 仍是
`INCOMPLETE/TIME_RANGE_EXHAUSTED`。`max_steps` 仍作为 accepted 步数额度。
各 trial 先确定可表示的目标时间,再用实际 `target-before.t` 推进状态、构造
dense interpolant 与提交时间;不得以舍入前的步长推进空间、舍入后的步长记时间。
worldtube 采样的坐标时间同样使用实际差值回推运动,不假设半步总是可表示。
- 初始 accepted 状态的 RHS/metric 失败直接报告具体 point reason,不缩步;后续 stage
的 `OUT_OF_DOMAIN/INVALID_METRIC` 可有界缩步重试;`TIME_UNAVAILABLE/INTERNAL_ERROR`
不靠无限缩步;最小步长或时间不可进导致失败时报告 `INCOMPLETE/INTEGRATION_ERROR`。
拒步上限耗尽时保留具体 stage 原因,或报告积分误差不可控。失败路径保证调用者
state 与 endpoint 的最后可信状态一致。
- directed inside→outside 事件的 DP 子积分定位使用同一 DP 误差控制(不混用 RK4),
成本计入 endpoint 但不伪造 accepted 主步。定位的 time bracket
停止容差取局部步长量级与真实 `nextafter` ULP 的组合(不随远端 `|t|` 放大),任何
可表示的非零 target−before 区间都实际积分,只有 target==before 才允许直接取状态;
`t+h==t` 明确失败或返回已接受可信状态,不伪造轨迹。子积分失败经独立 RayReason 透传
(`TIME_UNAVAILABLE→TIME_RANGE_EXHAUSTED`、`INVALID_METRIC`、`OUT_OF_DOMAIN`、
`INTEGRATION_ERROR`),与 worldtube descriptor 自身的 protocol/history 失败区分。
- 嵌入误差估计不保证找到全部几何事件;窄进出、first-crossing 与同一步内多个事件的
顺序必须独立验证,不能只凭步端点符号或误差通过认定事件完整。积分精度还须通过
逃逸方向、频移、null 残差及适用 backend 的独立守恒量验证;不靠反复归一化掩盖误差。
- DP54 accepted 区间的四次 dense interpolant 用于枚举事件候选。对分段匀速球形
worldtube,代入 `F=|x-center|²-radius²` 得到至多八次多项式;通过导数根划分
单调区间并隔离根,不能仅查看主步两端符号。根按过去传播方向排序,区分真正的
inside→outside crossing 与切触;motion 段边界截断主步并重新建立候选。
相机相对能量阈值的候选从同一 accepted 区间的 `L-L0` 获得。候选必须经误差受控
子积分验证,escape 与暗阈值按可信轨迹顺序处理;数值不可分辨的同时事件采用明确、
一致的优先规则,不能由 end descriptor 的排列决定终态。
`L-L0` 在动态时空中不假定单调:即使主步两端均低于阈值,也要处理步内的首次
upcrossing。阈值根按导数根分段,在局部 bracket 内确认并定位;不能用整个区间的
二分取代首次 crossing。所有候选的可信时间须在子积分后重新比较;当前不可分辨的
同时事件取 escape 优先。阈值是 `>=` 的闭事件,真正的步末 crossing 不能因根
合并到端点而丢失。无法确认事件时有界缩步,耗尽后报告积分失败,不回退为假成功。
`LOG_P0` 辅助监测策略保留 accepted-state 检查,不提供这一多项式事件定位能力;
生产 CLI 使用相机相对 `L-L0` 策略。
- lens-map 的积分 provenance 必须保存稳定的 stepper wire code、分组绝对/相对
容差、初始步长及上下限、拒步上限、初始计算/回溯配额与重试政策。render-only
replay 消费文件策略,不能由当前 CLI 默认值覆盖历史配置,也不从缺失字段推断
自适应积分来源。未知 stepper code 或无效配置须拒绝;只含固定 RK4 来源的旧文件
可按明确的固定步语义导入,但不能补成 DP54 结果。
- accepted、rejected 与实际 RHS 成本按持久 sample 身份统计;续追保存累计值,回填
替换而不是再次累加。off-mesh witness 与原地提升后的顶点是同一样本,不能重复
计费;事件子积分和失败 stage 的 RHS 也计入真实成本。渲染地图保存这些诊断值,
replay 的来源与成本统计应与原始 trace 一致。
---
@@ -873,11 +1363,18 @@ immutable 的 cache,不再逐帧做 tile 扫描、prefetch OpenMP 区域或打
multi-frame map 与 observer movie 走同一 union 路径。
movie PNG 编码/写盘由一个单 producer、单 writer 的有界队列(默认容量 2)承担,
与下一帧渲染重叠。producer 在 enqueue 前完成 sensor bloom、clean RGB8 与可选
mesh overlay RGB8 转换,job 只持有 8-bit buffer,HDR 在 submit 后即可释放。
与下一帧渲染重叠。producer 完成 sensor bloom、tone mapping 和 sRGB 转换,
生成 clean RGB8;可选诊断网格由去重边光栅化为预乘 alpha 的 sRGB RGBA8 层。
HDR 与临时线段由 producer 释放;job 独立拥有这两个输出 buffer,成功 enqueue
后 ownership 转交队列。writer 先写 clean 图,再以 source-over 原地合成诊断层、写出
mesh sibling,最后释放 job buffer。单帧、movie 与 lens-map replay 共用此合成规则。
writer 的首个错误持久保存,使后续 submit 立即失败;`finish()` drain 已接受 job
后 join writer,所有退出路径都必须 join,绝不为求重叠而提前打印 `Rendered ... ok`。
诊断网格是最终 mesh 的只读可视化:一像素抗锯齿边按端点终态分别着色相邻半边,
在中点切换颜色。合成位于 tone mapping 与 sRGB transfer 之后,clean 图与 HDR
保持独立。调色与透明度配置见 [`usage.md`](usage.md)。
fast mode 的单星精度由 deposit 模式与 `N` 决定:`nearest` 的格点间距是
每轴 `1/N` 个输出像素,单帧瞬时舍入误差至多是 `1/(2N)`;在
`--psf-fwhm-pixels` 不变时它与图像分辨率无关,只有增大 `N`(或使用保持
@@ -1425,7 +1922,9 @@ void rays_trace_generation(
- metric interpolation;
- spatial derivatives;
- 必要的 temporal derivatives;
- capture/infinity classification。
- metric/data 状态(成功、时间不足、空间域不足、invalid metric、内部错误);
- 渐近端声明与 escape worldtube 能力。物理暗终态由能量阈值 policy 判定,不由
位置分类决定。
---
@@ -1495,8 +1994,8 @@ map 共用同一入口。每个 RGB 通道独立、各向同性地把超过有
\(E\) 使用 exposure 之后的 renderer-scale 线性 HDR;\(e\) 同时决定每轮保留传播的
比例和有效传播距离;模型允许信号损失,不守恒。原始 `--hdr-output` FITS 在模型
运行前写出,因此始终是 bloom 前的 PSF HDR;tone-mapped PNG/PPM 与视频帧在模型
之后写出。视觉式多尺度 bloom 不在当前范围内。mesh overlay 在模型之后绘制,
不参与溢出传播。
之后写出。视觉式多尺度 bloom 不在当前范围内。mesh 诊断层在 tone mapping 与
sRGB transfer 后合成。
---
@@ -1529,7 +2028,7 @@ map 共用同一入口。每个 RGB 通道独立、各向同性地把超过有
验证:
- capture;
- 红移暗阈值截断与 shadow;
- Einstein ring;
- multiple images;
- adaptive refinement;
@@ -1557,7 +2056,7 @@ map 共用同一入口。每个 RGB 通道独立、各向同性地把超过有
\quad
g,
\quad
\text{captured/escaped classification}
\text{end/outcome classification}
\]
---
@@ -1586,7 +2085,7 @@ renderer 顶层架构原则上不应为 BBH 重新设计。
- time slab 最佳内存大小;
- BBH production node 数;
- 4D 输出总数据量;
- AH calibration 后 puncture cutoff;
- 相机相对能量阈值 `L-L0` 的默认值标定(当前 `L_dark=8`,可 CLI 覆盖);
- adaptive triangle refinement criterion;
- critical curve 附近最大 refinement level;
- Gaia 与 2MASS 的最终组合;
@@ -1597,7 +2096,9 @@ renderer 顶层架构原则上不应为 BBH 重新设计。
Reinhard 作为兼容模式保留;最终 production color management、传感器模型、
曝光标定和 HDR 视频编码规则仍未决定;
- 是否需要 diffuse Milky Way background;
- 是否将 ray redshift 变量定义为 `log(alpha p^0)` 或其他更方便的量。
- 阈值监测量当前统一采用相机相对增长 `L-L0`(对全部 backend 生效),不再使用
绝对 `L`、`ln(p^0)` 或 Killing 相对量;这些量均不得与真正的 infinity
`g=E_camera/E_source` 混同。
---
@@ -1635,8 +2136,8 @@ $e_{(0)}=u$ 同时满足自由落体方程;四加速度为零时费米–沃
DOP853 默认 rtol=$10^{-10}$、atol=$10^{-12}$,可配置并通过解析径向自由落体、
圆轨道和圆轨道平行输运的收敛回归验证。视界不终止相机;默认 $r=10^{-3}M$
只是可配置的奇点数值保护边界,不等于精确撞击奇点。它独立于光线的 $1.5M$
捕获 cutoff;该 cutoff 内的轨迹可输出,但当前 renderer 的光线会立即被捕获。
只是可配置的奇点数值保护边界,不等于精确撞击奇点。相机轨迹与光线终态解耦:
相机合法性与位置 cutoff 无关,光线正常暗终态由 §18 的能量阈值 policy 决定。
CSV 保持 21 列不变,记录 $\tau=k/\mathrm{fps}$ 与积分得到的真实坐标时间。
只保留不超过请求持续本征时或提前终止时刻的规则采样,包含 $\tau=0$。
+1161
View File
File diff suppressed because it is too large. Load diff
+96
View File
@@ -0,0 +1,96 @@
#ifndef ASYMPTOTIC_H
#define ASYMPTOTIC_H
#include "geodesic.h"
#include "spacetime.h"
/* Status codes for the common asymptotic-exterior module. Unsupported and
* exhausted are reported explicitly; callers must not turn them into a
* plausible-looking escape. */
typedef enum {
ASYMPTOTIC_INVALID = -1,
ASYMPTOTIC_OK = 0,
ASYMPTOTIC_UNSUPPORTED = 1,
ASYMPTOTIC_TIME_RANGE_EXHAUSTED = 2
} AsymptoticStatus;
/* Unified canonical photon state in the asymptotic reference frame. `w` is
* the unit past-propagation direction: along the renderer's backward
* integration the spatial position moves as x(s) = x0 + s w, s = t0 - t. */
typedef struct {
SpacetimeEndId end_id;
double t;
double x[3];
double w[3];
double log_alpha_p0;
} AsymptoticPhotonState;
typedef enum {
ASYMPTOTIC_ROUTE_INSIDE, /* camera in a worldtube: activate at the camera */
ASYMPTOTIC_ROUTE_ENTRY, /* camera outside, entry event produced */
ASYMPTOTIC_ROUTE_ESCAPED,
ASYMPTOTIC_ROUTE_TIME_RANGE_EXHAUSTED,
ASYMPTOTIC_ROUTE_INVALID
} AsymptoticRouteKind;
typedef struct {
AsymptoticRouteKind kind;
SpacetimeEndId end_id;
/* Activation state, backend coordinates, for INSIDE and ENTRY. */
double activate_t;
double x[3];
double Pi[3];
/* Current L at the activation event (camera when inside, entry event when
* externing). `log_alpha_p0_camera` is the reference L at the camera event
* used by the camera-relative dark threshold, and must be kept separate. */
double log_alpha_p0;
double log_alpha_p0_camera;
/* Terminal infinity endpoint for ESCAPED. */
double n_infinity[3];
double frequency_ratio;
/* Diagnostic reason for an ASYMPTOTIC_INVALID return: set specifically by the
* validation/fallback failure that refused the route, so the lifecycle can
* report the concrete cause instead of a generic preroute failure. NONE on
* success. */
RayReason failure_reason;
/* Nonzero when this route was produced by the generic bracketed first-entry
* localizer rather than a closed-form/fast entry solve. It records the
* number of evaluator calls the localizer spent; zero on the fast path. This
* is private in-memory provenance only and is not serialized. */
unsigned int entry_fallback_evaluations;
} AsymptoticRoute;
/* Pre-route one camera ray against every declared end's worldtube. */
AsymptoticStatus asymptotic_route_camera(const SpacetimeSource *source,
const ObserverState *observer,
const double direction[3],
AsymptoticRoute *route);
/* Directed inside->outside crossing helper for the geodesic lifecycle.
* Returns ASYMPTOTIC_OK, ASYMPTOTIC_TIME_RANGE_EXHAUSTED (the backend cannot
* describe the worldtube at this time), or ASYMPTOTIC_INVALID. */
int asymptotic_worldtube_value(const SpacetimeSource *source,
SpacetimeEndId end_id, double t,
const double x[3], double *value);
/* Finish an interior inside->outside crossing to an infinity endpoint.
* Returns ASYMPTOTIC_UNSUPPORTED for exterior models not implemented yet. */
AsymptoticStatus asymptotic_finish_escape(const SpacetimeSource *source,
SpacetimeEndId end_id, double t,
const double x[3], const double Pi[3],
double log_alpha_p0,
RayEndpoint *endpoint);
/* Canonical <-> backend bridge, valid only inside a supported exterior. */
int asymptotic_canonical_from_backend(const SpacetimeSource *source,
SpacetimeEndId end_id,
const MetricData *metric, double t,
const double x[3], const double Pi[3],
double log_alpha_p0,
AsymptoticPhotonState *out);
int asymptotic_backend_from_canonical(const SpacetimeSource *source,
const MetricData *metric,
const AsymptoticPhotonState *canonical,
double x[3], double Pi[3],
double *log_alpha_p0);
#endif
+326
View File
@@ -0,0 +1,326 @@
#include "asymptotic_entry.h"
#include <float.h>
#include <math.h>
#include <stddef.h>
/* See asymptotic_entry.h for the contract. This module deliberately keeps no
* global mutable state: every cache/scratch value lives on the stack of the
* calling trace, so it stays thread-safe under the coarse-grained OpenMP ray
* parallelism of the renderer. */
static void set_failure(RayReason *failure, RayReason reason) {
if (failure != NULL)
*failure = reason;
}
/* Left-to-right double accumulation, matching the geodesic event layer's
* worldtube F. */
static double dot3(const double a[3], const double b[3]) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
static int entry_state_finite(const AsymptoticRoute *state) {
if (!isfinite(state->activate_t) || !isfinite(state->log_alpha_p0) ||
!isfinite(state->log_alpha_p0_camera))
return 0;
for (int i = 0; i < 3; ++i)
if (!isfinite(state->x[i]) || !isfinite(state->Pi[i]))
return 0;
return 1;
}
AsymptoticStatus asymptotic_entry_geometry(const SpacetimeSource *source,
SpacetimeEndId end_id, double t,
const double x[3], double *F,
double *tol, RayReason *failure) {
set_failure(failure, RAY_REASON_PROTOCOL_ERROR);
if (source == NULL || x == NULL || F == NULL || tol == NULL) {
set_failure(failure, RAY_REASON_INVALID_ARGUMENT);
return ASYMPTOTIC_INVALID;
}
if (!isfinite(t)) {
set_failure(failure, RAY_REASON_WORLDTUBE_GEOMETRY_INVALID);
return ASYMPTOTIC_INVALID;
}
SpacetimeEscapeWorldtubeSample sample;
if (spacetime_escape_worldtube_sample(source, end_id, t, &sample)) {
set_failure(failure, RAY_REASON_WORLDTUBE_SAMPLE_FAILED);
return ASYMPTOTIC_INVALID;
}
if (!sample.valid) {
/* The backend cannot describe the worldtube at this time; this is history
* exhaustion, never a miss. */
set_failure(failure, RAY_REASON_TIME_RANGE_EXHAUSTED);
return ASYMPTOTIC_TIME_RANGE_EXHAUSTED;
}
if (!(sample.radius > 0.0) || !isfinite(sample.radius) ||
!isfinite(sample.radius_rate)) {
set_failure(failure, RAY_REASON_WORLDTUBE_GEOMETRY_INVALID);
return ASYMPTOTIC_INVALID;
}
for (int i = 0; i < 3; ++i) {
if (!isfinite(sample.center[i]) || !isfinite(sample.velocity[i])) {
set_failure(failure, RAY_REASON_WORLDTUBE_GEOMETRY_INVALID);
return ASYMPTOTIC_INVALID;
}
}
double d[3];
for (int i = 0; i < 3; ++i) {
d[i] = x[i] - sample.center[i];
if (!isfinite(d[i])) {
set_failure(failure, RAY_REASON_WORLDTUBE_GEOMETRY_INVALID);
return ASYMPTOTIC_INVALID;
}
}
/* Exact same grouping as the geodesic event layer: F uses
* dot3(d,d) - radius^2, while the geometric ULP band accumulates d2 with an
* explicit left-to-right loop. Keeping both identical means a candidate that
* passes this validator at the tolerance threshold is grouped exactly like
* the geodesic's own f_before. */
const double value = dot3(d, d) - sample.radius * sample.radius;
const double r2 = sample.radius * sample.radius;
double d2 = 0.0;
for (int i = 0; i < 3; ++i)
d2 += d[i] * d[i];
const double geometry_tol = 128.0 * DBL_EPSILON * fmax(r2, d2);
/* Overflow to inf and NaN propagation both land here. */
if (!isfinite(value) || !isfinite(geometry_tol)) {
set_failure(failure, RAY_REASON_WORLDTUBE_GEOMETRY_INVALID);
return ASYMPTOTIC_INVALID;
}
*F = value;
*tol = geometry_tol;
set_failure(failure, RAY_REASON_NONE);
return ASYMPTOTIC_OK;
}
AsymptoticStatus asymptotic_entry_validate(const SpacetimeSource *source,
SpacetimeEndId end_id,
const AsymptoticRoute *candidate,
int *valid, RayReason *failure) {
set_failure(failure, RAY_REASON_PROTOCOL_ERROR);
if (source == NULL || candidate == NULL || valid == NULL) {
set_failure(failure, RAY_REASON_INVALID_ARGUMENT);
return ASYMPTOTIC_INVALID;
}
*valid = 0;
if (candidate->kind != ASYMPTOTIC_ROUTE_ENTRY) {
set_failure(failure, RAY_REASON_PROTOCOL_ERROR);
return ASYMPTOTIC_INVALID;
}
if (!entry_state_finite(candidate)) {
set_failure(failure, RAY_REASON_PROTOCOL_ERROR);
return ASYMPTOTIC_INVALID;
}
double value = 0.0, tol = 0.0;
const AsymptoticStatus status = asymptotic_entry_geometry(
source, end_id, candidate->activate_t, candidate->x, &value, &tol,
failure);
if (status != ASYMPTOTIC_OK)
return status;
/* Boundary-near means within the geometric ULP band on either side. Outside
* and arbitrary-deep-inside are both non-candidates, not protocol errors. */
*valid = fabs(value) <= tol;
set_failure(failure, RAY_REASON_NONE);
return ASYMPTOTIC_OK;
}
/* Map an evaluator's own failure to a diagnostic reason. The worldtube
* callback/history/geometry reasons come from asymptotic_entry_geometry; this
* only covers the case where the evaluator itself refuses to produce a state. */
static AsymptoticStatus entry_evaluator_failure(AsymptoticStatus status,
RayReason *failure) {
switch (status) {
case ASYMPTOTIC_TIME_RANGE_EXHAUSTED:
set_failure(failure, RAY_REASON_TIME_RANGE_EXHAUSTED);
break;
case ASYMPTOTIC_UNSUPPORTED:
set_failure(failure, RAY_REASON_UNSUPPORTED);
break;
default:
/* The evaluator refused to produce a state, so the first entry is not
* confirmed. */
set_failure(failure, RAY_REASON_ENTRY_UNCONFIRMED);
break;
}
return status;
}
AsymptoticStatus asymptotic_entry_localize(
const SpacetimeSource *source, SpacetimeEndId end_id,
AsymptoticEntryEvaluator evaluate, void *context,
double outside_parameter, double inside_parameter, AsymptoticRoute *out,
unsigned int *evaluations, RayReason *failure) {
set_failure(failure, RAY_REASON_ENTRY_UNCONFIRMED);
if (evaluations != NULL)
*evaluations = 0;
if (source == NULL || evaluate == NULL || out == NULL) {
set_failure(failure, RAY_REASON_INVALID_ARGUMENT);
return ASYMPTOTIC_INVALID;
}
if (!isfinite(outside_parameter) || !isfinite(inside_parameter) ||
!(outside_parameter < inside_parameter)) {
set_failure(failure, RAY_REASON_INVALID_ARGUMENT);
return ASYMPTOTIC_INVALID;
}
unsigned int count = 0;
AsymptoticRoute lo_state, hi_state;
double f_lo = 0.0, f_hi = 0.0, tol_lo = 0.0, tol_hi = 0.0;
AsymptoticStatus status = evaluate(context, outside_parameter, &lo_state);
++count;
if (status != ASYMPTOTIC_OK) {
if (evaluations != NULL)
*evaluations = count;
return entry_evaluator_failure(status, failure);
}
if (!entry_state_finite(&lo_state)) {
if (evaluations != NULL)
*evaluations = count;
set_failure(failure, RAY_REASON_ENTRY_UNCONFIRMED);
return ASYMPTOTIC_INVALID;
}
status = asymptotic_entry_geometry(source, end_id, lo_state.activate_t,
lo_state.x, &f_lo, &tol_lo, failure);
if (status != ASYMPTOTIC_OK) {
if (evaluations != NULL)
*evaluations = count;
return status;
}
status = evaluate(context, inside_parameter, &hi_state);
++count;
if (status != ASYMPTOTIC_OK) {
if (evaluations != NULL)
*evaluations = count;
return entry_evaluator_failure(status, failure);
}
if (!entry_state_finite(&hi_state)) {
if (evaluations != NULL)
*evaluations = count;
set_failure(failure, RAY_REASON_ENTRY_UNCONFIRMED);
return ASYMPTOTIC_INVALID;
}
status = asymptotic_entry_geometry(source, end_id, hi_state.activate_t,
hi_state.x, &f_hi, &tol_hi, failure);
if (status != ASYMPTOTIC_OK) {
if (evaluations != NULL)
*evaluations = count;
return status;
}
if (!(f_lo >= 0.0) || !(f_hi < 0.0)) {
/* The caller owns the first-entry bracket; a bracket that does not straddle
* the boundary is an unconfirmed entry, never an escape. */
set_failure(failure, RAY_REASON_ENTRY_UNCONFIRMED);
if (evaluations != NULL)
*evaluations = count;
return ASYMPTOTIC_INVALID;
}
/* Parameter increases backward, so the inside end must not be later in
* coordinate time than the outside end. */
if (!(hi_state.activate_t <= lo_state.activate_t)) {
set_failure(failure, RAY_REASON_ENTRY_UNCONFIRMED);
if (evaluations != NULL)
*evaluations = count;
return ASYMPTOTIC_INVALID;
}
if (f_lo == 0.0) {
/* Exact boundary at the outside end with a strictly inside other end: this
* is the legitimate inward first-entry state already on the worldtube, so
* no bisection is needed. */
*out = lo_state;
out->kind = ASYMPTOTIC_ROUTE_ENTRY;
out->end_id = end_id;
if (evaluations != NULL)
*evaluations = count;
set_failure(failure, RAY_REASON_NONE);
return ASYMPTOTIC_OK;
}
double lo = outside_parameter;
double hi = inside_parameter;
unsigned int iteration = 0;
for (; iteration < ASYMPTOTIC_ENTRY_BISECTION_LIMIT; ++iteration) {
const double span = hi - lo;
const double mid = isfinite(span) ? lo + span * 0.5
: lo * 0.5 + hi * 0.5;
if (!(mid > lo && mid < hi))
break; /* Parameter midpoint cannot be represented; bracket is adjacent. */
AsymptoticRoute mid_state;
status = evaluate(context, mid, &mid_state);
++count;
if (status != ASYMPTOTIC_OK) {
if (evaluations != NULL)
*evaluations = count;
return entry_evaluator_failure(status, failure);
}
if (!entry_state_finite(&mid_state) ||
!(hi_state.activate_t <= mid_state.activate_t &&
mid_state.activate_t <= lo_state.activate_t)) {
if (evaluations != NULL)
*evaluations = count;
set_failure(failure, RAY_REASON_ENTRY_UNCONFIRMED);
return ASYMPTOTIC_INVALID;
}
double f_mid = 0.0, tol_mid = 0.0;
status = asymptotic_entry_geometry(source, end_id, mid_state.activate_t,
mid_state.x, &f_mid, &tol_mid, failure);
if (status != ASYMPTOTIC_OK) {
if (evaluations != NULL)
*evaluations = count;
return status;
}
if (f_mid >= 0.0) {
lo = mid;
lo_state = mid_state;
} else {
hi = mid;
hi_state = mid_state;
f_hi = f_mid;
tol_hi = tol_mid;
}
}
if (iteration >= ASYMPTOTIC_ENTRY_BISECTION_LIMIT) {
/* The parameter interval never contracted to adjacent doubles within the
* implementation guard; do not fabricate an entry. */
set_failure(failure, RAY_REASON_ENTRY_UNCONFIRMED);
if (evaluations != NULL)
*evaluations = count;
return ASYMPTOTIC_INVALID;
}
/* Final consistency: keep the strict-inside adjacent endpoint. Its negative
* residual need not fit the fast-path band at coarse coordinate resolution;
* the bracket, rather than a radial displacement, establishes the entry. */
if (!(f_hi < 0.0) ||
!(hi_state.activate_t <= lo_state.activate_t) ||
!isfinite(hi_state.activate_t) || !isfinite(hi_state.log_alpha_p0) ||
!isfinite(hi_state.log_alpha_p0_camera)) {
set_failure(failure, RAY_REASON_ENTRY_UNCONFIRMED);
if (evaluations != NULL)
*evaluations = count;
return ASYMPTOTIC_INVALID;
}
for (int i = 0; i < 3; ++i) {
if (!isfinite(hi_state.x[i]) || !isfinite(hi_state.Pi[i])) {
set_failure(failure, RAY_REASON_ENTRY_UNCONFIRMED);
if (evaluations != NULL)
*evaluations = count;
return ASYMPTOTIC_INVALID;
}
}
*out = hi_state;
out->kind = ASYMPTOTIC_ROUTE_ENTRY;
out->end_id = end_id;
if (evaluations != NULL)
*evaluations = count;
set_failure(failure, RAY_REASON_NONE);
return ASYMPTOTIC_OK;
}
+136
View File
@@ -0,0 +1,136 @@
#ifndef ASYMPTOTIC_ENTRY_H
#define ASYMPTOTIC_ENTRY_H
#include "asymptotic.h"
/* Backend-independent numerical entry localizer for the common asymptotic
* exterior.
*
* The camera pre-route must decide, without touching any backend metric outside
* a worldtube, whether a past-directed camera ray crosses an escape worldtube
* from outside to inside and where the FIRST such entry lies. A supported
* exterior model may provide a closed-form (quadratic/analytic) entry; when it
* cannot, the caller supplies a path-parameter bracket that is already known to
* straddle the first entry and this module refines it numerically.
*
* The module never evaluates a metric, never loads a slab, never sweeps the
* movie in reverse and never snaps a position onto a radial shell. It only
* consumes the worldtube `escape_worldtube_sample` callback through
* `asymptotic_entry_geometry`, and an opaque evaluator callback that
* repropagates the exact supported exterior geodesic from its camera state to a
* path parameter. The driver is therefore independent of the exterior model
* and does not solve the entry equation itself.
*
* Parameter and time convention (18A.5/18A.6): the evaluator parameter
* increases along the renderer's backward propagation; the returned state's
* `activate_t` is the coordinate time at that parameter, so it is
* nonincreasing as the parameter increases. `outside_parameter` is the
* smaller-parameter end (worldtube outside, or exactly on the boundary) and
* `inside_parameter` is the larger-parameter end that is strictly inside.
*
* The caller is responsible for providing a correct first-entry bracket. This
* driver does not search arbitrary samples for a crossing; a bracket that does
* not straddle the boundary is reported as an unconfirmed entry
* (RAY_REASON_ENTRY_UNCONFIRMED), never as an escape. */
typedef AsymptoticStatus (*AsymptoticEntryEvaluator)(void *context,
double parameter,
AsymptoticRoute *state);
/* Convenience bundle for callers that want to keep the callback and its opaque
* context together. Not required by any entry point. */
typedef struct {
AsymptoticEntryEvaluator evaluate;
void *context;
} AsymptoticEntryPropagator;
/* Hard implementation guard on the number of bisection refinements. This
* bounds the double-parameter bisection; it is not a physical parameter. Any
* bracket near a finite nonzero entry contracts in roughly 60 halvings;
* very wide exponent ranges may instead exhaust the guard explicitly. */
#define ASYMPTOTIC_ENTRY_BISECTION_LIMIT 256u
/* Evaluate the worldtube function
*
* F(t, x) = |x - center(t)|^2 - radius(t)^2
*
* at one state through the worldtube sample callback alone (no metric
* evaluation). `*F` is accumulated with the same left-to-right double
* arithmetic as the geodesic event layer, and `*tol` is the matching geometric
* ULP band 128 * DBL_EPSILON * max(radius^2, |x-center|^2).
*
* Status / failure reason:
* ASYMPTOTIC_OK -> *failure = RAY_REASON_NONE
* ASYMPTOTIC_TIME_RANGE_EXHAUSTED -> RAY_REASON_TIME_RANGE_EXHAUSTED
* ASYMPTOTIC_INVALID (callback failed)-> RAY_REASON_WORLDTUBE_SAMPLE_FAILED
* ASYMPTOTIC_INVALID (bad geometry) -> RAY_REASON_WORLDTUBE_GEOMETRY_INVALID
*
* A non-positive/non-finite radius, non-finite radius_rate, centers or
* velocities, and any non-finite (overflow/NaN) F or tolerance are geometry
* failures. `*failure` may be NULL. */
AsymptoticStatus asymptotic_entry_geometry(const SpacetimeSource *source,
SpacetimeEndId end_id, double t,
const double x[3], double *F,
double *tol, RayReason *failure);
/* Validate one caller-produced entry candidate. The candidate must be finite
* (activate_t, x, Pi, log_alpha_p0, log_alpha_p0_camera), carry kind
* ASYMPTOTIC_ROUTE_ENTRY, and sit
* on the worldtube boundary within the geometric ULP band:
*
* |F| <= tol -> *valid = 1
*
* A candidate outside the worldtube (F > tol) or arbitrarily deep inside
* (F < -tol) is NOT an entry candidate: it yields *valid = 0 but still returns
* ASYMPTOTIC_OK, because the caller owns the exterior solve and a non-candidate
* is not a protocol error. A wrong kind or non-finite state is a protocol
* error. Worldtube callback/history/geometry failures propagate with the same
* reasons as asymptotic_entry_geometry.
*
* This function does not fabricate a velocity or re-derive the entry: the
* caller already guarantees that its exterior solve produced an inward entry. */
AsymptoticStatus asymptotic_entry_validate(const SpacetimeSource *source,
SpacetimeEndId end_id,
const AsymptoticRoute *candidate,
int *valid, RayReason *failure);
/* Numerically locate the first outside -> inside entry inside a known bracket.
*
* `evaluate(context, parameter, state)` repropagates the exact exterior geodesic
* from its camera state to `parameter`; it must fill `state->activate_t` (the
* coordinate time at that parameter), `state->x`, `state->Pi`,
* `state->log_alpha_p0`, `state->log_alpha_p0_camera` and `state->end_id`.
* Every call is counted into `*evaluations` (may be NULL).
*
* The two bracket endpoints are evaluated first. The driver requires
* F(outside_parameter) >= 0 and F(inside_parameter) < 0; a bracket with an
* exact boundary at the outside end (F == 0) and a strictly inside other end is
* returned directly as the legitimate inward entry. Otherwise it bisects the
* parameter. Each midpoint reconstructs a fresh state through the evaluator
* (no propagation from a prior midpoint, so no accumulated rounding or
* projection error), keeps the low end outside (F >= 0) and the high end
* strictly inside (F < 0), and never turns an F >= 0 midpoint into an escape.
*
* Bisection stops only when the parameter midpoint can no longer be represented
* strictly between the two ends (adjacent doubles), not on an F tolerance, so a
* curved trajectory is not stopped early by a coarser coordinate-time
* resolution. On success the strictly-inside endpoint adjacent to the entry in
* path parameter is returned: its Pi/L/L_camera are copied from the evaluator
* state unchanged, with kind forced to ENTRY and end_id set to `end_id`. The
* result is a representable bracketing of the entry (the true entry lies
* between the final outside and inside endpoints), not an absolute positional
* error claim; the returned inside state may legitimately have F < -tol.
*
* `ASYMPTOTIC_ENTRY_BISECTION_LIMIT` is an implementation guard on parameter
* representability, not a physical parameter. If the bracket never contracts,
* if the endpoints do not straddle the boundary, or if the endpoint coordinate
* times are not ordered (inside time <= outside time), the result is
* ASYMPTOTIC_INVALID with RAY_REASON_ENTRY_UNCONFIRMED. Callback, history and
* geometry failures from the evaluator/geometry propagate their exact status
* and reason; nothing is silently reported as a successful entry or escape. */
AsymptoticStatus asymptotic_entry_localize(
const SpacetimeSource *source, SpacetimeEndId end_id,
AsymptoticEntryEvaluator evaluate, void *context,
double outside_parameter, double inside_parameter, AsymptoticRoute *out,
unsigned int *evaluations, RayReason *failure);
#endif
+42
View File
@@ -0,0 +1,42 @@
#ifndef ASYMPTOTIC_GL48_H
#define ASYMPTOTIC_GL48_H
/* 48-point Gauss-Legendre nodes and weights on [-1, 1], used only for the
* bounded residual of the Schwarzschild coordinate-time transfer. Generated
* with numpy.polynomial.legendre.leggauss(48); double precision. */
static const double gl48_nodes[48] = {
-0.99877100725242607, -0.99353017226635076, -0.98412458372282685,
-0.97059159254624727, -0.9529877031604308, -0.93138669070655433,
-0.90587913671556963, -0.87657202027424785, -0.84358826162439349,
-0.80706620402944262, -0.76715903251574036, -0.72403413092381463,
-0.67787237963266389, -0.6288673967765136, -0.57722472608397268,
-0.523160974722233, -0.46690290475095841, -0.40868648199071672,
-0.34875588629216075, -0.28736248735545555, -0.22476379039468905,
-0.16122235606889174, -0.097004699209462697, -0.032380170962869367,
0.032380170962869367, 0.097004699209462697, 0.16122235606889174,
0.22476379039468905, 0.28736248735545555, 0.34875588629216075,
0.40868648199071672, 0.46690290475095841, 0.523160974722233,
0.57722472608397268, 0.6288673967765136, 0.67787237963266389,
0.72403413092381463, 0.76715903251574036, 0.80706620402944262,
0.84358826162439349, 0.87657202027424785, 0.90587913671556963,
0.93138669070655433, 0.9529877031604308, 0.97059159254624727,
0.98412458372282685, 0.99353017226635076, 0.99877100725242607};
static const double gl48_weights[48] = {
0.0031533460523098418, 0.0073275539012758505, 0.011477234579234699,
0.015579315722943481, 0.019616160457356105, 0.023570760839324009,
0.027426509708357052, 0.031167227832798117, 0.034777222564770421,
0.038241351065830473, 0.041545082943464533, 0.044674560856694245,
0.04761665849249027, 0.050359035553854216, 0.052890189485193424,
0.05519950369998404, 0.057277292100402881, 0.059114839698395358,
0.060704439165893562, 0.062039423159892415, 0.063114192286253756,
0.06392423858464788, 0.06446616443594981, 0.064737696812683626,
0.064737696812683626, 0.06446616443594981, 0.06392423858464788,
0.063114192286253756, 0.062039423159892415, 0.060704439165893562,
0.059114839698395358, 0.057277292100402881, 0.05519950369998404,
0.052890189485193424, 0.050359035553854216, 0.04761665849249027,
0.044674560856694245, 0.041545082943464533, 0.038241351065830473,
0.034777222564770421, 0.031167227832798117, 0.027426509708357052,
0.023570760839324009, 0.019616160457356105, 0.015579315722943481,
0.011477234579234699, 0.0073275539012758505, 0.0031533460523098418};
#endif
+554
View File
@@ -0,0 +1,554 @@
#include "asymptotic_schwarzschild.h"
#include <float.h>
#include <math.h>
#include <stddef.h>
static const double kPi = 3.14159265358979323846;
static double dot3(const double a[3], const double b[3]) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
static void cross3(const double a[3], const double b[3], double out[3]) {
out[0] = a[1] * b[2] - a[2] * b[1];
out[1] = a[2] * b[0] - a[0] * b[2];
out[2] = a[0] * b[1] - a[1] * b[0];
}
static double normalize3(double v[3]) {
const double length = sqrt(dot3(v, v));
if (length > 0.0)
for (int i = 0; i < 3; ++i)
v[i] /= length;
return length;
}
/* ------------------------------------------------------------------------- *
* Complex arithmetic and Carlson R_F.
* ------------------------------------------------------------------------- */
typedef struct {
double re, im;
} cs;
static cs cs_add(cs a, cs b) { return (cs){a.re + b.re, a.im + b.im}; }
static cs cs_sub(cs a, cs b) { return (cs){a.re - b.re, a.im - b.im}; }
static cs cs_mul(cs a, cs b) {
return (cs){a.re * b.re - a.im * b.im, a.re * b.im + a.im * b.re};
}
static cs cs_scale(cs a, double s) { return (cs){a.re * s, a.im * s}; }
static double cs_abs(cs a) { return hypot(a.re, a.im); }
static cs cs_inv(cs z) {
const double d = z.re * z.re + z.im * z.im;
return (cs){z.re / d, -z.im / d};
}
static cs cs_sqrt(cs z) {
const double r = hypot(z.re, z.im);
double re = sqrt(0.5 * (r + fabs(z.re)));
double im = sqrt(0.5 * (r - fabs(z.re)));
if (z.re < 0.0) {
const double t = re;
re = im;
im = t;
}
if (z.im < 0.0)
im = -im;
return (cs){re, im};
}
static cs cs_cbrt(cs z) {
/* Cardano needs the real cube root of real arguments (disc > 0); the
* principal complex root is correct for the conjugate pair (disc < 0). */
if (z.im == 0.0)
return (cs){cbrt(z.re), 0.0};
const double r = hypot(z.re, z.im);
const double theta = atan2(z.im, z.re);
const double cr = cbrt(r);
return (cs){cr * cos(theta / 3.0), cr * sin(theta / 3.0)};
}
static cs rf_naive(cs x, cs y, cs z) {
for (int iteration = 0; iteration < 80; ++iteration) {
const cs sx = cs_sqrt(x), sy = cs_sqrt(y), sz = cs_sqrt(z);
const cs lambda =
cs_add(cs_add(cs_mul(sx, sy), cs_mul(sy, sz)), cs_mul(sz, sx));
x = cs_scale(cs_add(x, lambda), 0.25);
y = cs_scale(cs_add(y, lambda), 0.25);
z = cs_scale(cs_add(z, lambda), 0.25);
const cs a = cs_scale(cs_add(cs_add(x, y), z), 1.0 / 3.0);
const cs X = cs_sub((cs){1.0, 0.0}, cs_mul(x, cs_inv(a)));
const cs Y = cs_sub((cs){1.0, 0.0}, cs_mul(y, cs_inv(a)));
const cs Z = cs_sub((cs){1.0, 0.0}, cs_mul(z, cs_inv(a)));
if (fmax(fmax(cs_abs(X), cs_abs(Y)), cs_abs(Z)) < 1e-12) {
const cs e2 = cs_add(cs_add(cs_mul(X, Y), cs_mul(Y, Z)),
cs_mul(Z, X));
const cs e3 = cs_mul(cs_mul(X, Y), Z);
const cs series = cs_add(
cs_add((cs){1.0, 0.0}, cs_scale(cs_mul(e2, e3), -3.0 / 44.0)),
cs_add(cs_scale(cs_mul(e2, e2), 1.0 / 24.0),
cs_add(cs_scale(e2, -1.0 / 10.0), cs_scale(e3, 1.0 / 14.0))));
return cs_mul(series, cs_inv(cs_sqrt(a)));
}
}
return (cs){NAN, NAN};
}
/* R_F via Carlson duplication. The three-real-root branch is handled by the
* real Legendre form, so the only complex calls here come from the conjugate
* root pair, whose arguments are off the real axis and take the principal
* square-root branch consistently. */
static cs rf(cs x, cs y, cs z) { return rf_naive(x, y, z); }
/* Incomplete elliptic integral of the first kind with parameter m = k^2:
* F(phi,m) = sin(phi) R_F(cos^2 phi, 1 - m sin^2 phi, 1). */
static double ellipf(double phi, double m) {
const double s = sin(phi), c = cos(phi);
const cs r = rf((cs){c * c, 0.0}, (cs){1.0 - m * s * s, 0.0},
(cs){1.0, 0.0});
return s * r.re;
}
/* Leading-order estimate of phi for a candidate ordered real-root branch. */
static double phi_three_real(double u0, double A, double B, double C) {
if (!(u0 > A) || !(u0 < B) || !(A < B) || !(B < C))
return NAN;
const double sA = sqrt((0.0 - A) / (B - A));
const double s0 = sqrt((u0 - A) / (B - A));
const double m = (B - A) / (C - A);
return sqrt(2.0) / sqrt(C - A) * (ellipf(asin(s0), m) - ellipf(asin(sA), m));
}
/* Roots of 2 beta^2 u^3 - beta^2 u^2 + 1 = 0 through the depressed cubic
* w^3 + P w + Q = 0 with u = w + 1/6. */
static void cubic_roots(double beta, cs e[3]) {
const double b2 = beta * beta;
const double c = 1.0 / (2.0 * b2);
const double P = -1.0 / 12.0;
const double Q = c - 1.0 / 108.0;
const double halfQ = 0.5 * Q;
const cs disc = (cs){halfQ * halfQ + (P * P * P) / 27.0, 0.0};
const cs sq = cs_sqrt(disc);
const cs u1 = cs_cbrt(cs_add((cs){-halfQ, 0.0}, sq));
const cs u2 = cs_cbrt(cs_add((cs){-halfQ, 0.0}, cs_scale(sq, -1.0)));
const cs omega = (cs){cos(2.0 * kPi / 3.0), sin(2.0 * kPi / 3.0)};
const cs omega2 = cs_mul(omega, omega);
e[0] = cs_add(cs_add(u1, u2), (cs){1.0 / 6.0, 0.0});
e[1] = cs_add(cs_add(cs_mul(omega, u1), cs_mul(omega2, u2)),
(cs){1.0 / 6.0, 0.0});
e[2] = cs_add(cs_add(cs_mul(omega2, u1), cs_mul(omega, u2)),
(cs){1.0 / 6.0, 0.0});
/* Cardano loses relative accuracy in the near-double-root regime. Polish
* the real roots with Newton so the grazing turning root keeps full
* relative precision. */
for (int i = 0; i < 3; ++i) {
if (fabs(e[i].im) > 1e-9 * fmax(1.0, fabs(e[i].re)))
continue;
double u = e[i].re;
for (int step = 0; step < 20; ++step) {
const double p = 2.0 * b2 * u * u * u - b2 * u * u + 1.0;
const double dp = 6.0 * b2 * u * u - 2.0 * b2 * u;
if (dp == 0.0)
break;
const double du = p / dp;
u -= du;
if (fabs(du) <= 1e-18 * fmax(1.0, fabs(u)))
break;
}
e[i] = (cs){u, 0.0};
}
}
double asymptotic_schwarzschild_phi(double rho, double beta) {
if (!isfinite(rho) || rho <= 0.0 || !isfinite(beta) || beta < 0.0)
return NAN;
if (beta == 0.0)
return 0.0;
const double u0 = 1.0 / rho;
cs e[3];
cubic_roots(beta, e);
const double imag_tol = 1e-11 * fmax(1.0, fabs(e[0].re));
if (fabs(e[0].im) < imag_tol && fabs(e[1].im) < imag_tol &&
fabs(e[2].im) < imag_tol) {
/* Three real roots: use the real Legendre form, which is accurate up to
* and through the grazing limit. */
double r[3] = {e[0].re, e[1].re, e[2].re};
for (int i = 0; i < 2; ++i)
for (int j = i + 1; j < 3; ++j)
if (r[j] < r[i]) {
const double t = r[i];
r[i] = r[j];
r[j] = t;
}
const double real_value = phi_three_real(u0, r[0], r[1], r[2]);
if (isfinite(real_value))
return real_value;
}
const cs a = rf(cs_scale(e[0], -1.0), cs_scale(e[1], -1.0),
cs_scale(e[2], -1.0));
const cs b = rf(cs_sub((cs){u0, 0.0}, e[0]),
cs_sub((cs){u0, 0.0}, e[1]),
cs_sub((cs){u0, 0.0}, e[2]));
const cs s = cs_scale(cs_sub(a, b), 2.0);
/* The real integral requires a real S; a non-negligible imaginary part
* means the principal branch failed. Report it instead of silently using
* a wrong angle. */
if (!isfinite(s.re) || fabs(s.im) > 1e-6 * fmax(1.0, fabs(s.re)))
return NAN;
return fabs(s.re) / sqrt(2.0);
}
double asymptotic_schwarzschild_turning_rho(double beta) {
if (!isfinite(beta) || beta <= 3.0 * sqrt(3.0))
return INFINITY;
/* Larger positive root of f(rho) = rho^3 - beta^2 rho + 2 beta^2.
* f(3) = 27 - beta^2 < 0 and f(beta+2) > 0, and f is monotone on the
* bracket beyond its local minimum, so a bracketed bisection is safe. */
const double b2 = beta * beta;
double lo = 3.0, hi = beta + 2.0;
for (int iteration = 0; iteration < 200; ++iteration) {
const double mid = 0.5 * (lo + hi);
const double f = mid * mid * mid - b2 * mid + 2.0 * b2;
if (f < 0.0)
lo = mid;
else
hi = mid;
if (hi - lo <= 4.0 * DBL_EPSILON * hi)
break;
}
double rho = 0.5 * (lo + hi);
for (int step = 0; step < 20; ++step) {
const double f = rho * rho * rho - b2 * rho + 2.0 * b2;
const double fp = 3.0 * rho * rho - b2;
if (fp == 0.0)
break;
const double next = rho - f / fp;
if (!(next > lo && next < hi))
break;
rho = next;
}
return rho;
}
/* ------------------------------------------------------------------------- *
* Coordinate-time transfer (ingoing Kerr-Schild time, M = 1 units).
* ------------------------------------------------------------------------- */
#include "asymptotic_gl48.h"
static double sch_i2(double u_cam, double u_R, double beta) {
/* I2 = int_{u_cam}^{u_R} du / (1 + sqrt(P(u))). Substitute
* u = u_R - (u_R - u_cam) t^2 to remove the grazing branch point. */
const double span = u_R - u_cam;
if (!(span > 0.0))
return 0.0;
double sum = 0.0;
for (int i = 0; i < 48; ++i) {
const double t = 0.5 * (gl48_nodes[i] + 1.0);
const double u = u_R - span * t * t;
const double P = 1.0 - beta * beta * u * u + 2.0 * beta * beta * u * u * u;
const double f = 1.0 / (1.0 + sqrt(P));
sum += gl48_weights[i] * f * 2.0 * span * t;
}
return 0.5 * sum;
}
/* Positive coordinate time to travel outward from R to rho_cam. */
static double sch_time_transfer(double rho_cam, double rho_R, double beta) {
const double u_cam = 1.0 / rho_cam, u_R = 1.0 / rho_R;
const double dphi =
asymptotic_schwarzschild_phi(rho_R, beta) -
asymptotic_schwarzschild_phi(rho_cam, beta);
const double elementary =
(rho_cam - rho_R) + 4.0 * log(u_R / u_cam) -
4.0 * log((1.0 - 2.0 * u_R) / (1.0 - 2.0 * u_cam));
return elementary + (beta * dphi - beta * beta * sch_i2(u_cam, u_R, beta));
}
/* ------------------------------------------------------------------------- *
* Rotation and canonical <-> backend bridging.
* ------------------------------------------------------------------------- */
static void rotate_axis(const double v[3], const double axis[3], double angle,
double out[3]) {
const double c = cos(angle), s = sin(angle);
double cross[3];
cross3(axis, v, cross);
const double adotv = dot3(axis, v);
for (int i = 0; i < 3; ++i)
out[i] = v[i] * c + cross[i] * s + axis[i] * adotv * (1.0 - c);
}
/* The algebraic monopole formulas below assume the asymptotic frame axes are
* the backend Cartesian axes; a rotated frame would require rotating the
* momentum and the worldtube. */
static int sch_frame_is_aligned(const SpacetimeAsymptoticEnd *end) {
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j) {
const double expected = i == j ? 1.0 : 0.0;
if (fabs(end->frame_axes[i][j] - expected) > 1e-12)
return 0;
}
return 1;
}
int asymptotic_schwarzschild_canonical_from_state(
const SpacetimeAsymptoticEnd *end, const MetricData *metric, double t,
const double x[3], const double Pi[3], double log_alpha_p0,
SchwarzschildCanonical *out) {
if (end == NULL || metric == NULL || out == NULL || end->mass <= 0.0 ||
!sch_frame_is_aligned(end))
return -1;
double beta_dot_pi = 0.0;
for (int i = 0; i < 3; ++i)
beta_dot_pi += metric->beta[i] * Pi[i];
const double energy = exp(log_alpha_p0) * (metric->alpha - beta_dot_pi);
if (!isfinite(energy) || energy <= 0.0)
return -1;
double rel[3];
for (int i = 0; i < 3; ++i)
rel[i] = x[i] - end->frame_origin[i];
const double radius = sqrt(dot3(rel, rel));
if (!(radius > 0.0))
return -1;
double Lvec[3];
cross3(rel, Pi, Lvec);
const double Lmag = sqrt(dot3(Lvec, Lvec));
const double denom = metric->alpha - beta_dot_pi;
out->end_id = end->end_id;
out->t = t;
out->rho = radius / end->mass;
out->energy = energy;
for (int i = 0; i < 3; ++i)
out->rhat[i] = rel[i] / radius;
if (Lmag > 0.0) {
out->beta = (Lmag / denom) / end->mass;
for (int i = 0; i < 3; ++i)
out->Lhat[i] = Lvec[i] / Lmag;
} else {
out->beta = 0.0;
out->Lhat[0] = out->Lhat[1] = out->Lhat[2] = 0.0;
}
double inv[3][3];
const double det =
metric->gamma[0][0] * (metric->gamma[1][1] * metric->gamma[2][2] -
metric->gamma[1][2] * metric->gamma[2][1]) -
metric->gamma[0][1] * (metric->gamma[1][0] * metric->gamma[2][2] -
metric->gamma[1][2] * metric->gamma[2][0]) +
metric->gamma[0][2] * (metric->gamma[1][0] * metric->gamma[2][1] -
metric->gamma[1][1] * metric->gamma[2][0]);
inv[0][0] = (metric->gamma[1][1] * metric->gamma[2][2] -
metric->gamma[1][2] * metric->gamma[2][1]) / det;
inv[0][1] = (metric->gamma[0][2] * metric->gamma[2][1] -
metric->gamma[0][1] * metric->gamma[2][2]) / det;
inv[0][2] = (metric->gamma[0][1] * metric->gamma[1][2] -
metric->gamma[0][2] * metric->gamma[1][1]) / det;
inv[1][0] = (metric->gamma[1][2] * metric->gamma[2][0] -
metric->gamma[1][0] * metric->gamma[2][2]) / det;
inv[1][1] = (metric->gamma[0][0] * metric->gamma[2][2] -
metric->gamma[0][2] * metric->gamma[2][0]) / det;
inv[1][2] = (metric->gamma[0][2] * metric->gamma[1][0] -
metric->gamma[0][0] * metric->gamma[1][2]) / det;
inv[2][0] = (metric->gamma[1][0] * metric->gamma[2][1] -
metric->gamma[1][1] * metric->gamma[2][0]) / det;
inv[2][1] = (metric->gamma[0][1] * metric->gamma[2][0] -
metric->gamma[0][0] * metric->gamma[2][1]) / det;
inv[2][2] = (metric->gamma[0][0] * metric->gamma[1][1] -
metric->gamma[0][1] * metric->gamma[1][0]) / det;
double dxdt[3];
for (int i = 0; i < 3; ++i) {
dxdt[i] = -metric->beta[i];
for (int j = 0; j < 3; ++j)
dxdt[i] += metric->alpha * inv[i][j] * Pi[j];
}
double radial = 0.0;
for (int i = 0; i < 3; ++i)
radial += -dxdt[i] * out->rhat[i];
out->radial_sign = radial > 0.0 ? 1 : (radial < 0.0 ? -1 : 0);
return 0;
}
int asymptotic_schwarzschild_state_from_canonical(
const SpacetimeAsymptoticEnd *end, const SchwarzschildCanonical *c,
double x[3], double Pi[3], double *log_alpha_p0) {
if (end == NULL || c == NULL || x == NULL || Pi == NULL ||
!sch_frame_is_aligned(end))
return -1;
const double rho = c->rho;
if (!(rho > 2.0))
return -1;
const double Q = 1.0 - c->beta * c->beta * (1.0 - 2.0 / rho) / (rho * rho);
if (!(Q >= 0.0))
return -1;
const double sqrtQ = sqrt(Q);
double e_phi[3] = {0.0, 0.0, 0.0};
if (c->beta > 0.0)
cross3(c->Lhat, c->rhat, e_phi);
const double s_aff = -(double)c->radial_sign; /* physical (future) radial */
const double kr = s_aff * c->energy * sqrtQ;
const double ktang = c->beta * c->energy / rho;
double kvec[3];
for (int i = 0; i < 3; ++i)
kvec[i] = kr * c->rhat[i] + ktang * e_phi[i];
const double kt_s = c->energy / (1.0 - 2.0 / rho);
const double kt_ks = kt_s + (2.0 / (rho - 2.0)) * kr;
const double alpha = 1.0 / sqrt(1.0 + 2.0 / rho);
const double ak0 = alpha * kt_ks;
if (!isfinite(ak0) || ak0 <= 0.0)
return -1;
for (int i = 0; i < 3; ++i) {
x[i] = end->frame_origin[i] + end->mass * rho * c->rhat[i];
const double kcov = kvec[i] + (2.0 / rho) * c->rhat[i] * (kr + kt_ks);
Pi[i] = kcov / ak0;
}
if (log_alpha_p0 != NULL)
*log_alpha_p0 = log(ak0);
return 0;
}
int asymptotic_schwarzschild_inward_state_at_radius(
const SpacetimeAsymptoticEnd *end, const SchwarzschildCanonical *camera,
double rho, double x[3], double Pi[3], double *log_alpha_p0,
double *activate_t) {
if (end == NULL || camera == NULL || x == NULL || Pi == NULL)
return -1;
if (!(rho > 2.0) || !(rho <= camera->rho))
return -1;
/* The inward branch only exists while the orbit has not turned before the
* requested radius. Allow a few ULP at the grazing limit so rounding in the
* turning root does not reject a legitimate boundary radius; a genuine
* inside-the-turning radius still fails through the Q >= 0 check below. */
const double rho_turn = asymptotic_schwarzschild_turning_rho(camera->beta);
if (isfinite(rho_turn) &&
rho < rho_turn - 16.0 * DBL_EPSILON * fmax(1.0, rho_turn))
return -1;
const double dphi = asymptotic_schwarzschild_phi(rho, camera->beta) -
asymptotic_schwarzschild_phi(camera->rho, camera->beta);
if (!isfinite(dphi))
return -1;
double rhat_rho[3];
if (camera->beta > 0.0)
rotate_axis(camera->rhat, camera->Lhat, -dphi, rhat_rho);
else
for (int i = 0; i < 3; ++i)
rhat_rho[i] = camera->rhat[i];
SchwarzschildCanonical state = *camera;
state.rho = rho;
for (int i = 0; i < 3; ++i)
state.rhat[i] = rhat_rho[i];
state.radial_sign = -1;
if (asymptotic_schwarzschild_state_from_canonical(end, &state, x, Pi,
log_alpha_p0))
return -1;
if (activate_t != NULL) {
const double T = sch_time_transfer(camera->rho, rho, camera->beta);
*activate_t = camera->t - end->mass * T;
}
return 0;
}
int asymptotic_schwarzschild_finish(const SpacetimeAsymptoticEnd *end,
const SchwarzschildCanonical *canonical,
double n_infinity[3],
double *frequency_ratio) {
if (end == NULL || canonical == NULL || n_infinity == NULL)
return -1;
const double phi = asymptotic_schwarzschild_phi(canonical->rho,
canonical->beta);
if (!isfinite(phi))
return -1;
if (canonical->beta > 0.0) {
double e_phi[3];
cross3(canonical->Lhat, canonical->rhat, e_phi);
for (int i = 0; i < 3; ++i)
n_infinity[i] = cos(phi) * canonical->rhat[i] -
sin(phi) * e_phi[i];
} else {
const double s = canonical->radial_sign >= 0 ? 1.0 : -1.0;
for (int i = 0; i < 3; ++i)
n_infinity[i] = s * canonical->rhat[i];
}
normalize3(n_infinity);
if (frequency_ratio != NULL)
*frequency_ratio = 1.0 / canonical->energy;
return 0;
}
int asymptotic_schwarzschild_preroute(
const SpacetimeAsymptoticEnd *end, double worldtube_radius,
const SchwarzschildCanonical *camera, SchwarzschildRouteKind *kind,
double *activate_t, double x[3], double Pi[3], double *log_alpha_p0,
double n_infinity[3], double *frequency_ratio) {
if (end == NULL || camera == NULL || kind == NULL)
return -1;
const double R = worldtube_radius;
if (!(R > 2.0) || !(camera->rho >= R))
return -1;
if (R / 1.0 < 64.0) {
*kind = SCH_ROUTE_UNSUPPORTED;
return 0;
}
const double beta_R = R / sqrt(1.0 - 2.0 / R);
if (camera->radial_sign >= 0) {
/* Past propagation is outward or tangent: no entry, immediate infinity
* endpoint. (radial_sign == 0 means the camera is on the boundary with a
* tangent ray.) */
const double phi = asymptotic_schwarzschild_phi(camera->rho, camera->beta);
if (!isfinite(phi)) {
*kind = SCH_ROUTE_UNSUPPORTED;
return 0;
}
if (camera->beta > 0.0) {
double e_phi[3];
cross3(camera->Lhat, camera->rhat, e_phi);
for (int i = 0; i < 3; ++i)
n_infinity[i] = cos(phi) * camera->rhat[i] - sin(phi) * e_phi[i];
} else {
for (int i = 0; i < 3; ++i)
n_infinity[i] = camera->rhat[i];
}
normalize3(n_infinity);
*frequency_ratio = 1.0 / camera->energy;
*kind = SCH_ROUTE_ESCAPED;
return 0;
}
if (camera->beta < beta_R) {
/* Shared exact inward transfer, also used by the common entry fallback. */
if (asymptotic_schwarzschild_inward_state_at_radius(
end, camera, R, x, Pi, log_alpha_p0, activate_t))
return -1;
*kind = SCH_ROUTE_ENTRY;
return 0;
}
/* Inward but misses: turn before R and escape. */
const double rho_turn = asymptotic_schwarzschild_turning_rho(camera->beta);
if (!isfinite(rho_turn) || rho_turn > camera->rho) {
*kind = SCH_ROUTE_UNSUPPORTED;
return 0;
}
const double total =
2.0 * asymptotic_schwarzschild_phi(rho_turn, camera->beta) -
asymptotic_schwarzschild_phi(camera->rho, camera->beta);
if (!isfinite(total)) {
*kind = SCH_ROUTE_UNSUPPORTED;
return 0;
}
if (camera->beta > 0.0) {
double e_phi[3];
cross3(camera->Lhat, camera->rhat, e_phi);
for (int i = 0; i < 3; ++i)
n_infinity[i] = cos(total) * camera->rhat[i] - sin(total) * e_phi[i];
} else {
for (int i = 0; i < 3; ++i)
n_infinity[i] = camera->rhat[i];
}
normalize3(n_infinity);
*frequency_ratio = 1.0 / camera->energy;
*kind = SCH_ROUTE_ESCAPED;
return 0;
}
+79
View File
@@ -0,0 +1,79 @@
#ifndef ASYMPTOTIC_SCHWARZSCHILD_H
#define ASYMPTOTIC_SCHWARZSCHILD_H
#include "geodesic.h"
#include "spacetime.h"
/* Canonical photon state for a fixed, concentric Schwarzschild monopole
* exterior. All radial quantities are in units of the mass: rho = r / M.
* `Lhat` is the (unit) conserved angular-momentum direction = normalize(x x
* Pi); `beta` is the impact parameter b/M > 0. `radial_sign` is the sign of
* dr/ds along the renderer's past propagation (s = t_camera - t): +1 outward
* into the past, -1 inward into the past. `energy` is E = -p_t with the
* camera normalization E_camera = 1. */
typedef struct {
SpacetimeEndId end_id;
double t;
double rho;
double rhat[3];
double Lhat[3];
double beta;
double energy;
int radial_sign;
} SchwarzschildCanonical;
/* Angular primitive Phi(rho, beta): the azimuth swept on the outward branch
* from radius rho to infinity. Returns NAN outside the supported domain. */
double asymptotic_schwarzschild_phi(double rho, double beta);
/* Larger positive turning radius for the given impact parameter, or INFINITY
* when no turning point exists (beta <= 3 sqrt(3)). */
double asymptotic_schwarzschild_turning_rho(double beta);
/* Convert a backend state into the canonical form. `metric` must be the
* Schwarzschild Kerr-Schild metric at (t, x). */
int asymptotic_schwarzschild_canonical_from_state(
const SpacetimeAsymptoticEnd *end, const MetricData *metric, double t,
const double x[3], const double Pi[3], double log_alpha_p0,
SchwarzschildCanonical *out);
/* Rebuild the backend state at the stored radius / radial directions. */
int asymptotic_schwarzschild_state_from_canonical(
const SpacetimeAsymptoticEnd *end, const SchwarzschildCanonical *canonical,
double x[3], double Pi[3], double *log_alpha_p0);
/* Repropagate the exact inward orbit from the ORIGINAL camera canonical state
* to `rho >= turning_rho` (and > 2) by recomputing the swept angle and the
* coordinate-time integral -- never by projecting a nearby state. Fills the
* backend entry state, its local L = ln(alpha p^0), and the activation
* coordinate time. Returns -1 when `rho` lies outside the reachable inward
* domain. This is the radius-parameter evaluator used by the common first
* entry localizer; `R` may dip slightly below the worldtube radius because the
* common driver only needs a strictly-inside bracket. */
int asymptotic_schwarzschild_inward_state_at_radius(
const SpacetimeAsymptoticEnd *end, const SchwarzschildCanonical *camera,
double rho, double x[3], double Pi[3], double *log_alpha_p0,
double *activate_t);
/* Infinity endpoint for an outward crossing at the canonical radius. */
int asymptotic_schwarzschild_finish(const SpacetimeAsymptoticEnd *end,
const SchwarzschildCanonical *canonical,
double n_infinity[3],
double *frequency_ratio);
/* Pre-route a camera ray outside the worldtube. Fills one of the route
* kinds. `worldtube_radius` is R/M. */
typedef enum {
SCH_ROUTE_ENTRY,
SCH_ROUTE_ESCAPED,
SCH_ROUTE_TIME_RANGE_EXHAUSTED,
SCH_ROUTE_UNSUPPORTED
} SchwarzschildRouteKind;
int asymptotic_schwarzschild_preroute(
const SpacetimeAsymptoticEnd *end, double worldtube_radius,
const SchwarzschildCanonical *camera, SchwarzschildRouteKind *kind,
double *activate_t, double x[3], double Pi[3], double *log_alpha_p0,
double n_infinity[3], double *frequency_ratio);
#endif
+7 -7
View File
@@ -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");
}
+740 -164
View File
File diff suppressed because it is too large. Load diff
+131 -4
View File
@@ -8,20 +8,62 @@
#include "spacetime.h"
#include <stddef.h>
#include <stdint.h>
typedef struct {
double image_x, image_y;
double camera_direction[3];
double n_infinity[3];
double log_frequency_ratio;
RayEndpointStatus status;
RayOutcome outcome;
RayReason reason;
/* Asymptotic end this escaped vertex belongs to; a triangle must not
* interpolate across two different ends. */
SpacetimeEndId end_id;
int traced;
/* Retry continuation, valid when outcome == RAY_OUTCOME_UNRESOLVED: resume
* from this last accepted state instead of replaying the ray. */
double continuation_t;
double continuation_x[3];
double continuation_Pi[3];
double continuation_log_alpha_p0;
double continuation_log_alpha_p0_0;
unsigned int continuation_steps;
unsigned int continuation_limit;
/* Adaptive (DP54) resume control/cost state, valid with the fields above when
* outcome == RAY_OUTCOME_UNRESOLVED. An integration start time of zero is a
* legal coordinate time; the legacy RK4 path leaves these zero. */
double continuation_integration_start_time;
double continuation_next_step;
unsigned int continuation_rejected_steps;
unsigned long continuation_rhs_evaluations;
unsigned int continuation_previous_rejected;
/* Granted total coordinate-time lookback budget accumulated for this
* continuation. Zero when the legacy RK4 path supplies no time quota. */
double continuation_lookback_limit;
/* Persistent off-mesh probe witness; also participates in completion checks.
* When set, probe_edge holds the sorted edge (a,b) this witness samples. */
int diagnostic_probe;
size_t probe_edge[2];
/* Actual integration cost for this persistent vertex, copied from the
* endpoint for every outcome (not only UNRESOLVED). The endpoint already
* carries cumulative accepted/rejected/RHS counts for a resumed trace, so
* each install overwrites rather than accumulates and a witness promoted in
* place keeps one physical sample counted exactly once. `uint64_t` with the
* saturated flag records a counter that had to be capped instead of wrapping. */
uint64_t trace_accepted_steps;
uint64_t trace_rejected_steps;
uint64_t trace_rhs_evaluations;
int trace_cost_saturated;
} LensVertex;
typedef struct {
size_t vertex[3];
unsigned int level;
int evaluated;
/* Set when a boundary triangle containing UNRESOLVED vertices was blackened
* as a finite-resolution approximation rather than resolved. */
int approx_black;
} LensTriangle;
/* Per-frame staged wall-clock breakdown for one movie frame. All fields are
@@ -51,20 +93,74 @@ typedef struct {
double jacobian_minimum;
double min_edge_pixels;
double min_area_pixels2;
/* Retry budget for UNRESOLVED vertices. retry_step_increment == 0 disables
* retry. max_total_steps is the per-ray hard cap on accepted steps; when a
* UUU / escape-containing triangle reaches it, the frame is reported as
* budget-incomplete instead of silently blackened. */
unsigned int retry_step_increment;
unsigned int max_total_steps;
/* Independent coordinate-time retry budget. The step and time quotas are
* saturated separately and a retry is requested only while a quota that
* actually blocked the ray can still grow. Both zero on a DP trace derive
* from trace->max_lookback_time; a zero increment with a positive cap keeps
* the time budget fixed. Ignored by the legacy RK4 path. */
double retry_lookback_increment;
double max_total_lookback_time;
} RefinementConfig;
typedef enum {
FRAME_SAMPLE_VERTEX,
FRAME_SAMPLE_PROBE
FRAME_SAMPLE_PROBE,
FRAME_SAMPLE_RETRY
} FrameSampleKind;
typedef struct {
FrameSampleKind kind;
size_t vertex_id;
size_t edge_vertex[2];
/* For FRAME_SAMPLE_RETRY: the new total accepted-step budget. */
unsigned int step_limit;
/* For FRAME_SAMPLE_RETRY: the new total coordinate-time lookback budget;
* zero keeps the trace config budget. */
double lookback_limit;
int cached;
LensVertex vertex;
} FrameSample;
/* E/D/U triangle accounting for one finalized mesh. Counts use the
* rendering categories: E=ESCAPED, D=DARK, U=UNRESOLVED; triangles
* containing an INCOMPLETE vertex are counted as errors and are never
* blackened. */
typedef struct {
size_t escaped_only; /* EEE */
size_t dark_only; /* DDD */
size_t eed_edd; /* EED / EDD boundary */
size_t uud_udd; /* UUD / UDD */
size_t u_with_escape; /* UEE / UED / UUE */
size_t uuu; /* UUU */
size_t error; /* any INCOMPLETE vertex */
size_t approx_black_triangles;
double approx_black_area_pixels2;
double approx_black_max_edge_pixels;
double approx_black_max_area_pixels2;
size_t approx_black_level_stops;
size_t retry_requests;
size_t budget_incomplete_triangles;
} FrameBoundaryStats;
/* Truthful integration cost aggregated over a finalized mesh. Every
* persistent vertex (including promoted probe witnesses) is counted exactly
* once, so summing after refinement does not double-count retries. Counters
* saturate at UINT64_MAX and increment `saturated_vertices` instead of
* wrapping. `vertices` is the number of traced vertices the cost covers. */
typedef struct {
uint64_t accepted_steps;
uint64_t rejected_steps;
uint64_t rhs_evaluations;
size_t vertices;
size_t saturated_vertices;
} FrameTraceStats;
typedef struct {
LensVertex *vertices;
LensTriangle *triangles;
@@ -77,6 +173,18 @@ typedef struct {
int samples_include_probes;
size_t *probe_slots;
size_t probe_slot_capacity;
/* Cumulative count of retry rays requested across all generations. */
size_t retry_requests;
/* Persistent off-mesh probe witnesses, keyed by their edge (sorted vertex
* ids). Slot value is witness_vertex_id + 1; 0 is empty. Witnesses are
* promoted in place to a formal midpoint when their edge is later split, so
* one physical sample always has one stable vertex id. */
size_t *witness_slots;
size_t witness_slot_capacity;
/* Compact list of live witness vertex ids for rehashing and accounting. */
size_t *witness_vertices;
size_t witness_count, witness_capacity;
size_t diagnostic_probe_count;
} FrameLensMesh;
typedef enum {
@@ -125,6 +233,12 @@ const FrameSample *frame_lens_mesh_samples(const FrameLensMesh *mesh,
size_t *count);
int frame_lens_mesh_install_sample(FrameLensMesh *mesh, size_t sample_id,
const RayEndpoint *endpoint);
/* Rebuild the full adaptive resume state recorded in a vertex's continuation
* fields into `state`. Returns 0 on success and -1 when the vertex has no
* valid resume payload. The frame refinement loop and the movie scheduler
* both use this so neither can silently drop a control field. */
int frame_vertex_continuation_state(const LensVertex *vertex,
GeodesicRayState *state);
/* Applies a nonempty completed generation; callers skip frames for which
* prepare_generation returned zero. Returns the number of newly added
* vertices, zero when no topology change was made, or -1 on failure. */
@@ -140,6 +254,21 @@ int frame_lens_mesh_refine_with_progress(
const ObserverState *observer, const GeodesicTraceConfig *trace,
const RefinementConfig *config, FrameRefinementProgressCallback callback,
void *context);
/* Recompute per-triangle approximate-black provenance and E/D/U accounting
* from the finalized mesh. Call after refinement has converged. Leaves the
* shared UNRESOLVED vertices untouched. */
void frame_lens_mesh_boundary_stats(FrameLensMesh *mesh,
const RefinementConfig *config,
FrameBoundaryStats *stats);
/* Aggregate the per-vertex integration cost of a finalized mesh. Unlike
* boundary_stats this does not modify the mesh and needs no config. */
void frame_lens_mesh_trace_stats(const FrameLensMesh *mesh,
FrameTraceStats *stats);
/* Fill retry defaults (derived from the trace step budget) when the caller
* did not configure them explicitly. A zero max_total_steps disables retry
* only if retry_step_increment is also zero. */
void frame_retry_config_defaults(RefinementConfig *config,
const GeodesicTraceConfig *trace);
/* Whether frame_splat_catalog() should run the per-frame catalog prefetch or
* rely on a movie-level union prefetch that already completed. */
@@ -174,8 +303,6 @@ size_t frame_splat_catalog(const FrameLensMesh *mesh,
const FrameSplatProgress *progress,
FastPsfAccumulator *fast,
MovieFrameTiming *timing);
void frame_draw_mesh(const FrameLensMesh *mesh, double *hdr, int width,
int height, double gray, double opacity);
void frame_lens_mesh_destroy(FrameLensMesh *mesh);
#endif
+2153 -74
View File
File diff suppressed because it is too large. Load diff
+211 -12
View File
@@ -4,28 +4,199 @@
#include "observer.h"
#include "spacetime.h"
/* Rendering/completion category. This is deliberately separate from the
* diagnostic reason below, and from the ray-pool lifecycle. */
typedef enum {
RAY_ENDPOINT_ESCAPED,
RAY_ENDPOINT_CAPTURED,
RAY_ENDPOINT_MAX_STEPS,
RAY_ENDPOINT_INTEGRATION_FAILURE
} RayEndpointStatus;
RAY_OUTCOME_ESCAPED = 0, /* reached an infinity end; carries a payload */
RAY_OUTCOME_DARK, /* normal dark terminal (currently redshift limit) */
RAY_OUTCOME_UNRESOLVED, /* trustworthy trajectory, compute budget exhausted */
RAY_OUTCOME_INCOMPLETE /* history/domain/metric/integration/protocol error */
} RayOutcome;
/* Diagnostic reason. Different DARK reasons must not create a mesh seam; the
* reason is for accounting and provenance only.
*
* Wire compatibility: the original coarse reasons keep their frozen numeric
* codes 0..9 exactly. Every appended detail reason therefore has a larger
* numeric value, and an older reader that validates `reason > RAY_REASON_IO_ERROR`
* safely rejects a map produced with a detail reason instead of silently
* reinterpreting it. A current reader accepts the frozen 0..9 codes and the
* appended detail codes, and rejects anything at or above RAY_REASON_COUNT.
* RAY_REASON_COUNT is a sentinel/count, never a serialized wire value. */
typedef enum {
RAY_REASON_NONE = 0,
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,
/* ---- Appended detail reasons (ids > RAY_REASON_IO_ERROR). ---- */
/* Protocol-derived. */
RAY_REASON_INVALID_ARGUMENT, /* null/nonunit/bad left bound */
RAY_REASON_UNKNOWN_STEPPER, /* unrecognized stepper code */
RAY_REASON_INVALID_STEPPER_CONFIG, /* stepper config rejected */
RAY_REASON_ASYMPTOTIC_LIFECYCLE_INVALID, /* declared ends inconsistent */
RAY_REASON_CAMERA_PREROUTE_FAILED, /* preroute returned INVALID */
RAY_REASON_INVALID_ROUTE_KIND, /* unknown pre-route kind */
RAY_REASON_INVALID_CONTINUATION, /* retry without valid payload */
RAY_REASON_METRIC_INTERNAL_ERROR, /* SPACETIME_POINT_INTERNAL_ERROR */
RAY_REASON_END_DESCRIPTOR_FAILED, /* end descriptor retrieval failed */
RAY_REASON_WORLDTUBE_SAMPLE_FAILED, /* worldtube sample callback failed */
RAY_REASON_WORLDTUBE_GEOMETRY_INVALID, /* nonfinite/nonpositive radius/rate */
RAY_REASON_WORLDTUBE_EVALUATION_FAILED, /* worldtube value eval invalid */
RAY_REASON_OUTSIDE_WORLDTUBE, /* single-end pre-step F > geom_tol */
RAY_REASON_ESCAPE_LOCALIZATION_FAILED, /* crossing localizer invalid */
RAY_REASON_ESCAPE_TRANSFER_FAILED, /* finish_escape returned invalid */
RAY_REASON_INVALID_ESCAPE_DIRECTION, /* legacy sky normalize invalid */
/* Integration-derived. */
RAY_REASON_INVALID_STEP_INTERVAL, /* remaining/proposal nonpositive */
RAY_REASON_TIME_STEP_UNREPRESENTABLE, /* target >= t0 after rounding */
RAY_REASON_MIN_STEP_REACHED, /* actual_h < min_step */
RAY_REASON_REJECTION_LIMIT, /* error-estimate rejections exhausted */
RAY_REASON_NONFINITE_TRIAL, /* nonfinite trial hit reject quota */
RAY_REASON_SUBINTEGRATION_TARGET_INVALID,/* subintegrate target in future */
RAY_REASON_SUBINTEGRATION_TARGET_MISSED, /* subintegr. discrepancy > snap */
RAY_REASON_ESCAPE_EVENT_UNCONFIRMED, /* escape event retry exhausted */
RAY_REASON_THRESHOLD_EVENT_UNCONFIRMED, /* threshold event retry exhausted */
/* I/O-derived. */
RAY_REASON_SLAB_LOAD_FAILED, /* spacetime_load_slab failed */
RAY_REASON_ENTRY_UNCONFIRMED, /* no trustworthy first-entry bracket */
RAY_REASON_COUNT /* sentinel: valid ids are < COUNT */
} 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);
/* Nonzero when `reason` is one of the valid wire/provenance codes
* (0 <= reason < RAY_REASON_COUNT). Pure and thread-safe. */
int ray_reason_valid(RayReason reason);
/* Coarse category of a reason, for callers that only need the historical
* bucket. The frozen coarse reasons 0..9 map to themselves; each appended
* detail reason maps onto one of the existing coarse categories
* (PROTOCOL_ERROR / INTEGRATION_ERROR / UNSUPPORTED / IO_ERROR).
* ESCAPE_LOCALIZATION_FAILED historically surfaced as PROTOCOL_ERROR (the RK4
* crossing localizer collapsed to ASYMPTOTIC_INVALID at its caller) and
* INVALID_ESCAPE_DIRECTION historically surfaced as INTEGRATION_ERROR.
* Out-of-range values return the documented sentinel RAY_REASON_COUNT. Pure
* and thread-safe. */
RayReason ray_reason_category(RayReason reason);
/* Monitored quantity used by the dark-redshift termination policy. `LOG_P0`
* 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
* frequency ratio g. */
typedef enum {
THRESHOLD_DISABLED = 0,
THRESHOLD_LOG_ALPHA_P0, /* absolute L = ln(alpha p^0) */
THRESHOLD_LOG_P0, /* ln(p^0) = L - ln(alpha) */
THRESHOLD_LOG_ENERGY_GROWTH /* L - L0, local energy growth since the start */
} ThresholdKind;
typedef struct {
ThresholdKind kind;
double value; /* terminate when the monitored quantity reaches this */
unsigned int policy_version;
} ThresholdPolicy;
typedef struct {
double n_infinity[3];
double frequency_ratio; /* E_camera / E_infinity */
double magnification; /* Filled by the future local inverse lens map. */
RayEndpointStatus status;
/* End this escape belongs to; SPACETIME_END_NONE when not applicable. */
SpacetimeEndId end_id;
RayOutcome outcome;
RayReason reason;
/* Last trusted state at termination. For an ESCAPED endpoint this is the
* (finite) numerical truncation position, not the true parameter end. */
double stop_coordinate_time;
unsigned int accepted_steps; /* accepted (spent) steps */
/* Actual total accepted-step budget the trace was configured with, kept
* distinct from the spent `accepted_steps`. Production endpoints always
* carry it so a retry layer never has to infer a grant from the spent count;
* zero only on legacy/synthetic endpoints. For a resumed trace this is the
* new total grant, not the additional steps. */
unsigned int accepted_step_limit;
/* Last trusted continuous state, used to resume an UNRESOLVED ray from its
* last accepted step instead of replaying it from the camera. */
double final_x[3];
double final_Pi[3];
double final_log_alpha_p0;
double final_log_alpha_p0_0; /* original reference L0 for retries */
/* Monitored threshold value at termination, NAN when not applicable. */
double threshold_value;
/* Adaptive (DP54) controller state and cumulative cost at termination,
* copied from the last trusted ray state so a retry resumes without
* replaying. The controller fields below (integration window, suggested
* step, previous-rejection flag, rejected-step count) are not applicable to
* RK4 and stay zero there. `rhs_evaluations` is different: it is the real
* cumulative metric-evaluation cost for every stepper, RK4 included. */
double integration_start_time; /* adaptive window start (entry/activation) */
double next_step; /* positive trial-step magnitude, 0 if unset */
unsigned int rejected_steps; /* cumulative rejected trials (DP54 only) */
unsigned long rhs_evaluations; /* cumulative actual RHS evaluations */
unsigned int previous_rejected;/* 1 when the last trial was rejected */
/* Coordinate-time lookback budget actually granted to this trace, recorded
* so the frame/movie retry layer can accumulate the per-sample total
* independently of the accepted-step budget. Zero for the legacy RK4 path
* and for endpoints that never reached a trusted adaptive state. This is a
* resource quota, not part of the continuous resume state. */
double lookback_limit;
} RayEndpoint;
/* ODE stepper selection for the past-directed geodesic integrator. RK4 is
* the legacy fixed-step path and is value 0 so that zero-initialized configs
* keep their historical behavior. DP54 is the explicit adaptive
* Dormand-Prince 5(4) stepper; both paths share the same termination,
* lookback-budget and retry-control interfaces. */
typedef enum {
GEODESIC_STEPPER_RK4 = 0,
GEODESIC_STEPPER_DP54 = 1
} GeodesicStepper;
typedef struct {
double coordinate_time_step;
unsigned int max_steps;
/* A positive value terminates a backwards ray whose horizon redshift has
* made log(alpha p^0) reach this value. Zero disables this analytic/demo
* criterion; numerical moving-puncture backends use their AH-calibrated
* spatial cutoff instead. */
double capture_log_alpha_p0;
/* Normal dark terminal for every backend. No backend may substitute a
* position/horizon cutoff for physical capture. */
ThresholdPolicy threshold;
/* Stepper selection (see GeodesicStepper). */
GeodesicStepper stepper;
/* DP54 tolerances. When stepper == GEODESIC_STEPPER_DP54 all four must be
* finite and strictly positive; they are never silently defaulted. Error
* scales: position uses atol_x + rtol*max(|dx|, |h*dxdt|) so a distant
* coordinate origin cannot relax the local length scale; Pi uses
* atol_Pi + rtol*max(|Pi_before|, |Pi_candidate|); L uses
* atol_L + rtol*max(1, |dL|). */
double atol_x;
double atol_Pi;
double atol_L;
double rtol;
/* DP54 trial-step magnitude bounds in coordinate time. Both finite and
* 0 < min_step <= max_step. The initial trial step and the driver
* suggestion reuse coordinate_time_step. */
double min_step;
double max_step;
/* DP54 maximum number of consecutive rejected trials before reporting an
* integration error. Must be nonzero. */
unsigned int consecutive_rejection_limit;
/* DP54 explicit coordinate-time lookback budget (length, finite and
* strictly positive). DP54 never derives its history from the legacy
* coordinate_time_step * max_steps product: reaching
* integration_start_time - max_lookback_time while the trajectory is still
* trustworthy is UNRESOLVED/BUDGET_EXHAUSTED, distinct from a source slab
* data time failure (TIME_RANGE_EXHAUSTED). */
double max_lookback_time;
} GeodesicTraceConfig;
typedef struct {
@@ -33,7 +204,22 @@ typedef struct {
double x[3];
double Pi[3];
double log_alpha_p0;
unsigned int steps;
/* Reference L at the start of this ray's integration, carried unchanged
* through retries so THRESHOLD_LOG_ENERGY_GROWTH stays camera-relative. */
double log_alpha_p0_0;
unsigned int steps; /* accepted steps (all steppers) */
/* Adaptive DP54 controller state; the controller fields default to zero and
* are ignored by the RK4 path. `integration_start_time` is the inner
* activation/entry time (not the L0 camera energy); `next_step` is a
* positive trial-step magnitude and is initialized from
* config->coordinate_time_step on the first adaptive advance.
* `rhs_evaluations` is the real cumulative RHS cost for every stepper;
* `rejected_steps`/`previous_rejected` remain DP54-only. */
double integration_start_time;
double next_step;
unsigned int rejected_steps; /* cumulative rejected trials (DP54 only) */
unsigned long rhs_evaluations; /* cumulative actual RHS evaluations */
unsigned int previous_rejected;/* 1 when the last trial was rejected */
} GeodesicRayState;
typedef enum {
@@ -52,8 +238,21 @@ int geodesic_initialize_past_ray(const MetricSlab *slab,
const ObserverState *observer,
const double camera_direction[3],
GeodesicRayState *state);
/* Metric-based core of the initialization above; used by the asymptotic
* pre-route, which evaluates the metric at the camera event directly. */
int geodesic_initialize_past_ray_metric(const MetricData *metric,
const ObserverState *observer,
const double camera_direction[3],
GeodesicRayState *state);
GeodesicAdvanceResult geodesic_advance_past_ray(
const MetricSlab *slab, GeodesicRayState *state,
double slab_left_time, const GeodesicTraceConfig *config,
RayEndpoint *endpoint);
/* Resume a past ray from its last trusted state and integrate to the total
* step budget in `config->max_steps` (state->steps counts steps already
* consumed). Used to retry UNRESOLVED rays without replaying them from the
* camera. */
RayEndpoint geodesic_trace_past_from_state(const SpacetimeSource *source,
const GeodesicRayState *state,
const GeodesicTraceConfig *config);
#endif
+2 -2
View File
@@ -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;
+185 -26
View File
@@ -10,7 +10,16 @@
/* All scalar fields are explicitly little-endian; never serialize C structs
* because their padding and size_t width are ABI-dependent. */
static const unsigned char lens_map_magic[8] = {'G', 'R', 'L', 'E', 'N', 'S', 1, 0};
enum { LENS_MAP_VERSION = 1, LENS_MAP_ENDIAN = 0x01020304u };
/* Version 2 stores the two-level RayOutcome instead of the removed
* RayEndpointStatus. Version 3 appends the adaptive DP54 policy and the
* per-vertex integration cost; it still supports v2 import as a legacy
* fixed-step RK4 map. Version 1 is rejected: its old captured bit cannot be
* upgraded into the new dark/unresolved/error provenance. */
enum {
LENS_MAP_VERSION = 3,
LENS_MAP_VERSION_LEGACY = 2,
LENS_MAP_ENDIAN = 0x01020304u
};
static uint32_t crc32_update(uint32_t crc, const void *data, size_t size) {
const unsigned char *bytes = data;
@@ -60,15 +69,67 @@ static int unit_vector(const double v[3]) {
const double n2 = v[0]*v[0] + v[1]*v[1] + v[2]*v[2];
return isfinite(n2) && fabs(n2 - 1.0) <= 1e-9;
}
/* Explicit switch over the stable wire codes; unknown codes are rejected
* instead of being reinterpreted as a C enum value. */
static int integrator_code_known(uint32_t code) {
switch ((GeodesicStepper)code) {
case GEODESIC_STEPPER_RK4:
case GEODESIC_STEPPER_DP54:
return 1;
default:
return 0;
}
}
/* Shared read/write schema. It deliberately does not validate mesh contents
* (valid_mesh does that per frame): it only proves the provenance is a
* complete, self-consistent policy. RK4 maps must state that no adaptive
* configuration applies; DP54 maps must carry it in full. */
static int provenance_valid(const LensMapProvenance *p) {
if (p == NULL || !isfinite(p->threshold_value) ||
p->threshold_kind > THRESHOLD_LOG_ENERGY_GROWTH ||
!isfinite(p->min_edge_pixels) || p->min_edge_pixels < 0.0 ||
!isfinite(p->min_area_pixels2) || p->min_area_pixels2 < 0.0 ||
!isfinite(p->coordinate_time_step) || p->coordinate_time_step <= 0.0 ||
!integrator_code_known(p->integrator))
return 0;
if (p->integrator == (uint32_t)GEODESIC_STEPPER_RK4) {
return p->atol_x == 0.0 && p->atol_Pi == 0.0 && p->atol_L == 0.0 &&
p->rtol == 0.0 && p->min_step == 0.0 && p->max_step == 0.0 &&
p->max_lookback_time == 0.0 && p->retry_lookback_increment == 0.0 &&
p->max_total_lookback_time == 0.0 &&
p->max_consecutive_rejections == 0;
}
/* DP54. */
return p->initial_max_steps != 0 && isfinite(p->atol_x) && p->atol_x > 0.0 &&
isfinite(p->atol_Pi) && p->atol_Pi > 0.0 &&
isfinite(p->atol_L) && p->atol_L > 0.0 && isfinite(p->rtol) &&
p->rtol > 0.0 && isfinite(p->min_step) && p->min_step > 0.0 &&
isfinite(p->max_step) && p->max_step > 0.0 &&
p->min_step <= p->max_step &&
p->coordinate_time_step >= p->min_step &&
p->coordinate_time_step <= p->max_step &&
isfinite(p->max_lookback_time) && p->max_lookback_time > 0.0 &&
p->max_consecutive_rejections != 0 &&
isfinite(p->retry_lookback_increment) &&
p->retry_lookback_increment >= 0.0 &&
isfinite(p->max_total_lookback_time) &&
p->max_total_lookback_time >= p->max_lookback_time;
}
static int valid_mesh(const FrameLensMesh *m) {
if (m == NULL || m->vertex_count == 0 || m->triangle_count == 0) return 0;
for (size_t i = 0; i < m->vertex_count; ++i) {
const LensVertex *v = &m->vertices[i];
if (!v->traced || v->status < RAY_ENDPOINT_ESCAPED ||
v->status > RAY_ENDPOINT_INTEGRATION_FAILURE || !isfinite(v->image_x) ||
if (!v->traced || v->outcome > RAY_OUTCOME_INCOMPLETE ||
!ray_reason_valid(v->reason) || !isfinite(v->image_x) ||
!isfinite(v->image_y) || !isfinite(v->log_frequency_ratio) ||
!unit_vector(v->camera_direction) ||
(v->status == RAY_ENDPOINT_ESCAPED && !unit_vector(v->n_infinity))) return 0;
!unit_vector(v->camera_direction))
return 0;
if (v->outcome == RAY_OUTCOME_ESCAPED &&
(!unit_vector(v->n_infinity) || v->end_id == SPACETIME_END_NONE))
return 0;
}
for (size_t i = 0; i < m->triangle_count; ++i)
for (int j = 0; j < 3; ++j)
@@ -77,34 +138,65 @@ static int valid_mesh(const FrameLensMesh *m) {
}
int lens_map_write(const char *path, int width, int height, double fov,
const LensMapProvenance *provenance,
const LensMapFrame *frames, size_t frame_count) {
if (path == NULL || frames == NULL || width <= 0 || height <= 0 ||
!isfinite(fov) || fov <= 0.0 || fov >= 179.0 || frame_count == 0 ||
frame_count > UINT64_MAX) return -1;
if (path == NULL || provenance == NULL || frames == NULL || width <= 0 ||
height <= 0 || !isfinite(fov) || fov <= 0.0 || fov >= 179.0 ||
frame_count == 0 || frame_count > UINT64_MAX ||
!provenance_valid(provenance))
return -1;
for (size_t i = 0; i < frame_count; ++i) if (!valid_mesh(&frames[i].mesh)) return -1;
FILE *file = fopen(path, "wb"); if (file == NULL) return -1;
int failed = write_bytes(file, lens_map_magic, sizeof lens_map_magic, NULL) ||
write_u32(file, LENS_MAP_VERSION, NULL) || write_u32(file, LENS_MAP_ENDIAN, NULL) ||
write_u32(file, (uint32_t)width, NULL) || write_u32(file, (uint32_t)height, NULL) ||
write_double(file, fov, NULL) || write_u64(file, (uint64_t)frame_count, NULL);
write_double(file, fov, NULL) || write_u64(file, (uint64_t)frame_count, NULL) ||
write_u32(file, provenance->threshold_kind, NULL) ||
write_u32(file, provenance->threshold_policy_version, NULL) ||
write_double(file, provenance->threshold_value, NULL) ||
write_u32(file, provenance->retry_step_increment, NULL) ||
write_u32(file, provenance->max_total_steps, NULL) ||
write_u32(file, provenance->max_level, NULL) ||
write_u32(file, provenance->integrator, NULL) ||
write_double(file, provenance->min_edge_pixels, NULL) ||
write_double(file, provenance->min_area_pixels2, NULL) ||
write_double(file, provenance->coordinate_time_step, NULL) ||
write_u32(file, provenance->initial_max_steps, NULL) ||
write_double(file, provenance->atol_x, NULL) ||
write_double(file, provenance->atol_Pi, NULL) ||
write_double(file, provenance->atol_L, NULL) ||
write_double(file, provenance->rtol, NULL) ||
write_double(file, provenance->min_step, NULL) ||
write_double(file, provenance->max_step, NULL) ||
write_double(file, provenance->max_lookback_time, NULL) ||
write_double(file, provenance->retry_lookback_increment, NULL) ||
write_double(file, provenance->max_total_lookback_time, NULL) ||
write_u32(file, provenance->max_consecutive_rejections, NULL);
for (size_t f = 0; !failed && f < frame_count; ++f) {
const FrameLensMesh *m = &frames[f].mesh; uint32_t crc = UINT32_MAX;
failed = write_u64(file, frames[f].frame_id, NULL) ||
write_double(file, frames[f].coordinate_time, NULL) ||
write_double(file, frames[f].proper_time, NULL) ||
write_u64(file, (uint64_t)m->vertex_count, NULL) ||
write_u64(file, (uint64_t)m->triangle_count, NULL);
write_u64(file, (uint64_t)m->triangle_count, NULL) ||
write_u64(file, (uint64_t)m->retry_requests, NULL);
for (size_t i = 0; !failed && i < m->vertex_count; ++i) {
const LensVertex *v = &m->vertices[i];
failed = write_double(file, v->image_x, &crc) || write_double(file, v->image_y, &crc);
for (int j = 0; !failed && j < 3; ++j) failed = write_double(file, v->camera_direction[j], &crc);
for (int j = 0; !failed && j < 3; ++j) failed = write_double(file, v->n_infinity[j], &crc);
failed = failed || write_double(file, v->log_frequency_ratio, &crc) ||
write_u32(file, (uint32_t)v->status, &crc);
write_u32(file, (uint32_t)v->end_id, &crc) ||
write_u32(file, (uint32_t)v->outcome, &crc) ||
write_u32(file, (uint32_t)v->reason, &crc) ||
write_u64(file, v->trace_accepted_steps, &crc) ||
write_u64(file, v->trace_rejected_steps, &crc) ||
write_u64(file, v->trace_rhs_evaluations, &crc);
}
for (size_t i = 0; !failed && i < m->triangle_count; ++i) {
for (int j = 0; j < 3; ++j) failed = failed || write_u64(file, m->triangles[i].vertex[j], &crc);
failed = failed || write_u32(file, m->triangles[i].level, &crc);
failed = failed || write_u32(file, m->triangles[i].level, &crc) ||
write_u32(file, (uint32_t)m->triangles[i].approx_black, &crc);
}
failed = failed || write_u32(file, crc ^ UINT32_MAX, NULL);
}
@@ -118,41 +210,103 @@ void lens_map_destroy(LensMap *map) {
free(map->frames); *map = (LensMap){0};
}
int lens_map_read(const char *path, LensMap *map) {
int lens_map_read(const char *path, LensMapProvenance *provenance,
LensMap *map) {
if (path == NULL || map == NULL) return -1;
*map = (LensMap){0}; FILE *file = fopen(path, "rb"); if (file == NULL) return -1;
unsigned char magic[8]; uint32_t version, endian, width, height; uint64_t count;
int failed = read_bytes(file, magic, sizeof magic, NULL) || memcmp(magic, lens_map_magic, sizeof magic) ||
LensMapProvenance prov = {0};
int failed = read_bytes(file, magic, sizeof magic, NULL) ||
memcmp(magic, lens_map_magic, sizeof magic) ||
read_u32(file, &version, NULL) || read_u32(file, &endian, NULL) ||
read_u32(file, &width, NULL) || read_u32(file, &height, NULL) ||
read_double(file, &map->horizontal_fov_deg, NULL) || read_u64(file, &count, NULL) ||
version != LENS_MAP_VERSION || endian != LENS_MAP_ENDIAN || width == 0 || height == 0 ||
width > INT32_MAX || height > INT32_MAX || !isfinite(map->horizontal_fov_deg) ||
map->horizontal_fov_deg <= 0.0 || map->horizontal_fov_deg >= 179.0 || count == 0 ||
count > SIZE_MAX / sizeof *map->frames;
read_double(file, &map->horizontal_fov_deg, NULL) || read_u64(file, &count, NULL);
/* Branch on the version before reading any version-specific header field, so
* a v2 file is never parsed with the v3 layout. */
if (failed)
goto done;
if (version != LENS_MAP_VERSION && version != LENS_MAP_VERSION_LEGACY) {
failed = 1;
goto done;
}
failed = read_u32(file, &prov.threshold_kind, NULL) ||
read_u32(file, &prov.threshold_policy_version, NULL) ||
read_double(file, &prov.threshold_value, NULL) ||
read_u32(file, &prov.retry_step_increment, NULL) ||
read_u32(file, &prov.max_total_steps, NULL) ||
read_u32(file, &prov.max_level, NULL) ||
read_u32(file, &prov.integrator, NULL) ||
read_double(file, &prov.min_edge_pixels, NULL) ||
read_double(file, &prov.min_area_pixels2, NULL) ||
read_double(file, &prov.coordinate_time_step, NULL) ||
read_u32(file, &prov.initial_max_steps, NULL);
if (!failed && version == LENS_MAP_VERSION) {
failed = read_double(file, &prov.atol_x, NULL) ||
read_double(file, &prov.atol_Pi, NULL) ||
read_double(file, &prov.atol_L, NULL) ||
read_double(file, &prov.rtol, NULL) ||
read_double(file, &prov.min_step, NULL) ||
read_double(file, &prov.max_step, NULL) ||
read_double(file, &prov.max_lookback_time, NULL) ||
read_double(file, &prov.retry_lookback_increment, NULL) ||
read_double(file, &prov.max_total_lookback_time, NULL) ||
read_u32(file, &prov.max_consecutive_rejections, NULL);
}
/* Legacy v2 carried no adaptive policy. Its missing fields stay zero, and
* its integrator must be RK4: a v2 DP54 claim is impossible and rejected. */
if (!failed && version == LENS_MAP_VERSION_LEGACY &&
prov.integrator != (uint32_t)GEODESIC_STEPPER_RK4)
failed = 1;
if (!failed &&
(endian != LENS_MAP_ENDIAN || width == 0 || height == 0 ||
width > INT32_MAX || height > INT32_MAX ||
!isfinite(map->horizontal_fov_deg) ||
map->horizontal_fov_deg <= 0.0 || map->horizontal_fov_deg >= 179.0 ||
count == 0 || count > SIZE_MAX / sizeof *map->frames ||
!provenance_valid(&prov)))
failed = 1;
if (failed) goto done;
map->provenance = prov;
map->file_version = version;
if (provenance != NULL)
*provenance = prov;
map->width = (int)width; map->height = (int)height; map->frame_count = (size_t)count;
map->frames = calloc(map->frame_count, sizeof *map->frames); if (map->frames == NULL) { failed = 1; goto done; }
for (size_t f = 0; !failed && f < map->frame_count; ++f) {
LensMapFrame *frame = &map->frames[f]; uint64_t vertices, triangles; uint32_t stored_crc, crc = UINT32_MAX;
LensMapFrame *frame = &map->frames[f]; uint64_t vertices, triangles, retry_requests;
uint32_t stored_crc, crc = UINT32_MAX;
failed = read_u64(file, &frame->frame_id, NULL) || read_double(file, &frame->coordinate_time, NULL) ||
read_double(file, &frame->proper_time, NULL) || read_u64(file, &vertices, NULL) || read_u64(file, &triangles, NULL) ||
read_u64(file, &retry_requests, NULL) ||
!isfinite(frame->coordinate_time) || !isfinite(frame->proper_time) || vertices == 0 || triangles == 0 ||
vertices > SIZE_MAX / sizeof *frame->mesh.vertices || triangles > SIZE_MAX / sizeof *frame->mesh.triangles;
vertices > SIZE_MAX / sizeof *frame->mesh.vertices || triangles > SIZE_MAX / sizeof *frame->mesh.triangles ||
retry_requests > SIZE_MAX;
if (failed) break;
frame->mesh.vertices = calloc((size_t)vertices, sizeof *frame->mesh.vertices);
frame->mesh.triangles = calloc((size_t)triangles, sizeof *frame->mesh.triangles);
if (frame->mesh.vertices == NULL || frame->mesh.triangles == NULL) { failed = 1; break; }
frame->mesh.vertex_count = frame->mesh.vertex_capacity = (size_t)vertices;
frame->mesh.triangle_count = frame->mesh.triangle_capacity = (size_t)triangles;
frame->mesh.retry_requests = (size_t)retry_requests;
for (size_t i = 0; !failed && i < frame->mesh.vertex_count; ++i) {
LensVertex *v = &frame->mesh.vertices[i]; uint32_t status;
LensVertex *v = &frame->mesh.vertices[i]; uint32_t end_id, outcome, reason;
failed = read_double(file, &v->image_x, &crc) || read_double(file, &v->image_y, &crc);
for (int j = 0; !failed && j < 3; ++j) failed = read_double(file, &v->camera_direction[j], &crc);
for (int j = 0; !failed && j < 3; ++j) failed = read_double(file, &v->n_infinity[j], &crc);
failed = failed || read_double(file, &v->log_frequency_ratio, &crc) || read_u32(file, &status, &crc) ||
status > RAY_ENDPOINT_INTEGRATION_FAILURE;
v->status = (RayEndpointStatus)status; v->traced = 1;
failed = failed || read_double(file, &v->log_frequency_ratio, &crc) ||
read_u32(file, &end_id, &crc) || read_u32(file, &outcome, &crc) ||
read_u32(file, &reason, &crc) || outcome > RAY_OUTCOME_INCOMPLETE ||
!ray_reason_valid((RayReason)reason);
if (failed) break;
v->end_id = (SpacetimeEndId)end_id;
v->outcome = (RayOutcome)outcome;
v->reason = (RayReason)reason;
v->traced = 1;
if (version == LENS_MAP_VERSION) {
failed = failed || read_u64(file, &v->trace_accepted_steps, &crc) ||
read_u64(file, &v->trace_rejected_steps, &crc) ||
read_u64(file, &v->trace_rhs_evaluations, &crc);
}
}
for (size_t i = 0; !failed && i < frame->mesh.triangle_count; ++i) {
for (int j = 0; j < 3; ++j) {
@@ -163,7 +317,12 @@ int lens_map_read(const char *path, LensMap *map) {
}
frame->mesh.triangles[i].vertex[j] = (size_t)index;
}
failed = failed || read_u32(file, &frame->mesh.triangles[i].level, &crc); frame->mesh.triangles[i].evaluated = 1;
uint32_t approx_black = 0;
failed = failed ||
read_u32(file, &frame->mesh.triangles[i].level, &crc) ||
read_u32(file, &approx_black, &crc);
frame->mesh.triangles[i].approx_black = approx_black != 0;
frame->mesh.triangles[i].evaluated = 1;
}
failed = failed || read_u32(file, &stored_crc, NULL) || stored_crc != (crc ^ UINT32_MAX) || !valid_mesh(&frame->mesh);
}
+50 -3
View File
@@ -16,17 +16,64 @@ typedef struct {
FrameLensMesh mesh;
} LensMapFrame;
/* File-level provenance. Stored explicitly so a replay can be attributed to
* the terminal policy and integration settings that produced it. */
typedef struct {
uint32_t threshold_kind; /* ThresholdKind */
uint32_t threshold_policy_version;
double threshold_value;
uint32_t retry_step_increment;
uint32_t max_total_steps;
uint32_t max_level;
/* Stable wire code: 0 = fixed-step RK4, 1 = adaptive Dormand-Prince 5(4).
* Never serialize the C enum directly; unknown codes are rejected. */
uint32_t integrator;
double min_edge_pixels;
double min_area_pixels2;
/* Integration source: the coordinate-time step and the initial per-ray
* accepted-step budget used for the first trace. */
double coordinate_time_step;
uint32_t initial_max_steps;
/* Adaptive DP54 configuration. For an RK4 map every field here is exactly
* zero, which makes "not applicable" explicit on the wire instead of
* implying a silently defaulted stepper. For a DP54 map the tolerances,
* step bounds and explicit lookback budget are finite and strictly positive,
* min_step <= coordinate_time_step <= max_step, and the consecutive
* rejection limit is nonzero. */
double atol_x;
double atol_Pi;
double atol_L;
double rtol;
double min_step;
double max_step;
double max_lookback_time;
/* Retry resource policy: coordinate-time increment per retry and the total
* lookback cap. `retry_lookback_increment` may be zero (fixed time budget);
* a production DP54 map requires max_total_lookback_time >=
* max_lookback_time (the initial grant). Both zero on an RK4 map. */
double retry_lookback_increment;
double max_total_lookback_time;
uint32_t max_consecutive_rejections;
} LensMapProvenance;
typedef struct {
int width, height;
double horizontal_fov_deg;
LensMapProvenance provenance;
/* On-disk format version actually read (2 for legacy RK4, 3 for the current
* adaptive-aware format). 0 when the struct was not produced by a read.
* A v2 map carries no per-vertex integration cost, so those replay as zero;
* the unknown cost is explicit through this version rather than implied. */
uint32_t file_version;
LensMapFrame *frames;
size_t frame_count;
} LensMap;
int lens_map_write(const char *path, int width, int height,
double horizontal_fov_deg, const LensMapFrame *frames,
size_t frame_count);
int lens_map_read(const char *path, LensMap *map);
double horizontal_fov_deg,
const LensMapProvenance *provenance,
const LensMapFrame *frames, size_t frame_count);
int lens_map_read(const char *path, LensMapProvenance *provenance, LensMap *map);
void lens_map_destroy(LensMap *map);
#endif
+1508 -190
View File
File diff suppressed because it is too large. Load diff
+506
View File
@@ -0,0 +1,506 @@
#include "mesh_overlay.h"
#include <limits.h>
#include <math.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
/* ------------------------------------------------------------------------- */
/* Settings */
/* ------------------------------------------------------------------------- */
MeshOverlaySettings mesh_overlay_default_settings(void) {
static const unsigned char defaults[MESH_OVERLAY_CATEGORY_COUNT][3] = {
{0x7F, 0x84, 0x9C}, /* ESCAPE Catppuccin Mocha overlay1 */
{0xCB, 0xA6, 0xF7}, /* DARK mauve */
{0xF9, 0xE2, 0xAF}, /* UNRESOLVED yellow */
{0xF3, 0x8B, 0xA8}, /* INCOMPLETE red */
{0x89, 0xB4, 0xFA}, /* UNTRACED blue */
};
MeshOverlaySettings settings;
memcpy(settings.colors, defaults, sizeof settings.colors);
settings.opacity = 0.5;
return settings;
}
static int overlay_hex_nibble(char digit, unsigned char *value) {
if (digit >= '0' && digit <= '9') {
*value = (unsigned char)(digit - '0');
return 0;
}
if (digit >= 'a' && digit <= 'f') {
*value = (unsigned char)(digit - 'a' + 10);
return 0;
}
if (digit >= 'A' && digit <= 'F') {
*value = (unsigned char)(digit - 'A' + 10);
return 0;
}
return -1;
}
int mesh_overlay_parse_color(const char *text, unsigned char rgb[3]) {
if (text == NULL || rgb == NULL)
return -1;
if (strlen(text) != 7 || text[0] != '#')
return -1;
unsigned char parsed[3];
for (int channel = 0; channel < 3; ++channel) {
unsigned char high, low;
if (overlay_hex_nibble(text[1 + 2 * channel], &high) ||
overlay_hex_nibble(text[2 + 2 * channel], &low))
return -1;
parsed[channel] = (unsigned char)((high << 4) | low);
}
rgb[0] = parsed[0];
rgb[1] = parsed[1];
rgb[2] = parsed[2];
return 0;
}
/* ------------------------------------------------------------------------- */
/* Edge extraction */
/* ------------------------------------------------------------------------- */
typedef struct {
size_t low;
size_t high;
} OverlayEdge;
static int overlay_edge_compare(const void *lhs, const void *rhs) {
const OverlayEdge *a = lhs;
const OverlayEdge *b = rhs;
if (a->low != b->low)
return a->low < b->low ? -1 : 1;
if (a->high != b->high)
return a->high < b->high ? -1 : 1;
return 0;
}
static unsigned char overlay_vertex_category(const LensVertex *vertex) {
if (!vertex->traced)
return (unsigned char)MESH_OVERLAY_CATEGORY_UNTRACED;
switch (vertex->outcome) {
case RAY_OUTCOME_ESCAPED:
return (unsigned char)MESH_OVERLAY_CATEGORY_ESCAPE;
case RAY_OUTCOME_DARK:
return (unsigned char)MESH_OVERLAY_CATEGORY_DARK;
case RAY_OUTCOME_UNRESOLVED:
return (unsigned char)MESH_OVERLAY_CATEGORY_UNRESOLVED;
case RAY_OUTCOME_INCOMPLETE:
default:
return (unsigned char)MESH_OVERLAY_CATEGORY_INCOMPLETE;
}
}
int mesh_overlay_prepare(const FrameLensMesh *mesh, MeshOverlayLines *lines) {
if (lines == NULL)
return -1;
lines->lines = NULL;
lines->count = 0;
if (mesh == NULL)
return -1;
/* An overflowing triangle count is rejected before any pointer is
* dereferenced so a corrupt mesh cannot drive an out-of-bounds read. */
if (mesh->triangle_count > SIZE_MAX / 3)
return -1;
if (mesh->triangle_count == 0)
return 0;
const size_t raw_count = mesh->triangle_count * 3;
if (raw_count > SIZE_MAX / sizeof(OverlayEdge))
return -1;
if (mesh->triangles == NULL || mesh->vertices == NULL)
return -1;
OverlayEdge *raw = malloc(raw_count * sizeof *raw);
if (raw == NULL)
return -1;
for (size_t triangle = 0; triangle < mesh->triangle_count; ++triangle) {
const LensTriangle *leaf = &mesh->triangles[triangle];
for (int edge = 0; edge < 3; ++edge) {
const size_t from = leaf->vertex[edge];
const size_t to = leaf->vertex[(edge + 1) % 3];
if (from >= mesh->vertex_count || to >= mesh->vertex_count) {
free(raw);
return -1;
}
const LensVertex *a = &mesh->vertices[from];
const LensVertex *b = &mesh->vertices[to];
if (!isfinite(a->image_x) || !isfinite(a->image_y) ||
!isfinite(b->image_x) || !isfinite(b->image_y)) {
free(raw);
return -1;
}
OverlayEdge *slot = &raw[3 * triangle + (size_t)edge];
slot->low = from < to ? from : to;
slot->high = from < to ? to : from;
}
}
qsort(raw, raw_count, sizeof *raw, overlay_edge_compare);
size_t unique = 0;
for (size_t i = 0; i < raw_count; ++i) {
if (unique == 0 || raw[unique - 1].low != raw[i].low ||
raw[unique - 1].high != raw[i].high)
raw[unique++] = raw[i];
}
if (unique > SIZE_MAX / sizeof(MeshOverlayLine)) {
free(raw);
return -1;
}
MeshOverlayLine *out = NULL;
if (unique != 0) {
out = malloc(unique * sizeof *out);
if (out == NULL) {
free(raw);
return -1;
}
}
for (size_t i = 0; i < unique; ++i) {
const LensVertex *a = &mesh->vertices[raw[i].low];
const LensVertex *b = &mesh->vertices[raw[i].high];
out[i].x0 = a->image_x;
out[i].y0 = a->image_y;
out[i].x1 = b->image_x;
out[i].y1 = b->image_y;
out[i].category0 = overlay_vertex_category(a);
out[i].category1 = overlay_vertex_category(b);
}
free(raw);
lines->lines = out;
lines->count = unique;
return 0;
}
void mesh_overlay_lines_destroy(MeshOverlayLines *lines) {
if (lines == NULL)
return;
free(lines->lines);
lines->lines = NULL;
lines->count = 0;
}
/* ------------------------------------------------------------------------- */
/* Rasterization */
/* ------------------------------------------------------------------------- */
static double overlay_fractional_part(double value) { return value - floor(value); }
static void blend_overlay(unsigned char *pixels, int width, int height, int x,
int y, const unsigned char rgb[3], double alpha,
int rgba) {
if (alpha <= 0.0 || x < 0 || x >= width || y < 0 || y >= height)
return;
if (alpha > 1.0)
alpha = 1.0;
const int stride = rgba ? 4 : 3;
unsigned char *pixel = pixels + stride * ((size_t)y * (size_t)width + (size_t)x);
for (int channel = 0; channel < stride; ++channel) {
const double source = channel == 3 ? 255.0 : (double)rgb[channel];
const double mixed =
(double)pixel[channel] * (1.0 - alpha) + source * alpha;
long value = lround(mixed);
if (value < 0)
value = 0;
if (value > 255)
value = 255;
pixel[channel] = (unsigned char)value;
}
}
/* Liang-Barsky clip of the (major, minor) segment to the inclusive box. Keeps
* every subsequent cast and loop bounded even for huge finite coordinates.
* Returns 1 when a nonempty clipped segment remains, 0 when fully outside. The
* clipped outputs are guaranteed finite and inside the box before the caller
* casts them: a nonfinite interpolation result (cancellation) is skipped, and a
* finite roundoff overshoot is clamped back into the box. The segment direction
* keeps x0 <= x1 and the box clamp is monotone, so the order is preserved. */
static int clip_overlay_segment(double *x0, double *y0, double *x1, double *y1,
double xmin, double xmax, double ymin,
double ymax) {
const double dx = *x1 - *x0;
const double dy = *y1 - *y0;
if (!isfinite(dx) || !isfinite(dy))
return 0;
double t0 = 0.0, t1 = 1.0;
const double p[4] = {-dx, dx, -dy, dy};
const double q[4] = {*x0 - xmin, xmax - *x0, *y0 - ymin, ymax - *y0};
for (int i = 0; i < 4; ++i) {
if (p[i] == 0.0) {
if (q[i] < 0.0)
return 0;
} else {
const double r = q[i] / p[i];
if (p[i] < 0.0) {
if (r > t1)
return 0;
if (r > t0)
t0 = r;
} else {
if (r < t0)
return 0;
if (r < t1)
t1 = r;
}
}
}
double nx0 = *x0 + t0 * dx;
double ny0 = *y0 + t0 * dy;
double nx1 = *x0 + t1 * dx;
double ny1 = *y0 + t1 * dy;
if (!isfinite(nx0) || !isfinite(ny0) || !isfinite(nx1) || !isfinite(ny1))
return 0;
if (nx0 < xmin)
nx0 = xmin;
if (nx0 > xmax)
nx0 = xmax;
if (ny0 < ymin)
ny0 = ymin;
if (ny0 > ymax)
ny0 = ymax;
if (nx1 < xmin)
nx1 = xmin;
if (nx1 > xmax)
nx1 = xmax;
if (ny1 < ymin)
ny1 = ymin;
if (ny1 > ymax)
ny1 = ymax;
*x0 = nx0;
*y0 = ny0;
*x1 = nx1;
*y1 = ny1;
return 1;
}
static void plot_overlay_aa(unsigned char *pixels, int width, int height,
int steep, int x, int y, double coverage,
const unsigned char rgb[3], double opacity,
int rgba) {
if (coverage > 0.0)
blend_overlay(pixels, width, height, steep ? y : x, steep ? x : y, rgb,
coverage * opacity, rgba);
}
/* Xiaolin Wu line rasterization, one pixel wide, with a color that switches to
* the second endpoint category at the major-axis midpoint. A single pass
* colors the whole edge, so a midpoint pixel is never blended from both halves. */
static void draw_overlay_line(unsigned char *pixels, int width, int height,
const MeshOverlayLine *line,
const MeshOverlaySettings *settings, int rgba) {
double x0 = line->x0, y0 = line->y0;
double x1 = line->x1, y1 = line->y1;
const unsigned char *first = settings->colors[line->category0];
const unsigned char *second = settings->colors[line->category1];
const int steep = fabs(y1 - y0) > fabs(x1 - x0);
if (steep) {
double swap = x0;
x0 = y0;
y0 = swap;
swap = x1;
x1 = y1;
y1 = swap;
}
if (x0 > x1) {
double swap = x0;
x0 = x1;
x1 = swap;
swap = y0;
y0 = y1;
y1 = swap;
const unsigned char *color_swap = first;
first = second;
second = color_swap;
}
/* The switch is fixed to the true midpoint of the unclipped edge. Halving
* each endpoint separately cannot overflow for finite same-sign endpoints. */
const double midpoint = 0.5 * x0 + 0.5 * x1;
const int major_limit = steep ? height : width;
const int minor_limit = steep ? width : height;
double cx0 = x0, cy0 = y0, cx1 = x1, cy1 = y1;
if (!clip_overlay_segment(&cx0, &cy0, &cx1, &cy1, -1.0, (double)major_limit,
-1.0, (double)minor_limit))
return;
x0 = cx0;
y0 = cy0;
x1 = cx1;
y1 = cy1;
const double dx = x1 - x0;
/* A zero-length edge has no coverage; do not turn it into a vertex dot. */
if (!(dx > 0.0))
return;
const double gradient = (y1 - y0) / dx;
/* After the steep/orientation normalization |gradient| <= 1, so it is finite
* for a finite nonzero dx; this guard keeps a pathological subnormal dx from
* ever reaching a float-to-int cast. */
if (!isfinite(gradient))
return;
const int first_column = (int)round(x0);
const int last_column = (int)round(x1);
if (first_column == last_column) {
/* Wu's two endpoint formulas overlap in the same column for a subpixel
* segment. Paint its length-weighted coverage once, rather than applying
* two alpha blends that make tiny edges brighter than full-length ones. */
const double center_y = 0.5 * y0 + 0.5 * y1;
const int row = (int)floor(center_y);
const double fraction = overlay_fractional_part(center_y);
const unsigned char *color = first_column < midpoint ? first : second;
plot_overlay_aa(pixels, width, height, steep, first_column, row,
dx * (1.0 - fraction), color, settings->opacity, rgba);
plot_overlay_aa(pixels, width, height, steep, first_column, row + 1,
dx * fraction, color, settings->opacity, rgba);
return;
}
double x_end = (double)first_column;
double y_end = y0 + gradient * (x_end - x0);
if (!isfinite(y_end))
return;
double x_gap = 1.0 - overlay_fractional_part(x0 + 0.5);
const int x_pixel_start = (int)x_end;
int y_pixel = (int)floor(y_end);
const unsigned char *start_color = (x_pixel_start < midpoint) ? first : second;
plot_overlay_aa(pixels, width, height, steep, x_pixel_start, y_pixel,
(1.0 - overlay_fractional_part(y_end)) * x_gap, start_color,
settings->opacity, rgba);
plot_overlay_aa(pixels, width, height, steep, x_pixel_start, y_pixel + 1,
overlay_fractional_part(y_end) * x_gap, start_color,
settings->opacity, rgba);
double inter_y = y_end + gradient;
x_end = (double)last_column;
y_end = y1 + gradient * (x_end - x1);
if (!isfinite(inter_y) || !isfinite(y_end))
return;
x_gap = overlay_fractional_part(x1 + 0.5);
const int x_pixel_end = (int)x_end;
y_pixel = (int)floor(y_end);
const unsigned char *end_color = (x_pixel_end < midpoint) ? first : second;
plot_overlay_aa(pixels, width, height, steep, x_pixel_end, y_pixel,
(1.0 - overlay_fractional_part(y_end)) * x_gap, end_color,
settings->opacity, rgba);
plot_overlay_aa(pixels, width, height, steep, x_pixel_end, y_pixel + 1,
overlay_fractional_part(y_end) * x_gap, end_color,
settings->opacity, rgba);
for (int x = x_pixel_start + 1; x < x_pixel_end; ++x) {
y_pixel = (int)floor(inter_y);
const unsigned char *color = (x < midpoint) ? first : second;
plot_overlay_aa(pixels, width, height, steep, x, y_pixel,
1.0 - overlay_fractional_part(inter_y), color,
settings->opacity, rgba);
plot_overlay_aa(pixels, width, height, steep, x, y_pixel + 1,
overlay_fractional_part(inter_y), color,
settings->opacity, rgba);
inter_y += gradient;
}
}
static int draw_overlay(const MeshOverlayLines *lines, unsigned char *pixels,
int width, int height,
const MeshOverlaySettings *settings, int rgba) {
if (lines == NULL || pixels == NULL || settings == NULL)
return -1;
if (width <= 0 || height <= 0)
return -1;
/* Keep the raster's y+1 / x+1 and clipped-box endpoint casts strictly inside
* `int`, and keep the RGB8/RGBA8 byte count inside `size_t`. */
if (width > INT_MAX - 2 || height > INT_MAX - 2)
return -1;
const size_t dim_width = (size_t)width;
const size_t dim_height = (size_t)height;
if (dim_width > SIZE_MAX / dim_height)
return -1;
const size_t pixel_count = dim_width * dim_height;
if (pixel_count > SIZE_MAX / (rgba ? 4 : 3))
return -1;
if (!isfinite(settings->opacity) || settings->opacity < 0.0 ||
settings->opacity > 1.0)
return -1;
/* A line batch larger than any allocatable MeshOverlayLine array cannot be
* real; reject it before dereferencing the array. */
if (lines->count > SIZE_MAX / sizeof(MeshOverlayLine))
return -1;
if (lines->count != 0 && lines->lines == NULL)
return -1;
/* Validate the whole batch before drawing so an invalid line cannot leave a
* partially painted image behind. Endpoint differences too large to
* represent (e.g. -DBL_MAX..+DBL_MAX) are rejected up front, before the
* midpoint or clipping math can produce a nonfinite value. */
for (size_t i = 0; i < lines->count; ++i) {
const MeshOverlayLine *line = &lines->lines[i];
if (line->category0 >= MESH_OVERLAY_CATEGORY_COUNT ||
line->category1 >= MESH_OVERLAY_CATEGORY_COUNT)
return -1;
if (!isfinite(line->x0) || !isfinite(line->y0) || !isfinite(line->x1) ||
!isfinite(line->y1))
return -1;
if (!isfinite(line->x1 - line->x0) || !isfinite(line->y1 - line->y0))
return -1;
}
for (size_t i = 0; i < lines->count; ++i)
draw_overlay_line(pixels, width, height, &lines->lines[i], settings, rgba);
return 0;
}
int mesh_overlay_draw_rgb8(const MeshOverlayLines *lines, unsigned char *pixels,
int width, int height,
const MeshOverlaySettings *settings) {
return draw_overlay(lines, pixels, width, height, settings, 0);
}
void mesh_overlay_layer_destroy(MeshOverlayLayer *layer) {
if (layer == NULL)
return;
free(layer->rgba);
*layer = (MeshOverlayLayer){0};
}
int mesh_overlay_build_layer(const FrameLensMesh *mesh, int width, int height,
const MeshOverlaySettings *settings,
MeshOverlayLayer *layer) {
if (layer == NULL)
return -1;
*layer = (MeshOverlayLayer){0};
if (width <= 0 || height <= 0 || width > INT_MAX - 2 ||
height > INT_MAX - 2 || (size_t)width > SIZE_MAX / (size_t)height ||
(size_t)width * height > SIZE_MAX / 4 || settings == NULL ||
!isfinite(settings->opacity) || settings->opacity < 0.0 ||
settings->opacity > 1.0)
return -1;
MeshOverlayLines lines = {0};
if (mesh_overlay_prepare(mesh, &lines))
return -1;
unsigned char *pixels = calloc((size_t)width * height, 4);
const int result = pixels == NULL ? -1 :
draw_overlay(&lines, pixels, width, height, settings, 1);
mesh_overlay_lines_destroy(&lines);
if (result) {
free(pixels);
return -1;
}
layer->rgba = pixels;
layer->width = width;
layer->height = height;
return 0;
}
int mesh_overlay_composite_rgb8(const MeshOverlayLayer *layer,
unsigned char *rgb8, int width, int height) {
if (layer == NULL || layer->rgba == NULL || rgb8 == NULL || width <= 0 ||
height <= 0 || width != layer->width || height != layer->height ||
(size_t)width > SIZE_MAX / (size_t)height ||
(size_t)width * height > SIZE_MAX / 4)
return -1;
const size_t count = (size_t)width * height;
for (size_t pixel = 0; pixel < count; ++pixel) {
const unsigned char *source = &layer->rgba[4 * pixel];
const unsigned int alpha = source[3];
if (alpha == 0)
continue;
for (int channel = 0; channel < 3; ++channel) {
const unsigned int value = source[channel] +
(rgb8[3 * pixel + channel] * (255 - alpha) + 127) / 255;
rgb8[3 * pixel + channel] = value > 255 ? 255 : (unsigned char)value;
}
}
return 0;
}
+103
View File
@@ -0,0 +1,103 @@
#ifndef MESH_OVERLAY_H
#define MESH_OVERLAY_H
#include "frame.h"
#include <stddef.h>
/* Diagnostic overlay for a finalized lens mesh. It is a pure consumer of
* FrameLensMesh: it copies the coordinates it needs and never aliases the
* mutable mesh, so it can run alongside a writer without sharing state.
*
* Categories mirror the rendering terminal classes plus UNTRACED for a vertex
* that never received an endpoint (traced == 0). UNRESOLVED and INCOMPLETE are
* kept distinct so the overlay does not hide a retryable/completion shortfall
* behind the normal dark terminal. */
enum MeshOverlayCategory {
MESH_OVERLAY_CATEGORY_ESCAPE = 0,
MESH_OVERLAY_CATEGORY_DARK,
MESH_OVERLAY_CATEGORY_UNRESOLVED,
MESH_OVERLAY_CATEGORY_INCOMPLETE,
MESH_OVERLAY_CATEGORY_UNTRACED,
MESH_OVERLAY_CATEGORY_COUNT
};
typedef struct {
/* Display-sRGB #RRGGBB color per category. Index by MeshOverlayCategory. */
unsigned char colors[MESH_OVERLAY_CATEGORY_COUNT][3];
/* Coverage multiplier in [0, 1] applied on top of the one-pixel AA weight. */
double opacity;
} MeshOverlaySettings;
/* One undirected mesh edge. (x0, y0) and (x1, y1) are the two endpoints in
* image-plane pixel coordinates; category0 belongs to endpoint 0 and
* category1 to endpoint 1. Each half of the edge carries the color of its
* adjacent vertex with an abrupt switch at the major-axis midpoint. */
typedef struct {
double x0, y0, x1, y1;
unsigned char category0, category1;
} MeshOverlayLine;
typedef struct {
MeshOverlayLine *lines;
size_t count;
} MeshOverlayLines;
/* Owned display-sRGB overlay, byte order R,G,B,A with premultiplied RGB.
* Transparent pixels are all zero. No live mesh or palette is retained. */
typedef struct {
unsigned char *rgba;
int width, height;
} MeshOverlayLayer;
/* Allocate/rasterize a transparent layer from the unique mesh edges. The output
* must be empty; failures leave it empty. Temporary lines are freed before
* returning. The caller owns the layer until destroyed or submitted. */
int mesh_overlay_build_layer(const FrameLensMesh *mesh, int width, int height,
const MeshOverlaySettings *settings,
MeshOverlayLayer *layer);
void mesh_overlay_layer_destroy(MeshOverlayLayer *layer);
/* Composite a valid premultiplied layer onto RGB8 in place. Layer dimensions
* must match. Zero alpha leaves RGB bytes unchanged; the layer is read-only. */
int mesh_overlay_composite_rgb8(const MeshOverlayLayer *layer,
unsigned char *rgb8, int width, int height);
/* Catppuccin Mocha diagnostics palette with opacity 0.5:
* ESCAPE #7F849C, DARK #CBA6F7, UNRESOLVED #F9E2AF, INCOMPLETE #F38BA8,
* UNTRACED #89B4FA. */
MeshOverlaySettings mesh_overlay_default_settings(void);
/* Parse a strict `#RRGGBB` color into rgb[3]. Returns 0 on success and -1 for
* NULL arguments, a wrong length/prefix, or a non-hex digit. Both upper- and
* lower-case hex digits are accepted. */
int mesh_overlay_parse_color(const char *text, unsigned char rgb[3]);
/* Build the unique undirected edge set of the mesh's leaf triangles. Edges are
* canonicalized to (min vertex id, max vertex id), sorted and deduplicated so
* shared and boundary edges are emitted exactly once regardless of winding.
* Off-mesh probe witnesses have no triangle edge and are never emitted.
* Returns 0 on success (count may be 0) and -1 on invalid arguments, an
* out-of-range vertex index, a nonfinite coordinate, or allocation overflow.
* The output handle must be empty (zero-initialized or previously destroyed).
* On success the caller owns lines->lines and must release it with
* mesh_overlay_lines_destroy. */
int mesh_overlay_prepare(const FrameLensMesh *mesh, MeshOverlayLines *lines);
/* Release the edge array and reset the handle to empty. Safe on NULL. */
void mesh_overlay_lines_destroy(MeshOverlayLines *lines);
/* Rasterize the edges onto an interleaved RGB8 image (width*height*3 bytes),
* one pixel wide with Xiaolin Wu coverage AA, mixing directly in display sRGB
* with alpha = coverage * settings->opacity. Endpoint categories are chosen by
* position along the major axis relative to the midpoint; edges are clipped
* safely to the image so offscreen or huge coordinates cannot loop unbounded or
* overflow an integer conversion. Returns 0 on success and -1 on NULL
* arguments, a nonpositive size, an opacity outside [0, 1] or nonfinite, an
* out-of-range category, a nonfinite line coordinate/difference, or an
* overflowing image/batch size. Invalid batches are rejected before painting. */
int mesh_overlay_draw_rgb8(const MeshOverlayLines *lines, unsigned char *pixels,
int width, int height,
const MeshOverlaySettings *settings);
#endif
+14 -10
View File
@@ -16,23 +16,27 @@ 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,
/* The clean file is already written, so drawing in place on clean_rgb8 can
* never alter it; mesh_path receives the augmented buffer. */
if (job->draw_mesh) {
if (mesh_overlay_composite_rgb8(&job->mesh_layer, job->clean_rgb8, job->width,
job->height) ||
write_rgb8_image(job->mesh_path, job->clean_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;
}
@@ -46,11 +50,11 @@ static void *movie_output_writer_main(void *opaque) {
if (queue->count == 0 && queue->producer_done)
break;
const size_t slot = queue->head;
const MovieOutputJob job = queue->jobs[slot];
MovieOutputJob job = queue->jobs[slot];
/* Ownership moved into the local copy; clear the slot so destroy() cannot
* free the same buffers a second time. */
queue->jobs[slot].clean_rgb8 = NULL;
queue->jobs[slot].mesh_rgb8 = NULL;
queue->jobs[slot].mesh_layer = (MeshOverlayLayer){0};
queue->head = (queue->head + 1) % queue->capacity;
--queue->count;
pthread_cond_signal(&queue->not_full);
@@ -72,7 +76,7 @@ static void *movie_output_writer_main(void *opaque) {
}
pthread_mutex_unlock(&queue->mutex);
free(job.clean_rgb8);
free(job.mesh_rgb8);
mesh_overlay_layer_destroy(&job.mesh_layer);
pthread_mutex_lock(&queue->mutex);
}
pthread_mutex_unlock(&queue->mutex);
@@ -213,7 +217,7 @@ void movie_output_queue_destroy(MovieOutputQueue *queue) {
if (queue->jobs != NULL)
for (size_t i = 0; i < queue->capacity; ++i) {
free(queue->jobs[i].clean_rgb8);
free(queue->jobs[i].mesh_rgb8);
mesh_overlay_layer_destroy(&queue->jobs[i].mesh_layer);
}
pthread_cond_destroy(&queue->not_empty);
pthread_cond_destroy(&queue->not_full);
+19 -10
View File
@@ -1,6 +1,7 @@
#ifndef MOVIE_OUTPUT_H
#define MOVIE_OUTPUT_H
#include "mesh_overlay.h"
#include "optics.h"
#include <limits.h>
@@ -10,13 +11,16 @@
/* Bounded, single-producer/single-writer movie output queue.
*
* The producer (the render loop) performs all HDR work and the tone map before
* submitting; a job therefore carries finished 8-bit RGB buffers, never a
* double HDR framebuffer. The writer thread encodes/writes them in submit
* order while the producer renders the next frame.
* submitting, and rasterizes the independent premultiplied RGBA8 mesh overlay.
* A job carries finished RGB8 and optional immutable RGBA8, never a double HDR
* framebuffer or a live mesh reference. The
* writer thread encodes/writes them in submit order while the producer renders
* the next frame.
*
* Ownership contract for submit(): on success the queue owns clean_rgb8 and
* mesh_rgb8 and frees them after writing; on failure they remain owned by the
* caller.
* Ownership contract for submit(): on success the queue owns clean_rgb8 and the
* mesh_layer buffer and releases them after writing; on failure they remain
* owned by the caller, who must free(clean_rgb8) and call
* mesh_overlay_layer_destroy(&mesh_layer).
*
* `capacity` bounds the queued jobs only; the writer may additionally hold one
* already-popped job, so the true in-memory bound is capacity + 1 jobs. With
@@ -29,10 +33,14 @@ typedef struct {
int draw_mesh;
unsigned char *clean_rgb8;
unsigned char *mesh_rgb8; /* NULL when draw_mesh is false */
int width;
int height;
/* Producer-rasterized, premultiplied display-sRGB RGBA8. The writer only
* composites it after writing the clean image; no mesh/palette is retained.
* Empty when draw_mesh is false. */
MeshOverlayLayer mesh_layer;
size_t images;
size_t catalog_stars;
PsfSplatStats psf_stats;
@@ -47,9 +55,10 @@ typedef struct {
} MovieOutputJob;
/* Optional custom writer. Returns 0 on success; the default writer writes
* clean_rgb8 to output_path and, when draw_mesh is set, mesh_rgb8 to
* mesh_path, then prints the "Rendered ... (<note>)" and PSF lines. The queue
* owns and frees clean_rgb8/mesh_rgb8 after the writer returns. */
* clean_rgb8 to output_path and, when draw_mesh is set, composites mesh_layer in
* place on clean_rgb8 before writing the augmented buffer to mesh_path. It
* then prints the "Rendered ... (<note>)" and PSF lines. The queue owns and
* releases clean_rgb8 and mesh_layer after the writer returns. */
typedef int (*MovieOutputWriteFn)(void *context, const MovieOutputJob *job,
const PngWriteSettings *settings);
+2 -2
View File
@@ -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",
+206 -19
View File
@@ -1,5 +1,8 @@
#include "ray.h"
#include "asymptotic.h"
#include <math.h>
#include <omp.h>
#include <stdlib.h>
@@ -11,8 +14,14 @@ int ray_pool_init(RayPool *p, size_t capacity) {
if (!(RAY_ALLOC(t) && RAY_ALLOC(x0) && RAY_ALLOC(x1) && RAY_ALLOC(x2) &&
RAY_ALLOC(p0) && RAY_ALLOC(p1) && RAY_ALLOC(p2) && RAY_ALLOC(observer) &&
RAY_ALLOC(direction0) && RAY_ALLOC(direction1) && RAY_ALLOC(direction2) &&
RAY_ALLOC(log_alpha_p0) && RAY_ALLOC(steps) && RAY_ALLOC(frame_id) &&
RAY_ALLOC(vertex_id) && RAY_ALLOC(status) && RAY_ALLOC(endpoint))) {
RAY_ALLOC(log_alpha_p0) && RAY_ALLOC(log_alpha_p0_0) &&
RAY_ALLOC(activate_t) && RAY_ALLOC(steps) &&
RAY_ALLOC(step_limit) && RAY_ALLOC(integration_start_time) &&
RAY_ALLOC(next_step) && RAY_ALLOC(rejected_steps) &&
RAY_ALLOC(rhs_evaluations) && RAY_ALLOC(previous_rejected) &&
RAY_ALLOC(lookback_limit) && RAY_ALLOC(continuation) &&
RAY_ALLOC(frame_id) && RAY_ALLOC(vertex_id) && RAY_ALLOC(status) &&
RAY_ALLOC(endpoint))) {
ray_pool_destroy(p);
return -1;
}
@@ -29,6 +38,7 @@ int ray_pool_append(RayPool *p, const ObserverState *observer,
if (observer == NULL || direction == NULL)
return -1;
p->t[i] = observer->coordinate_time;
p->activate_t[i] = observer->coordinate_time;
p->observer[i] = observer;
p->direction0[i] = direction[0];
p->direction1[i] = direction[1];
@@ -37,28 +47,181 @@ int ray_pool_append(RayPool *p, const ObserverState *observer,
p->vertex_id[i] = vertex_id;
p->status[i] = RAY_POOL_PENDING;
p->endpoint[i] = (RayEndpoint){.magnification = 1.0,
.status = RAY_ENDPOINT_INTEGRATION_FAILURE};
.end_id = SPACETIME_END_NONE,
.outcome = RAY_OUTCOME_INCOMPLETE,
.reason = RAY_REASON_NONE,
.stop_coordinate_time = NAN,
.accepted_steps = 0,
.final_x = {NAN, NAN, NAN},
.final_Pi = {NAN, NAN, NAN},
.final_log_alpha_p0 = NAN,
.final_log_alpha_p0_0 = NAN,
.threshold_value = NAN};
p->step_limit[i] = 0;
/* Adaptive control state starts unset; ray_pool_preroute() seeds the window
* start from the resolved activation time. Initializing here as well keeps
* a directly advanced pool slot well-defined. */
p->integration_start_time[i] = observer->coordinate_time;
p->next_step[i] = 0.0;
p->rejected_steps[i] = 0;
p->rhs_evaluations[i] = 0;
p->previous_rejected[i] = 0;
p->lookback_limit[i] = 0.0;
p->continuation[i] = 0;
p->log_alpha_p0_0[i] = 0.0;
++p->count;
return 0;
}
void ray_pool_activate_in_time_range(RayPool *p, const MetricSlab *slab) {
int ray_pool_append_continuation_state(RayPool *p, size_t frame_id,
size_t vertex_id,
const GeodesicRayState *state,
unsigned int accepted_limit,
double lookback_limit) {
if (p == NULL || p->count == p->capacity || state == NULL)
return -1;
const size_t i = p->count;
p->t[i] = state->coordinate_time;
p->activate_t[i] = state->coordinate_time;
p->observer[i] = NULL;
p->direction0[i] = p->direction1[i] = p->direction2[i] = 0.0;
p->x0[i] = state->x[0]; p->x1[i] = state->x[1]; p->x2[i] = state->x[2];
p->p0[i] = state->Pi[0]; p->p1[i] = state->Pi[1]; p->p2[i] = state->Pi[2];
p->log_alpha_p0[i] = state->log_alpha_p0;
p->log_alpha_p0_0[i] = state->log_alpha_p0_0;
p->steps[i] = state->steps;
p->step_limit[i] = accepted_limit;
p->integration_start_time[i] = state->integration_start_time;
p->next_step[i] = state->next_step;
p->rejected_steps[i] = state->rejected_steps;
p->rhs_evaluations[i] = state->rhs_evaluations;
p->previous_rejected[i] = state->previous_rejected;
p->lookback_limit[i] = lookback_limit;
p->continuation[i] = 1;
p->frame_id[i] = frame_id;
p->vertex_id[i] = vertex_id;
p->status[i] = RAY_POOL_PENDING;
p->endpoint[i] = (RayEndpoint){.magnification = 1.0,
.end_id = SPACETIME_END_NONE,
.outcome = RAY_OUTCOME_INCOMPLETE,
.reason = RAY_REASON_NONE,
.stop_coordinate_time = NAN,
.accepted_steps = state->steps,
.accepted_step_limit = accepted_limit,
.final_x = {NAN, NAN, NAN},
.final_Pi = {NAN, NAN, NAN},
.final_log_alpha_p0 = NAN,
.final_log_alpha_p0_0 = state->log_alpha_p0_0,
.threshold_value = NAN};
++p->count;
return 0;
}
int ray_pool_append_continuation(RayPool *p, size_t frame_id,
size_t vertex_id, double t, const double x[3],
const double Pi[3], double log_alpha_p0,
double log_alpha_p0_0, unsigned int steps,
unsigned int limit) {
if (x == NULL || Pi == NULL)
return -1;
const GeodesicRayState state = {.coordinate_time = t,
.x = {x[0], x[1], x[2]},
.Pi = {Pi[0], Pi[1], Pi[2]},
.log_alpha_p0 = log_alpha_p0,
.log_alpha_p0_0 = log_alpha_p0_0,
.steps = steps};
return ray_pool_append_continuation_state(p, frame_id, vertex_id, &state,
limit, 0.0);
}
void ray_pool_preroute(RayPool *p, const SpacetimeSource *source) {
if (p == NULL || source == NULL)
return;
#pragma omp parallel for schedule(static)
for (size_t i = 0; i < p->count; ++i) {
if (p->status[i] != RAY_POOL_PENDING || p->t[i] > slab->t_hi ||
p->t[i] <= slab->t_lo)
if (p->status[i] != RAY_POOL_PENDING || p->continuation[i])
continue;
GeodesicRayState state;
if (geodesic_initialize_past_ray(
slab, p->observer[i],
(double[]){p->direction0[i], p->direction1[i], p->direction2[i]},
&state)) {
AsymptoticRoute route;
const AsymptoticStatus status = asymptotic_route_camera(
source, p->observer[i],
(double[]){p->direction0[i], p->direction1[i], p->direction2[i]},
&route);
if (status == ASYMPTOTIC_UNSUPPORTED || status == ASYMPTOTIC_INVALID) {
p->endpoint[i].outcome = RAY_OUTCOME_INCOMPLETE;
p->endpoint[i].reason = status == ASYMPTOTIC_UNSUPPORTED
? RAY_REASON_UNSUPPORTED
: (ray_reason_valid(route.failure_reason) &&
route.failure_reason != RAY_REASON_NONE
? route.failure_reason
: RAY_REASON_CAMERA_PREROUTE_FAILED);
p->endpoint[i].end_id = route.end_id;
p->status[i] = RAY_POOL_FAILED;
continue;
}
p->x0[i] = state.x[0]; p->x1[i] = state.x[1]; p->x2[i] = state.x[2];
p->p0[i] = state.Pi[0]; p->p1[i] = state.Pi[1]; p->p2[i] = state.Pi[2];
p->log_alpha_p0[i] = state.log_alpha_p0;
p->steps[i] = state.steps;
if (status == ASYMPTOTIC_TIME_RANGE_EXHAUSTED) {
p->endpoint[i].outcome = RAY_OUTCOME_INCOMPLETE;
p->endpoint[i].reason = RAY_REASON_TIME_RANGE_EXHAUSTED;
p->endpoint[i].end_id = route.end_id;
p->status[i] = RAY_POOL_TERMINATED;
continue;
}
if (route.kind == ASYMPTOTIC_ROUTE_ESCAPED) {
for (int axis = 0; axis < 3; ++axis)
p->endpoint[i].n_infinity[axis] = route.n_infinity[axis];
p->endpoint[i].frequency_ratio = route.frequency_ratio;
p->endpoint[i].end_id = route.end_id;
p->endpoint[i].outcome = RAY_OUTCOME_ESCAPED;
p->endpoint[i].reason = RAY_REASON_NONE;
p->status[i] = RAY_POOL_TERMINATED;
continue;
}
if (route.kind == ASYMPTOTIC_ROUTE_TIME_RANGE_EXHAUSTED) {
p->endpoint[i].outcome = RAY_OUTCOME_INCOMPLETE;
p->endpoint[i].reason = RAY_REASON_TIME_RANGE_EXHAUSTED;
p->endpoint[i].end_id = route.end_id;
p->status[i] = RAY_POOL_TERMINATED;
continue;
}
if (route.kind != ASYMPTOTIC_ROUTE_INSIDE &&
route.kind != ASYMPTOTIC_ROUTE_ENTRY) {
p->endpoint[i].outcome = RAY_OUTCOME_INCOMPLETE;
p->endpoint[i].reason = RAY_REASON_INVALID_ROUTE_KIND;
p->status[i] = RAY_POOL_FAILED;
continue;
}
p->activate_t[i] = route.activate_t;
/* The adaptive window starts at the resolved activation/entry state; the
* first trial step and all cost counters are freshly initialized. An
* integration start time of zero is a legal coordinate time. */
p->integration_start_time[i] = route.activate_t;
p->next_step[i] = 0.0;
p->rejected_steps[i] = 0;
p->rhs_evaluations[i] = 0;
p->previous_rejected[i] = 0;
p->x0[i] = route.x[0];
p->x1[i] = route.x[1];
p->x2[i] = route.x[2];
p->p0[i] = route.Pi[0];
p->p1[i] = route.Pi[1];
p->p2[i] = route.Pi[2];
p->log_alpha_p0[i] = route.log_alpha_p0;
/* Camera-event reference, distinct from the entry-state L. */
p->log_alpha_p0_0[i] = route.log_alpha_p0_camera;
}
}
void ray_pool_activate_in_time_range(RayPool *p, const MetricSlab *slab) {
for (size_t i = 0; i < p->count; ++i) {
if (p->status[i] != RAY_POOL_PENDING || p->activate_t[i] > slab->t_hi ||
p->activate_t[i] <= slab->t_lo)
continue;
p->t[i] = p->activate_t[i];
/* Continuation rays keep the accepted-step count they already consumed.
* Activation never resets adaptive control state; for a new ray it was
* already seeded by preroute, for a continuation it comes from the
* installed resume state. */
if (!p->continuation[i])
p->steps[i] = 0;
p->status[i] = RAY_POOL_ACTIVE;
}
}
@@ -77,16 +240,34 @@ void ray_pool_advance_active(RayPool *p, const MetricSlab *slab,
.x = {p->x0[i], p->x1[i], p->x2[i]},
.Pi = {p->p0[i], p->p1[i], p->p2[i]},
.log_alpha_p0 = p->log_alpha_p0[i],
.steps = p->steps[i]};
.log_alpha_p0_0 = p->log_alpha_p0_0[i],
.steps = p->steps[i],
.integration_start_time = p->integration_start_time[i],
.next_step = p->next_step[i],
.rejected_steps = p->rejected_steps[i],
.rhs_evaluations = p->rhs_evaluations[i],
.previous_rejected = p->previous_rejected[i]};
GeodesicTraceConfig per_ray = *config;
if (p->step_limit[i] != 0)
per_ray.max_steps = p->step_limit[i];
if (p->lookback_limit[i] != 0.0)
per_ray.max_lookback_time = p->lookback_limit[i];
const GeodesicAdvanceResult result =
geodesic_advance_past_ray(slab, &s, slab->t_lo, config, &p->endpoint[i]);
geodesic_advance_past_ray(slab, &s, slab->t_lo, &per_ray, &p->endpoint[i]);
p->t[i] = s.coordinate_time;
p->x0[i] = s.x[0]; p->x1[i] = s.x[1]; p->x2[i] = s.x[2];
p->p0[i] = s.Pi[0]; p->p1[i] = s.Pi[1]; p->p2[i] = s.Pi[2];
p->log_alpha_p0[i] = s.log_alpha_p0;
p->steps[i] = s.steps;
p->integration_start_time[i] = s.integration_start_time;
p->next_step[i] = s.next_step;
p->rejected_steps[i] = s.rejected_steps;
p->rhs_evaluations[i] = s.rhs_evaluations;
p->previous_rejected[i] = s.previous_rejected;
if (result == GEODESIC_ADVANCE_TERMINATED)
p->status[i] = RAY_POOL_TERMINATED;
p->status[i] = p->endpoint[i].outcome == RAY_OUTCOME_UNRESOLVED
? RAY_POOL_UNRESOLVED
: RAY_POOL_TERMINATED;
else if (result == GEODESIC_ADVANCE_FAILED)
p->status[i] = RAY_POOL_FAILED;
}
@@ -107,7 +288,13 @@ void ray_pool_destroy(RayPool *p) {
free(p->t); free(p->x0); free(p->x1); free(p->x2); free(p->observer);
free(p->direction0); free(p->direction1); free(p->direction2);
free(p->p0); free(p->p1); free(p->p2); free(p->log_alpha_p0);
free(p->steps); free(p->frame_id); free(p->vertex_id); free(p->status);
free(p->log_alpha_p0_0);
free(p->activate_t); free(p->steps); free(p->step_limit);
free(p->integration_start_time); free(p->next_step);
free(p->rejected_steps); free(p->rhs_evaluations);
free(p->previous_rejected); free(p->lookback_limit);
free(p->continuation); free(p->frame_id); free(p->vertex_id);
free(p->status);
free(p->endpoint);
*p = (RayPool){0};
}
+46 -1
View File
@@ -10,14 +10,38 @@ typedef enum {
RAY_POOL_PENDING,
RAY_POOL_ACTIVE,
RAY_POOL_TERMINATED,
RAY_POOL_UNRESOLVED, /* trustworthy but budget-exhausted; retryable */
RAY_POOL_FAILED
} RayPoolStatus;
typedef struct {
double *t, *x0, *x1, *x2, *p0, *p1, *p2, *log_alpha_p0;
double *t, *x0, *x1, *x2, *p0, *p1, *p2, *log_alpha_p0, *log_alpha_p0_0;
/* Coordinate time at which the pre-routed interior state becomes valid.
* For a camera inside a worldtube this equals the camera time; for an
* exterior hit it is the earlier entry time. */
double *activate_t;
const ObserverState **observer;
double *direction0, *direction1, *direction2;
unsigned int *steps;
/* Per-ray total accepted-step limit; zero means use the trace config. */
unsigned int *step_limit;
/* Adaptive (DP54) per-ray control state, copied verbatim from the resume
* state and never reset by activation. All fields are ignored by the legacy
* RK4 path. `integration_start_time` is the activation/entry time of the
* adaptive window (not the camera energy reference), `next_step` is a
* positive trial-step magnitude (zero means "derive from the config"),
* `rejected_steps`/`rhs_evaluations` are cumulative cost and
* `previous_rejected` flags a rejected last trial. */
double *integration_start_time;
double *next_step;
unsigned int *rejected_steps;
unsigned long *rhs_evaluations;
unsigned int *previous_rejected;
/* Per-ray total lookback time budget; zero means use the trace config. */
double *lookback_limit;
/* Nonzero for a retry that resumes from a saved state instead of from the
* camera; such rays are not pre-routed. */
uint8_t *continuation;
size_t *frame_id, *vertex_id;
uint8_t *status;
RayEndpoint *endpoint;
@@ -28,6 +52,27 @@ int ray_pool_init(RayPool *pool, size_t capacity);
int ray_pool_append(RayPool *pool, const ObserverState *observer,
const double direction[3],
size_t frame_id, size_t vertex_id);
/* Append a retry that resumes an UNRESOLVED ray from its last accepted state.
* Every continuous and adaptive control field of `state` is copied so the
* resumed ray never replays from the camera, resets L0, or forgets its
* accumulated rejection/RHS counters. `accepted_limit` is the new total
* accepted-step budget; `lookback_limit` is the new total coordinate-time
* lookback budget (zero means use the trace config). Neither quota is part of
* the continuous state. */
int ray_pool_append_continuation_state(RayPool *pool, size_t frame_id,
size_t vertex_id,
const GeodesicRayState *state,
unsigned int accepted_limit,
double lookback_limit);
/* Legacy scattered-field wrapper around the state interface above. It builds
* an RK4-compatible state (zero adaptive control) and a zero lookback quota. */
int ray_pool_append_continuation(RayPool *pool, size_t frame_id,
size_t vertex_id, double t, const double x[3],
const double Pi[3], double log_alpha_p0,
double log_alpha_p0_0, unsigned int steps,
unsigned int limit);
/* Pre-route every still-PENDING ray once, before the slab sweep. */
void ray_pool_preroute(RayPool *pool, const SpacetimeSource *source);
void ray_pool_activate_in_time_range(RayPool *pool, const MetricSlab *slab);
void ray_pool_advance_active(RayPool *pool, const MetricSlab *slab,
const GeodesicTraceConfig *config);
+94 -14
View File
@@ -1,6 +1,9 @@
#ifndef SPACETIME_H
#define SPACETIME_H
#include <stddef.h>
#include <stdint.h>
typedef struct {
double alpha;
double beta[3];
@@ -11,12 +14,61 @@ typedef struct {
double d_gamma[3][3][3]; /* d_gamma[spatial derivative][j][k] */
} MetricData;
/* Result of evaluating the metric at one event. `OK` is zero so that legacy
* `if (eval(...))` call sites keep working. These codes describe data
* availability only; they never express a physical capture. */
typedef enum {
SPACETIME_POINT_OK = 0,
SPACETIME_POINT_TIME_UNAVAILABLE,
SPACETIME_POINT_OUT_OF_DOMAIN,
SPACETIME_POINT_INVALID_METRIC,
SPACETIME_POINT_INTERNAL_ERROR
} SpacetimePointStatus;
/* Optional legacy region query for backends that declare no asymptotic end.
* It can only report ACTIVE or ESCAPED; it can never report a physical
* capture, and it is not required by spacetime_source_finalize(). */
typedef enum {
SPACETIME_RAY_ACTIVE,
SPACETIME_RAY_ESCAPED,
SPACETIME_RAY_CAPTURED
SPACETIME_RAY_ESCAPED
} SpacetimeRayStatus;
/* Stable identifier for one asymptotic end (infinity) of a backend. Backends
* may describe more than one; the current analytic backends expose one. */
typedef uint32_t SpacetimeEndId;
#define SPACETIME_END_NONE ((SpacetimeEndId)0xffffffffu)
typedef enum {
ASYMPTOTIC_EXTERIOR_MINKOWSKI,
ASYMPTOTIC_EXTERIOR_SCHWARZSCHILD_MONOPOLE
} AsymptoticExteriorKind;
/* Declared asymptotic end. `frame_origin` and the columns of `frame_axes`
* express the asymptotic reference frame in backend coordinates; spatial
* `n_infinity` values use the same coordinate axes as the observer tetrad. */
typedef struct {
SpacetimeEndId end_id;
AsymptoticExteriorKind exterior_kind;
double mass;
double frame_origin[3];
double frame_axes[3][3];
} SpacetimeAsymptoticEnd;
/* Escape worldtube sample at one coordinate time. A zero `radius_rate` and a
* time-independent `velocity` describe the fixed/constant-velocity cases used
* in this phase. `valid == 0` means the backend cannot describe the worldtube
* at this time (history exhausted); callers must not treat that as a miss. */
typedef struct {
double center[3];
double velocity[3];
double radius;
double radius_rate;
/* Nonzero when `velocity` and `radius_rate` are exact throughout the
* current motion segment, so the first entry has a closed quadratic form. */
int velocity_constant;
int valid;
} SpacetimeEscapeWorldtubeSample;
typedef struct SpacetimeSource SpacetimeSource;
typedef struct MetricSlab MetricSlab;
@@ -27,17 +79,31 @@ struct MetricSlab {
};
typedef struct {
int (*eval)(const SpacetimeSource *source, double t, const double x[3],
MetricData *metric);
SpacetimePointStatus (*eval)(const SpacetimeSource *source, double t,
const double x[3], MetricData *metric);
SpacetimeRayStatus (*classify)(const SpacetimeSource *source, double t,
const double x[3]);
int (*load_slab)(const SpacetimeSource *source, double t_hi, double t_lo,
MetricSlab **out);
void (*free_slab)(MetricSlab *slab);
int (*eval_slab)(const MetricSlab *slab, double t, const double x[3],
MetricData *metric);
SpacetimePointStatus (*eval_slab)(const MetricSlab *slab, double t,
const double x[3], MetricData *metric);
SpacetimeRayStatus (*classify_slab)(const MetricSlab *slab, double t,
const double x[3]);
/* Declared asymptotic ends and their moving escape worldtubes. Backends
* without an escape sphere may leave these NULL. */
size_t (*asymptotic_end_count)(const SpacetimeSource *source);
int (*asymptotic_end)(const SpacetimeSource *source, size_t index,
SpacetimeAsymptoticEnd *out);
int (*escape_worldtube_sample)(const SpacetimeSource *source,
SpacetimeEndId end_id, double t,
SpacetimeEscapeWorldtubeSample *out);
/* Coordinate time of the next motion-segment boundary reached while
* integrating backward in time, i.e. the largest boundary strictly less
* than `t`. Return NAN when the worldtube description has a single open
* segment. */
double (*escape_worldtube_next_segment)(const SpacetimeSource *source,
SpacetimeEndId end_id, double t);
/* Analytic backends have negligible per-ray metric state. A numerical
* backend must opt in once its metric slabs and evaluator workspaces need
* to reserve memory alongside the private HDR render buffers. */
@@ -55,27 +121,41 @@ struct SpacetimeSource {
int spacetime_create_default(SpacetimeSource *source);
int spacetime_create_minkowski(SpacetimeSource *source, double escape_radius);
int spacetime_create_schwarzschild_ks(SpacetimeSource *source, double mass,
double escape_radius,
double capture_radius);
/* Moving Alcubierre bubble with x_s(t) = vs*t and x_s(0) = 0. Requires
* |vs| < 1, R > 0, and sigma > 0. */
double escape_radius);
/* Moving Alcubierre bubble with x_s(t) = vs*t and x_s(0) = 0. Requires a
* finite vs, R > 0, and sigma > 0. Sub- and super-luminal |vs| are accepted;
* classify() only ever reports ACTIVE or ESCAPED, and the shared
* camera-relative dark policy may terminate a ray as DARK for any finite vs. */
int spacetime_create_alcubierre(SpacetimeSource *source, double vs,
double radius, double sigma);
/* Bubble-centered escape radius used by the Alcubierre backend; also lets
* callers size their integration step budget. */
double spacetime_alcubierre_escape_radius(double radius, double sigma);
void spacetime_destroy(SpacetimeSource *source);
int spacetime_eval(const SpacetimeSource *source, double t, const double x[3],
MetricData *metric);
SpacetimePointStatus spacetime_eval(const SpacetimeSource *source, double t,
const double x[3], MetricData *metric);
SpacetimeRayStatus spacetime_classify(const SpacetimeSource *source, double t,
const double x[3]);
int spacetime_load_slab(const SpacetimeSource *source, double t_hi, double t_lo,
MetricSlab **out);
void spacetime_free_slab(MetricSlab *slab);
int spacetime_slab_eval(const MetricSlab *slab, double t, const double x[3],
MetricData *metric);
SpacetimePointStatus spacetime_slab_eval(const MetricSlab *slab, double t,
const double x[3], MetricData *metric);
SpacetimeRayStatus spacetime_slab_classify(const MetricSlab *slab, double t,
const double x[3]);
size_t spacetime_asymptotic_end_count(const SpacetimeSource *source);
int spacetime_asymptotic_end(const SpacetimeSource *source, size_t index,
SpacetimeAsymptoticEnd *out);
int spacetime_escape_worldtube_sample(const SpacetimeSource *source,
SpacetimeEndId end_id, double t,
SpacetimeEscapeWorldtubeSample *out);
double spacetime_escape_worldtube_next_segment(const SpacetimeSource *source,
SpacetimeEndId end_id, double t);
/* Common structural validation that every successful constructor must pass
* before returning. A source that passes is a promise that it can safely
* enter ray tracing; backend-specific history/segment validation stays in the
* backend constructor. On failure the constructor must destroy its context. */
int spacetime_source_finalize(SpacetimeSource *source);
int spacetime_limits_render_workers_by_memory(const SpacetimeSource *source);
#endif
+51 -10
View File
@@ -72,8 +72,9 @@ static double alcubierre_shape_derivative(double r, double radius,
* = -v_s (delta_jx d_i f + delta_ix d_j f) / 2,
* where d_i differentiates at fixed t (only the spatial argument of f moves
* with t). K encodes the time dependence required by the 3+1 null-ray RHS. */
static int alcubierre_eval(const SpacetimeSource *source, double t,
const double x[3], MetricData *metric) {
static SpacetimePointStatus alcubierre_eval(const SpacetimeSource *source,
double t, const double x[3],
MetricData *metric) {
const AlcubierreContext *context = source->context;
const double vs = context->vs;
const double dx = x[0] - vs * t;
@@ -81,7 +82,7 @@ static int alcubierre_eval(const SpacetimeSource *source, double t,
double df[3] = {0.0, 0.0, 0.0};
double f;
if (!isfinite(r2))
return -1;
return SPACETIME_POINT_INVALID_METRIC;
const double r = sqrt(r2);
*metric = (MetricData){
.alpha = 1.0,
@@ -103,14 +104,11 @@ static int alcubierre_eval(const SpacetimeSource *source, double t,
metric->K[i][j] =
-0.5 * vs * ((j == 0 ? df[i] : 0.0) + (i == 0 ? df[j] : 0.0));
}
return 0;
return SPACETIME_POINT_OK;
}
/* A warp bubble has no curvature singularity or horizon for |v_s| < 1, so
* rays are only ever ACTIVE or ESCAPED; the exotic matter that would source
* the bubble is treated as optically transparent. The escape sphere follows
* the bubble, so rays terminate only once the metric is flat to machine
* precision at their current location. */
/* The exotic matter that would source the bubble is treated as optically
* transparent. The escape sphere follows the moving bubble. */
static SpacetimeRayStatus alcubierre_classify(const SpacetimeSource *source,
double t, const double x[3]) {
const AlcubierreContext *context = source->context;
@@ -127,9 +125,48 @@ static void alcubierre_destroy(SpacetimeSource *source) {
source->ops = NULL;
}
static size_t alcubierre_asymptotic_end_count(const SpacetimeSource *source) {
(void)source;
return 1;
}
static int alcubierre_asymptotic_end(const SpacetimeSource *source,
size_t index,
SpacetimeAsymptoticEnd *out) {
(void)source;
if (index != 0)
return -1;
*out = (SpacetimeAsymptoticEnd){
.end_id = 0,
.exterior_kind = ASYMPTOTIC_EXTERIOR_MINKOWSKI,
.mass = 0.0,
.frame_origin = {0.0, 0.0, 0.0},
.frame_axes = {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}};
return 0;
}
static int alcubierre_escape_worldtube_sample(
const SpacetimeSource *source, SpacetimeEndId end_id, double t,
SpacetimeEscapeWorldtubeSample *out) {
const AlcubierreContext *context = source->context;
if (end_id != 0)
return -1;
*out = (SpacetimeEscapeWorldtubeSample){
.center = {context->vs * t, 0.0, 0.0},
.velocity = {context->vs, 0.0, 0.0},
.radius = context->escape_radius,
.radius_rate = 0.0,
.velocity_constant = 1,
.valid = 1};
return 0;
}
static const SpacetimeOps alcubierre_ops = {
.eval = alcubierre_eval,
.classify = alcubierre_classify,
.asymptotic_end_count = alcubierre_asymptotic_end_count,
.asymptotic_end = alcubierre_asymptotic_end,
.escape_worldtube_sample = alcubierre_escape_worldtube_sample,
.destroy = alcubierre_destroy,
};
@@ -139,7 +176,7 @@ double spacetime_alcubierre_escape_radius(double radius, double sigma) {
int spacetime_create_alcubierre(SpacetimeSource *source, double vs,
double radius, double sigma) {
if (source == NULL || !isfinite(vs) || fabs(vs) >= 1.0 ||
if (source == NULL || !isfinite(vs) ||
!isfinite(radius) || radius <= 0.0 || !isfinite(sigma) || sigma <= 0.0)
return -1;
/* Reject parameter combinations whose derived domain overflows or does not
@@ -156,6 +193,10 @@ int spacetime_create_alcubierre(SpacetimeSource *source, double vs,
context->escape_radius = escape_radius;
source->ops = &alcubierre_ops;
source->context = context;
if (spacetime_source_finalize(source)) {
alcubierre_destroy(source);
return -1;
}
return 0;
}
+91 -12
View File
@@ -1,5 +1,6 @@
#include "spacetime.h"
#include <math.h>
#include <stddef.h>
#include <stdlib.h>
@@ -8,17 +9,18 @@ void spacetime_destroy(SpacetimeSource *source) {
source->ops->destroy(source);
}
int spacetime_eval(const SpacetimeSource *source, double t, const double x[3],
MetricData *metric) {
return source == NULL || source->ops == NULL
? -1
SpacetimePointStatus spacetime_eval(const SpacetimeSource *source, double t,
const double x[3], MetricData *metric) {
return source == NULL || source->ops == NULL || source->ops->eval == NULL
? SPACETIME_POINT_INTERNAL_ERROR
: source->ops->eval(source, t, x, metric);
}
SpacetimeRayStatus spacetime_classify(const SpacetimeSource *source, double t,
const double x[3]) {
return source == NULL || source->ops == NULL
? SPACETIME_RAY_CAPTURED
/* A missing or incomplete source is never reported as escaped. */
return source == NULL || source->ops == NULL || source->ops->classify == NULL
? SPACETIME_RAY_ACTIVE
: source->ops->classify(source, t, x);
}
@@ -46,10 +48,12 @@ void spacetime_free_slab(MetricSlab *slab) {
free(slab);
}
int spacetime_slab_eval(const MetricSlab *slab, double t, const double x[3],
MetricData *metric) {
if (slab == NULL || t < slab->t_lo || t > slab->t_hi)
return -1;
SpacetimePointStatus spacetime_slab_eval(const MetricSlab *slab, double t,
const double x[3], MetricData *metric) {
if (slab == NULL || slab->source == NULL || slab->source->ops == NULL)
return SPACETIME_POINT_INTERNAL_ERROR;
if (t < slab->t_lo || t > slab->t_hi)
return SPACETIME_POINT_TIME_UNAVAILABLE;
if (slab->source->ops->eval_slab != NULL)
return slab->source->ops->eval_slab(slab, t, x, metric);
return spacetime_eval(slab->source, t, x, metric);
@@ -57,13 +61,88 @@ int spacetime_slab_eval(const MetricSlab *slab, double t, const double x[3],
SpacetimeRayStatus spacetime_slab_classify(const MetricSlab *slab, double t,
const double x[3]) {
if (slab == NULL || t < slab->t_lo || t > slab->t_hi)
return SPACETIME_RAY_CAPTURED;
if (slab == NULL || slab->source == NULL)
return SPACETIME_RAY_ACTIVE;
if (t < slab->t_lo || t > slab->t_hi)
return SPACETIME_RAY_ACTIVE;
if (slab->source->ops->classify_slab != NULL)
return slab->source->ops->classify_slab(slab, t, x);
return spacetime_classify(slab->source, t, x);
}
size_t spacetime_asymptotic_end_count(const SpacetimeSource *source) {
return source == NULL || source->ops == NULL ||
source->ops->asymptotic_end_count == NULL
? 0
: source->ops->asymptotic_end_count(source);
}
int spacetime_asymptotic_end(const SpacetimeSource *source, size_t index,
SpacetimeAsymptoticEnd *out) {
return source == NULL || source->ops == NULL || out == NULL ||
source->ops->asymptotic_end == NULL
? -1
: source->ops->asymptotic_end(source, index, out);
}
int spacetime_escape_worldtube_sample(const SpacetimeSource *source,
SpacetimeEndId end_id, double t,
SpacetimeEscapeWorldtubeSample *out) {
if (source == NULL || source->ops == NULL || out == NULL ||
source->ops->escape_worldtube_sample == NULL)
return -1;
return source->ops->escape_worldtube_sample(source, end_id, t, out);
}
double spacetime_escape_worldtube_next_segment(const SpacetimeSource *source,
SpacetimeEndId end_id,
double t) {
if (source == NULL || source->ops == NULL ||
source->ops->escape_worldtube_next_segment == NULL)
return NAN;
return source->ops->escape_worldtube_next_segment(source, end_id, t);
}
int spacetime_source_finalize(SpacetimeSource *source) {
if (source == NULL || source->ops == NULL || source->context == NULL)
return -1;
const SpacetimeOps *ops = source->ops;
if (ops->eval == NULL || ops->destroy == NULL)
return -1;
const size_t count = spacetime_asymptotic_end_count(source);
if (count == 0)
return 0; /* legacy backend without asymptotic ends */
if (ops->asymptotic_end == NULL || ops->escape_worldtube_sample == NULL)
return -1;
if (count > 64)
return -1;
SpacetimeEndId ids[64];
for (size_t i = 0; i < count; ++i) {
SpacetimeAsymptoticEnd end;
if (spacetime_asymptotic_end(source, i, &end))
return -1;
if (end.end_id == SPACETIME_END_NONE)
return -1;
for (size_t j = 0; j < i; ++j)
if (ids[j] == end.end_id)
return -1;
ids[i] = end.end_id;
if (end.exterior_kind != ASYMPTOTIC_EXTERIOR_MINKOWSKI &&
end.exterior_kind != ASYMPTOTIC_EXTERIOR_SCHWARZSCHILD_MONOPOLE)
return -1;
if (!isfinite(end.mass) || end.mass < 0.0)
return -1;
for (int k = 0; k < 3; ++k) {
if (!isfinite(end.frame_origin[k]))
return -1;
for (int l = 0; l < 3; ++l)
if (!isfinite(end.frame_axes[k][l]))
return -1;
}
}
return 0;
}
int spacetime_limits_render_workers_by_memory(const SpacetimeSource *source) {
return source != NULL && source->ops != NULL &&
source->ops->limit_render_workers_by_memory;
+46 -3
View File
@@ -6,15 +6,16 @@ typedef struct {
double escape_radius;
} MinkowskiContext;
static int minkowski_eval(const SpacetimeSource *source, double t,
const double x[3], MetricData *metric) {
static SpacetimePointStatus minkowski_eval(const SpacetimeSource *source,
double t, const double x[3],
MetricData *metric) {
(void)source;
(void)t;
(void)x;
*metric = (MetricData){
.alpha = 1.0,
.gamma = {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}};
return 0;
return SPACETIME_POINT_OK;
}
static SpacetimeRayStatus minkowski_classify(const SpacetimeSource *source,
@@ -33,9 +34,47 @@ static void minkowski_destroy(SpacetimeSource *source) {
source->ops = NULL;
}
static size_t minkowski_asymptotic_end_count(const SpacetimeSource *source) {
(void)source;
return 1;
}
static int minkowski_asymptotic_end(const SpacetimeSource *source,
size_t index, SpacetimeAsymptoticEnd *out) {
(void)source;
if (index != 0)
return -1;
*out = (SpacetimeAsymptoticEnd){
.end_id = 0,
.exterior_kind = ASYMPTOTIC_EXTERIOR_MINKOWSKI,
.mass = 0.0,
.frame_origin = {0.0, 0.0, 0.0},
.frame_axes = {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}};
return 0;
}
static int minkowski_escape_worldtube_sample(
const SpacetimeSource *source, SpacetimeEndId end_id, double t,
SpacetimeEscapeWorldtubeSample *out) {
const MinkowskiContext *context = source->context;
if (end_id != 0)
return -1;
*out = (SpacetimeEscapeWorldtubeSample){.center = {0.0, 0.0, 0.0},
.velocity = {0.0, 0.0, 0.0},
.radius = context->escape_radius,
.radius_rate = 0.0,
.velocity_constant = 1,
.valid = 1};
(void)t;
return 0;
}
static const SpacetimeOps minkowski_ops = {
.eval = minkowski_eval,
.classify = minkowski_classify,
.asymptotic_end_count = minkowski_asymptotic_end_count,
.asymptotic_end = minkowski_asymptotic_end,
.escape_worldtube_sample = minkowski_escape_worldtube_sample,
.destroy = minkowski_destroy,
};
@@ -48,6 +87,10 @@ int spacetime_create_minkowski(SpacetimeSource *source, double escape_radius) {
context->escape_radius = escape_radius;
source->ops = &minkowski_ops;
source->context = context;
if (spacetime_source_finalize(source)) {
minkowski_destroy(source);
return -1;
}
return 0;
}
+65 -18
View File
@@ -6,22 +6,26 @@
typedef struct {
double mass;
double escape_radius;
double capture_radius;
} SchwarzschildKsContext;
/* Schwarzschild in ingoing Cartesian Kerr--Schild coordinates:
* g_mu_nu = eta_mu_nu + (2 M / r) l_mu l_nu, l_mu = (1, x_i / r).
* These slices are regular at r = 2 M; only the physical r = 0 singularity
* is excluded by the conservative capture cutoff. */
static int schwarzschild_ks_eval(const SpacetimeSource *source, double t,
const double x[3], MetricData *metric) {
* These slices are regular at r = 2 M. Only r = 0 is a coordinate
* singularity; it is reported as a data/domain status, not as a physical
* capture. Normal dark endpoints come from the redshift threshold in the
* geodesic layer (see design section 18). */
static SpacetimePointStatus schwarzschild_ks_eval(const SpacetimeSource *source,
double t, const double x[3],
MetricData *metric) {
const SchwarzschildKsContext *context = source->context;
double r2 = 0.0;
(void)t;
for (int i = 0; i < 3; ++i)
r2 += x[i] * x[i];
if (!isfinite(r2) || r2 <= 0.0)
return -1;
if (!isfinite(r2))
return SPACETIME_POINT_INVALID_METRIC;
if (r2 <= 0.0)
return SPACETIME_POINT_OUT_OF_DOMAIN; /* r = 0 coordinate singularity */
const double r = sqrt(r2);
const double m = context->mass;
const double f = 2.0 * m / r;
@@ -79,11 +83,14 @@ static int schwarzschild_ks_eval(const SpacetimeSource *source, double t,
}
metric->K[i][j] = (d_beta_cov_i_j - connection_term_ij +
d_beta_cov_j_i - connection_term_ji) /
(2.0 * alpha);
(2.0 * alpha);
}
return 0;
return SPACETIME_POINT_OK;
}
/* Optional legacy region test: reports the escape sphere only. It never
* reports a physical capture; the normal dark terminal is the redshift
* threshold in the geodesic layer. */
static SpacetimeRayStatus schwarzschild_ks_classify(
const SpacetimeSource *source, double t, const double x[3]) {
const SchwarzschildKsContext *context = source->context;
@@ -91,8 +98,8 @@ static SpacetimeRayStatus schwarzschild_ks_classify(
(void)t;
for (int i = 0; i < 3; ++i)
r2 += x[i] * x[i];
if (!isfinite(r2) || r2 <= context->capture_radius * context->capture_radius)
return SPACETIME_RAY_CAPTURED;
if (!isfinite(r2))
return SPACETIME_RAY_ACTIVE;
return r2 >= context->escape_radius * context->escape_radius
? SPACETIME_RAY_ESCAPED
: SPACETIME_RAY_ACTIVE;
@@ -104,28 +111,68 @@ static void schwarzschild_ks_destroy(SpacetimeSource *source) {
source->ops = NULL;
}
static size_t schwarzschild_ks_asymptotic_end_count(
const SpacetimeSource *source) {
(void)source;
return 1;
}
static int schwarzschild_ks_asymptotic_end(
const SpacetimeSource *source, size_t index, SpacetimeAsymptoticEnd *out) {
const SchwarzschildKsContext *context = source->context;
if (index != 0)
return -1;
*out = (SpacetimeAsymptoticEnd){
.end_id = 0,
.exterior_kind = ASYMPTOTIC_EXTERIOR_SCHWARZSCHILD_MONOPOLE,
.mass = context->mass,
.frame_origin = {0.0, 0.0, 0.0},
.frame_axes = {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}};
return 0;
}
static int schwarzschild_ks_escape_worldtube_sample(
const SpacetimeSource *source, SpacetimeEndId end_id, double t,
SpacetimeEscapeWorldtubeSample *out) {
const SchwarzschildKsContext *context = source->context;
if (end_id != 0)
return -1;
*out = (SpacetimeEscapeWorldtubeSample){.center = {0.0, 0.0, 0.0},
.velocity = {0.0, 0.0, 0.0},
.radius = context->escape_radius,
.radius_rate = 0.0,
.velocity_constant = 1,
.valid = 1};
(void)t;
return 0;
}
static const SpacetimeOps schwarzschild_ks_ops = {
.eval = schwarzschild_ks_eval,
.classify = schwarzschild_ks_classify,
.asymptotic_end_count = schwarzschild_ks_asymptotic_end_count,
.asymptotic_end = schwarzschild_ks_asymptotic_end,
.escape_worldtube_sample = schwarzschild_ks_escape_worldtube_sample,
.destroy = schwarzschild_ks_destroy,
};
int spacetime_create_schwarzschild_ks(SpacetimeSource *source, double mass,
double escape_radius,
double capture_radius) {
if (source == NULL || mass <= 0.0 || escape_radius <= 2.0 * mass ||
capture_radius <= 0.0 || capture_radius >= 2.0 * mass ||
capture_radius >= escape_radius)
double escape_radius) {
if (source == NULL || mass <= 0.0 || escape_radius <= 2.0 * mass)
return -1;
SchwarzschildKsContext *context = malloc(sizeof *context);
if (context == NULL)
return -1;
*context = (SchwarzschildKsContext){mass, escape_radius, capture_radius};
*context = (SchwarzschildKsContext){mass, escape_radius};
source->ops = &schwarzschild_ks_ops;
source->context = context;
if (spacetime_source_finalize(source)) {
schwarzschild_ks_destroy(source);
return -1;
}
return 0;
}
int spacetime_create_default(SpacetimeSource *source) {
return spacetime_create_schwarzschild_ks(source, 1.0, 256.0, 1.5);
return spacetime_create_schwarzschild_ks(source, 1.0, 256.0);
}
+1 -1
View File
@@ -98,7 +98,7 @@ int main(int argc, char **argv) {
}
printf("selection: raw_magnification=[%.17g,%.17g) exposure=%.17g max_cache_flux=%.17g min_y=%.17g\n",selected_min_mag,selected_max_mag,exposure,max_flux,min_y);
LensMap map={0}; StarCatalog catalog={0};
if (lens_map_read(argv[1],&map) || map.frame_count!=1 ||
if (lens_map_read(argv[1],NULL,&map) || map.frame_count!=1 ||
last>map.frames[0].mesh.triangle_count || catalog_load_csv(&catalog,argv[2]) ||
catalog.count>32768) return 2;
if (blackbody_backend_init(NULL, 0, NAN, NAN, NULL, stderr)) return 1;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

After

Width:  |  Height:  |  Size: 188 KiB

+674
View File
@@ -0,0 +1,674 @@
#!/usr/bin/env python3
"""Exercise the production DP54 default, v3 lens-map provenance/replay and the
adaptive retry/budget policy from the CLI.
The production default is now adaptive Dormand-Prince 5(4). These checks
require that a run without --integrator exports wire code 1 and is physically
and bit-for-bit equal to an explicit --integrator dp54 run, so `make test`
truly covers the production default rather than only the explicit path.
--integrator rk4 stays available for the legacy wire code and for a
fixed-step reference convergence check. Small CPU 16x8/32x16 scenes keep the
runtime short; physical comparisons use stored lens-map endpoints, not only the
rendered PNG.
"""
import math
import os
import re
import struct
import subprocess
import sys
import tempfile
import zlib
from pathlib import Path
# Keep scratch data inside the pre-approved OpenCode scratch directory instead
# of creating directories directly under the system temporary root.
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 wire offsets (see src/lens_map.c). The header is deliberately not part of
# the payload CRC.
VERSION_OFFSET = 8
FRAME_COUNT_OFFSET = 32
PROVENANCE_OFFSET = 40
PROVENANCE_V3_OFFSET = 100
VERTEX_COUNT_OFFSET = 200
TRIANGLE_COUNT_OFFSET = 208
VERTEX_START = 224
VERTEX_SIZE = 108
TRIANGLE_SIZE = 32
ATOL_FIELDS = ('atol_x', 'atol_Pi', 'atol_L', 'rtol', 'min_step', 'max_step',
'max_lookback_time', 'retry_lookback_increment',
'max_total_lookback_time')
# Machine-roundoff floor for comparing two adaptive integrations; below this a
# difference carries no convergence information.
ROUNDOFF_FLOOR = 1e-12
ENDPOINT_ASSERT = 1e-6
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{result.stderr}')
return result
def image_payload(path):
data = path.read_bytes()
assert data[:8] == b'\x89PNG\r\n\x1a\n', f'not a PNG: {path}'
offset, compressed = 8, bytearray()
while offset < len(data):
count, kind = struct.unpack_from('>I4s', data, offset)
payload = data[offset + 8:offset + 8 + count]
if kind == b'IDAT':
compressed.extend(payload)
offset += count + 12
return bytes(zlib.decompress(compressed))
def map_provenance(path):
data = path.read_bytes()
assert data[:8] == b'GRLENS\x01\x00'
version = struct.unpack_from('<I', data, VERSION_OFFSET)[0]
frame_count = struct.unpack_from('<Q', data, FRAME_COUNT_OFFSET)[0]
base = PROVENANCE_OFFSET
prov = {}
prov['threshold_kind'], prov['threshold_policy_version'] = struct.unpack_from(
'<II', data, base)
prov['threshold_value'] = struct.unpack_from('<d', data, base + 8)[0]
(prov['retry_step_increment'], prov['max_total_steps'], prov['max_level'],
prov['integrator']) = struct.unpack_from('<IIII', data, base + 16)
prov['min_edge_pixels'] = struct.unpack_from('<d', data, base + 32)[0]
prov['min_area_pixels2'] = struct.unpack_from('<d', data, base + 40)[0]
prov['coordinate_time_step'] = struct.unpack_from('<d', data, base + 48)[0]
prov['initial_max_steps'] = struct.unpack_from('<I', data, base + 56)[0]
if version == 3:
off = PROVENANCE_V3_OFFSET
for name in ATOL_FIELDS:
prov[name] = struct.unpack_from('<d', data, off)[0]
off += 8
prov['max_consecutive_rejections'] = struct.unpack_from('<I', data, off)[0]
return version, frame_count, prov
def map_vertices(path):
data = path.read_bytes()
version = struct.unpack_from('<I', data, VERSION_OFFSET)[0]
assert version == 3, f'expected a v3 map, got v{version}'
vertices = struct.unpack_from('<Q', data, VERTEX_COUNT_OFFSET)[0]
triangles = struct.unpack_from('<Q', data, TRIANGLE_COUNT_OFFSET)[0]
offset = VERTEX_START
values = []
for _ in range(vertices):
values.append(struct.unpack_from('<9dIIIQQQ', data, offset))
offset += VERTEX_SIZE
return values, data[offset:offset + triangles * TRIANGLE_SIZE]
def endpoint_deviation(a, b):
"""(mismatched provenance count, max direction/log-g deviation).
Vertices whose end_id/outcome/reason differ are counted as mismatches and
excluded from the numeric deviation; the caller requires zero mismatches.
"""
mismatches = 0
worst = 0.0
assert len(a) == len(b)
for x, y in zip(a, b):
if x[9:12] != y[9:12]:
mismatches += 1
continue
for k in (3, 4, 5, 6, 7, 8):
worst = max(worst, abs(x[k] - y[k]))
return mismatches, worst
def trace_cost(text, label):
match = re.search(label + r' trace cost: accepted=(\d+) rejected=(\d+) '
r'rhs=(\d+)', text)
return None if match is None else match.groups()
def sum_rhs(vertices):
return sum(v[14] for v in vertices)
def alcubierre_budget(escape, vs, dark_threshold):
"""Reference implementation of the production Alcubierre time allowance:
B = 5*escape / max(|1-|v_s||, exp(-D)), with the DBL_MIN..DBL_MAX/4
saturation and the log-space fallback used when the ordinary division is
not finite and positive. exp(D) is never formed."""
sep = abs(1.0 - abs(vs))
floor = math.exp(-dark_threshold)
denom = max(sep, floor)
upper = sys.float_info.max / 4.0
if denom > 0.0 and math.isfinite(denom):
scaled = 5.0 * (escape / denom)
if math.isfinite(scaled) and scaled > 0.0:
return min(max(scaled, sys.float_info.min), upper)
log_sep = math.log(sep) if sep > 0.0 else -math.inf
log_budget = math.log(5.0) + math.log(escape) - max(log_sep, -dark_threshold)
if not math.isfinite(log_budget):
log_budget = math.log(upper)
log_budget = min(log_budget, math.log(upper))
log_budget = max(log_budget, math.log(sys.float_info.min))
return min(max(math.exp(log_budget), sys.float_info.min), upper)
with tempfile.TemporaryDirectory(prefix='gr-adaptive-cli-',
dir=str(TMP_ROOT)) as directory:
tmp = Path(directory)
for backend in ('minkowski', 'schwarzschild'):
binary = BUILD / f'{backend}_sky'
if not binary.exists():
print(f'{backend}: binary absent, skipping', flush=True)
continue
help_text = run(binary, '--help').stdout
for option in ('--integrator', '--ode-rtol', '--ode-atol-x',
'--ode-atol-pi', '--ode-atol-l', '--ode-initial-step',
'--ode-min-step', '--ode-max-step',
'--ode-max-rejections', '--trace-max-steps',
'--trace-lookback-time', '--retry-step-increment',
'--max-total-steps', '--retry-lookback-increment',
'--max-total-lookback-time'):
assert option in help_text, (backend, option)
ext = 'png' if '.png' in help_text else 'ppm'
hdr_available = '--hdr-output' in help_text
hdr_args = ['--hdr-output'] if hdr_available else []
common = ['--catalog', 'assets/sky_grid_5deg.csv', '--width', 32,
'--height', 16, '--fov-deg', 80, '--exposure', 1e-3,
'--coarse-cell-pixels', 8, '--refine-max-level', 0,
'--psf-relative-tail', 1e-4]
def single(name, *options, ok=True, env=ENV, use_common=common):
out = tmp / f'{backend}_{name}.{ext}'
result = run(binary, *use_common, '--output', out, *options,
ok=ok, env=env)
return out, result
# 1) The production default (no --integrator) must be DP54 and must
# match an explicit --integrator dp54 run physically and in its
# PNG/HDR output.
dflt_map = tmp / f'{backend}_dflt.grlens'
dflt_out, dflt_run = single('dflt', *hdr_args, '--verbose',
'--lens-map-output', dflt_map)
version, frame_count, dflt_prov = map_provenance(dflt_map)
assert version == 3 and frame_count == 1
assert dflt_prov['integrator'] == 1, dflt_prov
assert dflt_prov['min_step'] == 1e-12, dflt_prov
assert dflt_prov['max_step'] == {'minkowski': 16.0,
'schwarzschild': 8.0}[backend], dflt_prov
assert dflt_prov['min_step'] <= dflt_prov['coordinate_time_step'] \
<= dflt_prov['max_step']
assert dflt_prov['atol_x'] > 0 and dflt_prov['rtol'] > 0
assert dflt_prov['max_lookback_time'] > 0
assert dflt_prov['max_consecutive_rejections'] > 0
expl_map = tmp / f'{backend}_expl.grlens'
expl_out, _ = single('expl', *hdr_args, '--integrator', 'dp54',
'--lens-map-output', expl_map)
assert dflt_map.read_bytes() == expl_map.read_bytes(), \
'default map differs from explicit dp54'
assert image_payload(dflt_out) == image_payload(expl_out)
if hdr_available:
dflt_hdr = dflt_out.with_name(dflt_out.stem + '_HDR.fits')
expl_hdr = expl_out.with_name(expl_out.stem + '_HDR.fits')
assert dflt_hdr.read_bytes() == expl_hdr.read_bytes()
dflt_vertices, _ = map_vertices(dflt_map)
# 2) The legacy RK4 wire code must be 0 and its cost counters must be
# real (nonzero RHS evaluations), not legacy zeros.
rk4_map = tmp / f'{backend}_rk4.grlens'
single('rk4', '--integrator', 'rk4', '--lens-map-output', rk4_map)
_, _, rk4_prov = map_provenance(rk4_map)
assert rk4_prov['integrator'] == 0, rk4_prov
rk4_vertices, _ = map_vertices(rk4_map)
assert sum_rhs(rk4_vertices) > 0, 'RK4 RHS cost counters are not real'
# 3) Same-camera tolerance convergence with three levels. Outcome,
# reason and end must not change between levels, and the deviation
# from the tightest reference must shrink as the tolerance tightens.
# These are local ODE tolerances, not a global sky-error bound.
tol_maps = {}
for tol in ('1e-7', '1e-9', '1e-12'):
m = tmp / f'{backend}_tol_{tol}.grlens'
single(f'tol_{tol}', '--integrator', 'dp54', '--ode-rtol', tol,
'--ode-atol-x', tol, '--ode-atol-pi', tol,
'--ode-atol-l', tol, '--lens-map-output', m)
tol_maps[tol] = map_vertices(m)[0]
mism7, err7 = endpoint_deviation(tol_maps['1e-7'], tol_maps['1e-12'])
mism9, err9 = endpoint_deviation(tol_maps['1e-9'], tol_maps['1e-12'])
assert mism7 == 0 and mism9 == 0, \
f'{backend}: tolerance levels disagree on outcome/end'
assert err9 < ENDPOINT_ASSERT, (backend, 'default vs tight', err9)
assert err7 + ROUNDOFF_FLOOR >= err9, \
f'{backend}: tightening tolerance did not reduce error ' \
f'({err7} -> {err9})'
print(f'{backend}: default==dp54, tol errors 1e-7={err7:.3g} '
f'1e-9={err9:.3g}', flush=True)
# 4) Render-only replay of the default map must be bit-identical and
# its stored statistics must equal the live trace cost.
replay_out = tmp / f'{backend}_replay.{ext}'
replay_run = run(binary, *common, *hdr_args, '--lens-map-input',
dflt_map, '--verbose', '--output', replay_out)
assert image_payload(replay_out) == image_payload(dflt_out)
if hdr_available:
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.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)
# 5) Explicit zeros in the retry policy must survive, not be filled in
# by the derived defaults.
zero_step_map = tmp / f'{backend}_zero_step.grlens'
single('zero_step', '--integrator', 'dp54', '--retry-step-increment',
'0', '--lens-map-output', zero_step_map)
_, _, zero_step = map_provenance(zero_step_map)
assert zero_step['retry_step_increment'] == 0, zero_step
assert zero_step['max_total_steps'] > 0, zero_step
zero_time_map = tmp / f'{backend}_zero_time.grlens'
single('zero_time', '--integrator', 'dp54',
'--retry-lookback-increment', '0', '--lens-map-output',
zero_time_map)
_, _, zero_time = map_provenance(zero_time_map)
assert zero_time['retry_lookback_increment'] == 0, zero_time
assert zero_time['max_total_lookback_time'] >= \
zero_time['max_lookback_time'], zero_time
# 6) RK4 rejects every DP-only option rather than silently ignoring it.
rk4_errors = [
(['--integrator', 'rk4', '--ode-rtol', 1e-9], 'applies only'),
(['--integrator', 'rk4', '--ode-min-step', 1e-9], 'applies only'),
(['--integrator', 'rk4', '--ode-max-step', 1e-3], 'applies only'),
(['--integrator', 'rk4', '--ode-max-rejections', 4], 'applies only'),
(['--integrator', 'rk4', '--trace-lookback-time', 1], 'applies only'),
(['--integrator', 'rk4', '--retry-lookback-increment', 1], 'applies only'),
(['--integrator', 'rk4', '--max-total-lookback-time', 2], 'applies only'),
]
# DP cross-field validation and malformed values fail immediately.
invalid = [
(['--integrator', 'bogus'], None),
(['--integrator'], None),
(['--integrator', 'dp54', '--ode-rtol', 0], None),
(['--integrator', 'dp54', '--ode-rtol', -1], None),
(['--integrator', 'dp54', '--ode-atol-x', 'nan'], None),
(['--integrator', 'dp54', '--ode-max-rejections', 0], None),
(['--integrator', 'dp54', '--trace-max-steps', 0], None),
(['--integrator', 'dp54', '--ode-initial-step', 5,
'--ode-max-step', 1], 'min <= initial <= max'),
(['--integrator', 'dp54', '--ode-min-step', 4,
'--ode-initial-step', 2], 'min <= initial <= max'),
(['--integrator', 'dp54', '--trace-lookback-time', 0], None),
(['--integrator', 'dp54', '--retry-step-increment', -1], None),
]
for options, message in rk4_errors + invalid:
missing = tmp / 'adaptive_should_not_exist.csv'
result = run(binary, '--catalog', missing, *options, ok=False)
if message:
assert message in result.stderr, (options, result.stderr)
assert not missing.exists(), result.stderr
# 7) Replay must consume the stored policy: explicit tracing options
# cannot be layered on top of --lens-map-input.
conflict = run(binary, *common, '--lens-map-input', dflt_map,
'--integrator', 'rk4', '--output',
tmp / 'conflict.png', ok=False)
assert 'cannot be combined with --lens-map-input' in conflict.stderr, \
conflict.stderr
conflict2 = run(binary, *common, '--lens-map-input', dflt_map,
'--trace-max-steps', 100, '--output',
tmp / 'conflict2.png', ok=False)
assert 'cannot be combined with --lens-map-input' in conflict2.stderr, \
conflict2.stderr
conflict3 = run(binary, *common, '--lens-map-input', dflt_map,
'--integrator', 'dp54', '--ode-rtol', 1e-9,
'--output', tmp / 'conflict3.png', ok=False)
assert 'cannot be combined with --lens-map-input' in conflict3.stderr, \
conflict3.stderr
# 8) Single vs movie: the same physical observer event must agree, and
# threads/slabs must not change the stored adaptive endpoints.
track = tmp / f'{backend}.csv'
observer_test = TESTDIR / f'test_observer_{backend}'
if observer_test.exists():
run(observer_test, track)
moving_single = tmp / f'{backend}_moving.grlens'
single('moving', '--observer-position', 3, -4, 5,
'--observer-velocity', 0.2, -0.1, 0.3,
'--look-ra-deg', 37, '--look-dec-deg', -23,
'--camera-roll-deg', 19, '--lens-map-output', moving_single)
movie_map = tmp / f'{backend}_movie.grlens'
run(binary, *common, '--observer-track', track, '--frames-dir', tmp,
'--frames-prefix', f'{backend}_movie', '--duration', 0,
'--fps', 1, '--lens-map-output', movie_map)
single_v, _ = map_vertices(moving_single)
movie_v, _ = map_vertices(movie_map)
mism, dev = endpoint_deviation(single_v, movie_v)
assert mism == 0 and dev < ENDPOINT_ASSERT, (mism, dev)
thread_maps = []
for threads in (1, 2, 4):
m = tmp / f'{backend}_movie_{threads}thr.grlens'
run(binary, *common, '--observer-track', track, '--frames-dir',
tmp, '--frames-prefix', f'{backend}_m{threads}',
'--duration', 0, '--fps', 1, '--lens-map-output', m,
env=dict(ENV, OMP_NUM_THREADS=str(threads)))
thread_maps.append(m)
reference = thread_maps[0].read_bytes()
for m in thread_maps[1:]:
assert m.read_bytes() == reference, \
f'{backend}: DP movie map changed across threads'
slab_maps = []
for slab in (2, 8, 64):
m = tmp / f'{backend}_slab_{slab}.grlens'
run(binary, *common, '--observer-track', track, '--frames-dir',
tmp, '--frames-prefix', f'{backend}_s{slab}',
'--slab-duration', slab, '--duration', 0, '--fps', 1,
'--lens-map-output', m)
slab_maps.append(m)
base_v, _ = map_vertices(slab_maps[0])
for m in slab_maps[1:]:
other_v, _ = map_vertices(m)
mism, dev = endpoint_deviation(base_v, other_v)
assert mism == 0 and dev < ENDPOINT_ASSERT, (mism, dev)
print(f'{backend}: DP movie threads/slabs agree', flush=True)
# 9) Budget: a tiny initial coordinate-time budget leaves UNRESOLVED
# rays; with no room to grow the publication gate refuses the frame,
# while retry increments that can grow resolve it.
if backend == 'schwarzschild':
refused = tmp / f'{backend}_refused.{ext}'
small = ['--integrator', 'dp54', '--trace-lookback-time', 1e-6,
'--retry-lookback-increment', 0,
'--max-total-lookback-time', 1e-6]
result = run(binary, *common, '--output', refused, *small, ok=False)
assert 'Incomplete render refused' in result.stderr, result.stderr
assert not refused.exists()
allow = tmp / f'{backend}_allow.{ext}'
run(binary, *common, '--output', allow, '--allow-incomplete', *small)
assert allow.exists()
direct = tmp / f'{backend}_direct.{ext}'
direct_result = subprocess.run(
[str(binary), *map(str, common), '--output', str(direct),
'--integrator', 'dp54', '--trace-lookback-time', '4000'],
env=ENV, capture_output=True, text=True)
if direct_result.returncode == 0:
retried = tmp / f'{backend}_retried.{ext}'
run(binary, *common, '--output', retried, '--integrator', 'dp54',
'--trace-lookback-time', 1e-6,
'--retry-lookback-increment', 25,
'--max-total-lookback-time', 4000)
assert image_payload(retried)
print('schwarzschild: retry budget resolves previously '
'unresolved rays', flush=True)
else:
print('schwarzschild: direct budget scene still unresolved; '
'retry-resolution case skipped', flush=True)
# 10) Fixed-step reference convergence on a small scene. The
# accepted budget is explicit and large so the finer step does
# not silently shorten the traced history. Both fixed-step
# levels and the DP54 default must agree below ENDPOINT_ASSERT.
# If the reference itself does not converge this must FAIL and
# be reported, never loosened into a false zero.
ref_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 ref_map(tag, *options):
m = tmp / f'{backend}_ref_{tag}.grlens'
run(binary, *ref_common, '--output',
tmp / f'{backend}_ref_{tag}.{ext}', '--lens-map-output', m,
*options)
return map_vertices(m)[0]
rk4_04 = ref_map('rk4_04', '--integrator', 'rk4',
'--ode-initial-step', 0.04, '--trace-max-steps',
'262144')
rk4_02 = ref_map('rk4_02', '--integrator', 'rk4',
'--ode-initial-step', 0.02, '--trace-max-steps',
'262144')
dp_tight = ref_map('dp_tight', '--ode-rtol', '1e-12',
'--ode-atol-x', '1e-12', '--ode-atol-pi',
'1e-12', '--ode-atol-l', '1e-12')
assert sum(1 for v in rk4_02 if v[10] == 0) > 0, \
'reference scene has no escaped ray'
mism, ref_dev = endpoint_deviation(rk4_04, rk4_02)
assert mism == 0, 'RK4 reference levels disagree on outcome/end'
assert ref_dev < ENDPOINT_ASSERT, \
f'RK4 reference not converged: {ref_dev}; report to parent'
mism, dp_dev = endpoint_deviation(dp_tight, rk4_02)
assert mism == 0, 'DP54 default disagrees with RK4 reference'
assert dp_dev < ENDPOINT_ASSERT, (dp_dev,)
print(f'schwarzschild: RK4 ref convergence {ref_dev:.3g}, '
f'DP default vs fine RK4 {dp_dev:.3g}', flush=True)
print(f'{backend}: adaptive CLI checks passed', flush=True)
# Alcubierre production policy: sub- and super-luminal velocities share one
# finite resource allowance B = 5*escape / max(|1-|v_s||, exp(-D)); the DP54
# default must validate, actually integrate the warp feature, and reach both
# the shared DARK terminal and escapes. All images are tiny 8x4/16x8.
alc = BUILD / 'alcubierre_sky'
if not alc.exists():
print('alcubierre: binary absent, skipping', flush=True)
else:
alc_help = run(alc, '--help').stdout
ext = 'png' if '.png' in alc_help else 'ppm'
assert '|v_s| < 1' not in alc_help, 'help still claims |v_s| < 1'
escape1 = 1.0 + 20.0 / 1.0 # R = sigma = 1
def alc_camera(vs, ra_deg, dec_deg=0.0):
return ['--observer-position', '0', '0', '0',
'--observer-velocity', repr(vs), '0', '0',
'--look-ra-deg', repr(ra_deg),
'--look-dec-deg', repr(dec_deg)]
def alc_map_prov(tag, vs, D=8.0, radius=1.0, extra=(), allow=True,
width=8, height=4, cell=4, camera=None,
catalog=True):
m = tmp / f'alc_{tag}.grlens'
args = ['--alcubierre-vs', repr(vs), '--alcubierre-radius',
repr(radius), '--alcubierre-sigma', '1',
'--dark-threshold', repr(D), '--width', str(width),
'--height', str(height), '--fov-deg', 80, '--exposure',
'1e-3', '--coarse-cell-pixels', str(cell),
'--refine-max-level', 0, '--psf-relative-tail', 1e-4]
if catalog:
args += ['--catalog', 'assets/sky_grid_5deg.csv']
if camera is not None:
args += camera
if allow:
args.append('--allow-incomplete')
args += ['--output', str(tmp / f'alc_{tag}.{ext}'),
'--lens-map-output', str(m), *extra]
run(alc, *args)
return map_provenance(m)[2], m
# 1) Ordinary sub-luminal separation: B is exactly 5*escape/sep and is
# independent of the step count and of the initial step.
p_sub, _ = alc_map_prov('sub', 0.3, camera=alc_camera(0.3, 0.0))
assert p_sub['integrator'] == 1, p_sub
assert math.isclose(p_sub['max_lookback_time'],
alcubierre_budget(escape1, 0.3, 8.0), rel_tol=1e-12)
assert p_sub['min_step'] <= p_sub['coordinate_time_step'] \
<= p_sub['max_step']
p_steps, _ = alc_map_prov('sub_steps', 0.3,
extra=('--trace-max-steps', '8'),
camera=alc_camera(0.3, 0.0))
assert p_steps['initial_max_steps'] == 8
assert math.isclose(p_steps['max_lookback_time'],
alcubierre_budget(escape1, 0.3, 8.0), rel_tol=1e-12)
p_step, _ = alc_map_prov('sub_step', 0.3,
extra=('--ode-initial-step', '0.02'),
camera=alc_camera(0.3, 0.0))
assert abs(p_step['coordinate_time_step'] - 0.02) < 1e-15
assert math.isclose(p_step['max_lookback_time'],
alcubierre_budget(escape1, 0.3, 8.0), rel_tol=1e-12)
# 2) Near-luminal (vs = 1, separation 0): the exp(-D) floor makes the
# allowance finite, and it grows with the dark threshold D.
p8, _ = alc_map_prov('near8', 1.0, D=8.0, camera=alc_camera(1.0, 0.0))
p12, _ = alc_map_prov('near12', 1.0, D=12.0, camera=alc_camera(1.0, 0.0))
assert math.isclose(p8['max_lookback_time'],
alcubierre_budget(escape1, 1.0, 8.0), rel_tol=1e-12)
assert math.isclose(p12['max_lookback_time'],
alcubierre_budget(escape1, 1.0, 12.0), rel_tol=1e-12)
assert p12['max_lookback_time'] > p8['max_lookback_time']
# 3) Both sides of the threshold-derived vcut and exactly luminal
# values use either separation or the finite floor; a tiny film traces
# and resolves without INCOMPLETE outcomes.
vcut = 1.0 - math.exp(-8.0)
for vs in (0.999, math.nextafter(vcut, 0.0),
math.nextafter(vcut, 1.0), 0.9999, math.nextafter(1.0, 0.0),
math.nextafter(1.0, 2.0)):
pv, mv = alc_map_prov(f'vcut_{vs!r}', vs, camera=alc_camera(vs, 0.0))
assert math.isclose(pv['max_lookback_time'],
alcubierre_budget(escape1, vs, 8.0),
rel_tol=1e-12), (vs, pv['max_lookback_time'])
vv, _ = map_vertices(mv)
assert 3 not in {v[10] for v in vv}, (vs, 'INCOMPLETE ray')
assert sum_rhs(vv) > 0, vs
# 4) The default camera (0,0,15 for R=5) must work with a superluminal
# bubble: the generic radius-15 camera lies inside escape radius 25.
pd, md = alc_map_prov('defaultcam', 2.0, radius=5.0)
dv, _ = map_vertices(md)
assert 3 not in {v[10] for v in dv}, 'default camera left INCOMPLETE rays'
assert sum_rhs(dv) > 0
# 5) Real small images at the critical velocities: the direction along
# the bubble motion is the DARK direction (Pi_x = sign(vs)); the
# central vertex must be DARK and some edge ray must escape. The
# 16x8 film with a 4-pixel coarse cell places a vertex exactly at the
# image center.
for vs in (1.0, -1.0, 2.0, -2.0):
ra = 0.0 if vs < 0.0 else 180.0
pimg, mimg = alc_map_prov(f'img_{vs!r}', vs, width=16, height=8,
cell=4, camera=alc_camera(vs, ra))
verts, _ = map_vertices(mimg)
outcomes = {v[10] for v in verts}
assert 3 not in outcomes, (vs, 'INCOMPLETE outcome', outcomes)
assert 0 in outcomes and 1 in outcomes, (vs, outcomes)
assert sum_rhs(verts) > 0, vs
central = min(verts, key=lambda v: (v[0] - 8.0) ** 2
+ (v[1] - 4.0) ** 2)
assert central[10] == 1, \
(vs, 'central vertex is not DARK', central[0], central[1],
central[10])
# 6) Extreme tiny-budget quota path: an explicit 4-step, 1-time
# allowance from an inside camera must stop as UNRESOLVED, never as
# a fabricated escape from a trace that took no steps.
extreme = (alc_camera(0.99999999, 0.0)
+ ['--alcubierre-vs', '0.99999999', '--alcubierre-radius',
'1', '--alcubierre-sigma', '1', '--catalog',
'assets/sky_grid_5deg.csv', '--width', '8', '--height',
'4', '--fov-deg', '80', '--exposure', '1e-3',
'--coarse-cell-pixels', '4', '--refine-max-level', 0,
'--psf-relative-tail', '1e-4'])
ext_map = tmp / 'alc_extreme.grlens'
run(alc, *extreme, '--integrator', 'dp54',
'--trace-lookback-time', '1', '--trace-max-steps', '4',
'--max-total-steps', '4', '--retry-step-increment', '0',
'--max-total-lookback-time', '1', '--retry-lookback-increment', '0',
'--allow-incomplete', '--output', tmp / f'alc_extreme.{ext}',
'--lens-map-output', ext_map)
_, _, ext_prov = map_provenance(ext_map)
assert ext_prov['integrator'] == 1
assert abs(ext_prov['max_lookback_time'] - 1.0) < 1e-15, ext_prov
assert ext_prov['initial_max_steps'] == 4, ext_prov
ext_vertices, _ = map_vertices(ext_map)
assert all(v[10] == 2 for v in ext_vertices), \
('a 1-time/4-step trace fabricated a non-UNRESOLVED outcome',
[v[10] for v in ext_vertices])
# 7) Large dark threshold (D=1000) with vs=1: exp(-1000) underflows to
# 0 and the separation is exactly 0, so the log fallback saturates
# the default allowance to DBL_MAX/4. A 4-step/4-total cap with
# retry disabled keeps the trace short while the provenance records
# the non-masked saturated default. An explicit --trace-lookback
# overrides it without masking the independent step allowance.
psat, _ = alc_map_prov('sat1000', 1.0, D=1000.0,
extra=('--trace-max-steps', '4',
'--max-total-steps', '4',
'--retry-step-increment', '0'),
camera=alc_camera(1.0, 0.0))
assert math.isclose(psat['max_lookback_time'],
sys.float_info.max / 4.0, rel_tol=1e-12), \
psat['max_lookback_time']
assert psat['initial_max_steps'] == 4
psat_ov, _ = alc_map_prov('sat1000_ov', 1.0, D=1000.0,
extra=('--trace-lookback-time', '1',
'--trace-max-steps', '4',
'--max-total-steps', '4',
'--retry-step-increment', '0'),
camera=alc_camera(1.0, 0.0))
assert psat_ov['max_lookback_time'] == 1.0, psat_ov
assert psat_ov['initial_max_steps'] == 4, psat_ov
# Explicit time override must not mask the (default) step allowance.
pind, _ = alc_map_prov('indep', 1.0, D=8.0,
extra=('--trace-lookback-time', '1'),
camera=alc_camera(1.0, 0.0))
assert pind['max_lookback_time'] == 1.0, pind
assert pind['initial_max_steps'] > 1, pind
# 8) RK4 guard: a super-luminal default (vs=2, small estimate) is no
# longer rejected; the guard triggers only when the *derived default*
# estimate exceeds the cap (D=12, vs=1), and an explicit
# --trace-max-steps is a user allowance that bypasses it.
run(alc, '--integrator', 'rk4', '--alcubierre-vs', '2',
'--alcubierre-radius', '1', '--alcubierre-sigma', '1',
'--catalog', 'assets/sky_grid_5deg.csv', '--width', '8',
'--height', '4', '--fov-deg', '80', '--exposure', '1e-3',
'--coarse-cell-pixels', '4', '--refine-max-level', 0,
'--psf-relative-tail', '1e-4', '--allow-incomplete',
'--output', tmp / f'alc_rk4_v2.{ext}')
rk4_guard = run(
alc, '--integrator', 'rk4', '--alcubierre-vs', '1',
'--alcubierre-radius', '1', '--alcubierre-sigma', '1',
'--dark-threshold', '12', '--catalog', 'assets/sky_grid_5deg.csv',
'--width', '8', '--height', '4', '--fov-deg', '80',
'--exposure', '1e-3', '--coarse-cell-pixels', '4',
'--refine-max-level', 0, '--psf-relative-tail', '1e-4',
'--output', tmp / f'alc_rk4_guard.{ext}', ok=False)
assert rk4_guard.returncode == 2, rk4_guard.stderr
assert 'cap' in rk4_guard.stderr, rk4_guard.stderr
run(alc, '--integrator', 'rk4', '--alcubierre-vs', '1',
'--alcubierre-radius', '1', '--alcubierre-sigma', '1',
'--dark-threshold', '12', '--trace-max-steps', '4',
'--catalog', 'assets/sky_grid_5deg.csv', '--width', '8',
'--height', '4', '--fov-deg', '80', '--exposure', '1e-3',
'--coarse-cell-pixels', '4', '--refine-max-level', 0,
'--psf-relative-tail', '1e-4', '--allow-incomplete',
'--output', tmp / f'alc_rk4_expl.{ext}')
# 9) Impossible parameters stay clearly rejected.
for bad in (['--alcubierre-vs', 'nan'], ['--alcubierre-vs', 'inf'],
['--alcubierre-radius', '0'], ['--alcubierre-sigma', '0']):
bad_out = tmp / f'alc_bad.{ext}'
rejected = run(alc, *bad, '--catalog', 'assets/sky_grid_5deg.csv',
'--width', '8', '--height', '4', '--fov-deg', '80',
'--exposure', '1e-3', '--coarse-cell-pixels', '4',
'--refine-max-level', 0, '--psf-relative-tail', '1e-4',
'--output', bad_out, ok=False)
assert rejected.returncode != 0, rejected.stderr
assert not bad_out.exists()
print('alcubierre: budget/provenance, vcut scans, vcut images with '
'central DARK, default camera, large-D saturation, explicit '
'overrides and RK4 guard all passed', flush=True)
+116 -7
View File
@@ -35,8 +35,24 @@ int main(void) {
SpacetimeSource source = {0};
MetricData metric;
CHECK(spacetime_create_alcubierre(&source, vs, radius, sigma) == 0);
CHECK(spacetime_create_alcubierre(&source, 1.0, radius, sigma) != 0);
CHECK(spacetime_create_alcubierre(&source, -1.5, radius, sigma) != 0);
/* Sub- and super-luminal velocities are both accepted. A successful
* constructor installs a context, so each acceptance uses its own temporary
* source that is destroyed immediately; the shared `source` above is never
* overwritten with a second live context. */
{
SpacetimeSource luminal = {0};
CHECK(spacetime_create_alcubierre(&luminal, 1.0, radius, sigma) == 0);
spacetime_destroy(&luminal);
}
{
SpacetimeSource superluminal = {0};
CHECK(spacetime_create_alcubierre(&superluminal, -1.5, radius, sigma) == 0);
spacetime_destroy(&superluminal);
}
/* Non-finite velocities stay rejected. */
CHECK(spacetime_create_alcubierre(&source, NAN, radius, sigma) != 0);
CHECK(spacetime_create_alcubierre(&source, INFINITY, radius, sigma) != 0);
CHECK(spacetime_create_alcubierre(&source, -INFINITY, radius, sigma) != 0);
CHECK(spacetime_create_alcubierre(&source, vs, 0.0, sigma) != 0);
CHECK(spacetime_create_alcubierre(&source, vs, radius, 0.0) != 0);
/* A derived escape radius that overflows or does not exceed R is rejected. */
@@ -214,8 +230,8 @@ int main(void) {
n[k] /= norm;
const RayEndpoint r0 = geodesic_trace_past(&source, &o0, n, &trace);
const RayEndpoint r1 = geodesic_trace_past(&source, &o1, n, &trace);
CHECK(r0.status == RAY_ENDPOINT_ESCAPED);
CHECK(r1.status == RAY_ENDPOINT_ESCAPED);
CHECK(r0.outcome == RAY_OUTCOME_ESCAPED);
CHECK(r1.outcome == RAY_OUTCOME_ESCAPED);
for (int k = 0; k < 3; ++k)
CHECK(fabs(r0.n_infinity[k] - r1.n_infinity[k]) < 1e-6);
CHECK(fabs(r0.frequency_ratio - r1.frequency_ratio) < 1e-6);
@@ -236,7 +252,7 @@ int main(void) {
const ObserverState observer = observer_fixed_at_origin();
const RayEndpoint ray = geodesic_trace_past(
&flat, &observer, (double[]){1, 0, 0}, &trace);
CHECK(ray.status == RAY_ENDPOINT_ESCAPED);
CHECK(ray.outcome == RAY_OUTCOME_ESCAPED);
CHECK(fabs(ray.n_infinity[0]) < 1e-12);
CHECK(fabs(ray.n_infinity[1]) < 1e-12);
CHECK(fabs(ray.n_infinity[2] + 1.0) < 1e-12);
@@ -277,7 +293,7 @@ int main(void) {
.max_steps = 2500000u};
const RayEndpoint ray = geodesic_trace_past(
&fast, &observer, (double[]){-1, 0, 0}, &trace);
CHECK(ray.status == RAY_ENDPOINT_ESCAPED);
CHECK(ray.outcome == RAY_OUTCOME_ESCAPED);
spacetime_destroy(&fast);
}
@@ -305,7 +321,7 @@ int main(void) {
.coordinate_time_step = 0.08 / (1 << level),
.max_steps = 1u << 20};
const RayEndpoint ray = geodesic_trace_past(&source, &observer, n, &trace);
CHECK(ray.status == RAY_ENDPOINT_ESCAPED);
CHECK(ray.outcome == RAY_OUTCOME_ESCAPED);
if (level > 0) {
double error = 0.0;
for (int k = 0; k < 3; ++k) {
@@ -322,6 +338,99 @@ int main(void) {
}
}
/* Production DP54 axial superluminal check. A comoving bubble-center camera
* at R = 1, sigma = 1 (escape radius 21) sees Pi_x = +-1 along the bubble
* axis. The shared camera-relative dark policy must fire at threshold 8 for
* the direction along the bubble motion and the opposite direction must
* escape. The vs = +-2 constants were computed independently with mpmath;
* this test has no dependency on any local experiment fixture. */
{
const double R1 = 1.0, sig1 = 1.0;
static const double vlist[] = {1.0, -1.0, 2.0, -2.0, 0.9999};
for (size_t k = 0; k < sizeof vlist / sizeof vlist[0]; ++k) {
const double v = vlist[k];
const double sgn = v > 0.0 ? 1.0 : -1.0;
SpacetimeSource fast = {0};
CHECK(spacetime_create_alcubierre(&fast, v, R1, sig1) == 0);
ObserverCamera cam = {.coordinate_time = 0.0,
.position = {0.0, 0.0, 0.0},
.velocity = {v, 0.0, 0.0},
.look_ra_deg = 0.0,
.look_dec_deg = 0.0,
.roll_deg = 0.0};
MetricData m;
CHECK(eval(&fast, 0.0, cam.position, &m) == 0);
ObserverState o;
CHECK(observer_from_coordinate_camera(&m, &cam, &o, NULL) ==
OBSERVER_BUILD_OK);
/* A coordinate-static center camera is timelike only for |v| < 1. */
ObserverCamera stat = cam;
stat.velocity[0] = stat.velocity[1] = stat.velocity[2] = 0.0;
ObserverState so;
const int static_ok = observer_from_coordinate_camera(&m, &stat, &so,
NULL) ==
OBSERVER_BUILD_OK;
CHECK(static_ok == (fabs(v) < 1.0));
/* An outer static camera in the flat exterior is always legal. */
{
const double pos[3] = {26.0, 0.0, 0.0};
MetricData om;
ObserverCamera oc = {.coordinate_time = 0.0,
.position = {26.0, 0.0, 0.0},
.look_ra_deg = 0.0,
.look_dec_deg = 0.0,
.roll_deg = 0.0};
CHECK(eval(&fast, 0.0, pos, &om) == 0);
ObserverState oo;
CHECK(observer_from_coordinate_camera(&om, &oc, &oo, NULL) ==
OBSERVER_BUILD_OK);
}
const GeodesicTraceConfig trace = {
.coordinate_time_step = 0.05,
.max_steps = 100000u,
.threshold = {.kind = THRESHOLD_LOG_ENERGY_GROWTH,
.value = 8.0,
.policy_version = 1},
.stepper = GEODESIC_STEPPER_DP54,
.atol_x = 1e-9,
.atol_Pi = 1e-9,
.atol_L = 1e-9,
.rtol = 1e-9,
.min_step = 1e-12,
.max_step = 0.4,
.consecutive_rejection_limit = 32,
.max_lookback_time = 30000.0};
const double n_dark[3] = {-sgn, 0.0, 0.0};
const double n_esc[3] = {sgn, 0.0, 0.0};
GeodesicRayState idark, iesc;
CHECK(geodesic_initialize_past_ray_metric(&m, &o, n_dark, &idark) == 0);
CHECK(geodesic_initialize_past_ray_metric(&m, &o, n_esc, &iesc) == 0);
printf("alcubierre vs=%.6g dark Pi_x=%.17g escape Pi_x=%.17g\n", v,
idark.Pi[0], iesc.Pi[0]);
CHECK(sgn * idark.Pi[0] > 0.999 && sgn * idark.Pi[0] < 1.000000001);
CHECK(sgn * iesc.Pi[0] < -0.999 && sgn * iesc.Pi[0] > -1.000000001);
const RayEndpoint dark = geodesic_trace_past(&fast, &o, n_dark, &trace);
CHECK(dark.outcome == RAY_OUTCOME_DARK);
CHECK(isfinite(dark.stop_coordinate_time));
CHECK(fabs(dark.threshold_value - 8.0) < 1e-6);
CHECK(fabs((dark.final_log_alpha_p0 - dark.final_log_alpha_p0_0) - 8.0) <
1e-6);
if (fabs(v) == 2.0) {
const double q = dark.final_x[0] - v * dark.stop_coordinate_time;
CHECK(fabs(fabs(q) - 1.2181434100155241) < 1e-6);
CHECK(fabs(dark.stop_coordinate_time + 7.09915163394274) < 1e-6);
}
const RayEndpoint esc = geodesic_trace_past(&fast, &o, n_esc, &trace);
CHECK(esc.outcome == RAY_OUTCOME_ESCAPED);
CHECK(isfinite(esc.frequency_ratio) && esc.frequency_ratio > 0.0);
if (fabs(v) == 2.0)
CHECK(fabs(esc.frequency_ratio - 3.0) < 1e-6);
for (int i = 0; i < 3; ++i)
CHECK(isfinite(esc.n_infinity[i]));
spacetime_destroy(&fast);
}
}
spacetime_destroy(&source);
puts("alcubierre regression passed");
return 0;
File diff suppressed because it is too large. Load diff
+781
View File
@@ -0,0 +1,781 @@
#include "asymptotic_entry.h"
#include "spacetime.h"
#include <float.h>
#include <math.h>
#include <stdio.h>
/* Backend-independent core regression for the numerical entry localizer. It
* deliberately links no analytic backend and no geodesic integrator: the fake
* SpacetimeSource exposes only `escape_worldtube_sample` (plus a deliberately
* trapped `eval`), and the evaluator is an analytic path-parameter callback.
*
* Nothing here depends on an untracked production track, CSV or binary. */
static int failures = 0;
#define CHECK(condition, message) \
do { \
if (!(condition)) { \
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, message); \
++failures; \
} \
} while (0)
#ifndef TEST_PI
#define TEST_PI 3.14159265358979323846
#endif
/* ------------------------------------------------------------------ */
/* Fake worldtube source */
/* ------------------------------------------------------------------ */
typedef struct {
double center0[3];
double center_vel[3]; /* dc/dt */
double radius0;
double radius_rate; /* dR/dt */
double valid_t_min; /* sample is valid for t >= valid_t_min */
int callback_fails; /* always return -1 */
int fail_at_call; /* 1-based sample-call index to fail, 0 disabled */
int nan_radius;
int zero_radius;
double hole_center; /* isolated invalid time window */
double hole_halfwidth; /* 0 disables the window */
int call_count;
int eval_calls; /* trap: how often the metric eval callback ran */
} EntryWorldtube;
static SpacetimePointStatus entry_eval_trap(const SpacetimeSource *source,
double t, const double x[3],
MetricData *metric) {
EntryWorldtube *wt = source->context;
++wt->eval_calls;
(void)t;
(void)x;
(void)metric;
/* This source is deliberately outside the metric domain. The localizer must
* never reach here because it does no metric evaluation. */
return SPACETIME_POINT_OUT_OF_DOMAIN;
}
static int entry_worldtube_cb(const SpacetimeSource *source,
SpacetimeEndId end_id, double t,
SpacetimeEscapeWorldtubeSample *out) {
EntryWorldtube *wt = source->context;
if (end_id != 0)
return -1;
++wt->call_count;
if (wt->fail_at_call > 0 && wt->call_count == wt->fail_at_call)
return -1;
if (wt->callback_fails)
return -1;
if (!isfinite(t) || t < wt->valid_t_min) {
*out = (SpacetimeEscapeWorldtubeSample){.valid = 0};
return 0;
}
if (wt->hole_halfwidth > 0.0 &&
fabs(t - wt->hole_center) <= wt->hole_halfwidth) {
*out = (SpacetimeEscapeWorldtubeSample){.valid = 0};
return 0;
}
if (wt->nan_radius) {
*out = (SpacetimeEscapeWorldtubeSample){.radius = NAN, .valid = 1};
return 0;
}
if (wt->zero_radius) {
*out = (SpacetimeEscapeWorldtubeSample){.radius = 0.0, .valid = 1};
return 0;
}
*out = (SpacetimeEscapeWorldtubeSample){
.center = {wt->center0[0] + wt->center_vel[0] * t,
wt->center0[1] + wt->center_vel[1] * t,
wt->center0[2] + wt->center_vel[2] * t},
.velocity = {wt->center_vel[0], wt->center_vel[1], wt->center_vel[2]},
.radius = wt->radius0 + wt->radius_rate * t,
.radius_rate = wt->radius_rate,
.velocity_constant = 1,
.valid = 1};
return 0;
}
static const SpacetimeOps entry_ops = {
.eval = entry_eval_trap,
.escape_worldtube_sample = entry_worldtube_cb,
};
static SpacetimeSource entry_source(EntryWorldtube *wt) {
return (SpacetimeSource){.ops = &entry_ops, .context = wt};
}
/* ------------------------------------------------------------------ */
/* Analytic path-parameter evaluator */
/* ------------------------------------------------------------------ */
typedef struct {
double camera_t;
double camera_x[3];
double w[3]; /* unit past direction (straight mode) */
int arc_mode;
double arc_center[3];
double arc_radius;
double arc_theta0;
double L0;
double L0camera;
int evaluator_fails_at;
AsymptoticStatus fail_status;
int evaluator_call_count;
int nonfinite_at;
int reversed_time_at;
} EntryEvaluator;
static void entry_trajectory(const EntryEvaluator *c, double parameter,
double x[3], double w[3], double *t) {
if (c->arc_mode) {
/* Circular analytic arc: parameter is arc length. Not a physical
* geodesic, but a generic curved callback that exercises the driver beyond
* straight lines. */
const double theta = c->arc_theta0 + parameter / c->arc_radius;
x[0] = c->arc_center[0] + c->arc_radius * cos(theta);
x[1] = c->arc_center[1] + c->arc_radius * sin(theta);
x[2] = c->arc_center[2];
w[0] = -sin(theta);
w[1] = cos(theta);
w[2] = 0.0;
} else {
for (int i = 0; i < 3; ++i) {
x[i] = c->camera_x[i] + parameter * c->w[i];
w[i] = c->w[i];
}
}
*t = c->camera_t - parameter;
}
static AsymptoticStatus entry_evaluator_cb(void *context, double parameter,
AsymptoticRoute *state) {
EntryEvaluator *c = context;
++c->evaluator_call_count;
if (c->evaluator_fails_at > 0 &&
c->evaluator_call_count == c->evaluator_fails_at)
return c->fail_status;
double x[3], w[3], t;
entry_trajectory(c, parameter, x, w, &t);
*state = (AsymptoticRoute){0};
state->kind = ASYMPTOTIC_ROUTE_ENTRY;
state->end_id = 0;
state->activate_t = t;
for (int i = 0; i < 3; ++i) {
state->x[i] = x[i];
state->Pi[i] = -w[i];
}
state->log_alpha_p0 = c->L0;
state->log_alpha_p0_camera = c->L0camera;
if (c->evaluator_call_count == c->nonfinite_at)
state->log_alpha_p0_camera = NAN;
if (c->evaluator_call_count == c->reversed_time_at)
state->activate_t = c->camera_t + 1.0;
return ASYMPTOTIC_OK;
}
/* Independent test-side oracle: the same worldtube F the driver sees, but
* computed directly from the analytic trajectory. Used only to find the true
* first entry for comparison. */
typedef struct {
const EntryEvaluator *ev;
const EntryWorldtube *wt;
} EntryOracle;
static double entry_oracle_F(void *context, double parameter) {
const EntryOracle *o = context;
double x[3], w[3], t;
entry_trajectory(o->ev, parameter, x, w, &t);
double d[3];
for (int i = 0; i < 3; ++i)
d[i] = x[i] - (o->wt->center0[i] + o->wt->center_vel[i] * t);
const double d2 = d[0] * d[0] + d[1] * d[1] + d[2] * d[2];
const double radius = o->wt->radius0 + o->wt->radius_rate * t;
return d2 - radius * radius;
}
static double entry_oracle_root(const EntryEvaluator *ev,
const EntryWorldtube *wt, double lo,
double hi) {
EntryOracle o = {.ev = ev, .wt = wt};
if (!(entry_oracle_F(&o, lo) >= 0.0 && entry_oracle_F(&o, hi) < 0.0))
return NAN;
for (int i = 0; i < 200; ++i) {
const double mid = 0.5 * (lo + hi);
if (!(mid > lo && mid < hi))
break;
if (entry_oracle_F(&o, mid) >= 0.0)
lo = mid;
else
hi = mid;
}
return 0.5 * (lo + hi);
}
static double path_parameter(const EntryEvaluator *ev,
const AsymptoticRoute *state) {
return ev->camera_t - state->activate_t;
}
/* ------------------------------------------------------------------ */
/* Tests */
/* ------------------------------------------------------------------ */
static void test_geometry_contract(void) {
EntryWorldtube wt = {.radius0 = 10.0, .valid_t_min = -1.0e300};
SpacetimeSource source = entry_source(&wt);
double F = NAN, tol = NAN;
RayReason reason = RAY_REASON_COUNT;
CHECK(asymptotic_entry_geometry(&source, 0, 0.0, (double[]){10.0, 0.0, 0.0},
&F, &tol, &reason) == ASYMPTOTIC_OK,
"boundary geometry status");
CHECK(F == 0.0, "boundary F is exactly zero");
CHECK(tol > 0.0 && isfinite(tol), "boundary tolerance finite positive");
CHECK(reason == RAY_REASON_NONE, "boundary reason none");
CHECK(asymptotic_entry_geometry(&source, 0, 0.0, (double[]){20.0, 0.0, 0.0},
&F, &tol, &reason) == ASYMPTOTIC_OK &&
F == 300.0,
"outside F is positive 300");
CHECK(asymptotic_entry_geometry(&source, 0, 0.0, (double[]){5.0, 0.0, 0.0},
&F, &tol, &reason) == ASYMPTOTIC_OK &&
F == -75.0,
"inside F is negative 75");
/* History exhaustion beats a miss. */
EntryWorldtube hole = {.radius0 = 10.0, .valid_t_min = 0.0};
source = entry_source(&hole);
CHECK(asymptotic_entry_geometry(&source, 0, -1.0, (double[]){10.0, 0.0, 0.0},
&F, &tol, &reason) ==
ASYMPTOTIC_TIME_RANGE_EXHAUSTED &&
reason == RAY_REASON_TIME_RANGE_EXHAUSTED,
"valid=0 is history exhaustion");
/* Callback failure is distinct from an invalid geometry. */
EntryWorldtube fail = {.radius0 = 10.0, .callback_fails = 1};
source = entry_source(&fail);
CHECK(asymptotic_entry_geometry(&source, 0, 0.0, (double[]){10.0, 0.0, 0.0},
&F, &tol, &reason) == ASYMPTOTIC_INVALID &&
reason == RAY_REASON_WORLDTUBE_SAMPLE_FAILED,
"callback failure reason");
EntryWorldtube nanr = {.radius0 = 10.0, .nan_radius = 1};
source = entry_source(&nanr);
CHECK(asymptotic_entry_geometry(&source, 0, 0.0, (double[]){10.0, 0.0, 0.0},
&F, &tol, &reason) == ASYMPTOTIC_INVALID &&
reason == RAY_REASON_WORLDTUBE_GEOMETRY_INVALID,
"NaN radius is invalid geometry");
EntryWorldtube zeror = {.radius0 = 10.0, .zero_radius = 1};
source = entry_source(&zeror);
CHECK(asymptotic_entry_geometry(&source, 0, 0.0, (double[]){10.0, 0.0, 0.0},
&F, &tol, &reason) == ASYMPTOTIC_INVALID &&
reason == RAY_REASON_WORLDTUBE_GEOMETRY_INVALID,
"non-positive radius is invalid geometry");
source = entry_source(&wt);
CHECK(asymptotic_entry_geometry(&source, 0, NAN, (double[]){10.0, 0.0, 0.0},
&F, &tol, &reason) == ASYMPTOTIC_INVALID &&
reason == RAY_REASON_WORLDTUBE_GEOMETRY_INVALID,
"NaN time is invalid geometry");
CHECK(asymptotic_entry_geometry(NULL, 0, 0.0, (double[]){10.0, 0.0, 0.0},
&F, &tol, &reason) == ASYMPTOTIC_INVALID &&
reason == RAY_REASON_INVALID_ARGUMENT,
"NULL source rejected");
CHECK(asymptotic_entry_geometry(&source, 0, 0.0, NULL, &F, &tol,
&reason) == ASYMPTOTIC_INVALID,
"NULL position rejected");
}
static void test_validate_contract(void) {
EntryWorldtube wt = {.radius0 = 10.0, .valid_t_min = -1.0e300};
SpacetimeSource source = entry_source(&wt);
int valid = -1;
RayReason reason = RAY_REASON_COUNT;
AsymptoticRoute candidate = {.kind = ASYMPTOTIC_ROUTE_ENTRY,
.end_id = 0,
.activate_t = 0.0,
.x = {10.0, 0.0, 0.0},
.Pi = {-1.0, 0.0, 0.0},
.log_alpha_p0 = 0.5,
.log_alpha_p0_camera = 0.25};
CHECK(asymptotic_entry_validate(&source, 0, &candidate, &valid, &reason) ==
ASYMPTOTIC_OK &&
valid == 1,
"boundary candidate is valid");
candidate.x[0] = 11.0; /* F = 21 > tol */
CHECK(asymptotic_entry_validate(&source, 0, &candidate, &valid, &reason) ==
ASYMPTOTIC_OK &&
valid == 0 && reason == RAY_REASON_NONE,
"outside candidate is valid=0 with OK status");
candidate.x[0] = 5.0; /* F = -75, far inside */
CHECK(asymptotic_entry_validate(&source, 0, &candidate, &valid, &reason) ==
ASYMPTOTIC_OK &&
valid == 0,
"deep-inside candidate is valid=0 with OK status");
candidate.x[0] = 10.0;
candidate.kind = ASYMPTOTIC_ROUTE_ESCAPED;
CHECK(asymptotic_entry_validate(&source, 0, &candidate, &valid, &reason) ==
ASYMPTOTIC_INVALID &&
valid == 0 && reason == RAY_REASON_PROTOCOL_ERROR,
"wrong candidate kind is a protocol error");
candidate.kind = ASYMPTOTIC_ROUTE_ENTRY;
candidate.x[0] = NAN;
CHECK(asymptotic_entry_validate(&source, 0, &candidate, &valid, &reason) ==
ASYMPTOTIC_INVALID &&
reason == RAY_REASON_PROTOCOL_ERROR,
"non-finite candidate is a protocol error");
/* History exhaustion propagates through validation. */
candidate.x[0] = 10.0;
candidate.activate_t = -1.0;
EntryWorldtube hole = {.radius0 = 10.0, .valid_t_min = 0.0};
source = entry_source(&hole);
CHECK(asymptotic_entry_validate(&source, 0, &candidate, &valid, &reason) ==
ASYMPTOTIC_TIME_RANGE_EXHAUSTED &&
reason == RAY_REASON_TIME_RANGE_EXHAUSTED,
"validation propagates history exhaustion");
}
static void test_fixed_sphere_localize(void) {
EntryWorldtube wt = {.radius0 = 10.0, .valid_t_min = -1.0e300};
SpacetimeSource source = entry_source(&wt);
EntryEvaluator ev = {.camera_t = 0.0,
.camera_x = {50.0, 0.0, 0.0},
.w = {-1.0, 0.0, 0.0},
.L0 = 0.75,
.L0camera = 0.5};
AsymptoticRoute out;
unsigned int evaluations = 0;
RayReason reason = RAY_REASON_COUNT;
CHECK(asymptotic_entry_localize(&source, 0, entry_evaluator_cb, &ev, 30.0,
45.0, &out, &evaluations,
&reason) == ASYMPTOTIC_OK,
"fixed-sphere localize succeeds");
CHECK(out.kind == ASYMPTOTIC_ROUTE_ENTRY && out.end_id == 0,
"localized kind and end");
const double s = path_parameter(&ev, &out);
const double s_true = entry_oracle_root(&ev, &wt, 30.0, 45.0);
CHECK(isfinite(s_true), "oracle found the same bracket");
CHECK(s >= s_true && s - s_true <= 1e-9,
"localized just past first entry");
CHECK(fabs(s - 40.0) <= 1e-9, "fixed-sphere entry at s=40");
CHECK(out.Pi[0] == 1.0 && out.Pi[1] == 0.0 && out.Pi[2] == 0.0,
"direction preserved exactly");
CHECK(out.log_alpha_p0 == 0.75 && out.log_alpha_p0_camera == 0.5,
"L and camera L preserved exactly");
CHECK(evaluations >= 2 && evaluations <= 260, "evaluation count bounded");
CHECK(ev.evaluator_call_count == (int)evaluations,
"evaluator calls counted once each");
CHECK(wt.eval_calls == 0, "no metric evaluation outside the worldtube");
}
static void test_too_early_hint(void) {
/* Outside endpoint is the camera (a deliberately too-early, corrupted
* bracket); the localizer still returns the true first entry, not the
* inside hint and not the camera. */
EntryWorldtube wt = {.radius0 = 10.0, .valid_t_min = -1.0e300};
SpacetimeSource source = entry_source(&wt);
EntryEvaluator ev = {.camera_t = 0.0,
.camera_x = {50.0, 0.0, 0.0},
.w = {-1.0, 0.0, 0.0},
.L0 = 0.1,
.L0camera = 0.2};
AsymptoticRoute out;
unsigned int evaluations = 0;
RayReason reason = RAY_REASON_COUNT;
CHECK(asymptotic_entry_localize(&source, 0, entry_evaluator_cb, &ev, 0.0,
45.0, &out, &evaluations,
&reason) == ASYMPTOTIC_OK,
"too-early hint still localizes");
const double s = path_parameter(&ev, &out);
CHECK(fabs(s - 40.0) <= 1e-9, "returns actual first entry, not the hint");
CHECK(s > 1.0 && s < 45.0, "not the camera and not the inside hint");
CHECK(evaluations <= 260, "hint evaluation budget");
}
static void test_moving_sphere_localize(void) {
EntryWorldtube wt = {.radius0 = 10.0,
.center_vel = {0.5, 0.0, 0.0},
.valid_t_min = -1.0e300};
SpacetimeSource source = entry_source(&wt);
EntryEvaluator ev = {.camera_t = 0.0,
.camera_x = {100.0, 0.0, 0.0},
.w = {-1.0, 0.0, 0.0},
.L0 = 0.3,
.L0camera = 0.4};
AsymptoticRoute out;
unsigned int evaluations = 0;
RayReason reason = RAY_REASON_COUNT;
CHECK(asymptotic_entry_localize(&source, 0, entry_evaluator_cb, &ev, 150.0,
200.0, &out, &evaluations,
&reason) == ASYMPTOTIC_OK,
"translated+ moving sphere localize");
const double s = path_parameter(&ev, &out);
const double s_true = entry_oracle_root(&ev, &wt, 150.0, 200.0);
CHECK(fabs(s - s_true) <= 1e-8 && fabs(s - 180.0) <= 1e-8,
"moving-sphere entry at s=180");
CHECK(evaluations <= 260, "moving-sphere evaluation budget");
}
static void test_radius_rate_localize(void) {
/* radius(t) = radius0 + radius_rate * t with radius_rate = -1 and t = -s, so
* R grows as 10 + s; the entry is at s = 45. */
EntryWorldtube wt = {.radius0 = 10.0,
.radius_rate = -1.0,
.valid_t_min = -1.0e300};
SpacetimeSource source = entry_source(&wt);
EntryEvaluator ev = {.camera_t = 0.0,
.camera_x = {100.0, 0.0, 0.0},
.w = {-1.0, 0.0, 0.0},
.L0 = 0.6,
.L0camera = 0.6};
AsymptoticRoute out;
unsigned int evaluations = 0;
RayReason reason = RAY_REASON_COUNT;
CHECK(asymptotic_entry_localize(&source, 0, entry_evaluator_cb, &ev, 30.0,
60.0, &out, &evaluations,
&reason) == ASYMPTOTIC_OK,
"linear radius-rate localize");
const double s = path_parameter(&ev, &out);
const double s_true = entry_oracle_root(&ev, &wt, 30.0, 60.0);
CHECK(fabs(s - s_true) <= 1e-8 && fabs(s - 45.0) <= 1e-8,
"linear radius-rate entry at s=45");
CHECK(evaluations <= 260, "radius-rate evaluation budget");
}
static void test_rotated_frame_localize(void) {
/* Camera on a rotated axis: (40,30,0), past direction toward the origin. */
EntryWorldtube wt = {.radius0 = 10.0, .valid_t_min = -1.0e300};
SpacetimeSource source = entry_source(&wt);
EntryEvaluator ev = {.camera_t = 0.0,
.camera_x = {40.0, 30.0, 0.0},
.w = {-0.8, -0.6, 0.0},
.L0 = 0.2,
.L0camera = 0.1};
AsymptoticRoute out;
unsigned int evaluations = 0;
RayReason reason = RAY_REASON_COUNT;
CHECK(asymptotic_entry_localize(&source, 0, entry_evaluator_cb, &ev, 30.0,
45.0, &out, &evaluations,
&reason) == ASYMPTOTIC_OK,
"rotated flat frame localize");
const double s = path_parameter(&ev, &out);
const double s_true = entry_oracle_root(&ev, &wt, 30.0, 45.0);
CHECK(fabs(s - s_true) <= 1e-9 && fabs(s - 40.0) <= 1e-9,
"rotated-frame entry at s=40");
CHECK(fabs(out.x[1] - 6.0) <= 1e-6, "rotated entry position on sphere");
}
static void test_grazing_first_entry(void) {
/* Grazing pass: the camera is offset by 9.9 from the sphere axis. The first
* entry at s ~ 48.589 is inside the bracket; the exit at s ~ 51.410 is not.
* Bisection must return the first entry, not the later exit. */
EntryWorldtube wt = {.radius0 = 10.0, .valid_t_min = -1.0e300};
SpacetimeSource source = entry_source(&wt);
EntryEvaluator ev = {.camera_t = 0.0,
.camera_x = {50.0, 9.9, 0.0},
.w = {-1.0, 0.0, 0.0},
.L0 = 0.0,
.L0camera = 0.0};
AsymptoticRoute out;
unsigned int evaluations = 0;
RayReason reason = RAY_REASON_COUNT;
CHECK(asymptotic_entry_localize(&source, 0, entry_evaluator_cb, &ev, 48.0,
50.0, &out, &evaluations,
&reason) == ASYMPTOTIC_OK,
"grazing first entry localizes");
const double s = path_parameter(&ev, &out);
const double first = 50.0 - sqrt(100.0 - 9.9 * 9.9);
CHECK(fabs(s - first) <= 1e-8, "grazing entry is the first crossing");
CHECK(s < 51.4, "not the later exit crossing");
CHECK(fabs(out.x[1] - 9.9) <= 1e-9, "grazing impact parameter preserved");
CHECK(evaluations <= 260, "grazing evaluation budget");
}
static void test_curved_arc_localize(void) {
/* Circular analytic arc of radius 30 and worldtube centered at (25,0,0)
* radius 8; entry at arc length ~ 87.40. */
EntryWorldtube wt = {.radius0 = 8.0,
.center0 = {25.0, 0.0, 0.0},
.valid_t_min = -1.0e300};
SpacetimeSource source = entry_source(&wt);
EntryEvaluator ev = {.camera_t = 0.0,
.arc_mode = 1,
.arc_center = {0.0, 0.0, 0.0},
.arc_radius = 30.0,
.arc_theta0 = TEST_PI,
.L0 = 0.9,
.L0camera = 0.8};
AsymptoticRoute out;
unsigned int evaluations = 0;
RayReason reason = RAY_REASON_COUNT;
const double lo = 30.0 * (5.9 - TEST_PI);
const double hi = 30.0 * (6.2 - TEST_PI);
const double s_true = entry_oracle_root(&ev, &wt, lo, hi);
CHECK(isfinite(s_true), "curved oracle bracket");
CHECK(asymptotic_entry_localize(&source, 0, entry_evaluator_cb, &ev, lo, hi,
&out, &evaluations,
&reason) == ASYMPTOTIC_OK,
"curved arc localize");
const double s = path_parameter(&ev, &out);
CHECK(s >= s_true - 1e-9 && s - s_true <= 1e-8,
"curved arc entry matches the oracle");
/* d^2(theta) = 1525 - 1500 cos(theta) = 8^2 on the arc. */
const double expected =
30.0 * (2.0 * TEST_PI - acos((1525.0 - 64.0) / 1500.0) - TEST_PI);
CHECK(fabs(s - expected) <= 1e-8, "curved arc entry matches analytic root");
CHECK(evaluations <= 260, "curved arc evaluation budget");
}
static void test_boundary_entry_exact(void) {
EntryWorldtube wt = {.radius0 = 10.0, .valid_t_min = -1.0e300};
SpacetimeSource source = entry_source(&wt);
EntryEvaluator ev = {.camera_t = 0.0,
.camera_x = {50.0, 0.0, 0.0},
.w = {-1.0, 0.0, 0.0},
.L0 = 0.4,
.L0camera = 0.4};
AsymptoticRoute out;
unsigned int evaluations = 0;
RayReason reason = RAY_REASON_COUNT;
/* F == 0 exactly at the outside endpoint and strictly inside at 45. */
CHECK(asymptotic_entry_localize(&source, 0, entry_evaluator_cb, &ev, 40.0,
45.0, &out, &evaluations,
&reason) == ASYMPTOTIC_OK,
"exact boundary entry localize");
CHECK(out.kind == ASYMPTOTIC_ROUTE_ENTRY && out.activate_t == -40.0 &&
out.x[0] == 10.0,
"boundary endpoint returned directly");
CHECK(evaluations == 2, "boundary path needs no bisection");
}
static void test_unconfirmed_bracket(void) {
EntryWorldtube wt = {.radius0 = 10.0, .valid_t_min = -1.0e300};
SpacetimeSource source = entry_source(&wt);
EntryEvaluator ev = {.camera_t = 0.0,
.camera_x = {50.0, 0.0, 0.0},
.w = {-1.0, 0.0, 0.0}};
AsymptoticRoute out;
unsigned int evaluations = 0;
RayReason reason = RAY_REASON_COUNT;
/* Both endpoints outside: no strict-inside bracket. */
CHECK(asymptotic_entry_localize(&source, 0, entry_evaluator_cb, &ev, 10.0,
20.0, &out, &evaluations,
&reason) == ASYMPTOTIC_INVALID &&
reason == RAY_REASON_ENTRY_UNCONFIRMED,
"false candidate outside bracket is unconfirmed, not escaped");
/* Both endpoints strictly inside: also no entry bracket. */
CHECK(asymptotic_entry_localize(&source, 0, entry_evaluator_cb, &ev, 45.0,
50.0, &out, &evaluations,
&reason) == ASYMPTOTIC_INVALID &&
reason == RAY_REASON_ENTRY_UNCONFIRMED,
"both-inside bracket is unconfirmed");
/* Reversed bracket ordering. */
CHECK(asymptotic_entry_localize(&source, 0, entry_evaluator_cb, &ev, 45.0,
30.0, &out, &evaluations,
&reason) == ASYMPTOTIC_INVALID &&
reason == RAY_REASON_INVALID_ARGUMENT,
"reversed bracket rejected");
}
static void test_callback_failure_propagation(void) {
EntryEvaluator ev = {.camera_t = 0.0,
.camera_x = {50.0, 0.0, 0.0},
.w = {-1.0, 0.0, 0.0}};
AsymptoticRoute out;
unsigned int evaluations = 0;
RayReason reason = RAY_REASON_COUNT;
EntryWorldtube fail = {.radius0 = 10.0,
.valid_t_min = -1.0e300,
.callback_fails = 1};
SpacetimeSource source = entry_source(&fail);
CHECK(asymptotic_entry_localize(&source, 0, entry_evaluator_cb, &ev, 30.0,
45.0, &out, &evaluations,
&reason) == ASYMPTOTIC_INVALID &&
reason == RAY_REASON_WORLDTUBE_SAMPLE_FAILED,
"endpoint callback failure propagates");
fail = (EntryWorldtube){.radius0 = 10.0,
.valid_t_min = -1.0e300,
.fail_at_call = 2};
source = entry_source(&fail);
CHECK(asymptotic_entry_localize(&source, 0, entry_evaluator_cb, &ev, 30.0,
45.0, &out, &evaluations,
&reason) == ASYMPTOTIC_INVALID &&
reason == RAY_REASON_WORLDTUBE_SAMPLE_FAILED,
"inside endpoint callback failure propagates");
fail = (EntryWorldtube){.radius0 = 10.0,
.valid_t_min = -1.0e300,
.fail_at_call = 3};
source = entry_source(&fail);
CHECK(asymptotic_entry_localize(&source, 0, entry_evaluator_cb, &ev, 30.0,
45.0, &out, &evaluations,
&reason) == ASYMPTOTIC_INVALID &&
reason == RAY_REASON_WORLDTUBE_SAMPLE_FAILED,
"midpoint callback failure propagates");
}
static void test_history_hole_propagation(void) {
/* The inside endpoint falls past the valid history: the driver must report
* TIME_RANGE_EXHAUSTED, never a miss or a fabricated entry. */
EntryWorldtube wt = {.radius0 = 10.0, .valid_t_min = -40.0};
SpacetimeSource source = entry_source(&wt);
EntryEvaluator ev = {.camera_t = 0.0,
.camera_x = {50.0, 0.0, 0.0},
.w = {-1.0, 0.0, 0.0}};
AsymptoticRoute out;
unsigned int evaluations = 0;
RayReason reason = RAY_REASON_COUNT;
CHECK(asymptotic_entry_localize(&source, 0, entry_evaluator_cb, &ev, 30.0,
45.0, &out, &evaluations,
&reason) == ASYMPTOTIC_TIME_RANGE_EXHAUSTED &&
reason == RAY_REASON_TIME_RANGE_EXHAUSTED,
"endpoint history hole propagates");
/* A midpoint-only history hole: both endpoints are valid, but the first
* bisection midpoint (t = -37.5) falls in an isolated invalid window. */
wt = (EntryWorldtube){.radius0 = 10.0,
.valid_t_min = -1.0e300,
.hole_center = -37.5,
.hole_halfwidth = 0.5};
source = entry_source(&wt);
CHECK(asymptotic_entry_localize(&source, 0, entry_evaluator_cb, &ev, 30.0,
45.0, &out, &evaluations,
&reason) == ASYMPTOTIC_TIME_RANGE_EXHAUSTED &&
reason == RAY_REASON_TIME_RANGE_EXHAUSTED,
"midpoint history hole propagates, never a miss");
}
static void test_evaluator_failure_propagation(void) {
EntryWorldtube wt = {.radius0 = 10.0, .valid_t_min = -1.0e300};
SpacetimeSource source = entry_source(&wt);
AsymptoticRoute out;
unsigned int evaluations = 0;
RayReason reason = RAY_REASON_COUNT;
EntryEvaluator ev = {.camera_t = 0.0,
.camera_x = {50.0, 0.0, 0.0},
.w = {-1.0, 0.0, 0.0},
.evaluator_fails_at = 1,
.fail_status = ASYMPTOTIC_TIME_RANGE_EXHAUSTED};
CHECK(asymptotic_entry_localize(&source, 0, entry_evaluator_cb, &ev, 30.0,
45.0, &out, &evaluations,
&reason) == ASYMPTOTIC_TIME_RANGE_EXHAUSTED &&
reason == RAY_REASON_TIME_RANGE_EXHAUSTED,
"evaluator history failure propagates");
ev = (EntryEvaluator){.camera_t = 0.0,
.camera_x = {50.0, 0.0, 0.0},
.w = {-1.0, 0.0, 0.0},
.evaluator_fails_at = 1,
.fail_status = ASYMPTOTIC_INVALID};
CHECK(asymptotic_entry_localize(&source, 0, entry_evaluator_cb, &ev, 30.0,
45.0, &out, &evaluations,
&reason) == ASYMPTOTIC_INVALID &&
reason == RAY_REASON_ENTRY_UNCONFIRMED,
"evaluator invalid failure maps to unconfirmed");
ev = (EntryEvaluator){.camera_t = 0.0,
.camera_x = {50.0, 0.0, 0.0},
.w = {-1.0, 0.0, 0.0},
.evaluator_fails_at = 2,
.fail_status = ASYMPTOTIC_INVALID};
CHECK(asymptotic_entry_localize(&source, 0, entry_evaluator_cb, &ev, 30.0,
45.0, &out, &evaluations,
&reason) == ASYMPTOTIC_INVALID,
"inside endpoint evaluator failure propagates");
}
static AsymptoticStatus wide_parameter_path(void *context, double parameter,
AsymptoticRoute *state) {
(void)context;
*state = (AsymptoticRoute){.kind = ASYMPTOTIC_ROUTE_ENTRY,
.end_id = 0,
.activate_t = -parameter,
.x = {2.0 - parameter * 1e-308, 0.0, 0.0},
.Pi = {1.0, 0.0, 0.0}};
return ASYMPTOTIC_OK;
}
static void test_representability_and_state_checks(void) {
EntryWorldtube wt = {.radius0 = 1.0, .valid_t_min = -DBL_MAX};
SpacetimeSource source = entry_source(&wt);
AsymptoticRoute out;
unsigned int evaluations = 0;
RayReason reason = RAY_REASON_COUNT;
/* Both endpoints are finite, but subtracting them overflows. This must not
* be mistaken for an adjacent bracket and return the far-inside endpoint. */
CHECK(asymptotic_entry_localize(&source, 0, wide_parameter_path, NULL,
-1.6e308, 1.6e308, &out, &evaluations,
&reason) == ASYMPTOTIC_OK,
"overflow-safe parameter midpoint");
CHECK(fabs(out.x[0] - 1.0) < 1e-14 && evaluations > 2,
"wide bracket contracts to entry, not initial inside endpoint");
wt.radius0 = 10.0;
EntryEvaluator ev = {.camera_x = {50.0, 0.0, 0.0},
.w = {-1.0, 0.0, 0.0}, .nonfinite_at = 1};
CHECK(asymptotic_entry_localize(&source, 0, entry_evaluator_cb, &ev, 40.0,
45.0, &out, &evaluations, &reason) ==
ASYMPTOTIC_INVALID && reason == RAY_REASON_ENTRY_UNCONFIRMED,
"boundary shortcut rejects nonfinite camera energy reference");
ev.evaluator_call_count = 0;
ev.nonfinite_at = 3;
CHECK(asymptotic_entry_localize(&source, 0, entry_evaluator_cb, &ev, 35.0,
45.0, &out, &evaluations, &reason) ==
ASYMPTOTIC_INVALID && reason == RAY_REASON_ENTRY_UNCONFIRMED,
"midpoint rejects nonfinite camera energy reference");
ev.evaluator_call_count = 0;
ev.nonfinite_at = 0;
ev.reversed_time_at = 3;
CHECK(asymptotic_entry_localize(&source, 0, entry_evaluator_cb, &ev, 35.0,
45.0, &out, &evaluations, &reason) ==
ASYMPTOTIC_INVALID && reason == RAY_REASON_ENTRY_UNCONFIRMED,
"midpoint cannot reverse coordinate time");
}
int main(void) {
test_geometry_contract();
test_validate_contract();
test_fixed_sphere_localize();
test_too_early_hint();
test_moving_sphere_localize();
test_radius_rate_localize();
test_rotated_frame_localize();
test_grazing_first_entry();
test_curved_arc_localize();
test_boundary_entry_exact();
test_unconfirmed_bracket();
test_callback_failure_propagation();
test_history_hole_propagation();
test_evaluator_failure_propagation();
test_representability_and_state_checks();
if (failures == 0)
puts("asymptotic entry regression passed");
else
fprintf(stderr, "%d asymptotic entry regression failures\n", failures);
return failures == 0 ? 0 : 1;
}
+65
View File
@@ -0,0 +1,65 @@
/* Exercise the private numerical kernel directly, including coefficient
* ranges that cannot be represented by a public double worldtube fixture.
* The build rule omits the separately compiled asymptotic.c. */
#include "../src/asymptotic.c"
#include <stdio.h>
static int failures;
#define CHECK(condition, message) do { \
if (!(condition)) { \
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, message); \
++failures; \
} \
} while (0)
static void check_scaled(int exponent) {
const long double scale = scalbnl(1.0L, exponent);
double root = -1.0;
EntryQuadratic k = {scale, -3.0L * scale, 2.0L * scale};
CHECK(entry_solve(&k, &root) == ENTRY_SOLVE_ENTRY && root == 1.0,
"common scale preserves smallest inward root");
k = (EntryQuadratic){scale, -scale, scale};
CHECK(entry_solve(&k, &root) == ENTRY_SOLVE_MISS,
"common scale preserves a clear miss");
k = (EntryQuadratic){0.0L, -scale, 2.0L * scale};
CHECK(entry_solve(&k, &root) == ENTRY_SOLVE_ENTRY && root == 2.0,
"common scale preserves linear entry");
k = (EntryQuadratic){scale, -scale, 0.0L};
CHECK(entry_solve(&k, &root) == ENTRY_SOLVE_ENTRY && root == 0.0,
"common scale preserves boundary entry");
k = (EntryQuadratic){-scale, scale, 2.0L * scale};
CHECK(entry_solve(&k, &root) == ENTRY_SOLVE_ENTRY && root == 2.0,
"common scale preserves concave entry");
}
static void test_product_cancellation(void) {
const long double u = scalbnl(1.0L, 1 - LDBL_MANT_DIG);
const EntryQuadratic k = {1.0L + u, -2.0L, 1.0L - 0.5L * u};
long double scale;
(void)entry_discriminant(&k, &scale);
/* Exact dyadic oracle: 4 - 4(1+u)(1-u/2) = -2u + 2u^2.
* A separately rounded 4*a*c is 4 and loses this nonzero discriminant. */
double root;
CHECK(entry_solve(&k, &root) == ENTRY_SOLVE_UNCERTAIN,
"product cancellation must remain uncertain, not a proven miss");
}
int main(void) {
check_scaled(0);
check_scaled(LDBL_MAX_EXP - 4);
check_scaled(LDBL_MIN_EXP + 4);
check_scaled(LDBL_MIN_EXP - LDBL_MANT_DIG + 2);
test_product_cancellation();
double root;
EntryQuadratic k = {LDBL_MIN, LDBL_MAX / 8.0L, 1.0L};
CHECK(entry_solve(&k, &root) == ENTRY_SOLVE_UNCERTAIN,
"scaling cannot silently erase a nonzero coefficient");
k = (EntryQuadratic){1.0L, 2.0L, -INFINITY};
CHECK(entry_solve(&k, &root) == ENTRY_SOLVE_UNCERTAIN,
"nonfinite coefficient is not a normal entry");
if (!failures)
puts("asymptotic quadratic regression passed");
return failures ? 1 : 0;
}
+660
View File
@@ -0,0 +1,660 @@
#include "asymptotic.h"
#include "asymptotic_schwarzschild.h"
#include "geodesic.h"
#include "observer.h"
#include "spacetime.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
static int failures = 0;
#define CHECK(condition, message) \
do { \
if (!(condition)) { \
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, message); \
++failures; \
} \
} while (0)
static double angle_between(const double a[3], const double b[3]) {
const double dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
const double cx = a[1] * b[2] - a[2] * b[1];
const double cy = a[2] * b[0] - a[0] * b[2];
const double cz = a[0] * b[1] - a[1] * b[0];
return atan2(sqrt(cx * cx + cy * cy + cz * cz), dot);
}
static void test_round_trip(void) {
SpacetimeSource source = {0};
CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0) == 0,
"create schwarzschild");
SpacetimeAsymptoticEnd end;
CHECK(spacetime_asymptotic_end(&source, 0, &end) == 0, "end descriptor");
SchwarzschildCanonical in = {.end_id = 0,
.t = 0.0,
.rho = 256.0,
.rhat = {1.0, 0.0, 0.0},
.Lhat = {0.0, 1.0, 0.0},
.beta = 5.0,
.energy = 1.0,
.radial_sign = 1};
double x[3], Pi[3], log_alpha_p0;
CHECK(asymptotic_schwarzschild_state_from_canonical(&end, &in, x, Pi,
&log_alpha_p0) == 0,
"state from canonical");
MetricData metric;
CHECK(spacetime_eval(&source, in.t, x, &metric) == 0, "metric");
SchwarzschildCanonical out;
CHECK(asymptotic_schwarzschild_canonical_from_state(
&end, &metric, in.t, x, Pi, log_alpha_p0, &out) == 0,
"canonical from state");
CHECK(fabs(out.beta - in.beta) < 1e-13, "beta round trip");
CHECK(fabs(out.energy - in.energy) < 1e-13, "energy round trip");
CHECK(out.radial_sign == in.radial_sign, "radial sign round trip");
const double axis = angle_between(out.rhat, in.rhat);
CHECK(axis < 1e-13, "position direction round trip");
spacetime_destroy(&source);
}
static void test_finish_matches_integration(void) {
SpacetimeSource near = {0}, far = {0};
CHECK(spacetime_create_schwarzschild_ks(&near, 1.0, 256.0) == 0,
"create near");
CHECK(spacetime_create_schwarzschild_ks(&far, 1.0, 1.0e5) == 0,
"create far");
SpacetimeAsymptoticEnd end;
CHECK(spacetime_asymptotic_end(&near, 0, &end) == 0, "near end");
const double betas[] = {0.0, 0.5, 4.0, 10.0, 30.0, 100.0, 250.0};
const int beta_count = (int)(sizeof betas / sizeof betas[0]);
for (int k = 0; k < beta_count; ++k) {
SchwarzschildCanonical canonical = {.end_id = 0,
.t = 0.0,
.rho = 256.0,
.rhat = {0.8, 0.6, 0.0},
.Lhat = {0.0, 0.0, 1.0},
.beta = betas[k],
.energy = 1.0,
.radial_sign = 1};
double x[3], Pi[3], log_alpha_p0;
CHECK(asymptotic_schwarzschild_state_from_canonical(
&end, &canonical, x, Pi, &log_alpha_p0) == 0,
"finish state build");
double n_analytic[3], freq_analytic;
CHECK(asymptotic_schwarzschild_finish(&end, &canonical, n_analytic,
&freq_analytic) == 0,
"analytic finish");
GeodesicRayState state = {.coordinate_time = 0.0,
.x = {x[0], x[1], x[2]},
.Pi = {Pi[0], Pi[1], Pi[2]},
.log_alpha_p0 = log_alpha_p0,
.steps = 0};
const GeodesicTraceConfig config = {.coordinate_time_step = 5.0,
.max_steps = 100000};
MetricSlab *slab = NULL;
CHECK(spacetime_load_slab(&far, 0.0, -1.0e6, &slab) == 0, "far slab");
RayEndpoint endpoint = {.frequency_ratio = 0, .magnification = 1.0,
.end_id = SPACETIME_END_NONE,
.outcome = RAY_OUTCOME_INCOMPLETE};
const GeodesicAdvanceResult result =
geodesic_advance_past_ray(slab, &state, -1.0e6, &config, &endpoint);
spacetime_free_slab(slab);
CHECK(result == GEODESIC_ADVANCE_TERMINATED &&
endpoint.outcome == RAY_OUTCOME_ESCAPED,
"far integration escapes");
/* Pipeline check only: the far integration at step 5 and escape radius
* 1e5 has its own O(1e-5..1e-3) error. Quantitative accuracy is checked
* against the high-precision reference constants below. */
const double angle_error =
angle_between(n_analytic, endpoint.n_infinity);
CHECK(angle_error < 1e-2, "finish direction matches far integration");
CHECK(fabs(freq_analytic - endpoint.frequency_ratio) /
freq_analytic < 1e-2,
"finish frequency matches far integration");
(void)angle_error;
}
spacetime_destroy(&near);
spacetime_destroy(&far);
}
/* Independent quadrature of the KS coordinate-time transfer for a camera
* outside the worldtube, used to check the analytic primitive. */
static double simpson(const double a, const double b, int panels,
double (*f)(double, const void *), const void *ctx) {
if (panels < 2)
panels = 2;
if (panels % 2)
++panels;
const double h = (b - a) / panels;
double sum = f(a, ctx) + f(b, ctx);
for (int i = 1; i < panels; ++i)
sum += (i % 2 ? 4.0 : 2.0) * f(a + i * h, ctx);
return sum * h / 3.0;
}
typedef struct {
double beta;
} TransferContext;
static double transfer_dt(double r, const void *context) {
const TransferContext *c = context;
const double Q = 1.0 - c->beta * c->beta * (1.0 - 2.0 / r) / (r * r);
return 1.0 / ((1.0 - 2.0 / r) * sqrt(Q)) + 2.0 / (r - 2.0);
}
static double transfer_dphi(double r, const void *context) {
const TransferContext *c = context;
const double Q = 1.0 - c->beta * c->beta * (1.0 - 2.0 / r) / (r * r);
return c->beta / (r * r * sqrt(Q));
}
static void test_preroute_entry(void) {
SpacetimeSource source = {0};
CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0) == 0,
"create schwarzschild");
const ObserverCamera camera = {.look_ra_deg = 0.0, .look_dec_deg = 0.0};
ObserverCamera positioned = camera;
positioned.position[0] = 500.0;
positioned.look_ra_deg = 180.0;
positioned.look_dec_deg = 0.0;
const double direction[3] = {cos(0.3), sin(0.3), 0.0};
MetricData metric;
CHECK(spacetime_eval(&source, 0.0, positioned.position, &metric) == 0,
"camera metric");
ObserverState observer;
CHECK(observer_from_coordinate_camera(&metric, &positioned, &observer,
NULL) == OBSERVER_BUILD_OK,
"camera observer");
MetricSlab *camera_slab = NULL;
CHECK(spacetime_load_slab(&source, 0.0, -1.0, &camera_slab) == 0,
"camera slab");
GeodesicRayState camera_state;
CHECK(geodesic_initialize_past_ray(camera_slab, &observer, direction,
&camera_state) == 0,
"camera state");
SpacetimeAsymptoticEnd end;
CHECK(spacetime_asymptotic_end(&source, 0, &end) == 0, "end");
SchwarzschildCanonical camera_can;
CHECK(asymptotic_schwarzschild_canonical_from_state(
&end, &metric, 0.0, camera_state.x, camera_state.Pi,
camera_state.log_alpha_p0, &camera_can) == 0,
"camera canonical");
spacetime_free_slab(camera_slab);
AsymptoticRoute route;
CHECK(asymptotic_route_camera(&source, &observer, direction, &route) ==
ASYMPTOTIC_OK &&
route.kind == ASYMPTOTIC_ROUTE_ENTRY,
"outside camera enters");
CHECK(route.entry_fallback_evaluations == 0,
"analytic entry stays on the fast path");
double value;
CHECK(asymptotic_worldtube_value(&source, route.end_id, route.activate_t,
route.x, &value) == 0 &&
fabs(value) < 1e-3,
"entry on worldtube");
MetricData entry_metric;
CHECK(spacetime_eval(&source, route.activate_t, route.x, &entry_metric) ==
0,
"entry metric");
SchwarzschildCanonical entry_can;
CHECK(asymptotic_schwarzschild_canonical_from_state(
&end, &entry_metric, route.activate_t, route.x, route.Pi,
route.log_alpha_p0, &entry_can) == 0,
"entry canonical");
CHECK(fabs(entry_can.beta - camera_can.beta) <
1e-12 * fmax(1.0, camera_can.beta),
"entry conserves impact parameter");
CHECK(fabs(entry_can.energy - camera_can.energy) < 1e-12,
"entry conserves energy");
CHECK(entry_can.radial_sign == -1, "entry is past-inward");
CHECK(route.activate_t < 0.0, "entry time is in the past");
const TransferContext context = {.beta = camera_can.beta};
const double t_analytic = -route.activate_t;
const double t_numeric =
simpson(256.0, 500.0, 20000, transfer_dt, &context);
CHECK(fabs(t_analytic - t_numeric) < 1e-9 * fmax(1.0, t_numeric),
"entry time matches quadrature");
const double dphi_numeric =
simpson(256.0, 500.0, 20000, transfer_dphi, &context);
const double dphi_entry = angle_between(camera_can.rhat, entry_can.rhat);
CHECK(fabs(dphi_entry - dphi_numeric) < 1e-9,
"entry azimuth matches quadrature");
if (fabs(t_analytic - t_numeric) >= 1e-9 * fmax(1.0, t_numeric) ||
fabs(dphi_entry - dphi_numeric) >= 1e-9)
fprintf(stderr, " beta=%.6g t_an=%.12g t_num=%.12g dphi_an=%.12g "
"dphi_num=%.12g\n",
camera_can.beta, t_analytic, t_numeric, dphi_entry,
dphi_numeric);
spacetime_destroy(&source);
}
/* An external camera's dark-threshold reference must be the L at the camera
* event, not the worldtube entry energy. Changing only the worldtube radius
* must not change the reference but may change the entry L. */
static void test_camera_reference_radius_independent(void) {
const double radii[2] = {128.0, 256.0};
double reference[2], entry[2];
for (int k = 0; k < 2; ++k) {
SpacetimeSource source = {0};
CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, radii[k]) == 0,
"create reference source");
ObserverCamera positioned = {.look_ra_deg = 180.0, .look_dec_deg = 0.0};
positioned.position[0] = 500.0;
MetricData metric;
CHECK(spacetime_eval(&source, 0.0, positioned.position, &metric) == 0,
"reference camera metric");
ObserverState observer;
CHECK(observer_from_coordinate_camera(&metric, &positioned, &observer,
NULL) == OBSERVER_BUILD_OK,
"reference camera observer");
const double direction[3] = {cos(0.05), sin(0.05), 0.0};
MetricSlab *slab = NULL;
GeodesicRayState camera_state;
CHECK(spacetime_load_slab(&source, 0.0, -1.0, &slab) == 0,
"reference camera slab");
CHECK(geodesic_initialize_past_ray(slab, &observer, direction,
&camera_state) == 0,
"reference camera state");
spacetime_free_slab(slab);
AsymptoticRoute route;
CHECK(asymptotic_route_camera(&source, &observer, direction, &route) ==
ASYMPTOTIC_OK &&
route.kind == ASYMPTOTIC_ROUTE_ENTRY,
"reference ray enters the worldtube");
CHECK(fabs(route.log_alpha_p0_camera - camera_state.log_alpha_p0) < 1e-12,
"route reference is the camera-event L");
reference[k] = route.log_alpha_p0_camera;
entry[k] = route.log_alpha_p0;
spacetime_destroy(&source);
}
CHECK(fabs(reference[0] - reference[1]) < 1e-12,
"camera reference is worldtube-radius independent");
CHECK(fabs(entry[0] - entry[1]) > 1e-6,
"entry energy depends on the worldtube radius");
}
/* High-precision (mpmath, 60 digits) reference values fixed into the ordinary
* C test: radial, complex-pair, three-real, grazing, and large-radius angle
* cases. */
static void test_phi_reference_constants(void) {
static const struct {
double rho, beta, value;
} cases[] = {
{256.0, 0.0, 0.0},
{256.0, 5.0, 0.019532484697919191145},
{256.0, 60.0, 0.23656231243306290715},
{64.0, 64.0, 1.4199914058161304301},
{256.0, 255.0, 1.4527184167466732533},
{1.0e6, 1.0, 1.0000000000001666664e-6},
{300.0, 3.0, 0.010000165840750676787},
{100.0, 5.3, 0.053024471018799209953},
};
for (size_t i = 0; i < sizeof cases / sizeof cases[0]; ++i) {
const double got =
asymptotic_schwarzschild_phi(cases[i].rho, cases[i].beta);
CHECK(fabs(got - cases[i].value) < 2e-13, "phi high-precision reference");
}
}
/* High-precision (mpmath, 60 digits) finish references covering radial,
* complex-pair, three-real, grazing, and large-radius scattering. The
* acceptance standard here is the error-budget-driven 1e-8 rad, not the
* measured ~1e-13. */
static void test_finish_reference_constants(void) {
SpacetimeSource source = {0};
CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0) == 0,
"create schwarzschild");
SpacetimeAsymptoticEnd end;
CHECK(spacetime_asymptotic_end(&source, 0, &end) == 0, "end");
static const struct {
double rho, beta, n[3];
} cases[] = {
{256.0, 0.0, {0.8, 0.6, 0.0}},
{256.0, 3.0, {0.80697631554502468, 0.59058380112341107, 0.0}},
{256.0, 60.0, {0.91833674808504193, 0.39579997109220491, 0.0}},
{256.0, 255.0, {0.69006511550899122, -0.72374728763399356, 0.0}},
{1.0e6, 1.0, {0.8000005999996, 0.5999991999997, 0.0}},
};
for (size_t i = 0; i < sizeof cases / sizeof cases[0]; ++i) {
SchwarzschildCanonical canonical = {.end_id = 0,
.t = 0.0,
.rho = cases[i].rho,
.rhat = {0.8, 0.6, 0.0},
.Lhat = {0.0, 0.0, 1.0},
.beta = cases[i].beta,
.energy = 2.5,
.radial_sign = 1};
double n_inf[3], frequency = 0.0;
CHECK(asymptotic_schwarzschild_finish(&end, &canonical, n_inf,
&frequency) == 0,
"finish reference runs");
CHECK(angle_between(n_inf, cases[i].n) < 1e-8,
"finish n_inf high-precision reference");
CHECK(fabs(frequency - 0.4) < 1e-10 * 0.4,
"finish frequency high-precision reference");
}
spacetime_destroy(&source);
}
/* Turning equation residual |Q| at the computed turning radius. The final
* scattering direction is validated by test_grazing_reference(). */
static void test_turning_reference(void) {
const double betas[] = {3.0 * sqrt(3.0) + 1e-9, 5.5, 6.0, 10.0,
60.0, 255.0, 3890.44};
for (size_t i = 0; i < sizeof betas / sizeof betas[0]; ++i) {
const double rho = asymptotic_schwarzschild_turning_rho(betas[i]);
CHECK(isfinite(rho) && rho > 3.0, "turning radius exists and is exterior");
const double Q =
1.0 - betas[i] * betas[i] * (1.0 - 2.0 / rho) / (rho * rho);
CHECK(fabs(Q) <= 1e-11, "turning equation residual");
}
}
/* High-precision entry coordinate-time and swept-azimuth references, checking
* both the KS time transfer and the entry direction construction. */
static void test_time_reference(void) {
SpacetimeSource source = {0};
CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0) == 0,
"create schwarzschild");
SpacetimeAsymptoticEnd end;
CHECK(spacetime_asymptotic_end(&source, 0, &end) == 0, "end");
static const struct {
double rho_cam, beta, time, dphi;
} cases[] = {
{500.0, 10.0, 246.7884398934447041137, 0.01907105306677434549856},
{500.0, 0.3, 246.693149021326385798, 0.0005718752307574524347376},
{256.5, 10.0, 0.5082474340167056157528, 0.00007620281793853560952548},
{256.5, 0.3, 0.5078666185420216617125, 0.000002284358278417004222584},
{1000.0, 50.0, 753.1528987272233278083, 0.1465476883815797019938},
{1.0e6, 10.0, 999777.308033789182372,
0.03906238263856681534781},
};
for (size_t i = 0; i < sizeof cases / sizeof cases[0]; ++i) {
SchwarzschildCanonical camera = {.end_id = 0,
.t = 0.0,
.rho = cases[i].rho_cam,
.rhat = {1.0, 0.0, 0.0},
.Lhat = {0.0, 0.0, 1.0},
.beta = cases[i].beta,
.energy = 1.0,
.radial_sign = -1};
SchwarzschildRouteKind kind = SCH_ROUTE_UNSUPPORTED;
double activate_t = 0.0, x[3], Pi[3], log_alpha_p0 = 0.0, n_inf[3],
frequency = 0.0;
CHECK(asymptotic_schwarzschild_preroute(
&end, 256.0, &camera, &kind, &activate_t, x, Pi, &log_alpha_p0,
n_inf, &frequency) == 0 &&
kind == SCH_ROUTE_ENTRY,
"reference pre-route entry");
/* Error-budget-driven mixed tolerance, well below one ODE step (0.1 M)
* and future metric cadence. */
const double time_tol = 1e-7 + 1e-11 * fabs(cases[i].time);
CHECK(fabs(-activate_t - cases[i].time) < time_tol,
"entry time high-precision reference");
const double radius =
sqrt(x[0] * x[0] + x[1] * x[1] + x[2] * x[2]);
const double rhat[3] = {x[0] / radius, x[1] / radius, x[2] / radius};
CHECK(fabs(angle_between(camera.rhat, rhat) - cases[i].dphi) < 2e-11,
"entry azimuth high-precision reference");
}
spacetime_destroy(&source);
}
/* Near-grazing references where the exterior integrals are most sensitive:
* the two sides of beta_R enter through different branches and the KS time
* integral has a near-singular endpoint. (A photon-sphere turning is not
* reachable from a camera outside R/M >= 64, so it is not tested here.) */
static void test_grazing_reference(void) {
SpacetimeSource source = {0};
CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0) == 0,
"create schwarzschild");
SpacetimeAsymptoticEnd end;
CHECK(spacetime_asymptotic_end(&source, 0, &end) == 0, "end");
const double beta_R = 256.0 / sqrt(1.0 - 2.0 / 256.0);
SchwarzschildCanonical hit = {.end_id = 0,
.t = 0.0,
.rho = 500.0,
.rhat = {1.0, 0.0, 0.0},
.Lhat = {0.0, 0.0, 1.0},
.beta = beta_R * (1.0 - 1e-12),
.energy = 1.0,
.radial_sign = -1};
SchwarzschildRouteKind kind;
double activate_t, x[3], Pi[3], log_alpha_p0, n_inf[3], frequency;
CHECK(asymptotic_schwarzschild_preroute(&end, 256.0, &hit, &kind,
&activate_t, x, Pi, &log_alpha_p0,
n_inf, &frequency) == 0 &&
kind == SCH_ROUTE_ENTRY,
"near-grazing inside enters");
const double dphi_ref = 1.0389037630217253661;
const double time_ref = 434.0116073725480308524;
const double radius = sqrt(x[0] * x[0] + x[1] * x[1] + x[2] * x[2]);
const double rhat[3] = {x[0] / radius, x[1] / radius, x[2] / radius};
CHECK(fabs(angle_between(hit.rhat, rhat) - dphi_ref) < 1e-8,
"near-grazing entry azimuth");
CHECK(fabs(-activate_t - time_ref) < 1e-7 + 1e-11 * time_ref,
"near-grazing entry time");
SchwarzschildCanonical miss = hit;
miss.beta = beta_R * (1.0 + 1e-12);
CHECK(asymptotic_schwarzschild_preroute(&end, 256.0, &miss, &kind,
&activate_t, x, Pi, &log_alpha_p0,
n_inf, &frequency) == 0 &&
kind == SCH_ROUTE_ESCAPED,
"near-grazing outside misses");
const double n_ref[3] = {-0.86581533530640297059,
-0.50036367289028986187, 0.0};
CHECK(angle_between(n_inf, n_ref) < 1e-8, "near-grazing miss n_inf");
spacetime_destroy(&source);
}
/* Deterministic coverage of the three pre-route branches: past-outward,
* past-inward hit, and past-inward miss (turn before the worldtube). */
static void test_preroute_branches(void) {
SpacetimeSource source = {0};
CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0) == 0,
"create schwarzschild");
SpacetimeAsymptoticEnd end;
CHECK(spacetime_asymptotic_end(&source, 0, &end) == 0, "end");
const double beta_R = 256.0 / sqrt(1.0 - 2.0 / 256.0);
SchwarzschildCanonical base = {.end_id = 0,
.t = 0.0,
.rho = 500.0,
.rhat = {1.0, 0.0, 0.0},
.Lhat = {0.0, 0.0, 1.0},
.beta = 10.0,
.energy = 1.0,
.radial_sign = -1};
SchwarzschildRouteKind kind;
double activate_t, x[3], Pi[3], log_alpha_p0, n_inf[3], frequency;
const double outward_eps = 1e-12;
SchwarzschildCanonical outward = base;
outward.radial_sign = 1;
CHECK(asymptotic_schwarzschild_preroute(&end, 256.0, &outward, &kind,
&activate_t, x, Pi, &log_alpha_p0,
n_inf, &frequency) == 0 &&
kind == SCH_ROUTE_ESCAPED,
"past-outward branch escapes");
CHECK(fabs(sqrt(n_inf[0]*n_inf[0]+n_inf[1]*n_inf[1]+n_inf[2]*n_inf[2]) -
1.0) < outward_eps,
"outward n_inf is unit");
CHECK(fabs(frequency - 1.0) < 1e-12, "outward frequency");
SchwarzschildCanonical hit = base;
CHECK(asymptotic_schwarzschild_preroute(&end, 256.0, &hit, &kind,
&activate_t, x, Pi, &log_alpha_p0,
n_inf, &frequency) == 0 &&
kind == SCH_ROUTE_ENTRY,
"past-inward hit branch enters");
/* Genuine on-boundary tangent: rho = R, beta = beta_R (so Q = 0), zero
* radial past component. It must not enter. */
SchwarzschildCanonical tangent = base;
tangent.rho = 256.0;
tangent.beta = beta_R;
tangent.radial_sign = 0;
CHECK(asymptotic_schwarzschild_preroute(&end, 256.0, &tangent, &kind,
&activate_t, x, Pi, &log_alpha_p0,
n_inf, &frequency) == 0 &&
kind == SCH_ROUTE_ESCAPED,
"on-boundary tangent escapes");
SchwarzschildCanonical miss = base;
miss.beta = beta_R + 5.0;
CHECK(asymptotic_schwarzschild_preroute(&end, 256.0, &miss, &kind,
&activate_t, x, Pi, &log_alpha_p0,
n_inf, &frequency) == 0 &&
kind == SCH_ROUTE_ESCAPED,
"past-inward miss branch escapes");
CHECK(fabs(sqrt(n_inf[0]*n_inf[0]+n_inf[1]*n_inf[1]+n_inf[2]*n_inf[2]) -
1.0) < outward_eps,
"miss n_inf is unit");
/* A turning ray is deflected away from the radial direction. */
CHECK(angle_between(n_inf, miss.rhat) > 1e-3,
"miss n_inf is deflected");
spacetime_destroy(&source);
}
/* Translated-origin wrapper around the analytic Schwarzschild KS source: the
* inner metric is evaluated at x - origin and the worldtube/end are shifted by
* the same origin. This models a black hole at a large coordinate offset; the
* double reconstruction of the boundary state loses the sub-ULP offset and
* trips the common entry fallback, while the exact inward orbit transfer stays
* valid. It exercises the production fallback path, not a synthetic
* nonlinearity. */
typedef struct {
SpacetimeSource inner;
double origin[3];
} ShiftedOriginContext;
static SpacetimePointStatus shifted_origin_eval(const SpacetimeSource *source,
double t, const double x[3],
MetricData *metric) {
const ShiftedOriginContext *ctx = source->context;
const double local[3] = {x[0] - ctx->origin[0], x[1] - ctx->origin[1],
x[2] - ctx->origin[2]};
return spacetime_eval(&ctx->inner, t, local, metric);
}
static SpacetimeRayStatus shifted_origin_classify(const SpacetimeSource *source,
double t,
const double x[3]) {
const ShiftedOriginContext *ctx = source->context;
const double local[3] = {x[0] - ctx->origin[0], x[1] - ctx->origin[1],
x[2] - ctx->origin[2]};
return spacetime_classify(&ctx->inner, t, local);
}
static size_t shifted_origin_end_count(const SpacetimeSource *source) {
const ShiftedOriginContext *ctx = source->context;
return spacetime_asymptotic_end_count(&ctx->inner);
}
static int shifted_origin_end(const SpacetimeSource *source, size_t index,
SpacetimeAsymptoticEnd *out) {
const ShiftedOriginContext *ctx = source->context;
if (spacetime_asymptotic_end(&ctx->inner, index, out))
return -1;
for (int i = 0; i < 3; ++i)
out->frame_origin[i] = ctx->origin[i];
return 0;
}
static int shifted_origin_worldtube(const SpacetimeSource *source,
SpacetimeEndId end_id, double t,
SpacetimeEscapeWorldtubeSample *out) {
const ShiftedOriginContext *ctx = source->context;
if (spacetime_escape_worldtube_sample(&ctx->inner, end_id, t, out))
return -1;
for (int i = 0; i < 3; ++i)
out->center[i] += ctx->origin[i];
return 0;
}
static void shifted_origin_destroy(SpacetimeSource *source) {
ShiftedOriginContext *ctx = source->context;
if (ctx != NULL) {
spacetime_destroy(&ctx->inner);
free(ctx);
}
source->context = NULL;
source->ops = NULL;
}
static const SpacetimeOps shifted_origin_ops = {
.eval = shifted_origin_eval,
.classify = shifted_origin_classify,
.asymptotic_end_count = shifted_origin_end_count,
.asymptotic_end = shifted_origin_end,
.escape_worldtube_sample = shifted_origin_worldtube,
.destroy = shifted_origin_destroy};
static void test_translated_origin_fallback(void) {
ShiftedOriginContext *ctx = malloc(sizeof *ctx);
CHECK(ctx != NULL, "shifted-origin context");
if (ctx == NULL)
return;
ctx->origin[0] = 1.0e6;
ctx->origin[1] = 2.0e6;
ctx->origin[2] = -3.0e6;
CHECK(spacetime_create_schwarzschild_ks(&ctx->inner, 1.0, 256.0) == 0,
"shifted-origin inner source");
SpacetimeSource source = {.ops = &shifted_origin_ops, .context = ctx};
ObserverCamera cam = {.look_ra_deg = 180.0, .look_dec_deg = 0.0};
for (int i = 0; i < 3; ++i)
cam.position[i] = ctx->origin[i];
cam.position[0] += 500.0;
MetricData metric;
CHECK(spacetime_eval(&source, 0.0, cam.position, &metric) == 0,
"shifted-origin camera metric");
ObserverState observer;
CHECK(observer_from_coordinate_camera(&metric, &cam, &observer, NULL) ==
OBSERVER_BUILD_OK,
"shifted-origin camera observer");
const double direction[3] = {cos(0.3), sin(0.3), 0.0};
AsymptoticRoute route;
CHECK(asymptotic_route_camera(&source, &observer, direction, &route) ==
ASYMPTOTIC_OK &&
route.kind == ASYMPTOTIC_ROUTE_ENTRY,
"shifted-origin entry found");
CHECK(route.entry_fallback_evaluations > 0,
"shifted-origin entry used the common fallback");
CHECK(route.failure_reason == RAY_REASON_NONE,
"shifted-origin fallback has no failure reason");
double value;
CHECK(asymptotic_worldtube_value(&source, route.end_id, route.activate_t,
route.x, &value) == 0 &&
value <= 0.0,
"shifted-origin fallback state is inside the worldtube");
CHECK(route.activate_t < 0.0, "shifted-origin entry is in the past");
spacetime_destroy(&source);
}
int main(void) {
test_round_trip();
test_finish_matches_integration();
test_preroute_entry();
test_camera_reference_radius_independent();
test_phi_reference_constants();
test_finish_reference_constants();
test_turning_reference();
test_time_reference();
test_grazing_reference();
test_preroute_branches();
test_translated_origin_fallback();
if (failures == 0)
puts("asymptotic schwarzschild regression passed");
else
fprintf(stderr, "%d asymptotic schwarzschild failures\n", failures);
return failures == 0 ? 0 : 1;
}
+111 -21
View File
@@ -88,31 +88,47 @@ def image_payload(path, dimensions=(64, 48), allow_black=False):
return raw
# Version 3 wire layout. The first 100 bytes are the v2 provenance; the v3
# adaptive policy appends 9 doubles and a u32 (76 bytes) so the provenance
# block ends at 176. Each frame header is 48 bytes, so vertex payload starts at
# 224. A v3 vertex keeps the 84-byte v2 record and appends three u64 cost
# counters; a triangle stays 32 bytes. The CRC covers only the vertex+triangle
# payload, never the header.
MAP_PROVENANCE_END = 176
MAP_FRAME_HEADER_START = MAP_PROVENANCE_END
MAP_VERTEX_START = MAP_PROVENANCE_END + 48
MAP_VERTEX_SIZE = 84 + 24
MAP_TRIANGLE_SIZE = 32
def map_vertices(path):
data = path.read_bytes()
assert data[:8] == b'GRLENS\x01\x00'
assert struct.unpack_from('<Q', data, 32)[0] == 1
vertices, triangles = struct.unpack_from('<QQ', data, 64)
offset = 80
assert struct.unpack_from('<I', data, 8)[0] == 3, 'expected v3 lens map'
assert struct.unpack_from('<Q', data, 32)[0] == 1 # frame_count
vertices, triangles = struct.unpack_from('<QQ', data, MAP_FRAME_HEADER_START + 24)
offset = MAP_VERTEX_START
values = []
for _ in range(vertices):
values.append(struct.unpack_from('<9dI', data, offset))
offset += 76
return values, data[offset:offset + triangles * 28]
values.append(struct.unpack_from('<9dIIIQQQ', data, offset))
offset += MAP_VERTEX_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)
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
assert '--sensor-bloom-transfer' in help_text
assert '--dark-threshold' in help_text
assert '--observer-inward-speed' not in help_text
assert '_mesh.' in help_text, help_text
# The synthetic grid is calibrated for the renderer's default exposure.
@@ -134,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.
@@ -147,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)
@@ -202,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),
@@ -240,19 +274,27 @@ with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
(['--sensor-bloom-limit', 1, '--sensor-bloom-transfer', 1.5], None),
(['--sensor-bloom-limit', 1, '--sensor-bloom-transfer', 'nan'], None),
(['--sensor-bloom-limit', 1, '--sensor-bloom-transfer', 'inf'], None),
(['--dark-threshold', 'nan'], None),
(['--dark-threshold', 'inf'], None),
(['--dark-threshold', 0], None),
(['--dark-threshold', -1], None),
]
if backend == 'schwarzschild':
errors += [(['--observer-position', 1.5, 0, 0, '--observer-velocity', -0.5, 0, 0], 'capture cutoff'),
(['--observer-position', 1.75, 0, 0], 'not timelike')]
errors += [(['--observer-position', 1.75, 0, 0], 'not timelike')]
render('inside', '--observer-position', 1.75, 0, 0,
'--observer-velocity', -0.5, 0, 0, '--look-ra-deg', 0, '--look-dec-deg', 0)
# A camera inside the old r=1.5 capture cutoff is a normal target.
render('inside_cutoff', '--observer-position', 1.5, 0, 0,
'--observer-velocity', -0.5, 0, 0, '--look-ra-deg', 0, '--look-dec-deg', 0)
# The camera-relative dark threshold is configurable.
render('dark_threshold_10', '--dark-threshold', 10)
for options, message in errors:
missing_catalog = tmp / 'should_not_be_created.csv'
result = run(binary, '--catalog', missing_catalog, *options, ok=False)
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)
@@ -271,8 +313,11 @@ with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
assert len(a) == len(b) and ta == tb
max_error = 0
for x, y in zip(a, b):
assert x[-1] == y[-1], 'ray classification mismatch'
max_error = max(max_error, *(abs(v - w) for v, w in zip(x[:-1], y[:-1])))
# Indices 9..11 are end_id/outcome/reason; indices 12..14 are the
# per-vertex integration cost counters, which are not physical ray
# provenance and may differ between the single and movie schedulers.
assert x[9:12] == y[9:12], 'ray endpoint provenance mismatch'
max_error = max(max_error, *(abs(v - w) for v, w in zip(x[:9], y[:9])))
assert max_error < 1e-9, max_error
# Movie frames keep the clean primary image and gain correctly named
@@ -289,6 +334,24 @@ with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
# A map import must still work without evaluating a camera/metric, and
# must follow the same clean-main plus mesh-sibling rule.
assert single == render('import', '--lens-map-input', single_map)
# A structurally valid v2 map with an explicit metric failure must not
# bypass the live-tracing publication gate. Recompute the payload CRC
# so this tests completion semantics rather than corruption detection.
incomplete_map = tmp / f'{backend}_incomplete.grlens'
payload = bytearray(single_map.read_bytes())
struct.pack_into('<II', payload, MAP_VERTEX_START + 76, 3, 5) # INCOMPLETE / INVALID_METRIC
struct.pack_into('<I', payload, len(payload)-4,
zlib.crc32(payload[MAP_VERTEX_START:-4]))
incomplete_map.write_bytes(payload)
refused = tmp / f'{backend}_refused.{ext}'
failure = run(binary, *common, '--lens-map-input', incomplete_map,
'--output', refused, ok=False)
assert 'Incomplete render refused' in failure.stderr
assert not refused.exists()
diagnostic = run(binary, *common, '--lens-map-input', incomplete_map,
'--allow-incomplete', '--output', refused)
assert 'publishing incomplete render' in diagnostic.stderr
assert refused.exists()
imported_mesh = tmp / f'{backend}_import_mesh.{ext}'
run(binary, *common, '--lens-map-input', single_map, '--draw-mesh',
'--output', imported_mesh)
@@ -334,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
@@ -380,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)
@@ -399,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 / f'alcubierre.{ext}')
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)
+1013 -20
View File
File diff suppressed because it is too large. Load diff
+111 -5
View File
@@ -3,18 +3,122 @@
#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, appended detail codes must be
* distinct and valid, the coarse category helper must stay inside the coarse
* range, and both out-of-range directions (negative and >= COUNT) must be
* rejected as UNKNOWN/sentinel. The frozen wire ids 0..9 are pinned. */
static int check_reason_names(void) {
static const char *const frozen[] = {
"NONE", "REDSHIFT_LIMIT", "BUDGET_EXHAUSTED",
"TIME_RANGE_EXHAUSTED", "OUT_OF_DOMAIN", "INVALID_METRIC",
"INTEGRATION_ERROR", "UNSUPPORTED", "PROTOCOL_ERROR",
"IO_ERROR"};
int failed = 0;
for (size_t i = 0; i < sizeof frozen / sizeof frozen[0]; ++i) {
const RayReason r = (RayReason)i;
if (!ray_reason_valid(r) || strcmp(ray_reason_name(r), frozen[i]) != 0 ||
ray_reason_category(r) != r) {
fprintf(stderr, "frozen reason id %zu is not stable\n", i);
failed = 1;
}
}
if ((unsigned)RAY_REASON_IO_ERROR + 1u >= (unsigned)RAY_REASON_COUNT) {
fputs("no appended detail reasons\n", stderr);
failed = 1;
}
for (unsigned i = 0; i < (unsigned)RAY_REASON_COUNT; ++i) {
const RayReason r = (RayReason)i;
const char *name = ray_reason_name(r);
if (!ray_reason_valid(r) || name == NULL || name[0] == '\0' ||
strcmp(name, "UNKNOWN") == 0) {
fprintf(stderr, "reason %u has no stable label\n", i);
failed = 1;
}
if (!ray_reason_valid(ray_reason_category(r))) {
fprintf(stderr, "reason %u has no valid category\n", i);
failed = 1;
}
}
if (ray_reason_category(RAY_REASON_WORLDTUBE_SAMPLE_FAILED) !=
RAY_REASON_PROTOCOL_ERROR ||
ray_reason_category(RAY_REASON_OUTSIDE_WORLDTUBE) !=
RAY_REASON_PROTOCOL_ERROR ||
ray_reason_category(RAY_REASON_ESCAPE_LOCALIZATION_FAILED) !=
RAY_REASON_PROTOCOL_ERROR ||
ray_reason_category(RAY_REASON_REJECTION_LIMIT) !=
RAY_REASON_INTEGRATION_ERROR ||
ray_reason_category(RAY_REASON_INVALID_ESCAPE_DIRECTION) !=
RAY_REASON_INTEGRATION_ERROR ||
ray_reason_category(RAY_REASON_ENTRY_UNCONFIRMED) !=
RAY_REASON_INTEGRATION_ERROR ||
ray_reason_category(RAY_REASON_SLAB_LOAD_FAILED) != RAY_REASON_IO_ERROR) {
fputs("detail reasons map to the wrong coarse category\n", stderr);
failed = 1;
}
if (ray_reason_valid((RayReason)RAY_REASON_COUNT) ||
ray_reason_valid((RayReason)-1)) {
fputs("out-of-range reason accepted as valid\n", stderr);
failed = 1;
}
if (strcmp(ray_reason_name((RayReason)RAY_REASON_COUNT), "UNKNOWN") != 0) {
fputs("sentinel reason name is not UNKNOWN\n", stderr);
failed = 1;
}
const RayReason unknown = (RayReason)((unsigned)RAY_REASON_COUNT + 7u);
if (strcmp(ray_reason_name(unknown), "UNKNOWN") != 0 ||
ray_reason_valid(unknown) ||
ray_reason_category(unknown) != (RayReason)RAY_REASON_COUNT) {
fputs("unknown reason is not the UNKNOWN sentinel\n", stderr);
failed = 1;
}
return failed;
}
/* A NULL, non-finite or non-unit direction must be rejected as INVALID_ARGUMENT
* without dereferencing the direction or running any trace. */
static int check_invalid_directions(const SpacetimeSource *source,
const ObserverState *observer) {
const GeodesicTraceConfig config = {.coordinate_time_step = 0.25,
.max_steps = 100};
int failed = 0;
const RayEndpoint null_dir =
geodesic_trace_past(source, observer, NULL, &config);
if (null_dir.outcome != RAY_OUTCOME_INCOMPLETE ||
null_dir.reason != RAY_REASON_INVALID_ARGUMENT) {
fputs("NULL direction is not INVALID_ARGUMENT\n", stderr);
failed = 1;
}
const double nan_dir[3] = {NAN, 0.0, 0.0};
const RayEndpoint nan =
geodesic_trace_past(source, observer, nan_dir, &config);
if (nan.outcome != RAY_OUTCOME_INCOMPLETE ||
nan.reason != RAY_REASON_INVALID_ARGUMENT) {
fputs("NaN direction is not INVALID_ARGUMENT\n", stderr);
failed = 1;
}
const double nonunit_dir[3] = {2.0, 0.0, 0.0};
const RayEndpoint nonunit =
geodesic_trace_past(source, observer, nonunit_dir, &config);
if (nonunit.outcome != RAY_OUTCOME_INCOMPLETE ||
nonunit.reason != RAY_REASON_INVALID_ARGUMENT) {
fputs("non-unit direction is not INVALID_ARGUMENT\n", stderr);
failed = 1;
}
return failed;
}
static int check_ray(const SpacetimeSource *source,
const ObserverState *observer,
const double local_direction[3],
const double expected[3]) {
const GeodesicTraceConfig config = {.coordinate_time_step = 0.25,
const double expected[3]) { const GeodesicTraceConfig config = {.coordinate_time_step = 0.25,
.max_steps = 100};
RayEndpoint ray =
geodesic_trace_past(source, observer, local_direction, &config);
if (ray.status != RAY_ENDPOINT_ESCAPED ||
if (ray.outcome != RAY_OUTCOME_ESCAPED ||
!nearly_equal(ray.frequency_ratio, 1.0) ||
!nearly_equal(ray.n_infinity[0], expected[0]) ||
!nearly_equal(ray.n_infinity[1], expected[1]) ||
@@ -36,7 +140,9 @@ 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_invalid_directions(&source, &observer) ||
check_ray(&source, &observer, (double[]){1.0, 0.0, 0.0},
(double[]){0.0, 0.0, -1.0}) ||
check_ray(&source, &observer, (double[]){0.0, 0.0, 1.0},
(double[]){1.0, 0.0, 0.0});
@@ -64,7 +170,7 @@ int main(void) {
&source, &final_observer, (double[]){1.0, 0.0, 0.0},
&(GeodesicTraceConfig){.coordinate_time_step = 0.25, .max_steps = 100});
const double expected_g = sqrt(1.0 + 3.04 * 3.04) + 3.04;
if (forward.status != RAY_ENDPOINT_ESCAPED ||
if (forward.outcome != RAY_OUTCOME_ESCAPED ||
!nearly_equal(forward.frequency_ratio, expected_g)) {
fputs("accelerated-observer Doppler regression failed\n", stderr);
result = 1;
File diff suppressed because it is too large. Load diff
+740
View File
@@ -0,0 +1,740 @@
/* Standalone regression for the diagnostic mesh overlay (src/mesh_overlay.c).
* It links only the overlay module, so it needs neither a catalog, ray tracing,
* FFTW, nor an output writer. Every assertion targets observable behavior:
* terminal-category colors, half-edge switching, AA coverage, deduplication,
* deterministic ordering, clipping safety and clean failure on bad input. */
#include "mesh_overlay.h"
#include <float.h>
#include <limits.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int fail(const char *message) {
fprintf(stderr, "%s\n", message);
return -1;
}
static LensVertex make_vertex(double x, double y, int traced,
RayOutcome outcome) {
LensVertex vertex;
memset(&vertex, 0, sizeof vertex);
vertex.image_x = x;
vertex.image_y = y;
vertex.traced = traced;
vertex.outcome = outcome;
return vertex;
}
static int channel_at(const unsigned char *rgb, int width, int x, int y,
int channel) {
return rgb[3 * ((size_t)y * (size_t)width + (size_t)x) + (size_t)channel];
}
static int test_default_settings(void) {
const MeshOverlaySettings settings = mesh_overlay_default_settings();
static const unsigned char expected[MESH_OVERLAY_CATEGORY_COUNT][3] = {
{0x7F, 0x84, 0x9C}, {0xCB, 0xA6, 0xF7}, {0xF9, 0xE2, 0xAF},
{0xF3, 0x8B, 0xA8}, {0x89, 0xB4, 0xFA}};
if (settings.opacity != 0.5)
return fail("default opacity is not 0.5");
for (int category = 0; category < MESH_OVERLAY_CATEGORY_COUNT; ++category)
for (int channel = 0; channel < 3; ++channel)
if (settings.colors[category][channel] != expected[category][channel])
return fail("default palette mismatch");
return 0;
}
static int test_parse_color(void) {
unsigned char rgb[3] = {1, 2, 3};
if (mesh_overlay_parse_color("#7F849C", rgb) != 0 || rgb[0] != 0x7F ||
rgb[1] != 0x84 || rgb[2] != 0x9C)
return fail("parse uppercase failed");
if (mesh_overlay_parse_color("#7f849c", rgb) != 0 || rgb[0] != 0x7F ||
rgb[1] != 0x84 || rgb[2] != 0x9C)
return fail("parse lowercase failed");
if (mesh_overlay_parse_color("#000000", rgb) != 0 || rgb[0] || rgb[1] ||
rgb[2])
return fail("parse black failed");
static const char *const bad[] = {"", "#", "7F849C",
"#7F849", "#7F849C0", "#GG849C",
"#7F84 9C", "#7F849c ", " #7F849C",
"#12345g", "#12345G0"};
for (size_t i = 0; i < sizeof bad / sizeof *bad; ++i)
if (mesh_overlay_parse_color(bad[i], rgb) != -1)
return fail("accepted an invalid color string");
if (mesh_overlay_parse_color(NULL, rgb) != -1)
return fail("accepted NULL text");
if (mesh_overlay_parse_color("#7F849C", NULL) != -1)
return fail("accepted NULL output");
return 0;
}
/* Deduplication, canonical ordering and per-vertex category assignment. */
static int test_prepare_dedup_categories(void) {
LensVertex vertices[4] = {
make_vertex(10, 10, 1, RAY_OUTCOME_ESCAPED),
make_vertex(30, 10, 1, RAY_OUTCOME_DARK),
make_vertex(10, 30, 1, RAY_OUTCOME_UNRESOLVED),
make_vertex(30, 30, 1, RAY_OUTCOME_INCOMPLETE)};
LensTriangle triangles[2] = {{{0, 1, 2}, 0, 0, 0}, {{0, 2, 3}, 0, 0, 0}};
FrameLensMesh mesh = {.vertices = vertices,
.vertex_count = 4,
.triangles = triangles,
.triangle_count = 2};
MeshOverlayLines lines = {0};
if (mesh_overlay_prepare(&mesh, &lines) != 0)
return fail("prepare failed on a valid quad");
/* The shared diagonal (0,2) must appear exactly once. */
if (lines.count != 5) {
mesh_overlay_lines_destroy(&lines);
return fail("unique edge count is not 5");
}
static const struct {
unsigned char c0, c1;
} expected[5] = {
{MESH_OVERLAY_CATEGORY_ESCAPE, MESH_OVERLAY_CATEGORY_DARK},
{MESH_OVERLAY_CATEGORY_ESCAPE, MESH_OVERLAY_CATEGORY_UNRESOLVED},
{MESH_OVERLAY_CATEGORY_ESCAPE, MESH_OVERLAY_CATEGORY_INCOMPLETE},
{MESH_OVERLAY_CATEGORY_DARK, MESH_OVERLAY_CATEGORY_UNRESOLVED},
{MESH_OVERLAY_CATEGORY_UNRESOLVED, MESH_OVERLAY_CATEGORY_INCOMPLETE}};
for (size_t i = 0; i < lines.count; ++i)
if (lines.lines[i].category0 != expected[i].c0 ||
lines.lines[i].category1 != expected[i].c1) {
mesh_overlay_lines_destroy(&lines);
return fail("edge category or deterministic order mismatch");
}
if (lines.lines[0].x0 != 10 || lines.lines[0].y0 != 10 ||
lines.lines[0].x1 != 30 || lines.lines[0].y1 != 10) {
mesh_overlay_lines_destroy(&lines);
return fail("edge coordinates mismatch");
}
/* Lines must copy coordinates, never alias the mutable mesh. */
vertices[0].image_x = 999;
if (lines.lines[0].x0 != 10) {
mesh_overlay_lines_destroy(&lines);
return fail("overlay lines alias the live mesh");
}
mesh_overlay_lines_destroy(&lines);
if (lines.lines != NULL || lines.count != 0)
return fail("destroy did not reset the handle");
mesh_overlay_lines_destroy(NULL);
return 0;
}
static int test_untraced_category(void) {
LensVertex vertices[3] = {
make_vertex(10, 10, 1, RAY_OUTCOME_ESCAPED),
/* traced == 0 must win over the stale outcome value. */
make_vertex(30, 10, 0, RAY_OUTCOME_ESCAPED),
make_vertex(10, 30, 1, RAY_OUTCOME_INCOMPLETE)};
LensTriangle triangle = {{0, 1, 2}, 0, 0, 0};
FrameLensMesh mesh = {.vertices = vertices,
.vertex_count = 3,
.triangles = &triangle,
.triangle_count = 1};
MeshOverlayLines lines = {0};
if (mesh_overlay_prepare(&mesh, &lines) != 0)
return fail("prepare failed for untraced mesh");
int saw_untraced = 0;
for (size_t i = 0; i < lines.count; ++i)
if (lines.lines[i].category0 == MESH_OVERLAY_CATEGORY_UNTRACED ||
lines.lines[i].category1 == MESH_OVERLAY_CATEGORY_UNTRACED)
saw_untraced = 1;
mesh_overlay_lines_destroy(&lines);
return saw_untraced ? 0 : fail("untraced vertex category missing");
}
/* Boundary edges are emitted regardless of winding, and reversing the winding
* cannot change the deterministic output. */
static int test_boundary_and_winding(void) {
LensVertex vertices[3] = {
make_vertex(10, 10, 1, RAY_OUTCOME_ESCAPED),
make_vertex(30, 10, 1, RAY_OUTCOME_DARK),
make_vertex(10, 30, 1, RAY_OUTCOME_UNRESOLVED)};
LensTriangle forward[1] = {{{0, 1, 2}, 0, 0, 0}};
LensTriangle reversed[1] = {{{2, 1, 0}, 0, 0, 0}};
FrameLensMesh mesh_a = {.vertices = vertices,
.vertex_count = 3,
.triangles = forward,
.triangle_count = 1};
FrameLensMesh mesh_b = {.vertices = vertices,
.vertex_count = 3,
.triangles = reversed,
.triangle_count = 1};
MeshOverlayLines a = {0}, b = {0};
if (mesh_overlay_prepare(&mesh_a, &a) != 0 ||
mesh_overlay_prepare(&mesh_b, &b) != 0) {
mesh_overlay_lines_destroy(&a);
mesh_overlay_lines_destroy(&b);
return fail("prepare failed for single triangle");
}
int ok = a.count == 3 && b.count == 3 &&
memcmp(a.lines, b.lines, a.count * sizeof *a.lines) == 0;
mesh_overlay_lines_destroy(&a);
mesh_overlay_lines_destroy(&b);
return ok ? 0 : fail("boundary/winding determinism failed");
}
/* Off-mesh probe witnesses have no triangle edge and must never be emitted or
* drawn as a vertex dot. */
static int test_isolated_witness_not_drawn(void) {
LensVertex vertices[4] = {
make_vertex(10, 50, 1, RAY_OUTCOME_ESCAPED),
make_vertex(30, 50, 1, RAY_OUTCOME_ESCAPED),
make_vertex(20, 30, 1, RAY_OUTCOME_ESCAPED),
make_vertex(90, 90, 1, RAY_OUTCOME_ESCAPED)};
vertices[3].diagnostic_probe = 1;
vertices[3].probe_edge[0] = 0;
vertices[3].probe_edge[1] = 1;
LensTriangle triangle = {{0, 1, 2}, 0, 0, 0};
FrameLensMesh mesh = {.vertices = vertices,
.vertex_count = 4,
.triangles = &triangle,
.triangle_count = 1};
const int width = 100, height = 100;
MeshOverlayLines lines = {0};
unsigned char *rgb = calloc((size_t)width * height * 3, 1);
const MeshOverlaySettings settings = mesh_overlay_default_settings();
if (rgb == NULL || mesh_overlay_prepare(&mesh, &lines) != 0) {
free(rgb);
mesh_overlay_lines_destroy(&lines);
return fail("prepare failed for witness mesh");
}
if (lines.count != 3) {
free(rgb);
mesh_overlay_lines_destroy(&lines);
return fail("isolated witness added an edge");
}
if (mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) != 0) {
free(rgb);
mesh_overlay_lines_destroy(&lines);
return fail("draw failed for witness mesh");
}
const int witness_painted =
channel_at(rgb, width, 90, 90, 0) != 0 ||
channel_at(rgb, width, 90, 90, 1) != 0 ||
channel_at(rgb, width, 90, 90, 2) != 0;
free(rgb);
mesh_overlay_lines_destroy(&lines);
return witness_painted ? fail("isolated witness vertex was drawn as a dot")
: 0;
}
static int test_draw_category_colors(void) {
MeshOverlaySettings settings = mesh_overlay_default_settings();
settings.opacity = 1.0;
const int width = 80, height = 40;
for (int category = 0; category < MESH_OVERLAY_CATEGORY_COUNT; ++category) {
unsigned char *rgb = calloc((size_t)width * height * 3, 1);
if (rgb == NULL)
return fail("allocation failed");
MeshOverlayLine line = {.x0 = 10,
.y0 = 20,
.x1 = 70,
.y1 = 20,
.category0 = (unsigned char)category,
.category1 = (unsigned char)category};
MeshOverlayLines lines = {.lines = &line, .count = 1};
int ok = mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) == 0;
for (int channel = 0; ok && channel < 3; ++channel)
if (channel_at(rgb, width, 40, 20, channel) !=
settings.colors[category][channel])
ok = 0;
free(rgb);
if (!ok)
return fail("category color mismatch");
}
return 0;
}
static int test_halves_and_switch(void) {
MeshOverlaySettings settings = mesh_overlay_default_settings();
settings.opacity = 1.0;
const int width = 60, height = 40;
unsigned char *rgb = calloc((size_t)width * height * 3, 1);
if (rgb == NULL)
return fail("allocation failed");
MeshOverlayLine line = {.x0 = 10,
.y0 = 20,
.x1 = 50,
.y1 = 20,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_DARK};
MeshOverlayLines lines = {.lines = &line, .count = 1};
int ok = mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) == 0;
/* The switch sits at the major-axis midpoint x = 30. Interior pixels are
* fully covered, so each must equal exactly one endpoint color: the whole
* edge is rasterized once, never as two blends that would smear the switch. */
for (int channel = 0; ok && channel < 3; ++channel) {
if (channel_at(rgb, width, 11, 20, channel) !=
settings.colors[MESH_OVERLAY_CATEGORY_ESCAPE][channel] ||
channel_at(rgb, width, 29, 20, channel) !=
settings.colors[MESH_OVERLAY_CATEGORY_ESCAPE][channel] ||
channel_at(rgb, width, 30, 20, channel) !=
settings.colors[MESH_OVERLAY_CATEGORY_DARK][channel] ||
channel_at(rgb, width, 49, 20, channel) !=
settings.colors[MESH_OVERLAY_CATEGORY_DARK][channel])
ok = 0;
}
free(rgb);
return ok ? 0 : fail("half-edge color switch failed");
}
static int test_antialiasing(void) {
MeshOverlaySettings settings = mesh_overlay_default_settings();
settings.opacity = 1.0;
const int width = 60, height = 60;
unsigned char *rgb = calloc((size_t)width * height * 3, 1);
if (rgb == NULL)
return fail("allocation failed");
MeshOverlayLine line = {.x0 = 10,
.y0 = 10,
.x1 = 50,
.y1 = 20,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_ESCAPE};
MeshOverlayLines lines = {.lines = &line, .count = 1};
int ok = mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) == 0;
const int full = settings.colors[MESH_OVERLAY_CATEGORY_ESCAPE][0];
int partial = 0;
for (int y = 0; y < height; ++y)
for (int x = 0; x < width; ++x) {
const int value = channel_at(rgb, width, x, y, 0);
if (value > 0 && value < full)
++partial;
}
free(rgb);
return ok && partial > 0 ? 0 : fail("no antialiased partial coverage");
}
static int test_subpixel_and_zero_length(void) {
MeshOverlaySettings settings = mesh_overlay_default_settings();
settings.opacity = 1.0;
unsigned char pixels[8 * 8 * 3] = {0};
MeshOverlayLine line = {.x0 = 2.1, .y0 = 3.0, .x1 = 2.4, .y1 = 3.0,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_ESCAPE};
MeshOverlayLines lines = {.lines = &line, .count = 1};
if (mesh_overlay_draw_rgb8(&lines, pixels, 8, 8, &settings))
return fail("subpixel draw failed");
for (int channel = 0; channel < 3; ++channel)
if (channel_at(pixels, 8, 2, 3, channel) !=
lround((line.x1 - line.x0) * settings.colors[0][channel]))
return fail("subpixel edge applied overlapping endpoint blends");
memset(pixels, 0, sizeof pixels);
line.x1 = line.x0;
if (mesh_overlay_draw_rgb8(&lines, pixels, 8, 8, &settings))
return fail("zero-length draw failed");
for (size_t i = 0; i < sizeof pixels; ++i)
if (pixels[i])
return fail("zero-length edge became a vertex dot");
return 0;
}
static int test_high_white_background(void) {
const MeshOverlaySettings settings = mesh_overlay_default_settings();
const int width = 80, height = 40;
unsigned char *rgb = malloc((size_t)width * height * 3);
if (rgb == NULL)
return fail("allocation failed");
memset(rgb, 255, (size_t)width * height * 3);
MeshOverlayLine line = {.x0 = 10,
.y0 = 20,
.x1 = 60,
.y1 = 20,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_ESCAPE};
MeshOverlayLines lines = {.lines = &line, .count = 1};
int ok = mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) == 0;
for (int channel = 0; ok && channel < 3; ++channel) {
const long expected =
lround(255.0 * (1.0 - settings.opacity) +
settings.colors[MESH_OVERLAY_CATEGORY_ESCAPE][channel] *
settings.opacity);
if (channel_at(rgb, width, 40, 20, channel) != expected)
ok = 0;
}
/* Still clearly visible against white. */
if (ok && channel_at(rgb, width, 40, 20, 0) == 255)
ok = 0;
free(rgb);
return ok ? 0 : fail("overlay not visible on a high-white background");
}
static int test_opacity_extremes(void) {
const int width = 60, height = 40;
MeshOverlayLine line = {.x0 = 10,
.y0 = 20,
.x1 = 50,
.y1 = 20,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_ESCAPE};
MeshOverlayLines lines = {.lines = &line, .count = 1};
unsigned char *rgb = malloc((size_t)width * height * 3);
if (rgb == NULL)
return fail("allocation failed");
MeshOverlaySettings settings = mesh_overlay_default_settings();
memset(rgb, 0x33, (size_t)width * height * 3);
settings.opacity = 0.0;
if (mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) != 0) {
free(rgb);
return fail("draw failed at opacity 0");
}
for (int i = 0; i < width * height * 3; ++i)
if (rgb[i] != 0x33) {
free(rgb);
return fail("opacity 0 changed the image");
}
memset(rgb, 0x00, (size_t)width * height * 3);
settings.opacity = 1.0;
int ok = mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) == 0;
for (int channel = 0; ok && channel < 3; ++channel)
if (channel_at(rgb, width, 30, 20, channel) !=
settings.colors[MESH_OVERLAY_CATEGORY_ESCAPE][channel])
ok = 0;
free(rgb);
return ok ? 0 : fail("opacity 1 did not apply the full color");
}
static int test_clipping_and_huge_coordinates(void) {
const MeshOverlaySettings settings = mesh_overlay_default_settings();
const int width = 64, height = 64;
unsigned char *rgb = calloc((size_t)width * height * 3, 1);
if (rgb == NULL)
return fail("allocation failed");
/* A horizontal line far beyond both image edges must still paint row 30 and
* terminate in bounded time. */
MeshOverlayLine huge = {.x0 = -1e15,
.y0 = 30,
.x1 = 1e15,
.y1 = 30,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_DARK};
MeshOverlayLines lines = {.lines = &huge, .count = 1};
int ok = mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) == 0;
int painted = 0;
for (int x = 0; x < width; ++x)
if (channel_at(rgb, width, x, 30, 0) != 0)
painted = 1;
if (!ok || !painted) {
free(rgb);
return fail("huge coordinate line was not clipped into view");
}
/* A fully offscreen line leaves the buffer untouched. */
memset(rgb, 0, (size_t)width * height * 3);
MeshOverlayLine offscreen = {.x0 = 1000,
.y0 = 1000,
.x1 = 2000,
.y1 = 1000,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_ESCAPE};
lines.lines = &offscreen;
if (mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) != 0) {
free(rgb);
return fail("offscreen line returned an error");
}
for (int i = 0; i < width * height * 3; ++i)
if (rgb[i] != 0) {
free(rgb);
return fail("offscreen line painted the image");
}
/* A huge diagonal must not overflow the integer conversions. */
MeshOverlayLine diagonal = {.x0 = -1e12,
.y0 = -1e12,
.x1 = 1e12,
.y1 = 1e12,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_DARK};
lines.lines = &diagonal;
if (mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) != 0) {
free(rgb);
return fail("huge diagonal returned an error");
}
free(rgb);
return 0;
}
static int test_extreme_magnitudes(void) {
const MeshOverlaySettings settings = mesh_overlay_default_settings();
const int width = 64, height = 64;
const size_t bytes = (size_t)width * height * 3;
unsigned char *rgb = malloc(bytes);
if (rgb == NULL)
return fail("allocation failed");
/* -DBL_MAX..+DBL_MAX overflows the endpoint difference to infinity: the whole
* batch must be rejected before painting and the buffer left untouched. */
MeshOverlayLine bad = {.x0 = -DBL_MAX,
.y0 = 10,
.x1 = DBL_MAX,
.y1 = 10,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_ESCAPE};
MeshOverlayLines lines = {.lines = &bad, .count = 1};
memset(rgb, 0x5A, bytes);
int rejected =
mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) == -1;
int unchanged = 1;
for (size_t i = 0; i < bytes; ++i)
if (rgb[i] != 0x5A)
unchanged = 0;
if (!rejected || !unchanged) {
free(rgb);
return fail("+-DBL_MAX x-delta not rejected unchanged");
}
bad.x0 = 10;
bad.x1 = 10;
bad.y0 = -DBL_MAX;
bad.y1 = DBL_MAX;
memset(rgb, 0x5A, bytes);
rejected = mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) == -1;
unchanged = 1;
for (size_t i = 0; i < bytes; ++i)
if (rgb[i] != 0x5A)
unchanged = 0;
if (!rejected || !unchanged) {
free(rgb);
return fail("+-DBL_MAX y-delta not rejected unchanged");
}
/* Same-sign DBL_MAX endpoints have a finite difference and a finite (non
* overflowing) midpoint; the segment is entirely offscreen, so it is skipped
* safely without painting. */
MeshOverlayLine same = {.x0 = DBL_MAX,
.y0 = 10,
.x1 = DBL_MAX,
.y1 = 30,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_DARK};
lines.lines = &same;
memset(rgb, 0x5A, bytes);
if (mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) != 0) {
free(rgb);
return fail("same-sign DBL_MAX segment was not handled safely");
}
for (size_t i = 0; i < bytes; ++i)
if (rgb[i] != 0x5A) {
free(rgb);
return fail("offscreen same-sign DBL_MAX segment painted the image");
}
free(rgb);
return 0;
}
static int test_clipped_midpoint_uses_original(void) {
MeshOverlaySettings settings = mesh_overlay_default_settings();
settings.opacity = 1.0;
const int width = 64, height = 40;
unsigned char *rgb = calloc((size_t)width * height * 3, 1);
if (rgb == NULL)
return fail("allocation failed");
/* The original midpoint is -40, so the entire visible span [0, 19] lies in
* the second half and every visible pixel must use category1. A midpoint
* recomputed from the clipped endpoints would wrongly color the left half. */
MeshOverlayLine line = {.x0 = -100,
.y0 = 20,
.x1 = 20,
.y1 = 20,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_DARK};
MeshOverlayLines lines = {.lines = &line, .count = 1};
int ok = mesh_overlay_draw_rgb8(&lines, rgb, width, height, &settings) == 0;
static const int probes[] = {0, 1, 5, 10, 19};
for (size_t p = 0; ok && p < sizeof probes / sizeof *probes; ++p)
for (int channel = 0; channel < 3; ++channel)
if (channel_at(rgb, width, probes[p], 20, channel) !=
settings.colors[MESH_OVERLAY_CATEGORY_DARK][channel])
ok = 0;
free(rgb);
return ok ? 0
: fail("clipped edge did not use the original midpoint category");
}
static int test_invalid_arguments(void) {
MeshOverlaySettings settings = mesh_overlay_default_settings();
unsigned char pixels[4 * 4 * 3] = {0};
MeshOverlayLines lines = {0};
if (mesh_overlay_prepare(NULL, &lines) != -1)
return fail("prepare accepted NULL mesh");
if (mesh_overlay_prepare(NULL, NULL) != -1)
return fail("prepare accepted NULL lines");
FrameLensMesh empty = {0};
if (mesh_overlay_prepare(&empty, &lines) != 0 || lines.count != 0 ||
lines.lines != NULL)
return fail("empty mesh did not produce an empty edge set");
LensVertex vertices[3] = {
make_vertex(10, 10, 1, RAY_OUTCOME_ESCAPED),
make_vertex(30, 10, 1, RAY_OUTCOME_ESCAPED),
make_vertex(10, 30, 1, RAY_OUTCOME_ESCAPED)};
LensTriangle triangle = {{0, 1, 2}, 0, 0, 0};
FrameLensMesh mesh = {.vertices = vertices,
.vertex_count = 2, /* vertex 2 is out of range */
.triangles = &triangle,
.triangle_count = 1};
if (mesh_overlay_prepare(&mesh, &lines) != -1)
return fail("prepare accepted an out-of-range vertex");
mesh.vertex_count = 3;
vertices[2].image_x = NAN;
if (mesh_overlay_prepare(&mesh, &lines) != -1)
return fail("prepare accepted a nonfinite coordinate");
vertices[2].image_x = 10;
mesh.triangles = NULL;
if (mesh_overlay_prepare(&mesh, &lines) != -1)
return fail("prepare accepted NULL triangles");
FrameLensMesh overflow = {0};
overflow.triangle_count = SIZE_MAX; /* > SIZE_MAX / 3, rejected before use */
if (mesh_overlay_prepare(&overflow, &lines) != -1)
return fail("prepare accepted an overflowing triangle count");
MeshOverlayLine line = {.x0 = 1,
.y0 = 1,
.x1 = 3,
.y1 = 1,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_ESCAPE};
MeshOverlayLines one = {.lines = &line, .count = 1};
if (mesh_overlay_draw_rgb8(NULL, pixels, 4, 4, &settings) != -1)
return fail("draw accepted NULL lines");
if (mesh_overlay_draw_rgb8(&one, NULL, 4, 4, &settings) != -1)
return fail("draw accepted NULL pixels");
if (mesh_overlay_draw_rgb8(&one, pixels, 0, 4, &settings) != -1)
return fail("draw accepted zero width");
if (mesh_overlay_draw_rgb8(&one, pixels, 4, -1, &settings) != -1)
return fail("draw accepted negative height");
if (mesh_overlay_draw_rgb8(&one, pixels, 4, 4, NULL) != -1)
return fail("draw accepted NULL settings");
MeshOverlayLines null_lines = {.lines = NULL, .count = 1};
if (mesh_overlay_draw_rgb8(&null_lines, pixels, 4, 4, &settings) != -1)
return fail("draw accepted a NULL line array with a nonzero count");
/* Dimensions too near INT_MAX would overflow the raster's y+1/x+1 casts. */
if (mesh_overlay_draw_rgb8(&one, pixels, INT_MAX, 1, &settings) != -1)
return fail("draw accepted a width near INT_MAX");
if (mesh_overlay_draw_rgb8(&one, pixels, 1, INT_MAX, &settings) != -1)
return fail("draw accepted a height near INT_MAX");
/* An unallocatable line count must be rejected before the array dereference. */
MeshOverlayLines overflow_lines = {.lines = &line, .count = SIZE_MAX};
if (mesh_overlay_draw_rgb8(&overflow_lines, pixels, 4, 4, &settings) != -1)
return fail("draw accepted an overflowing line count");
MeshOverlaySettings bad = settings;
bad.opacity = NAN;
if (mesh_overlay_draw_rgb8(&one, pixels, 4, 4, &bad) != -1)
return fail("draw accepted NaN opacity");
bad.opacity = 1.5;
if (mesh_overlay_draw_rgb8(&one, pixels, 4, 4, &bad) != -1)
return fail("draw accepted opacity above 1");
bad.opacity = -0.1;
if (mesh_overlay_draw_rgb8(&one, pixels, 4, 4, &bad) != -1)
return fail("draw accepted negative opacity");
MeshOverlayLine bad_category = {.x0 = 1,
.y0 = 1,
.x1 = 3,
.y1 = 1,
.category0 = 99,
.category1 = MESH_OVERLAY_CATEGORY_ESCAPE};
MeshOverlayLines bad_lines = {.lines = &bad_category, .count = 1};
if (mesh_overlay_draw_rgb8(&bad_lines, pixels, 4, 4, &settings) != -1)
return fail("draw accepted an out-of-range category");
MeshOverlayLine bad_coord = {.x0 = NAN,
.y0 = 1,
.x1 = 3,
.y1 = 1,
.category0 = MESH_OVERLAY_CATEGORY_ESCAPE,
.category1 = MESH_OVERLAY_CATEGORY_ESCAPE};
bad_lines.lines = &bad_coord;
if (mesh_overlay_draw_rgb8(&bad_lines, pixels, 4, 4, &settings) != -1)
return fail("draw accepted a nonfinite coordinate");
MeshOverlayLines none = {.lines = NULL, .count = 0};
if (mesh_overlay_draw_rgb8(&none, pixels, 4, 4, &settings) != 0)
return fail("draw failed on an empty edge set");
return 0;
}
static int test_rgba_layer(void) {
LensVertex vertices[3] = {
make_vertex(2, 4, 1, RAY_OUTCOME_ESCAPED),
make_vertex(12, 4, 1, RAY_OUTCOME_ESCAPED),
make_vertex(2, 12, 1, RAY_OUTCOME_ESCAPED)};
LensTriangle triangles[2] = {{{0, 1, 2}, 0, 0, 0},
{{2, 1, 0}, 0, 0, 0}};
FrameLensMesh mesh = {.vertices = vertices, .vertex_count = 3,
.triangles = triangles, .triangle_count = 1};
MeshOverlaySettings settings = mesh_overlay_default_settings();
settings.opacity = 0.5;
MeshOverlayLayer layer = {0}, duplicate = {0};
if (mesh_overlay_build_layer(&mesh, 16, 16, &settings, &layer))
return fail("RGBA layer build failed");
int ok = layer.width == 16 && layer.height == 16;
const unsigned char *on_edge = &layer.rgba[4 * (4 * 16 + 5)];
for (int c = 0; c < 3; ++c)
ok &= on_edge[c] == lround(settings.colors[0][c] * settings.opacity);
ok &= on_edge[3] == 128;
for (size_t p = 0; p < 16 * 16; ++p)
for (int c = 0; c < 3; ++c)
ok &= layer.rgba[4 * p + c] <= layer.rgba[4 * p + 3];
mesh.triangle_count = 2;
ok &= mesh_overlay_build_layer(&mesh, 16, 16, &settings, &duplicate) == 0;
if (duplicate.rgba != NULL)
ok &= memcmp(layer.rgba, duplicate.rgba, 16 * 16 * 4) == 0;
unsigned char image[16 * 16 * 3];
memset(image, 255, sizeof image);
ok &= mesh_overlay_composite_rgb8(&layer, image, 16, 16) == 0;
for (int c = 0; c < 3; ++c)
ok &= image[3 * (4 * 16 + 5) + c] == on_edge[c] + 127;
ok &= image[0] == 255; /* no overlay coverage, not a full-image gray tint */
mesh_overlay_layer_destroy(&layer);
mesh_overlay_layer_destroy(&duplicate);
ok &= layer.rgba == NULL && layer.width == 0 && layer.height == 0;
settings.opacity = 0.0;
ok &= mesh_overlay_build_layer(&mesh, 16, 16, &settings, &layer) == 0;
if (layer.rgba != NULL)
for (size_t i = 0; i < 16 * 16 * 4; ++i)
ok &= layer.rgba[i] == 0;
unsigned char before[sizeof image];
memcpy(before, image, sizeof image);
ok &= mesh_overlay_composite_rgb8(&layer, image, 16, 16) == 0;
ok &= memcmp(before, image, sizeof image) == 0;
ok &= mesh_overlay_composite_rgb8(&layer, image, 15, 16) == -1;
mesh_overlay_layer_destroy(&layer);
ok &= mesh_overlay_build_layer(&mesh, 0, 16, &settings, &layer) == -1;
ok &= mesh_overlay_build_layer(&mesh, INT_MAX, INT_MAX, &settings, &layer) == -1;
vertices[0].image_x = NAN;
ok &= mesh_overlay_build_layer(&mesh, 16, 16, &settings, &layer) == -1;
ok &= layer.rgba == NULL;
return ok ? 0 : fail("RGBA premultiplication/composition/ownership regression");
}
static int test_rgba_composition_extremes(void) {
unsigned char rgba[] = {0, 0, 0, 0, 20, 40, 60, 255, 10, 20, 30, 128};
const MeshOverlayLayer layer = {.rgba = rgba, .width = 3, .height = 1};
unsigned char rgb[] = {100, 110, 120, 100, 100, 100, 100, 100, 100};
const unsigned char expected[] = {100, 110, 120, 20, 40, 60, 60, 70, 80};
return mesh_overlay_composite_rgb8(&layer, rgb, 3, 1) == 0 &&
memcmp(rgb, expected, sizeof rgb) == 0
? 0 : fail("RGBA transparent/opaque/partial-alpha composition");
}
int main(void) {
if (test_default_settings() || test_parse_color() ||
test_prepare_dedup_categories() || test_untraced_category() ||
test_boundary_and_winding() || test_isolated_witness_not_drawn() ||
test_draw_category_colors() || test_halves_and_switch() ||
test_antialiasing() || test_subpixel_and_zero_length() ||
test_high_white_background() ||
test_opacity_extremes() || test_clipping_and_huge_coordinates() ||
test_extreme_magnitudes() || test_clipped_midpoint_uses_original() ||
test_invalid_arguments() || test_rgba_layer() ||
test_rgba_composition_extremes())
return 1;
return 0;
}
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Production CLI regression for post-tone-map mesh settings and replay."""
import os
from pathlib import Path
import subprocess
import sys
import tempfile
BUILD = Path(sys.argv[1] if len(sys.argv) > 1 else 'build/Release').resolve()
BINARY = BUILD / 'minkowski_sky'
ENV = dict(os.environ, OMP_NUM_THREADS='4')
TMP = Path('/tmp/opencode')
TMP.mkdir(parents=True, exist_ok=True)
COLORS = ['escape', 'dark', 'unresolved', 'incomplete', 'untraced']
COMMON = ['--catalog', 'assets/sky_grid_5deg.csv', '--width', '32', '--height',
'24', '--coarse-cell-pixels', '8', '--refine-max-level', '0',
'--exposure', '1e-3', '--psf-relative-tail', '1e-4']
def run(*args, ok=True):
result = subprocess.run([str(BINARY), *map(str, args)], env=ENV,
capture_output=True, text=True)
assert (result.returncode == 0) == ok, (args, result.stdout, result.stderr)
return result
help_text = run('--help').stdout
ext = 'png' if '.png' in help_text else 'ppm'
for category in COLORS:
assert f'--mesh-color-{category}' in help_text
for value in ['red', '123456', '#12345', '#1234567', '#GG1122', '']:
result = run(f'--mesh-color-{category}', value, ok=False)
assert result.stderr and 'Rendered' not in result.stdout
run(f'--mesh-color-{category}', ok=False)
for value in ['nan', 'inf', '-inf', '-0.1', '1.1', 'junk']:
run('--mesh-opacity', value, ok=False)
run('--mesh-opacity', ok=False)
custom = []
for category, color in zip(COLORS, ['#123ABC', '#ABC123', '#AA5533',
'#1122EE', '#112233']):
custom += [f'--mesh-color-{category}', color]
custom += ['--mesh-opacity', '0.85']
with tempfile.TemporaryDirectory(prefix='mesh-cli-', dir=TMP) as directory:
tmp = Path(directory)
baseline = tmp / f'baseline.{ext}'
run(*COMMON, '--output', baseline)
configured = tmp / f'configured.{ext}'
run(*COMMON, *custom, '--output', configured)
assert baseline.read_bytes() == configured.read_bytes()
assert not (tmp / f'configured_mesh.{ext}').exists()
for opacity in [0, 1]:
output = tmp / f'opacity{opacity}.{ext}'
run(*COMMON, *custom, '--mesh-opacity', opacity, '--draw-mesh',
'--output', output)
assert output.read_bytes() == baseline.read_bytes()
mesh = tmp / f'opacity{opacity}_mesh.{ext}'
assert (mesh.read_bytes() == baseline.read_bytes()) == (opacity == 0)
single_map = tmp / 'single.grlens'
single = tmp / f'single.{ext}'
run(*COMMON, *custom, '--draw-mesh', '--lens-map-output', single_map,
'--output', single)
replay = tmp / f'replay.{ext}'
run('--catalog', 'assets/sky_grid_5deg.csv', '--exposure', '1e-3',
'--psf-relative-tail', '1e-4', *custom, '--draw-mesh',
'--lens-map-input', single_map, '--output', replay)
assert single.read_bytes() == replay.read_bytes()
assert (tmp / f'single_mesh.{ext}').read_bytes() == (tmp / f'replay_mesh.{ext}').read_bytes()
track = tmp / 'track.csv'
run('--write-minkowski-accel-track', track, '--duration', 2, '--fps', 4,
'--proper-acceleration', 0.1)
movie_dir, replay_dir = tmp / 'movie', tmp / 'movie_replay'
movie_dir.mkdir()
replay_dir.mkdir()
movie_map = tmp / 'movie.grlens'
run(*COMMON, *custom, '--draw-mesh', '--observer-track', track,
'--duration', 1, '--fps', 1, '--frames-dir', movie_dir,
'--frames-prefix', 'frame', '--lens-map-output', movie_map)
run('--catalog', 'assets/sky_grid_5deg.csv', '--exposure', '1e-3',
'--psf-relative-tail', '1e-4', *custom, '--draw-mesh',
'--lens-map-input', movie_map, '--frames-dir', replay_dir,
'--frames-prefix', 'frame')
expected_names = {f'frame_{i:06d}{suffix}.{ext}'
for i in range(2) for suffix in ['', '_mesh']}
assert {p.name for p in movie_dir.iterdir()} == expected_names
assert {p.name for p in replay_dir.iterdir()} == expected_names
for name in expected_names:
assert (movie_dir / name).read_bytes() == (replay_dir / name).read_bytes(), name
print('mesh-overlay CLI checks passed: colors/opacity, clean fidelity, single/movie replay')
+213 -29
View File
@@ -2,12 +2,14 @@
#include "movie_output.h"
#include <errno.h>
#include <math.h>
#include <omp.h>
#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
@@ -24,6 +26,15 @@ static void check(int condition, const char *message) {
}
}
/* Scratch data lives in the pre-approved OpenCode scratch directory rather
* than directly under /tmp. */
#define TEST_TMP_ROOT "/tmp/opencode"
#ifdef ENABLE_PNG
#define TEST_IMAGE_EXT "png"
#else
#define TEST_IMAGE_EXT "ppm"
#endif
#define MAX_JOBS 64
#define JOB_WIDTH 16
#define JOB_HEIGHT 16
@@ -35,6 +46,9 @@ typedef struct {
size_t count;
size_t order[MAX_JOBS];
unsigned char pixels[MAX_JOBS][JOB_BYTES];
int mesh_draw_flags[MAX_JOBS];
size_t mesh_pixel_counts[MAX_JOBS];
unsigned char layers[MAX_JOBS][JOB_WIDTH * JOB_HEIGHT * 4];
} MockWriter;
static int mock_write(void *context, const MovieOutputJob *job,
@@ -44,6 +58,12 @@ static int mock_write(void *context, const MovieOutputJob *job,
if (mock->count < MAX_JOBS) {
mock->order[mock->count] = job->frame_id;
memcpy(mock->pixels[mock->count], job->clean_rgb8, JOB_BYTES);
mock->mesh_draw_flags[mock->count] = job->draw_mesh;
mock->mesh_pixel_counts[mock->count] = job->mesh_layer.rgba == NULL ? 0 :
(size_t)job->mesh_layer.width * job->mesh_layer.height;
if (job->mesh_layer.rgba != NULL)
memcpy(mock->layers[mock->count], job->mesh_layer.rgba,
sizeof mock->layers[mock->count]);
}
++mock->count;
if (mock->delay_ms > 0) {
@@ -76,6 +96,22 @@ static MovieOutputJob make_job(size_t frame_id) {
return job;
}
/* An independent premultiplied RGBA8 fixture with sparse nonzero pixels. */
static int make_layer(MeshOverlayLayer *layer, size_t count) {
*layer = (MeshOverlayLayer){0};
layer->rgba = calloc(JOB_WIDTH * JOB_HEIGHT, 4);
if (layer->rgba == NULL)
return -1;
layer->width = JOB_WIDTH;
layer->height = JOB_HEIGHT;
for (size_t i = 0; i < count; ++i) {
for (int channel = 0; channel < 3; ++channel)
layer->rgba[4 * i + channel] = (unsigned char)((i * 19 + channel * 3) % 129);
layer->rgba[4 * i + 3] = 128;
}
return 0;
}
/* Order and pixel fidelity for capacity 1 and 2. */
static void test_order_and_pixels(size_t capacity) {
MovieOutputQueue queue;
@@ -146,7 +182,9 @@ static void test_backpressure(void) {
}
/* Writer failure at frame N must propagate, unblock the producer, and join
* cleanly without losing the ownership contract. */
* cleanly. RGBA layers are attached to every job so both ownership paths
* are exercised: the queue releases accepted jobs, while the caller releases
* the job rejected after the failure is recorded. */
static void test_writer_failure(void) {
MovieOutputQueue queue;
MockWriter mock;
@@ -161,9 +199,16 @@ static void test_writer_failure(void) {
int saw_failure = 0;
for (size_t f = 0; f < 8; ++f) {
MovieOutputJob job = make_job(f);
if (movie_output_queue_submit(&queue, &job, NULL)) {
/* The queue no longer owns these buffers. */
job.draw_mesh = 1;
if (make_layer(&job.mesh_layer, 3)) {
free(job.clean_rgb8);
check(0, "failure: layer allocation");
break;
}
if (movie_output_queue_submit(&queue, &job, NULL)) {
/* The queue no longer owns these resources. */
free(job.clean_rgb8);
mesh_overlay_layer_destroy(&job.mesh_layer);
saw_failure = 1;
break;
}
@@ -193,6 +238,76 @@ static void test_empty_paths(void) {
movie_output_queue_destroy(&queue);
}
/* The queued job owns an independent RGBA layer: rasterizing a heap mesh, then
* releasing that source mesh before the writer runs, must not disturb the
* pixels the writer observes. */
static void test_lines_survive_source_release(void) {
LensVertex *vertices = calloc(3, sizeof *vertices);
LensTriangle *triangles = calloc(1, sizeof *triangles);
if (vertices == NULL || triangles == NULL) {
free(vertices);
free(triangles);
check(0, "source release: allocation");
return;
}
vertices[0] = (LensVertex){.image_x = 1.5,
.image_y = 1.5,
.outcome = RAY_OUTCOME_ESCAPED,
.traced = 1};
vertices[1] = (LensVertex){.image_x = JOB_WIDTH - 2.5,
.image_y = 2.5,
.outcome = RAY_OUTCOME_DARK,
.traced = 1};
vertices[2] = (LensVertex){.image_x = 5.5,
.image_y = JOB_HEIGHT - 2.5,
.outcome = RAY_OUTCOME_INCOMPLETE,
.traced = 0};
triangles[0].vertex[0] = 0;
triangles[0].vertex[1] = 1;
triangles[0].vertex[2] = 2;
FrameLensMesh mesh = {.vertices = vertices,
.vertex_count = 3,
.triangles = triangles,
.triangle_count = 1};
MeshOverlayLayer layer = {0};
const MeshOverlaySettings overlay = mesh_overlay_default_settings();
const int prepared = mesh_overlay_build_layer(&mesh, JOB_WIDTH, JOB_HEIGHT,
&overlay, &layer) == 0;
/* Release the source mesh (and its vertex/triangle arrays) before submit. */
free(vertices);
free(triangles);
check(prepared, "source release: overlay extraction");
MovieOutputQueue queue;
MockWriter mock;
memset(&mock, 0, sizeof mock);
mock.fail_at = -1;
const PngWriteSettings settings = {-1};
if (movie_output_queue_init(&queue, 1, &settings)) {
check(0, "source release: queue init");
mesh_overlay_layer_destroy(&layer);
return;
}
movie_output_queue_set_writer(&queue, mock_write, &mock);
MovieOutputJob job = make_job(0);
job.draw_mesh = 1;
job.mesh_layer = layer; /* ownership transferred to the job/queue */
int submitted = job.clean_rgb8 != NULL &&
movie_output_queue_submit(&queue, &job, NULL) == 0;
if (!submitted) {
free(job.clean_rgb8);
mesh_overlay_layer_destroy(&job.mesh_layer);
}
check(submitted && movie_output_queue_finish(&queue) == 0,
"source release: submit/finish");
check(mock.count == 1 && mock.mesh_draw_flags[0] == 1 &&
mock.mesh_pixel_counts[0] == JOB_WIDTH * JOB_HEIGHT &&
memcmp(mock.layers[0], (unsigned char[JOB_WIDTH * JOB_HEIGHT * 4]){0},
sizeof mock.layers[0]) != 0,
"source release: writer saw the intact RGBA overlay");
movie_output_queue_destroy(&queue);
}
#ifdef ENABLE_PNG
static int decode_png_rgb8(const char *path, unsigned char *out, int width,
int height) {
@@ -216,12 +331,12 @@ static int decode_png_rgb8(const char *path, unsigned char *out, int width,
return ok ? 0 : -1;
}
/* The default writer's clean and mesh files must decode to the submitted
* payloads. */
static void test_default_writer_success(void) {
char directory[] = "/tmp/movie_output_XXXXXX";
/* The default writer's clean file must decode unchanged and its mesh sibling
* must equal the core overlay drawn in place on that same clean payload. */
static void test_default_writer_mesh(void) {
char directory[] = TEST_TMP_ROOT "/movie_output_XXXXXX";
if (mkdtemp(directory) == NULL) {
check(0, "default success: mkdtemp");
check(0, "default mesh: mkdtemp");
return;
}
char clean_path[PATH_MAX];
@@ -231,7 +346,7 @@ static void test_default_writer_success(void) {
const PngWriteSettings settings = {-1};
MovieOutputQueue queue;
if (movie_output_queue_init(&queue, 1, &settings)) {
check(0, "default success: queue init");
check(0, "default mesh: queue init");
rmdir(directory);
return;
}
@@ -239,29 +354,35 @@ static void test_default_writer_success(void) {
job.draw_mesh = 1;
snprintf(job.output_path, sizeof job.output_path, "%s", clean_path);
snprintf(job.mesh_path, sizeof job.mesh_path, "%s", mesh_path);
job.mesh_rgb8 = make_rgb8(99);
int submitted = job.clean_rgb8 != NULL && job.mesh_rgb8 != NULL &&
int layer_ok = make_layer(&job.mesh_layer, 5) == 0;
unsigned char *clean_expected = make_rgb8(0);
unsigned char mesh_expected[JOB_BYTES];
int expected_ok = layer_ok && clean_expected != NULL;
if (expected_ok) {
memcpy(mesh_expected, clean_expected, JOB_BYTES);
expected_ok = mesh_overlay_composite_rgb8(&job.mesh_layer, mesh_expected,
JOB_WIDTH, JOB_HEIGHT) == 0;
}
int submitted = expected_ok && job.clean_rgb8 != NULL &&
movie_output_queue_submit(&queue, &job, NULL) == 0;
if (!submitted) {
free(job.clean_rgb8);
free(job.mesh_rgb8);
mesh_overlay_layer_destroy(&job.mesh_layer);
}
check(submitted && movie_output_queue_finish(&queue) == 0,
"default success: submit/finish");
"default mesh: submit/finish");
unsigned char clean_decoded[JOB_BYTES];
unsigned char mesh_decoded[JOB_BYTES];
unsigned char *clean_expected = make_rgb8(0);
unsigned char *mesh_expected = make_rgb8(99);
const int decoded_ok =
clean_expected != NULL && mesh_expected != NULL &&
decode_png_rgb8(clean_path, clean_decoded, JOB_WIDTH, JOB_HEIGHT) == 0 &&
decode_png_rgb8(mesh_path, mesh_decoded, JOB_WIDTH, JOB_HEIGHT) == 0;
check(decoded_ok && memcmp(clean_decoded, clean_expected, JOB_BYTES) == 0,
"default success: clean pixels");
check(decoded_ok && memcmp(mesh_decoded, mesh_expected, JOB_BYTES) == 0,
"default success: mesh pixels");
check(decoded_ok && clean_expected != NULL &&
memcmp(clean_decoded, clean_expected, JOB_BYTES) == 0,
"default mesh: clean image unchanged");
check(decoded_ok && expected_ok &&
memcmp(mesh_decoded, mesh_expected, JOB_BYTES) == 0,
"default mesh: mesh matches core overlay result");
free(clean_expected);
free(mesh_expected);
unlink(clean_path);
unlink(mesh_path);
rmdir(directory);
@@ -269,38 +390,101 @@ static void test_default_writer_success(void) {
}
#endif
/* The default writer must fail under a real filesystem error. */
static void test_default_writer_failure(void) {
/* Failure while writing the clean image happens before any overlay work; the
* queue must still release the job's resources. */
static void test_default_writer_failure_before_overlay(void) {
MovieOutputQueue queue;
const PngWriteSettings settings = {-1};
if (movie_output_queue_init(&queue, 1, &settings)) {
check(0, "default failure: queue init");
check(0, "failure before overlay: queue init");
return;
}
MovieOutputJob job = make_job(0);
job.draw_mesh = 1;
if (make_layer(&job.mesh_layer, 4)) {
free(job.clean_rgb8);
check(0, "failure before overlay: layer allocation");
movie_output_queue_destroy(&queue);
return;
}
snprintf(job.output_path, sizeof job.output_path,
"/nonexistent-directory-xyz/frame.png");
"/nonexistent-directory-xyz/frame." TEST_IMAGE_EXT);
snprintf(job.mesh_path, sizeof job.mesh_path,
"/nonexistent-directory-xyz/frame_mesh." TEST_IMAGE_EXT);
if (job.clean_rgb8 == NULL || movie_output_queue_submit(&queue, &job, NULL)) {
free(job.clean_rgb8);
check(0, "default failure: submit");
mesh_overlay_layer_destroy(&job.mesh_layer);
check(0, "failure before overlay: submit");
movie_output_queue_destroy(&queue);
return;
}
check(movie_output_queue_finish(&queue) != 0,
"default failure: finish reports unwritable path");
"failure before overlay: clean write error propagates");
movie_output_queue_destroy(&queue);
}
/* Failure while writing the mesh image happens after the clean file is written
* and the overlay is drawn; that later error must also propagate. */
static void test_default_writer_failure_after_overlay(void) {
char directory[] = TEST_TMP_ROOT "/movie_output_XXXXXX";
if (mkdtemp(directory) == NULL) {
check(0, "failure after overlay: mkdtemp");
return;
}
char clean_path[PATH_MAX];
snprintf(clean_path, sizeof clean_path, "%s/frame_000000." TEST_IMAGE_EXT,
directory);
MovieOutputQueue queue;
const PngWriteSettings settings = {-1};
if (movie_output_queue_init(&queue, 1, &settings)) {
check(0, "failure after overlay: queue init");
rmdir(directory);
return;
}
MovieOutputJob job = make_job(0);
job.draw_mesh = 1;
if (make_layer(&job.mesh_layer, 4)) {
free(job.clean_rgb8);
check(0, "failure after overlay: layer allocation");
rmdir(directory);
movie_output_queue_destroy(&queue);
return;
}
snprintf(job.output_path, sizeof job.output_path, "%s", clean_path);
snprintf(job.mesh_path, sizeof job.mesh_path,
"/nonexistent-directory-xyz/frame_mesh." TEST_IMAGE_EXT);
if (job.clean_rgb8 == NULL || movie_output_queue_submit(&queue, &job, NULL)) {
free(job.clean_rgb8);
mesh_overlay_layer_destroy(&job.mesh_layer);
check(0, "failure after overlay: submit");
rmdir(directory);
movie_output_queue_destroy(&queue);
return;
}
check(movie_output_queue_finish(&queue) != 0,
"failure after overlay: mesh write error propagates");
unlink(clean_path);
rmdir(directory);
movie_output_queue_destroy(&queue);
}
int main(void) {
/* Standalone/CI runs need not have an OpenCode-created scratch directory. */
if (mkdir(TEST_TMP_ROOT, 0700) != 0 && errno != EEXIST) {
perror("create movie-output test scratch directory");
return 1;
}
test_order_and_pixels(1);
test_order_and_pixels(2);
test_backpressure();
test_writer_failure();
test_empty_paths();
test_lines_survive_source_release();
#ifdef ENABLE_PNG
test_default_writer_success();
test_default_writer_mesh();
#endif
test_default_writer_failure();
test_default_writer_failure_before_overlay();
test_default_writer_failure_after_overlay();
if (failures != 0) {
fprintf(stderr, "%d movie-output failure(s)\n", failures);
return 1;
+8 -7
View File
@@ -113,7 +113,7 @@ int main(int argc, char **argv) {
/* Ingoing radial light seen from the horizon and its interior must still
* trace backwards to the external sky, rather than be classified captured. */
const GeodesicTraceConfig trace = {.coordinate_time_step = 0.05,
.max_steps = 8192, .capture_log_alpha_p0 = 8};
.max_steps = 8192, .threshold = {.kind = THRESHOLD_LOG_ENERGY_GROWTH, .value = 8.0, .policy_version = 3}};
for (int i = 0; i < 3; ++i) {
camera = (ObserverCamera){.position = {2.25 - 0.25 * i, 0, 0},
.velocity = {-0.5, 0, 0}};
@@ -121,12 +121,13 @@ int main(int argc, char **argv) {
CHECK(observer_from_coordinate_camera(&metric, &camera, &state, NULL) == OBSERVER_BUILD_OK);
CHECK(check_state(&metric, &camera, &state) == 0);
const RayEndpoint ray = geodesic_trace_past(&source, &state, (double[]){1, 0, 0}, &trace);
CHECK(ray.status == RAY_ENDPOINT_ESCAPED);
CHECK(ray.outcome == RAY_OUTCOME_ESCAPED);
CHECK(fabs(ray.n_infinity[0] - 1) < 1e-12);
/* Radial ingoing KS photon has k^r=-k^t and conserved E=k^t.
* Current escape convention measures Eulerian energy at finite R=256. */
/* Radial ingoing KS photon has k^r=-k^t and conserved E=k^t. The
* asymptotic exterior transfers the photon to infinity, where
* g = E_camera / E_infinity = 1 / k^t. */
const double energy = state.tetrad[0][0] - state.tetrad[1][0];
CHECK(fabs(ray.frequency_ratio - sqrt(1 + 2.0 / 256) / energy) < 2e-6);
CHECK(fabs(ray.frequency_ratio - 1.0 / energy) < 1e-10 * (1.0 / energy));
memset(camera.velocity, 0, sizeof camera.velocity);
if (i > 0)
CHECK(observer_from_coordinate_camera(&metric, &camera, &state, NULL) == OBSERVER_BUILD_NON_TIMELIKE);
@@ -139,14 +140,14 @@ int main(int argc, char **argv) {
CHECK(observer_from_coordinate_camera(&metric, &camera, &state, NULL) == OBSERVER_BUILD_OK);
const GeodesicTraceConfig trace = {.coordinate_time_step = 1, .max_steps = 2048};
const RayEndpoint ray = geodesic_trace_past(&source, &state, (double[]){1, 0, 0}, &trace);
CHECK(ray.status == RAY_ENDPOINT_ESCAPED);
CHECK(ray.outcome == RAY_OUTCOME_ESCAPED);
CHECK(fabs(ray.n_infinity[0] - 0.8) < 1e-12);
CHECK(fabs(ray.n_infinity[1] + 0.6) < 1e-12);
CHECK(fabs(ray.frequency_ratio - 0.8) < 1e-12);
camera.position[0] = 25; camera.position[1] = -30; camera.position[2] = 10;
CHECK(observer_from_coordinate_camera(&metric, &camera, &state, NULL) == OBSERVER_BUILD_OK);
const RayEndpoint shifted = geodesic_trace_past(&source, &state, (double[]){1, 0, 0}, &trace);
CHECK(shifted.status == ray.status && fabs(shifted.frequency_ratio - ray.frequency_ratio) < 1e-12);
CHECK(shifted.outcome == ray.outcome && fabs(shifted.frequency_ratio - ray.frequency_ratio) < 1e-12);
for (int i = 0; i < 3; ++i) CHECK(fabs(shifted.n_infinity[i] - ray.n_infinity[i]) < 1e-12);
camera.velocity[1] = 1;
CHECK(observer_from_coordinate_camera(&metric, &camera, &state, NULL) == OBSERVER_BUILD_NON_TIMELIKE);
+150
View File
@@ -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)
+296
View File
@@ -0,0 +1,296 @@
#!/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. The rejection quota is reached before any
minimum-step check, so the exact detailed reason is ``REJECTION_LIMIT``. Small
16x8 scenes keep every case short. Two-frame movie coverage uses the existing
``test_observer_<backend>`` fixture track duplicated to two rows (the
Schwarzschild metric is stationary, so the same tetrad is valid at both
times); the test then checks that each failing sample is counted once across
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 rejection-quota failure: tight tolerances, a single fixed
# step bound, and one allowed rejection. Every ray rejects its first trial step
# and reports the exact REJECTION_LIMIT reason instead of a fabricated terminal
# category.
INJECT = ['--integrator', 'dp54',
'--ode-rtol', '1e-15', '--ode-atol-x', '1e-15',
'--ode-atol-pi', '1e-15', '--ode-atol-l', '1e-15',
'--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 'REJECTION_LIMIT' 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=REJECTION_LIMIT' in err, err
assert 'vertex=' in err and 'film=(' in err, err
assert 'accepted=' in err and 'rejected=' in err and 'rhs=' in err, err
# Bounded detail: at most RAY_DIAG_REPS_PER_REASON per reason, remainder
# 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 'REJECTION_LIMIT' in err, err
assert 'phase=import' in err, err
assert 'Incomplete render refused' in err, err
assert err.index('frame 0') < err.index('Incomplete render refused'), err
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 'REJECTION_LIMIT' in err and 'phase=import' in err, err
assert 'stop_t=' not in err and 'trusted=1' not in err, err
# 6) Budget exhaustion is a distinct, always-on message, and its refusal
# 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)
+88 -6
View File
@@ -22,9 +22,9 @@ int main(void) {
ObserverState oriented_observer;
const GeodesicTraceConfig trace = {.coordinate_time_step = 0.1,
.max_steps = 4096,
.capture_log_alpha_p0 = 8.0};
.threshold = {.kind = THRESHOLD_LOG_ENERGY_GROWTH, .value = 8.0, .policy_version = 3}};
int result = 1;
if (spacetime_create_schwarzschild_ks(&spacetime, 1.0, 256.0, 1.5) ||
if (spacetime_create_schwarzschild_ks(&spacetime, 1.0, 256.0) ||
spacetime_eval(&spacetime, 0.0, (double[]){2.0, 0.0, 0.0}, &metric) ||
!isfinite(metric.alpha) || !isfinite(metric.gamma[0][0]) ||
!isfinite(metric.K[0][0]) ||
@@ -48,13 +48,95 @@ int main(void) {
&spacetime, &observer, (double[]){cos(0.10), sin(0.10), 0.0}, &trace);
const RayEndpoint outside_shadow = geodesic_trace_past(
&spacetime, &observer, (double[]){cos(0.30), sin(0.30), 0.0}, &trace);
if (central.status != RAY_ENDPOINT_CAPTURED ||
inside_shadow.status != RAY_ENDPOINT_CAPTURED ||
outside_shadow.status != RAY_ENDPOINT_ESCAPED) {
if (central.outcome != RAY_OUTCOME_DARK ||
inside_shadow.outcome != RAY_OUTCOME_DARK ||
outside_shadow.outcome != RAY_OUTCOME_ESCAPED) {
fprintf(stderr,
"Schwarzschild KS shadow regression failed (center=%d, inside=%d, "
"outside=%d)\n",
central.status, inside_shadow.status, outside_shadow.status);
central.outcome, inside_shadow.outcome, outside_shadow.outcome);
goto done;
}
/* The dark threshold must also be checked on the final accepted step when
* that step lands exactly on the slab's left boundary. */
{
ObserverState inner;
if (camera_at(&spacetime, 3.0, 180.0, 0.0, &inner))
goto done;
const GeodesicTraceConfig last_step = {
.coordinate_time_step = 0.125,
.max_steps = 1,
.threshold = {.kind = THRESHOLD_LOG_ENERGY_GROWTH,
.value = 0.01,
.policy_version = 3}};
const RayEndpoint endpoint = geodesic_trace_past(
&spacetime, &inner, (double[]){1.0, 0.0, 0.0}, &last_step);
if (endpoint.outcome != RAY_OUTCOME_DARK ||
endpoint.reason != RAY_REASON_REDSHIFT_LIMIT ||
!(endpoint.threshold_value >= 0.01)) {
fprintf(stderr,
"last-step dark threshold regression failed (outcome=%d reason=%d "
"value=%.12g)\n",
endpoint.outcome, endpoint.reason, endpoint.threshold_value);
goto done;
}
}
/* A budget-exhausted ray is UNRESOLVED (retryable), keeps its last trusted
* state, and resolves when resumed from that state. */
{
ObserverCamera camera = {.position = {30,0,0},
.velocity = {-0.99999999,0,0}, .look_ra_deg = 0};
ObserverState boosted;
MetricData m;
if (spacetime_eval(&spacetime, 0, camera.position, &m) ||
observer_from_coordinate_camera(&m, &camera, &boosted, NULL)) goto done;
GeodesicRayState initial;
if (geodesic_initialize_past_ray_metric(&m, &boosted,
(double[]){1,0,0}, &initial) ||
initial.log_alpha_p0 <= 8) goto done;
GeodesicTraceConfig disabled = trace;
disabled.threshold.kind = THRESHOLD_DISABLED;
RayEndpoint enabled = geodesic_trace_past(&spacetime, &boosted,
(double[]){1,0,0}, &trace);
RayEndpoint reference = geodesic_trace_past(&spacetime, &boosted,
(double[]){1,0,0}, &disabled);
if (enabled.outcome != RAY_OUTCOME_ESCAPED ||
reference.outcome != RAY_OUTCOME_ESCAPED ||
fabs(enabled.frequency_ratio/reference.frequency_ratio-1) > 1e-10) {
fputs("initial high-energy false-dark regression failed\n", stderr); goto done;
}
}
const GeodesicTraceConfig tiny = {
.coordinate_time_step = 0.1,
.max_steps = 30,
.threshold = {.kind = THRESHOLD_LOG_ENERGY_GROWTH,
.value = 8.0,
.policy_version = 3}};
const RayEndpoint unresolved = geodesic_trace_past(
&spacetime, &observer, (double[]){cos(0.30), sin(0.30), 0.0}, &tiny);
if (unresolved.outcome != RAY_OUTCOME_UNRESOLVED ||
unresolved.reason != RAY_REASON_BUDGET_EXHAUSTED ||
unresolved.end_id != SPACETIME_END_NONE) {
fputs("budget-exhausted ray classification regression failed\n", stderr);
goto done;
}
const GeodesicRayState continuation = {
.coordinate_time = unresolved.stop_coordinate_time,
.x = {unresolved.final_x[0], unresolved.final_x[1],
unresolved.final_x[2]},
.Pi = {unresolved.final_Pi[0], unresolved.final_Pi[1],
unresolved.final_Pi[2]},
.log_alpha_p0 = unresolved.final_log_alpha_p0,
.log_alpha_p0_0 = unresolved.final_log_alpha_p0_0,
.steps = unresolved.accepted_steps};
GeodesicTraceConfig more = tiny;
more.max_steps = 8192;
const RayEndpoint resumed =
geodesic_trace_past_from_state(&spacetime, &continuation, &more);
if (resumed.outcome != RAY_OUTCOME_ESCAPED) {
fprintf(stderr,
"resumed ray classification regression failed (outcome=%d)\n",
(int)resumed.outcome);
goto done;
}
/* A coarse field covering the shadow must genuinely refine: its initial
+293
View File
@@ -0,0 +1,293 @@
/*
* Independent physics oracle for the ray-termination policy (plan P0).
*
* This test does not read production endpoints for its central assertions.
* It builds Schwarzschild-KS states independently and checks:
* 1. the two radial null branches dr/ds = 1 and dr/ds = (2M-r)/(2M+r);
* 2. the critical impact parameter b = 3 sqrt(3) M and photon sphere r = 3M;
* 3. camera energy normalization E_camera = 1 and tetrad orthonormality;
* 4. the threshold proxy identity ln(p^0) = L - ln(alpha) with
* L = ln(alpha p^0).
*
* The radial-branch assertions are integrated with the production RK4 RHS in
* src/geodesic.c so that an error in the 3+1 reduction is caught against a
* closed-form invariant rather than against a second copy of the same algebra.
*/
#include "asymptotic_schwarzschild.h"
#include "geodesic.h"
#include "observer.h"
#include "spacetime.h"
#include <math.h>
#include <stdio.h>
static int failures = 0;
#define CHECK(condition, message) \
do { \
if (!(condition)) { \
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, message); \
++failures; \
} \
} while (0)
/* Static Eulerian orthonormal tetrad at x = (r0, 0, 0) for r0 > 0. At this
* point the KS spatial metric is diagonal, so the principal axes are already
* orthonormal (up to the radial scale sqrt(gamma_rr)). */
static void radial_static_observer(const MetricData *metric, double r0,
ObserverState *out) {
*out = (ObserverState){.coordinate_time = 0.0,
.coordinate_position = {r0, 0.0, 0.0}};
const double alpha = metric->alpha;
out->tetrad[0][0] = 1.0 / alpha;
for (int i = 0; i < 3; ++i)
out->tetrad[0][i + 1] = -metric->beta[i] / alpha;
const double radial_scale = sqrt(metric->gamma[0][0]);
out->tetrad[1][1] = 1.0 / radial_scale;
out->tetrad[2][2] = 1.0;
out->tetrad[3][3] = 1.0;
}
/* Integrate a purely radial past ray with the production stepper and return
* its final state. Output endpoint is not inspected. */
static int trace_radial(const SpacetimeSource *source, const ObserverState *o,
double direction, GeodesicRayState *state) {
MetricData metric;
if (spacetime_eval(source, o->coordinate_time, o->coordinate_position,
&metric) != SPACETIME_POINT_OK)
return -1;
const double n[3] = {direction, 0.0, 0.0};
if (geodesic_initialize_past_ray_metric(&metric, o, n, state))
return -1;
const GeodesicTraceConfig config = {.coordinate_time_step = 0.02,
.max_steps = 400,
.threshold = {.kind = THRESHOLD_DISABLED, .value = 0.0, .policy_version = 0}};
MetricSlab *slab = NULL;
if (spacetime_load_slab(source, 0.0, -1000.0, &slab))
return -1;
RayEndpoint endpoint = {.end_id = SPACETIME_END_NONE,
.outcome = RAY_OUTCOME_INCOMPLETE};
const GeodesicAdvanceResult result =
geodesic_advance_past_ray(slab, state, -1000.0, &config, &endpoint);
spacetime_free_slab(slab);
return result == GEODESIC_ADVANCE_FAILED ? -1 : 0;
}
static void test_radial_branches(void) {
SpacetimeSource source = {0};
CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0) == 0,
"create schwarzschild");
const double r0 = 10.0;
MetricData metric;
CHECK(spacetime_eval(&source, 0.0, (double[]){r0, 0.0, 0.0}, &metric) ==
SPACETIME_POINT_OK,
"metric at r0");
ObserverState observer;
radial_static_observer(&metric, r0, &observer);
/* Branch dr/ds = +1: the closed-form solution is r = r0 + s. */
GeodesicRayState outward;
CHECK(trace_radial(&source, &observer, 1.0, &outward) == 0, "outward trace");
const double s_out = -outward.coordinate_time;
const double invariant_out = outward.x[0] - r0 - s_out;
CHECK(fabs(invariant_out) < 1e-6, "outward branch r = r0 + s");
/* Branch dr/ds = (2M-r)/(2M+r): the closed-form invariant is
* (r-2M) + 4M ln(r-2M) + s = const. */
GeodesicRayState inward;
CHECK(trace_radial(&source, &observer, -1.0, &inward) == 0, "inward trace");
const double s_in = -inward.coordinate_time;
const double c0 = (r0 - 2.0) + 4.0 * log(r0 - 2.0);
const double c1 = (inward.x[0] - 2.0) + 4.0 * log(inward.x[0] - 2.0) + s_in;
CHECK(inward.x[0] > 2.0, "inward branch stays outside the horizon");
CHECK(inward.x[0] < r0, "inward branch decreases r");
CHECK(fabs(c1 - c0) < 1e-6, "inward branch closed-form invariant");
/* Both branches are time-reversal partners: the outward and inward states
* reach the same |dr/ds| magnitude in opposite senses at r0. */
CHECK(outward.x[0] > r0, "outward branch increases r");
spacetime_destroy(&source);
}
/* The production dark policy is the camera-relative growth A_0 = L - L_0,
* independent of the backend. This oracle retains the stationary-KS
* conserved Killing energy A_K = L - ln|E_K| as an independent cross-check of
* the same ray: it verifies E_K conservation and the identity
* A_K - A_0 = -ln|alpha_0 - beta_0.Pi_0|. It is not the production
* criterion. */
static void test_killing_energy_reference(void) {
SpacetimeSource source = {0};
CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0) == 0,
"create schwarzschild for killing reference");
const double r0 = 10.0;
MetricData start_metric;
CHECK(spacetime_eval(&source, 0.0, (double[]){r0, 0.0, 0.0}, &start_metric) ==
SPACETIME_POINT_OK,
"metric for killing reference");
ObserverState observer;
radial_static_observer(&start_metric, r0, &observer);
GeodesicRayState start;
CHECK(geodesic_initialize_past_ray_metric(&start_metric, &observer,
(double[]){1.0, 0.0, 0.0},
&start) == 0,
"initialize killing ray");
double beta0 = 0.0;
for (int i = 0; i < 3; ++i)
beta0 += start_metric.beta[i] * start.Pi[i];
const double ek0 = exp(start.log_alpha_p0) * (start_metric.alpha - beta0);
CHECK(isfinite(ek0) && fabs(ek0) > 0.0, "nonzero Killing energy");
GeodesicRayState end;
CHECK(trace_radial(&source, &observer, 1.0, &end) == 0,
"trace killing reference ray");
MetricData end_metric;
CHECK(spacetime_eval(&source, end.coordinate_time, end.x, &end_metric) ==
SPACETIME_POINT_OK,
"metric at killing reference end");
double beta1 = 0.0;
for (int i = 0; i < 3; ++i)
beta1 += end_metric.beta[i] * end.Pi[i];
const double ek1 = exp(end.log_alpha_p0) * (end_metric.alpha - beta1);
CHECK(fabs(ek1 / ek0 - 1.0) < 1e-6,
"Killing energy conserved along the geodesic");
const double a0 = end.log_alpha_p0 - start.log_alpha_p0;
const double ak = end.log_alpha_p0 - log(fabs(ek1));
const double predicted = -log(fabs(start_metric.alpha - beta0));
CHECK(fabs((ak - a0) - predicted) < 1e-9,
"A_K - A_0 equals the initial boost factor");
spacetime_destroy(&source);
}
static void test_critical_parameters(void) {
const double b_crit = 3.0 * sqrt(3.0);
CHECK(!isfinite(asymptotic_schwarzschild_turning_rho(b_crit - 1e-6)),
"no turning point below b_crit");
CHECK(!isfinite(asymptotic_schwarzschild_turning_rho(3.0)),
"no turning point for a deeply plunging ray");
const double just_above = asymptotic_schwarzschild_turning_rho(b_crit + 1e-6);
CHECK(isfinite(just_above) && just_above > 3.0 && just_above < 3.01,
"turning radius approaches the photon sphere at b_crit");
const double b6 = asymptotic_schwarzschild_turning_rho(6.0);
CHECK(isfinite(b6) && b6 > 3.0, "turning radius above the photon sphere");
/* Verify the turning radius is an independent root of
* f(rho) = rho^3 - b^2 rho + 2 b^2. */
const double residual = b6 * b6 * b6 - 36.0 * b6 + 72.0;
CHECK(fabs(residual) < 1e-9, "turning radius satisfies the radial equation");
}
static void test_observer_normalization(void) {
SpacetimeSource source = {0};
CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0) == 0,
"create schwarzschild for observer");
ObserverCamera camera = {.coordinate_time = 0.0,
.position = {30.0, 0.0, 0.0},
.velocity = {0.0, 0.0, 0.0},
.look_ra_deg = 0.0,
.look_dec_deg = 0.0};
MetricData metric;
ObserverState observer;
CHECK(spacetime_eval(&source, 0.0, camera.position, &metric) ==
SPACETIME_POINT_OK,
"metric at camera");
CHECK(observer_from_coordinate_camera(&metric, &camera, &observer, NULL) ==
OBSERVER_BUILD_OK,
"build observer");
/* Orthonormality of the production tetrad, independently of the geodesic
* layer: g(e_a, e_b) = diag(-1, 1, 1, 1). */
for (int a = 0; a < 4; ++a) {
for (int b = 0; b < 4; ++b) {
const double *ea = observer.tetrad[a];
const double *eb = observer.tetrad[b];
double inner = -metric.alpha * metric.alpha * ea[0] * eb[0];
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
inner += metric.gamma[i][j] * (ea[i + 1] + metric.beta[i] * ea[0]) *
(eb[j + 1] + metric.beta[j] * eb[0]);
const double expected = a == b ? (a == 0 ? -1.0 : 1.0) : 0.0;
CHECK(fabs(inner - expected) < 1e-10, "tetrad orthonormal");
}
}
const double local[3] = {0.3, 0.5, 0.9};
const double norm = sqrt(local[0] * local[0] + local[1] * local[1] +
local[2] * local[2]);
const double direction[3] = {local[0] / norm, local[1] / norm,
local[2] / norm};
GeodesicRayState state;
CHECK(geodesic_initialize_past_ray_metric(&metric, &observer, direction,
&state) == 0,
"initialize past ray");
/* gamma is diagonal at (30, 0, 0): gamma_xx = 1 + 2/r. */
double gamma_inv[3][3];
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
gamma_inv[i][j] = (i == j) ? 1.0 / metric.gamma[i][j] : 0.0;
/* Null constraint gamma^{ij} Pi_i Pi_j = 1. */
double null_residual = 0.0;
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
null_residual += gamma_inv[i][j] * state.Pi[i] * state.Pi[j];
CHECK(fabs(null_residual - 1.0) < 1e-10, "null constraint preserved");
/* Observed energy -g(k, e0) = 1 for the unit observer four-velocity. The
* photon four-momentum is reconstructed from the stored state:
* p^0 = exp(L)/alpha and p^i = alpha p^0 gamma^{ij} Pi_j - beta^i p^0. */
const double k0 = exp(state.log_alpha_p0) / metric.alpha;
double k[4] = {k0, 0.0, 0.0, 0.0};
for (int i = 0; i < 3; ++i) {
double covariant = 0.0;
for (int j = 0; j < 3; ++j)
covariant += gamma_inv[i][j] * state.Pi[j];
k[i + 1] = metric.alpha * k0 * covariant - metric.beta[i] * k0;
}
const double *e0 = observer.tetrad[0];
double inner = -metric.alpha * metric.alpha * k[0] * e0[0];
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
inner += metric.gamma[i][j] * (k[i + 1] + metric.beta[i] * k[0]) *
(e0[j + 1] + metric.beta[j] * e0[0]);
CHECK(fabs(inner + 1.0) < 1e-10, "camera energy normalized to one");
spacetime_destroy(&source);
}
static void test_threshold_proxies(void) {
SpacetimeSource source = {0};
CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0) == 0,
"create schwarzschild for proxies");
const double r0 = 30.0;
MetricData metric;
CHECK(spacetime_eval(&source, 0.0, (double[]){r0, 0.0, 0.0}, &metric) ==
SPACETIME_POINT_OK,
"metric for proxies");
ObserverState observer;
radial_static_observer(&metric, r0, &observer);
const double direction[3] = {1.0, 0.0, 0.0};
GeodesicRayState state;
CHECK(geodesic_initialize_past_ray_metric(&metric, &observer, direction,
&state) == 0,
"initialize proxy ray");
/* L = ln(alpha p^0) is stored; ln(p^0) = L - ln(alpha). Recompute p^0 from
* the tetrad and direction independently. */
const double k0 = observer.tetrad[0][0] - direction[0] * observer.tetrad[1][0] -
direction[1] * observer.tetrad[2][0] -
direction[2] * observer.tetrad[3][0];
const double log_p0 = state.log_alpha_p0 - log(metric.alpha);
CHECK(fabs(log_p0 - log(k0)) < 1e-12,
"ln(p^0) = L - ln(alpha) with L = ln(alpha p^0)");
/* For a static observer far outside, alpha -> 1 and the two proxies agree
* to O(M/r); this documents why a fixed L threshold is not a fixed p^0
* threshold. */
CHECK(fabs(state.log_alpha_p0 - log_p0) > 1e-3,
"L and ln(p^0) differ near the hole");
spacetime_destroy(&source);
}
int main(void) {
test_radial_branches();
test_killing_energy_reference();
test_critical_parameters();
test_observer_normalization();
test_threshold_proxies();
if (failures != 0) {
fprintf(stderr, "termination oracle: %d failure(s)\n", failures);
return 1;
}
return 0;
}
+158 -31
View File
@@ -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 |
@@ -54,12 +58,24 @@ Single-frame camera options cannot be combined with `--observer-track`,
`--frames-dir`, or `--lens-map-input`.
Schwarzschild uses Cartesian ingoing Kerr–Schild coordinates with `M=1`.
Cameras at and inside the horizon `r=2` are allowed with a valid timelike
coordinate velocity. The current backend excludes camera positions at or
inside its capture cutoff `r=1.5`; its finite escape radius is `256`.
These remain analytic demonstration settings, not criteria for future NR data.
Cameras at and inside the horizon `r=2` (and inside the old `r=1.5` guard) are
allowed with a valid timelike coordinate velocity; position never decides a ray
endpoint. Its finite escape radius is `256`. These remain analytic demonstration
settings, not criteria for future NR data.
Zero coordinate velocity at or inside the horizon is not timelike and is rejected.
The normal dark terminal, for every backend, is the camera-relative local energy
growth `L - L0 >= T` (default `T = 8`, overridable with `--dark-threshold`),
where `L = ln(alpha p^0)` and `L0` is the photon's `L` at the **camera event**
(kept distinct from the escape-worldtube entry energy for an external camera).
A constant camera boost cancels, so a large initial `L` alone does not produce a
dark ray. Neither the photon energy nor the frequency ratio is reset;
budget-exhausted and data/integration failures are separate
unresolved/incomplete outcomes.
Failed and unresolved midpoint probes are retained as diagnostic samples, not
discarded after refinement. Lens-map replay uses its saved geometric policy and
the same incomplete-output check as live tracing.
The following complete examples use the bundled synthetic catalog:
```sh
@@ -104,31 +120,39 @@ with the bubble center following the constant-velocity worldline
$$f(r) = \frac{\tanh(\sigma(r+R)) - \tanh(\sigma(r-R))}{2\tanh(\sigma R)},\qquad
r_s = \sqrt{(x-x_s)^2 + y^2 + z^2}.$$
The bubble therefore propagates through the coordinates, and the metric is
time-dependent: the renderer evaluates `f(r_s)` and its spatial derivatives at
each coordinate time, while the extrinsic curvature supplies the required
`d_t gamma` information to the 3+1 null-ray equations. The exotic matter that
would source the bubble is treated as optically transparent, so there is
**no capture**: rays are only active or escaped. This is why the backend
requires a sub-luminal `|v_s| < 1`; at or above `1` the metric develops an
ergoregion/event horizon and static observers cease to exist, which is outside
the current no-capture scope.
The renderer evaluates the moving bubble's time-dependent metric along each
ray. The exotic matter sourcing the bubble is treated as optically transparent.
The shared dark policy terminates rays when `L - L0 >= T` (`T = 8` by default,
set with `--dark-threshold`); this finite threshold can also be reached at
sub-luminal bubble velocities.
| Option | Meaning / default |
| --- | --- |
| `--alcubierre-vs V` | Constant shift parameter, `|V| < 1` (default 0.5) |
| `--alcubierre-vs V` | Constant bubble velocity `v_s` (any finite value, default 0.5) |
| `--alcubierre-radius R` | Bubble radius `R > 0` (default 5) |
| `--alcubierre-sigma S` | Wall sharpness `S > 0` (default 1) |
`f` decays to zero past `r_s = R` over a transition width `~1/sigma`, so the
finite escape sphere is bubble-centered with radius `R + 20/sigma` and needs no
CLI option; it follows the moving bubble, so rays terminate only once the local
metric is flat to below double precision. The single-frame camera default is
`(0,0,15)` at `t = 0`, when the bubble is still at the origin; it must lie
inside the escape sphere, or the observer build fails with an explicit error.
The per-ray step budget scales with the escape radius and `1/(1-|v_s|)`, so
near-luminal `v_s` still lets grazing rays escape; combinations whose
worst-case budget would exceed the internal cap are rejected at startup.
A camera must be timelike with an orthonormal tetrad. Its coordinate velocity
`V = dx/dt` must satisfy `|V - v_s f e_x| < 1`. A static camera requires
`|v_s f| < 1`; at the bubble center, the comoving velocity `(v_s, 0, 0)` is
timelike even for super-luminal bubbles. Cameras may lie inside or outside the
escape sphere; exterior rays are routed to their first entry or to infinity.
The escape sphere follows the bubble with radius `R + 20/sigma`; escaping rays
continue through a Minkowski exterior. The single-frame camera defaults to
`(0,0,15)` at `t = 0`. The finite-radius truncation leaves a residual shift of
order `|v_s| e^{-40}`; accuracy at extremely large velocities is not guaranteed.
Per-ray coordinate-time coverage is a **resource allowance**
$$B = \frac{5\,R_\text{escape}}{\max\!\big(|1-|v_s||,\; e^{-T}\big)},\qquad
R_\text{escape} = R + \frac{20}{\sigma},$$
with `T = --dark-threshold`, clamped to `[DBL_MIN, DBL_MAX/4]`. This is not a
completion guarantee: quota exhaustion returns `UNRESOLVED/BUDGET_EXHAUSTED`.
`--trace-lookback-time` overrides `B` independently of `--trace-max-steps`.
RK4 rejects a derived default step estimate above its internal cap; an explicit
`--trace-max-steps` bypasses that check.
Lensing and frequency shifts come from the bubble wall. The configuration is
invariant under the isometry `(t, x) -> (t + T, x + v_s T)`, so observers
@@ -139,7 +163,7 @@ example:
make -j PSF_BACKEND=cpu SPACETIME=alcubierre backend
./build/Release/alcubierre_sky --catalog assets/sky_grid_5deg.csv \
--observer-radius 15 --look-ra-deg 90 --look-dec-deg -90 \
--alcubierre-vs 0.5 --alcubierre-radius 5 --alcubierre-sigma 1 \
--alcubierre-vs 1.5 --alcubierre-radius 1 --alcubierre-sigma 1 \
--width 640 --height 360 --fov-deg 60 --exposure 1 \
--coarse-cell-pixels 16 --refine-max-level 2 --psf-direct \
--output output/imgs/alcubierre_wall.png
@@ -195,6 +219,59 @@ Movie rays from all frames share a newest-to-oldest coordinate-time sweep.
backends use logical slabs without metric I/O; [Nmesh](https://github.com/nmeshsource/nmesh) metric loading remains
future work.
## Geodesic integration and tracing budgets
The default `--integrator dp54` selects adaptive Dormand–Prince 5(4). Position, photon
direction/momentum, and log-energy errors have separate absolute tolerances:
```text
--ode-rtol R
--ode-atol-x X
--ode-atol-pi P
--ode-atol-l L
--ode-initial-step H
--ode-min-step HMIN
--ode-max-step HMAX
--ode-max-rejections N
```
All tolerance and step values must be finite and positive; the initial step
must lie between the step bounds. The position absolute tolerance has the
backend's coordinate-length units. Smaller tolerances control local ODE error,
not a guaranteed bound on final sky-direction or image error near critical rays.
Default minimum steps are `1e-12` in Minkowski and Schwarzschild (`M=1`), and
`1e-12 * R` in Alcubierre. This is a conservative numerical guard, not a measured
physical minimum. Default maximum steps are `16`, `8M`, and eight times
`min(0.1, 0.05/sigma)`, respectively. Schwarzschild starts at `0.1M`; increasing
the upper bound does not force large steps through strong-field regions.
Use tolerance convergence for near-critical rays rather than interpreting the
upper bound as a global accuracy guarantee. See the bounded
[step-bound experiments](benchmarks/adaptive_step_bounds_2026-10-05/README.md).
Tracing has independent accepted-step and coordinate-time budgets:
```text
--trace-max-steps N
--trace-lookback-time T
--retry-step-increment N
--max-total-steps N
--retry-lookback-increment T
--max-total-lookback-time T
```
Changing the accepted-step allowance does not change the time allowance. A
trustworthy ray that exhausts either allowance is unresolved and can be resumed
by refinement with additional resources; it is not a physical dark endpoint.
Retries keep the last accepted state and camera energy reference, without
relaxing numerical tolerances. A hard limit that prevents a required retry
blocks normal output; `--allow-incomplete` is a diagnostic override.
`--integrator rk4` retains the fixed-step comparison path. Adaptive-only
tolerances and time-budget overrides are not applicable to that path. Use an
explicit stepper when reproducing a fixed-step reference rather than relying
on the executable's default.
## Reuse a completed lens map
`--lens-map-output FILE` saves the finalized inverse-lens mesh after tracing
@@ -224,6 +301,13 @@ on import. Explicit conflicting dimensions or FOV are rejected. The reader
checks the format version, finite values, unit directions, triangle indices,
and per-frame CRCs.
The map also records the stepper, numerical tolerances and step bounds, tracing
and retry allowances, and accepted/rejected/RHS costs. Replay uses this saved
provenance; tracing-option overrides are rejected because replay does not
integrate rays. Version 3 preserves adaptive provenance. Version 2 imports as
fixed RK4 with unavailable adaptive fields and cost diagnostics; version 1 is
rejected because its capture semantics cannot be reconstructed reliably.
A movie export stores all final frame meshes in one file. To render it again,
pass `--lens-map-input FILE`, the catalog, `--frames-dir DIR`, and
`--frames-prefix NAME`; no observer track is needed on import.
@@ -257,9 +341,13 @@ source-sky/lens-map length. A locally escaped triangle is split only when
`e / max(s, 1e-15) > --refine-angle-rel`. `P` and `A`
prevent selecting a leaf already at or below the requested image-plane
long-edge and area scales.
Triangles whose three vertices disagree between capture and escape are split
independently of the direction-error thresholds, allowing the mesh to follow a
shadow boundary.
Triangles whose three vertices straddle a dark/escape or unresolved/dark
boundary are split independently of the direction-error thresholds, allowing the
mesh to follow a shadow boundary. Unresolved vertices with an escape vertex (or
three unresolved vertices) are retried with more step budget before any split;
at the configured total cap the render is reported incomplete unless
`--allow-incomplete` is given. A UUD/UDD boundary triangle at the geometric stop
scale is approximately blackened and recorded with its image-plane area.
Independently of the midpoint geometry test, an all-escaped triangle also
computes the discrete lens Jacobian
@@ -526,13 +614,52 @@ the sum of producer frame times only (it excludes tracing, prefetch, and the
final queue drain). The all-sky `Movie catalog prefetch:` line reports mark,
load+commit, and total tile time. Timing uses one clock read per bulk phase,
never inside the per-star or per-pixel hot loops.
With `--draw-mesh`, tone mapping runs only once per frame. The async writer
writes the clean RGB8 image first, then composites the producer-rasterized
premultiplied RGBA8 layer in place and writes the mesh sibling. Mesh preparation
and rasterization are included in the producer's frame total; composition and
image output are included in the writer summary.
Pass `--draw-mesh` to also write the final image-plane triangle mesh as a
`<output-stem>_mesh.png` sibling (`.ppm` in non-PNG builds). The main
tone-mapped image and any `--hdr-output` FITS file remain mesh-free. The
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.
overlay alpha-composites one-pixel-wide, coverage-antialiased triangle edges
onto the final sRGB8 image **after** sensor bloom, tone mapping, and the sRGB
transfer, preserving mesh contrast on saturated highlights. Each vertex colors
its incident half-edges; differently classified endpoints switch color at the
edge midpoint. Shared edges are drawn once. The premultiplied sRGB RGBA8 layer
uses source-over accumulation and composition, with the same rules for
single-frame, movie, and replay output.
The default palette is Catppuccin Mocha, with opacity `0.5`:
| Vertex category | Default color | CLI override |
| --- | --- | --- |
| `ESCAPED` | Overlay1 `#7F849C` (gray) | `--mesh-color-escape` |
| `DARK` | Mauve `#CBA6F7` (purple) | `--mesh-color-dark` |
| `UNRESOLVED` | Yellow `#F9E2AF` | `--mesh-color-unresolved` |
| `INCOMPLETE` | Red `#F38BA8` | `--mesh-color-incomplete` |
| Untraced | Blue `#89B4FA` | `--mesh-color-untraced` |
Color arguments are strict sRGB `#RRGGBB` values; quote them in the shell.
`--mesh-opacity` accepts a finite number in `[0,1]`. These settings do not
implicitly enable `--draw-mesh`. For example:
```sh
--draw-mesh --mesh-color-dark '#CBA6F7' --mesh-color-unresolved '#F9E2AF' --mesh-opacity 0.8
```
`UNRESOLVED` denotes trustworthy trajectories with exhausted compute budgets
(not just accepted-step limits), while `INCOMPLETE` denotes actual history,
domain, metric, integration, I/O, or protocol failures. Different dark reasons
share one color. Coloring is a read-only visualization of the finalized mesh.
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