Compare commits
15
Commits
04611e3e5a
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c88bbc7e6 | ||
|
|
80f9dcb3a3 | ||
|
|
4053d5d0c8 | ||
|
|
7c980c35aa | ||
|
|
789549ed2f | ||
|
|
95c05f84d9 | ||
|
|
76ed705d37 | ||
|
|
af8b83007f | ||
|
|
69a8cb5647 | ||
|
|
b3f1e837d5 | ||
|
|
0a46a7095b | ||
|
|
c894fdb11a | ||
|
|
f7380cbf75 | ||
|
|
7fde49308b | ||
|
|
09a7417961 |
No files matched your search
@@ -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. **验证记录**:列出复用的完整测试声明/日志、自己实际执行的专项命令及规模/结果、未执行验证的具体限制。绝不声称自己运行了只由他人报告的测试。
|
||||
|
||||
保持报告精炼、可操作;没有某类内容时直接省略或用一句话说明,不输出冗长模板或泛化风险清单。
|
||||
@@ -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 已看过你的工具输出。
|
||||
@@ -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。
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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`.
|
||||
@@ -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
|
||||
|
@@ -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
@@ -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"
|
||||
@@ -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
@@ -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))
|
||||
@@ -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()
|
||||
@@ -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.
|
||||
@@ -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))
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
+131
-4
@@ -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
File diff suppressed because it is too large.
Load diff
+211
-12
@@ -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
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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 |
Binary file not shown.
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
+111
-5
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression test for the stdout/stderr split of the production CLI.
|
||||
|
||||
Normal progress, startup/backend configuration, successful file outputs,
|
||||
timings and cache summaries are normal informational output and must go to
|
||||
stdout. Genuine warnings, errors and the Debug per-event direct-fallback
|
||||
diagnostic go to stderr. A mixed success/error line such as
|
||||
``Rendered ... (ok|write failed)`` must follow its outcome.
|
||||
|
||||
The checks capture the two streams separately so that a future change which
|
||||
silently moves a normal message to stderr (or an error to stdout) is caught.
|
||||
Small deterministic Minkowski scenes keep the runtime short.
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
# Keep scratch data inside the pre-approved OpenCode scratch directory.
|
||||
TMP_ROOT = Path('/tmp/opencode')
|
||||
TMP_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
BUILD = Path(sys.argv[1] if len(sys.argv) > 1 else 'build/Release').resolve()
|
||||
ENV = dict(os.environ, OMP_NUM_THREADS='4')
|
||||
|
||||
COMMON = ['--catalog', 'assets/sky_grid_5deg.csv', '--width', 16, '--height', 8,
|
||||
'--fov-deg', 80, '--exposure', 1e-3, '--coarse-cell-pixels', 8,
|
||||
'--refine-max-level', 0, '--psf-relative-tail', 1e-4]
|
||||
|
||||
|
||||
def run(binary, *args, env=ENV):
|
||||
return subprocess.run([str(binary), *map(str, args)], env=env,
|
||||
capture_output=True, text=True)
|
||||
|
||||
|
||||
def stderr_errors(result):
|
||||
"""Non-advisory stderr lines.
|
||||
|
||||
A Debug build deliberately logs each direct PSF fallback from the active
|
||||
splat worker; those ``Debug:`` lines are real diagnostics, not a misplaced
|
||||
normal message, so they are excluded when the Debug startup banner is
|
||||
present on stdout.
|
||||
"""
|
||||
lines = [line for line in result.stderr.splitlines() if line.strip()]
|
||||
if 'Debug build:' in result.stdout:
|
||||
lines = [line for line in lines if not line.startswith('Debug:')]
|
||||
return lines
|
||||
|
||||
|
||||
binary = BUILD / 'minkowski_sky'
|
||||
assert binary.exists(), f'missing {binary}'
|
||||
|
||||
# The image extension follows the build's compiled writer, exactly as
|
||||
# tests/test_camera_cli.py detects it from --help (a libpng build advertises
|
||||
# `.png`, a PNG-less build advertises `.ppm`).
|
||||
help_text = run(binary, '--help').stdout
|
||||
ext = 'png' if '.png' in help_text else 'ppm'
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix='gr-output-streams-',
|
||||
dir=str(TMP_ROOT)) as directory:
|
||||
tmp = Path(directory)
|
||||
|
||||
# 1) --help is informational: complete usage on stdout, nothing on stderr.
|
||||
help_result = run(binary, '--help')
|
||||
assert help_result.returncode == 0, help_result.stderr
|
||||
assert 'Usage:' in help_result.stdout, help_result.stdout
|
||||
assert help_result.stderr == '', help_result.stderr
|
||||
|
||||
# 2) An unknown option is an error: usage diagnostic on stderr only.
|
||||
bad_result = run(binary, '--not-an-option')
|
||||
assert bad_result.returncode != 0, bad_result.stdout
|
||||
assert bad_result.stdout == '', bad_result.stdout
|
||||
assert bad_result.stderr != '', 'missing error diagnostic on stderr'
|
||||
|
||||
# 3) A successful non-verbose single-frame render puts every startup,
|
||||
# statistics and success line on stdout with an empty stderr.
|
||||
single_out = tmp / f'single.{ext}'
|
||||
single = run(binary, *COMMON, '--output', single_out)
|
||||
assert single.returncode == 0, single.stderr
|
||||
assert single_out.exists(), single.stderr
|
||||
assert 'Blackbody backend:' in single.stdout, single.stdout
|
||||
assert 'PSF cache ready:' in single.stdout, single.stdout
|
||||
assert 'Rendered' in single.stdout and '(ok)' in single.stdout, single.stdout
|
||||
assert '(write failed)' not in single.stdout, single.stdout
|
||||
assert not stderr_errors(single), single.stderr
|
||||
|
||||
# 4) Verbose progress (including the trace-cost line) is still stdout.
|
||||
verbose_out = tmp / f'verbose.{ext}'
|
||||
verbose = run(binary, *COMMON, '--verbose', '--output', verbose_out)
|
||||
assert verbose.returncode == 0, verbose.stderr
|
||||
assert 'Frame 0: tracing' in verbose.stdout, verbose.stdout
|
||||
assert 'Frame 0 trace cost:' in verbose.stdout, verbose.stdout
|
||||
assert not stderr_errors(verbose), verbose.stderr
|
||||
|
||||
# 5) A short movie exercises the asynchronous writer; its per-frame logs,
|
||||
# timing summary and writer summary are stdout, stderr stays empty. The
|
||||
# 2 s fixture track plus --duration 1 --fps 1 yields two frames, so the
|
||||
# async producer/writer overlap is actually exercised.
|
||||
track = tmp / 'track.csv'
|
||||
track_run = run(binary, '--write-minkowski-accel-track', track,
|
||||
'--duration', 2, '--fps', 30, '--proper-acceleration', 1.52)
|
||||
assert track_run.returncode == 0, track_run.stderr
|
||||
assert track.exists(), track_run.stderr
|
||||
frames_dir = tmp / 'frames'
|
||||
frames_dir.mkdir()
|
||||
movie = run(binary, *COMMON, '--observer-track', track, '--frames-dir',
|
||||
frames_dir, '--frames-prefix', 'frame', '--duration', 1,
|
||||
'--fps', 1, '--verbose', '--output', tmp / f'movie.{ext}')
|
||||
assert movie.returncode == 0, movie.stderr
|
||||
assert (frames_dir / f'frame_000000.{ext}').exists(), movie.stderr
|
||||
assert (frames_dir / f'frame_000001.{ext}').exists(), movie.stderr
|
||||
assert 'Ray trace generation' in movie.stdout, movie.stdout
|
||||
assert movie.stdout.count('Rendered') == 2, movie.stdout
|
||||
assert movie.stdout.count('(ok)') == 2, movie.stdout
|
||||
assert 'Movie timing total' in movie.stdout, movie.stdout
|
||||
assert 'Movie writer summary:' in movie.stdout, movie.stdout
|
||||
assert 'Movie end-to-end wall:' in movie.stdout, movie.stdout
|
||||
assert not stderr_errors(movie), movie.stderr
|
||||
|
||||
# 6) A genuine warning goes to stderr and does not disturb the success line
|
||||
# on stdout. The fast-mode preview advisory is deterministic in the CPU
|
||||
# build.
|
||||
fast_out = tmp / f'fast.{ext}'
|
||||
fast = run(binary, *COMMON, '--fast-mode', '--output', fast_out)
|
||||
assert fast.returncode == 0, fast.stderr
|
||||
assert 'Fast mode is a preview approximation' in fast.stderr, fast.stderr
|
||||
assert fast.stdout.count('Rendered') == 1, fast.stdout
|
||||
assert '(ok)' in fast.stdout, fast.stdout
|
||||
# The only stderr content is the advisory: no normal line leaked across.
|
||||
assert len(stderr_errors(fast)) == 1, fast.stderr
|
||||
|
||||
# 7) A failed write must route the mixed success/error line to stderr and
|
||||
# leave stdout free of the success wording.
|
||||
missing_dir = tmp / 'missing_subdir' / f'out.{ext}'
|
||||
failed = run(binary, *COMMON, '--output', missing_dir)
|
||||
assert failed.returncode != 0, failed.stdout
|
||||
assert '(write failed)' in failed.stderr, failed.stderr
|
||||
assert '(write failed)' not in failed.stdout, failed.stdout
|
||||
|
||||
# 8) A rejected camera velocity is an error on stderr, not stdout.
|
||||
velocity = run(binary, *COMMON, '--observer-velocity', 10, 0, 0,
|
||||
'--output', tmp / f'velocity.{ext}')
|
||||
assert velocity.returncode != 0, velocity.stdout
|
||||
assert 'not timelike' in velocity.stderr, velocity.stderr
|
||||
# stdout may hold only the Debug startup banner; no render ran.
|
||||
assert 'Rendered' not in velocity.stdout, velocity.stdout
|
||||
|
||||
print('output-stream checks passed: normal success stdout / diagnostics '
|
||||
'stderr', flush=True)
|
||||
@@ -0,0 +1,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)
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user