Feat: Rework ray termination into escaped/dark/unresolved/incomplete
Replace the position capture cutoff with a camera-relative dark threshold shared by every backend, and carry explicit outcome/reason provenance through the ray, RayPool, adaptive mesh, lens-map and replay paths. - eval/eval_slab return SpacetimePointStatus; remove SPACETIME_RAY_CAPTURED and the Schwarzschild capture radius; decouple observer construction from ray position. - RayEndpoint stores RayOutcome/RayReason plus the last trusted state; budget exhaustion is retryable UNRESOLVED, data/integration failures are INCOMPLETE. - Normal dark terminal is L - L0 >= --dark-threshold (default 8), with L0 taken at the camera event and kept distinct from the worldtube entry energy; photon energy and frequency ratio are never reset. - Implement E/D/U triangle decisions with merged budget retries, persistent probe witnesses promoted in place by vertex identity, conformity settling, and approximate-black boundary provenance with achieved-scale statistics. - Add RayPool continuation state and per-ray step budgets. - Bump lens-map to v2 with explicit end/outcome/reason, approx_black, threshold/retry/geometry provenance and per-frame retry counts; reject v1. - Gate production output on incomplete/error results, overridable with --allow-incomplete. - Update AGENTS.md, the design document and usage docs; add the termination oracle and regression coverage. make -B -j4 BUILD_TYPE=Debug test passes with bit-identical reference HDRs.
This commit is contained in:
1 parent
7fde49308b
commit
f7380cbf75
31 files changed
+2349
-402
No files matched your search
@@ -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 或穿越视界本身不是暗终态。
|
||||
|
||||
## 开发与验证顺序
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ TEST_OUT_DIR := $(OBJECT_DIR)/$(HDR_BUILD_TAG)
|
||||
TEST_TARGET := $(TEST_OUT_DIR)/test_geodesic
|
||||
ASYMPTOTIC_TEST_TARGET := $(TEST_OUT_DIR)/test_asymptotic
|
||||
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
|
||||
@@ -218,6 +219,11 @@ $(FRAME_TEST_TARGET): tests/test_frame.c $(CORE_MINKOWSKI_SOURCES) $(CPU_FFTW_SO
|
||||
$(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 $@
|
||||
|
||||
@@ -267,12 +273,13 @@ FAST_PSF_FFTW_TEST_DEP :=
|
||||
FAST_PSF_FFTW_TEST_RUN :=
|
||||
endif
|
||||
|
||||
test: $(CAMERA_TEST_TARGETS) $(TEST_TARGET) $(ASYMPTOTIC_TEST_TARGET) $(ASYMPTOTIC_SCHWARZSCHILD_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) $(ASYMPTOTIC_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)
|
||||
$(TEST_OUT_DIR)/test_observer_minkowski
|
||||
$(TEST_OUT_DIR)/test_observer_schwarzschild
|
||||
$(TEST_TARGET)
|
||||
$(ASYMPTOTIC_TEST_TARGET)
|
||||
$(ASYMPTOTIC_SCHWARZSCHILD_TEST_TARGET)
|
||||
$(TERMINATION_ORACLE_TEST_TARGET)
|
||||
$(FRAME_TEST_TARGET)
|
||||
$(SCHWARZSCHILD_TEST_TARGET)
|
||||
$(ALCUBIERRE_TEST_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.
|
||||
@@ -254,8 +254,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`.
|
||||
@@ -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 {
|
||||
@@ -417,8 +418,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 +710,72 @@ 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。
|
||||
|
||||
---
|
||||
|
||||
@@ -963,6 +1010,21 @@ residual、Chebyshev 表或解析主项;运行期不得建表。
|
||||
- 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$ 解析外区:闭式约化与验证
|
||||
|
||||
@@ -1707,7 +1769,9 @@ void rays_trace_generation(
|
||||
- metric interpolation;
|
||||
- spatial derivatives;
|
||||
- 必要的 temporal derivatives;
|
||||
- capture/infinity classification。
|
||||
- metric/data 状态(成功、时间不足、空间域不足、invalid metric、内部错误);
|
||||
- 渐近端声明与 escape worldtube 能力。物理暗终态由能量阈值 policy 判定,不由
|
||||
位置分类决定。
|
||||
|
||||
---
|
||||
|
||||
@@ -1811,7 +1875,7 @@ map 共用同一入口。每个 RGB 通道独立、各向同性地把超过有
|
||||
|
||||
验证:
|
||||
|
||||
- capture;
|
||||
- 红移暗阈值截断与 shadow;
|
||||
- Einstein ring;
|
||||
- multiple images;
|
||||
- adaptive refinement;
|
||||
@@ -1839,7 +1903,7 @@ map 共用同一入口。每个 RGB 通道独立、各向同性地把超过有
|
||||
\quad
|
||||
g,
|
||||
\quad
|
||||
\text{captured/escaped classification}
|
||||
\text{end/outcome classification}
|
||||
\]
|
||||
|
||||
---
|
||||
@@ -1868,7 +1932,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 的最终组合;
|
||||
@@ -1879,7 +1943,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` 混同。
|
||||
|
||||
---
|
||||
|
||||
@@ -1917,8 +1983,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$。
|
||||
|
||||
+16
-4
@@ -539,6 +539,7 @@ AsymptoticStatus asymptotic_route_camera(const SpacetimeSource *source,
|
||||
route->Pi[i] = state.Pi[i];
|
||||
}
|
||||
route->log_alpha_p0 = state.log_alpha_p0;
|
||||
route->log_alpha_p0_camera = state.log_alpha_p0;
|
||||
return ASYMPTOTIC_OK;
|
||||
}
|
||||
/* A backend that declares ends must describe them consistently and use a
|
||||
@@ -587,6 +588,7 @@ AsymptoticStatus asymptotic_route_camera(const SpacetimeSource *source,
|
||||
route->Pi[k] = state.Pi[k];
|
||||
}
|
||||
route->log_alpha_p0 = state.log_alpha_p0;
|
||||
route->log_alpha_p0_camera = state.log_alpha_p0;
|
||||
return ASYMPTOTIC_OK;
|
||||
}
|
||||
}
|
||||
@@ -603,8 +605,13 @@ AsymptoticStatus asymptotic_route_camera(const SpacetimeSource *source,
|
||||
return ASYMPTOTIC_INVALID;
|
||||
if (first_end == SPACETIME_END_NONE)
|
||||
first_end = end.end_id;
|
||||
if (end.exterior_kind == ASYMPTOTIC_EXTERIOR_SCHWARZSCHILD_MONOPOLE)
|
||||
return schwarzschild_route(source, &end, &metric, &state, route);
|
||||
if (end.exterior_kind == ASYMPTOTIC_EXTERIOR_SCHWARZSCHILD_MONOPOLE) {
|
||||
const AsymptoticStatus status =
|
||||
schwarzschild_route(source, &end, &metric, &state, route);
|
||||
if (status == ASYMPTOTIC_OK)
|
||||
route->log_alpha_p0_camera = state.log_alpha_p0;
|
||||
return status;
|
||||
}
|
||||
if (end.exterior_kind != ASYMPTOTIC_EXTERIOR_MINKOWSKI)
|
||||
return ASYMPTOTIC_UNSUPPORTED;
|
||||
if (asymptotic_canonical_from_backend(source, end.end_id, &metric,
|
||||
@@ -639,6 +646,7 @@ AsymptoticStatus asymptotic_route_camera(const SpacetimeSource *source,
|
||||
if (have_entry) {
|
||||
*route = best;
|
||||
route->log_alpha_p0 = state.log_alpha_p0;
|
||||
route->log_alpha_p0_camera = state.log_alpha_p0;
|
||||
return ASYMPTOTIC_OK;
|
||||
}
|
||||
if (!have_miss) {
|
||||
@@ -678,8 +686,10 @@ AsymptoticStatus asymptotic_finish_escape(const SpacetimeSource *source,
|
||||
endpoint->n_infinity[i] = n_inf[i];
|
||||
endpoint->frequency_ratio = frequency;
|
||||
endpoint->end_id = end_id;
|
||||
endpoint->status = RAY_ENDPOINT_ESCAPED;
|
||||
endpoint->outcome = RAY_OUTCOME_ESCAPED;
|
||||
endpoint->reason = RAY_REASON_NONE;
|
||||
endpoint->magnification = 1.0;
|
||||
endpoint->threshold_value = NAN;
|
||||
return ASYMPTOTIC_OK;
|
||||
}
|
||||
if (end.exterior_kind != ASYMPTOTIC_EXTERIOR_MINKOWSKI)
|
||||
@@ -704,7 +714,9 @@ AsymptoticStatus asymptotic_finish_escape(const SpacetimeSource *source,
|
||||
endpoint->n_infinity[i] = n[i];
|
||||
endpoint->frequency_ratio = 1.0 / energy;
|
||||
endpoint->end_id = end_id;
|
||||
endpoint->status = RAY_ENDPOINT_ESCAPED;
|
||||
endpoint->outcome = RAY_OUTCOME_ESCAPED;
|
||||
endpoint->reason = RAY_REASON_NONE;
|
||||
endpoint->magnification = 1.0;
|
||||
endpoint->threshold_value = NAN;
|
||||
return ASYMPTOTIC_OK;
|
||||
}
|
||||
@@ -40,7 +40,11 @@ typedef struct {
|
||||
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;
|
||||
|
||||
+540
-69
@@ -89,9 +89,9 @@ int frame_lens_mesh_build_coarse(FrameLensMesh *mesh, int width, int height,
|
||||
const size_t bottom_left = vertex_index(column, row + 1, columns);
|
||||
const size_t bottom_right = vertex_index(column + 1, row + 1, columns);
|
||||
triangles[next_triangle++] =
|
||||
(LensTriangle){{top_left, bottom_left, bottom_right}, 0, 0};
|
||||
(LensTriangle){{top_left, bottom_left, bottom_right}, 0, 0, 0};
|
||||
triangles[next_triangle++] =
|
||||
(LensTriangle){{top_left, bottom_right, top_right}, 0, 0};
|
||||
(LensTriangle){{top_left, bottom_right, top_right}, 0, 0, 0};
|
||||
}
|
||||
*mesh = (FrameLensMesh){.vertices = vertices,
|
||||
.triangles = triangles,
|
||||
@@ -102,6 +102,38 @@ int frame_lens_mesh_build_coarse(FrameLensMesh *mesh, int width, int height,
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Store an endpoint into a lens vertex. For an UNRESOLVED result the last
|
||||
* accepted continuous state is kept so the ray can be resumed; `granted_limit`
|
||||
* is the total accepted-step budget that produced this result (0 when it is
|
||||
* the base trace config). */
|
||||
static void store_endpoint(LensVertex *vertex, const RayEndpoint *endpoint,
|
||||
unsigned int granted_limit) {
|
||||
vertex->outcome = endpoint->outcome;
|
||||
vertex->reason = endpoint->reason;
|
||||
vertex->end_id = endpoint->end_id;
|
||||
vertex->traced = 1;
|
||||
if (endpoint->outcome == RAY_OUTCOME_ESCAPED) {
|
||||
for (int axis = 0; axis < 3; ++axis)
|
||||
vertex->n_infinity[axis] = endpoint->n_infinity[axis];
|
||||
vertex->log_frequency_ratio = log(endpoint->frequency_ratio);
|
||||
return;
|
||||
}
|
||||
if (endpoint->outcome == RAY_OUTCOME_UNRESOLVED) {
|
||||
vertex->continuation_t = endpoint->stop_coordinate_time;
|
||||
for (int axis = 0; axis < 3; ++axis) {
|
||||
vertex->continuation_x[axis] = endpoint->final_x[axis];
|
||||
vertex->continuation_Pi[axis] = endpoint->final_Pi[axis];
|
||||
}
|
||||
vertex->continuation_log_alpha_p0 = endpoint->final_log_alpha_p0;
|
||||
vertex->continuation_log_alpha_p0_0 = endpoint->final_log_alpha_p0_0;
|
||||
vertex->continuation_steps = endpoint->accepted_steps;
|
||||
const unsigned int used =
|
||||
granted_limit != 0 ? granted_limit : endpoint->accepted_steps;
|
||||
if (used > vertex->continuation_limit)
|
||||
vertex->continuation_limit = used;
|
||||
}
|
||||
}
|
||||
|
||||
int frame_lens_mesh_trace(FrameLensMesh *mesh, const SpacetimeSource *spacetime,
|
||||
const ObserverState *observer,
|
||||
const GeodesicTraceConfig *trace) {
|
||||
@@ -115,14 +147,7 @@ int frame_lens_mesh_trace(FrameLensMesh *mesh, const SpacetimeSource *spacetime,
|
||||
LensVertex *vertex = &mesh->vertices[i];
|
||||
RayEndpoint endpoint = geodesic_trace_past(spacetime, observer,
|
||||
vertex->camera_direction, trace);
|
||||
vertex->status = endpoint.status;
|
||||
vertex->end_id = endpoint.end_id;
|
||||
vertex->traced = 1;
|
||||
if (endpoint.status == RAY_ENDPOINT_ESCAPED) {
|
||||
for (int axis = 0; axis < 3; ++axis)
|
||||
vertex->n_infinity[axis] = endpoint.n_infinity[axis];
|
||||
vertex->log_frequency_ratio = log(endpoint.frequency_ratio);
|
||||
}
|
||||
store_endpoint(vertex, &endpoint, 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -436,24 +461,54 @@ static int all_vertices_traced(const FrameLensMesh *mesh) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
int e, d, u, bad;
|
||||
} VertexMix;
|
||||
|
||||
static VertexMix triangle_mix(const FrameLensMesh *mesh,
|
||||
const LensTriangle *triangle) {
|
||||
VertexMix mix = {0, 0, 0, 0};
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
switch (mesh->vertices[triangle->vertex[i]].outcome) {
|
||||
case RAY_OUTCOME_ESCAPED: ++mix.e; break;
|
||||
case RAY_OUTCOME_DARK: ++mix.d; break;
|
||||
case RAY_OUTCOME_UNRESOLVED: ++mix.u; break;
|
||||
default: ++mix.bad; break;
|
||||
}
|
||||
}
|
||||
return mix;
|
||||
}
|
||||
|
||||
/* A discontinuous boundary that must be red-refined. Escape/dark and
|
||||
* unresolved/dark (with no escape vertex) are boundaries; errors are not. */
|
||||
static int terminal_mismatch(const LensVertex *a, const LensVertex *b,
|
||||
const LensVertex *c) {
|
||||
int escaped = 0, captured = 0, have_end = 0;
|
||||
int escaped = 0, dark = 0, unresolved = 0, bad = 0, have_end = 0;
|
||||
SpacetimeEndId end = SPACETIME_END_NONE;
|
||||
const LensVertex *vertices[] = {a, b, c};
|
||||
for (size_t i = 0; i < 3; ++i) {
|
||||
escaped |= vertices[i]->status == RAY_ENDPOINT_ESCAPED;
|
||||
captured |= vertices[i]->status == RAY_ENDPOINT_CAPTURED;
|
||||
if (vertices[i]->status == RAY_ENDPOINT_ESCAPED) {
|
||||
switch (vertices[i]->outcome) {
|
||||
case RAY_OUTCOME_ESCAPED:
|
||||
++escaped;
|
||||
if (!have_end) {
|
||||
end = vertices[i]->end_id;
|
||||
have_end = 1;
|
||||
} else if (vertices[i]->end_id != end) {
|
||||
return 1; /* two different infinity ends must not be interpolated */
|
||||
}
|
||||
break;
|
||||
case RAY_OUTCOME_DARK: ++dark; break;
|
||||
case RAY_OUTCOME_UNRESOLVED: ++unresolved; break;
|
||||
default: ++bad; break;
|
||||
}
|
||||
}
|
||||
return escaped && captured;
|
||||
if (bad > 0)
|
||||
return 0;
|
||||
if (escaped > 0 && dark > 0)
|
||||
return 1;
|
||||
if (unresolved > 0 && dark > 0 && escaped == 0)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int add_sample(FrameLensMesh *mesh, const FrameSample *sample) {
|
||||
@@ -512,6 +567,94 @@ static void index_probe(FrameLensMesh *mesh, size_t sample_id) {
|
||||
mesh->probe_slots[slot] = sample_id + 1;
|
||||
}
|
||||
|
||||
/* Rebuild the persistent witness index from the mesh vertices: a witness is a
|
||||
* vertex flagged diagnostic_probe that no triangle references. Consumed
|
||||
* witnesses (now formal midpoints) are un-flagged here. */
|
||||
static int witness_rebuild(FrameLensMesh *mesh) {
|
||||
unsigned char *used = calloc(mesh->vertex_count ? mesh->vertex_count : 1, 1);
|
||||
if (used == NULL)
|
||||
return -1;
|
||||
for (size_t t = 0; t < mesh->triangle_count; ++t)
|
||||
for (int j = 0; j < 3; ++j)
|
||||
if (mesh->triangles[t].vertex[j] < mesh->vertex_count)
|
||||
used[mesh->triangles[t].vertex[j]] = 1;
|
||||
size_t count = 0;
|
||||
for (size_t v = 0; v < mesh->vertex_count; ++v) {
|
||||
if (!mesh->vertices[v].diagnostic_probe)
|
||||
continue;
|
||||
if (used[v]) {
|
||||
mesh->vertices[v].diagnostic_probe = 0; /* promoted to a midpoint */
|
||||
continue;
|
||||
}
|
||||
if (count == mesh->witness_capacity) {
|
||||
size_t cap = mesh->witness_capacity ? mesh->witness_capacity * 2 : 8;
|
||||
size_t *list = realloc(mesh->witness_vertices, cap * sizeof *list);
|
||||
if (list == NULL) {
|
||||
free(used);
|
||||
return -1;
|
||||
}
|
||||
mesh->witness_vertices = list;
|
||||
mesh->witness_capacity = cap;
|
||||
}
|
||||
mesh->witness_vertices[count++] = v;
|
||||
}
|
||||
mesh->witness_count = count;
|
||||
mesh->diagnostic_probe_count = count;
|
||||
size_t cap = 16;
|
||||
while (cap < count * 2)
|
||||
cap *= 2;
|
||||
if (cap > mesh->witness_slot_capacity) {
|
||||
size_t *slots = realloc(mesh->witness_slots, cap * sizeof *slots);
|
||||
if (slots == NULL) {
|
||||
free(used);
|
||||
return -1;
|
||||
}
|
||||
mesh->witness_slots = slots;
|
||||
mesh->witness_slot_capacity = cap;
|
||||
}
|
||||
if (mesh->witness_slot_capacity != 0)
|
||||
memset(mesh->witness_slots, 0,
|
||||
mesh->witness_slot_capacity * sizeof *mesh->witness_slots);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
const size_t id = mesh->witness_vertices[i];
|
||||
const size_t a = mesh->vertices[id].probe_edge[0];
|
||||
const size_t b = mesh->vertices[id].probe_edge[1];
|
||||
size_t slot = probe_hash(a, b) & (mesh->witness_slot_capacity - 1);
|
||||
while (mesh->witness_slots[slot] != 0)
|
||||
slot = (slot + 1) & (mesh->witness_slot_capacity - 1);
|
||||
mesh->witness_slots[slot] = i + 1;
|
||||
}
|
||||
free(used);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static size_t witness_find(const FrameLensMesh *mesh, size_t a, size_t b) {
|
||||
if (a > b) { const size_t swap = a; a = b; b = swap; }
|
||||
if (mesh->witness_slot_capacity == 0)
|
||||
return SIZE_MAX;
|
||||
size_t slot = probe_hash(a, b) & (mesh->witness_slot_capacity - 1);
|
||||
while (mesh->witness_slots[slot] != 0) {
|
||||
const size_t id = mesh->witness_vertices[mesh->witness_slots[slot] - 1];
|
||||
if (mesh->vertices[id].probe_edge[0] == a &&
|
||||
mesh->vertices[id].probe_edge[1] == b)
|
||||
return id;
|
||||
slot = (slot + 1) & (mesh->witness_slot_capacity - 1);
|
||||
}
|
||||
return SIZE_MAX;
|
||||
}
|
||||
|
||||
static unsigned int retry_limit(unsigned int current,
|
||||
const RefinementConfig *config) {
|
||||
const unsigned int remaining = config->max_total_steps - current;
|
||||
return current + (config->retry_step_increment < remaining
|
||||
? config->retry_step_increment
|
||||
: remaining);
|
||||
}
|
||||
|
||||
static int triangle_allows_children(const FrameLensMesh *mesh,
|
||||
const LensTriangle *triangle,
|
||||
const RefinementConfig *config);
|
||||
|
||||
int frame_lens_mesh_prepare_generation(FrameLensMesh *mesh,
|
||||
const RefinementConfig *config) {
|
||||
if (mesh == NULL || config == NULL || mesh->sample_count != 0)
|
||||
@@ -528,16 +671,96 @@ int frame_lens_mesh_prepare_generation(FrameLensMesh *mesh,
|
||||
.vertex = mesh->vertices[i]}))
|
||||
return -1;
|
||||
}
|
||||
int emitted_probes = 0;
|
||||
/* Retry pass: merge one request per physical sample id. A triangle vertex
|
||||
* that is UNRESOLVED with an escape side (or an all-U triangle) is retried,
|
||||
* as is an off-mesh unresolved witness. A witness retry carries its edge so
|
||||
* the refinement decision sees the updated state; both passes share
|
||||
* retry_seen so a promoted witness is never requested twice. */
|
||||
if (config->retry_step_increment > 0 && mesh->vertex_count > 0) {
|
||||
unsigned char *retry_seen = calloc(mesh->vertex_count, 1);
|
||||
if (retry_seen == NULL)
|
||||
return -1;
|
||||
for (size_t i = 0; i < mesh->triangle_count; ++i) {
|
||||
const LensTriangle *triangle = &mesh->triangles[i];
|
||||
const VertexMix mix = triangle_mix(mesh, triangle);
|
||||
if (mix.bad > 0)
|
||||
continue;
|
||||
if (!((mix.u > 0 && mix.e > 0) || mix.u == 3))
|
||||
continue;
|
||||
for (int corner = 0; corner < 3; ++corner) {
|
||||
const size_t v = triangle->vertex[corner];
|
||||
LensVertex *vertex = &mesh->vertices[v];
|
||||
if (vertex->outcome != RAY_OUTCOME_UNRESOLVED || retry_seen[v])
|
||||
continue;
|
||||
retry_seen[v] = 1;
|
||||
if (vertex->continuation_limit >= config->max_total_steps)
|
||||
continue; /* capped: reported as budget-incomplete, not retried */
|
||||
const unsigned int limit = retry_limit(vertex->continuation_limit, config);
|
||||
if (limit <= vertex->continuation_limit)
|
||||
continue;
|
||||
FrameSample retry = {.kind = FRAME_SAMPLE_RETRY,
|
||||
.vertex_id = v,
|
||||
.step_limit = limit,
|
||||
.vertex = *vertex};
|
||||
if (add_sample(mesh, &retry)) {
|
||||
free(retry_seen);
|
||||
return -1;
|
||||
}
|
||||
++mesh->retry_requests;
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < mesh->witness_count; ++i) {
|
||||
const size_t v = mesh->witness_vertices[i];
|
||||
LensVertex *vertex = &mesh->vertices[v];
|
||||
if (retry_seen[v] || vertex->outcome != RAY_OUTCOME_UNRESOLVED)
|
||||
continue;
|
||||
retry_seen[v] = 1;
|
||||
if (vertex->continuation_limit >= config->max_total_steps)
|
||||
continue;
|
||||
const unsigned int limit = retry_limit(vertex->continuation_limit, config);
|
||||
if (limit <= vertex->continuation_limit)
|
||||
continue;
|
||||
FrameSample retry = {.kind = FRAME_SAMPLE_RETRY, .vertex_id = v,
|
||||
.edge_vertex = {vertex->probe_edge[0],
|
||||
vertex->probe_edge[1]},
|
||||
.step_limit = limit, .vertex = *vertex};
|
||||
if (add_sample(mesh, &retry)) {
|
||||
free(retry_seen);
|
||||
return -1;
|
||||
}
|
||||
if (mesh->probe_slot_capacity != 0)
|
||||
index_probe(mesh, mesh->sample_count - 1);
|
||||
emitted_probes = 1;
|
||||
++mesh->retry_requests;
|
||||
}
|
||||
free(retry_seen);
|
||||
}
|
||||
if (config->max_level == 0)
|
||||
/* Coarse vertices still need tracing when refinement is disabled. */
|
||||
return (int)mesh->sample_count;
|
||||
/* Every generation may batch newly inserted vertices with probes for its
|
||||
* new leaves: probe positions depend only on image-plane geometry. Their
|
||||
* endpoints are considered only after this complete generation finishes. */
|
||||
* endpoints are considered only after this complete generation finishes.
|
||||
* Unresolved/error triangles are handled by the retry pass or the boundary
|
||||
* accounting, so they request no probes here. */
|
||||
for (size_t i = 0; i < mesh->triangle_count; ++i) {
|
||||
const LensTriangle *triangle = &mesh->triangles[i];
|
||||
if (triangle->level >= config->max_level || triangle->evaluated)
|
||||
continue;
|
||||
const VertexMix mix = triangle_mix(mesh, triangle);
|
||||
if (mix.bad > 0)
|
||||
continue;
|
||||
if (mix.u > 0) {
|
||||
/* UUD/UDD may still be red-refined to locate the boundary, but only
|
||||
* while the geometry can support children; otherwise it is an
|
||||
* approximate-black boundary decision. U+escape and UUU are handled by
|
||||
* the retry pass instead. */
|
||||
const int dark_side_only = mix.e == 0 && mix.d > 0;
|
||||
if (!dark_side_only ||
|
||||
!triangle_allows_children(mesh, triangle, config))
|
||||
continue;
|
||||
}
|
||||
const unsigned int first_side = longest_side(mesh, triangle);
|
||||
const unsigned int side_count = terminal_mismatch(
|
||||
&mesh->vertices[triangle->vertex[0]],
|
||||
@@ -551,6 +774,21 @@ int frame_lens_mesh_prepare_generation(FrameLensMesh *mesh,
|
||||
const size_t b = triangle->vertex[(side + 1) % 3];
|
||||
if (find_probe(mesh, a, b) != SIZE_MAX)
|
||||
continue;
|
||||
/* A persistent witness (terminal or capped unresolved) is reused in
|
||||
* place: no retrace and, if its edge is later split, no duplicate. */
|
||||
const size_t wid = witness_find(mesh, a, b);
|
||||
if (wid != SIZE_MAX) {
|
||||
FrameSample cached = {.kind = FRAME_SAMPLE_PROBE,
|
||||
.vertex_id = wid,
|
||||
.edge_vertex = {a, b},
|
||||
.cached = 1,
|
||||
.vertex = mesh->vertices[wid]};
|
||||
if (add_sample(mesh, &cached))
|
||||
return -1;
|
||||
index_probe(mesh, mesh->sample_count - 1);
|
||||
emitted_probes = 1;
|
||||
continue;
|
||||
}
|
||||
FrameSample probe = {.kind = FRAME_SAMPLE_PROBE, .edge_vertex = {a, b}};
|
||||
const LensVertex *left = &mesh->vertices[a];
|
||||
const LensVertex *right = &mesh->vertices[b];
|
||||
@@ -564,9 +802,10 @@ int frame_lens_mesh_prepare_generation(FrameLensMesh *mesh,
|
||||
if (add_sample(mesh, &probe))
|
||||
return -1;
|
||||
index_probe(mesh, mesh->sample_count - 1);
|
||||
emitted_probes = 1;
|
||||
}
|
||||
}
|
||||
mesh->samples_include_probes = mesh->sample_count != 0;
|
||||
mesh->samples_include_probes = emitted_probes;
|
||||
return (int)mesh->sample_count;
|
||||
}
|
||||
|
||||
@@ -582,17 +821,12 @@ int frame_lens_mesh_install_sample(FrameLensMesh *mesh, size_t sample_id,
|
||||
if (mesh == NULL || endpoint == NULL || sample_id >= mesh->sample_count)
|
||||
return -1;
|
||||
FrameSample *sample = &mesh->samples[sample_id];
|
||||
LensVertex *vertex = sample->kind == FRAME_SAMPLE_VERTEX
|
||||
? &mesh->vertices[sample->vertex_id]
|
||||
: &sample->vertex;
|
||||
vertex->status = endpoint->status;
|
||||
vertex->end_id = endpoint->end_id;
|
||||
vertex->traced = 1;
|
||||
if (endpoint->status == RAY_ENDPOINT_ESCAPED) {
|
||||
for (int axis = 0; axis < 3; ++axis)
|
||||
vertex->n_infinity[axis] = endpoint->n_infinity[axis];
|
||||
vertex->log_frequency_ratio = log(endpoint->frequency_ratio);
|
||||
}
|
||||
LensVertex *vertex = sample->kind == FRAME_SAMPLE_PROBE
|
||||
? &sample->vertex
|
||||
: &mesh->vertices[sample->vertex_id];
|
||||
store_endpoint(vertex, endpoint, sample->step_limit);
|
||||
if (sample->kind == FRAME_SAMPLE_RETRY)
|
||||
sample->vertex = *vertex; /* witness retries are read back by find_probe */
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -618,8 +852,8 @@ static int discrete_jacobian(const FrameLensMesh *mesh,
|
||||
const LensVertex *a = &mesh->vertices[triangle->vertex[0]];
|
||||
const LensVertex *b = &mesh->vertices[triangle->vertex[1]];
|
||||
const LensVertex *c = &mesh->vertices[triangle->vertex[2]];
|
||||
if (a->status != RAY_ENDPOINT_ESCAPED || b->status != RAY_ENDPOINT_ESCAPED ||
|
||||
c->status != RAY_ENDPOINT_ESCAPED)
|
||||
if (a->outcome != RAY_OUTCOME_ESCAPED || b->outcome != RAY_OUTCOME_ESCAPED ||
|
||||
c->outcome != RAY_OUTCOME_ESCAPED)
|
||||
return 0;
|
||||
if (a->end_id != b->end_id || a->end_id != c->end_id)
|
||||
return 0;
|
||||
@@ -651,16 +885,22 @@ static int probe_requires_split(const FrameLensMesh *mesh,
|
||||
if (probe_id == SIZE_MAX)
|
||||
return 0;
|
||||
const LensVertex *probe = &mesh->samples[probe_id].vertex;
|
||||
if ((probe->status == RAY_ENDPOINT_ESCAPED) !=
|
||||
(a->status == RAY_ENDPOINT_ESCAPED) ||
|
||||
(probe->status == RAY_ENDPOINT_ESCAPED) !=
|
||||
(b->status == RAY_ENDPOINT_ESCAPED))
|
||||
return (probe->status == RAY_ENDPOINT_ESCAPED ||
|
||||
a->status == RAY_ENDPOINT_ESCAPED || b->status == RAY_ENDPOINT_ESCAPED) &&
|
||||
(probe->status == RAY_ENDPOINT_CAPTURED ||
|
||||
a->status == RAY_ENDPOINT_CAPTURED || b->status == RAY_ENDPOINT_CAPTURED);
|
||||
if (a->status != RAY_ENDPOINT_ESCAPED || b->status != RAY_ENDPOINT_ESCAPED ||
|
||||
probe->status != RAY_ENDPOINT_ESCAPED)
|
||||
if (probe->outcome == RAY_OUTCOME_INCOMPLETE ||
|
||||
probe->outcome == RAY_OUTCOME_UNRESOLVED)
|
||||
return 0; /* retained as a witness and retried, not a mapping estimate */
|
||||
if ((probe->outcome == RAY_OUTCOME_ESCAPED) !=
|
||||
(a->outcome == RAY_OUTCOME_ESCAPED) ||
|
||||
(probe->outcome == RAY_OUTCOME_ESCAPED) !=
|
||||
(b->outcome == RAY_OUTCOME_ESCAPED))
|
||||
return (probe->outcome == RAY_OUTCOME_ESCAPED ||
|
||||
a->outcome == RAY_OUTCOME_ESCAPED ||
|
||||
b->outcome == RAY_OUTCOME_ESCAPED) &&
|
||||
(probe->outcome == RAY_OUTCOME_DARK ||
|
||||
a->outcome == RAY_OUTCOME_DARK ||
|
||||
b->outcome == RAY_OUTCOME_DARK);
|
||||
if (a->outcome != RAY_OUTCOME_ESCAPED ||
|
||||
b->outcome != RAY_OUTCOME_ESCAPED ||
|
||||
probe->outcome != RAY_OUTCOME_ESCAPED)
|
||||
return 0;
|
||||
double predicted[3] = {a->n_infinity[0] + b->n_infinity[0],
|
||||
a->n_infinity[1] + b->n_infinity[1],
|
||||
@@ -686,7 +926,72 @@ static int triangle_allows_children(const FrameLensMesh *mesh,
|
||||
const double edge = fmax(image_edge_length(a, b),
|
||||
fmax(image_edge_length(b, c), image_edge_length(c, a)));
|
||||
const double area = image_triangle_area(a, b, c);
|
||||
return edge > config->min_edge_pixels && area > config->min_area_pixels2;
|
||||
return triangle->level < config->max_level &&
|
||||
edge > config->min_edge_pixels && area > config->min_area_pixels2;
|
||||
}
|
||||
|
||||
void frame_lens_mesh_boundary_stats(FrameLensMesh *mesh,
|
||||
const RefinementConfig *config,
|
||||
FrameBoundaryStats *stats) {
|
||||
if (stats == NULL)
|
||||
return;
|
||||
*stats = (FrameBoundaryStats){0};
|
||||
if (mesh == NULL || config == NULL)
|
||||
return;
|
||||
/* Off-mesh probes are samples too. Their failures cannot disappear just
|
||||
* because no inverse patch uses them. Infer orphanhood for replay as well. */
|
||||
unsigned char *used = calloc(mesh->vertex_count, 1);
|
||||
if (used == NULL) { ++stats->error; return; }
|
||||
for (size_t i = 0; i < mesh->triangle_count; ++i)
|
||||
for (int j = 0; j < 3; ++j) used[mesh->triangles[i].vertex[j]] = 1;
|
||||
for (size_t i = 0; i < mesh->vertex_count; ++i) {
|
||||
if (!used[i] && mesh->vertices[i].outcome == RAY_OUTCOME_INCOMPLETE)
|
||||
++stats->error;
|
||||
if (!used[i] && mesh->vertices[i].outcome == RAY_OUTCOME_UNRESOLVED)
|
||||
++stats->budget_incomplete_triangles;
|
||||
}
|
||||
free(used);
|
||||
for (size_t i = 0; i < mesh->triangle_count; ++i) {
|
||||
LensTriangle *triangle = &mesh->triangles[i];
|
||||
triangle->approx_black = 0;
|
||||
const VertexMix mix = triangle_mix(mesh, triangle);
|
||||
if (mix.bad > 0) {
|
||||
++stats->error;
|
||||
continue;
|
||||
}
|
||||
const int unresolved_with_escape = mix.u > 0 && mix.e > 0;
|
||||
const int all_unresolved = mix.u == 3;
|
||||
if (mix.u == 0) {
|
||||
if (mix.d == 0)
|
||||
++stats->escaped_only;
|
||||
else if (mix.e == 0)
|
||||
++stats->dark_only;
|
||||
else
|
||||
++stats->eed_edd;
|
||||
} else if (unresolved_with_escape) {
|
||||
++stats->u_with_escape;
|
||||
} else if (all_unresolved) {
|
||||
++stats->uuu;
|
||||
} else {
|
||||
++stats->uud_udd; /* UUD / UDD */
|
||||
if (!triangle_allows_children(mesh, triangle, config)) {
|
||||
triangle->approx_black = 1;
|
||||
++stats->approx_black_triangles;
|
||||
const LensVertex *a = &mesh->vertices[triangle->vertex[0]];
|
||||
const LensVertex *b = &mesh->vertices[triangle->vertex[1]];
|
||||
const LensVertex *c = &mesh->vertices[triangle->vertex[2]];
|
||||
stats->approx_black_area_pixels2 += image_triangle_area(a, b, c);
|
||||
stats->approx_black_max_edge_pixels = fmax(stats->approx_black_max_edge_pixels,
|
||||
fmax(image_edge_length(a,b), fmax(image_edge_length(b,c), image_edge_length(c,a))));
|
||||
stats->approx_black_max_area_pixels2 = fmax(stats->approx_black_max_area_pixels2,
|
||||
image_triangle_area(a,b,c));
|
||||
if (triangle->level >= config->max_level) ++stats->approx_black_level_stops;
|
||||
} else ++stats->budget_incomplete_triangles;
|
||||
}
|
||||
if (unresolved_with_escape || all_unresolved)
|
||||
++stats->budget_incomplete_triangles;
|
||||
}
|
||||
stats->retry_requests = mesh->retry_requests;
|
||||
}
|
||||
|
||||
static LensVertex midpoint_vertex(const LensVertex *a, const LensVertex *b) {
|
||||
@@ -727,7 +1032,8 @@ static int append_triangle(LensTriangle *triangles, size_t *count,
|
||||
unsigned int level, int evaluated) {
|
||||
if (*count >= capacity)
|
||||
return -1;
|
||||
triangles[(*count)++] = (LensTriangle){{a, b, c}, level, evaluated};
|
||||
triangles[(*count)++] =
|
||||
(LensTriangle){{a, b, c}, level, evaluated, 0};
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -756,10 +1062,75 @@ static int append_triangle_with_parent_winding(
|
||||
evaluated);
|
||||
}
|
||||
|
||||
/* Append off-mesh failed/unresolved probes as persistent witnesses and
|
||||
* rebuild the witness index. `consumed[i]` marks a probe sample that became
|
||||
* a formal midpoint this generation. */
|
||||
static int promote_witnesses(FrameLensMesh *mesh,
|
||||
const unsigned char *consumed) {
|
||||
int added = 0;
|
||||
for (size_t i = 0; i < mesh->sample_count; ++i) {
|
||||
const FrameSample *s = &mesh->samples[i];
|
||||
if (s->cached || s->kind != FRAME_SAMPLE_PROBE)
|
||||
continue;
|
||||
if (consumed != NULL && consumed[i])
|
||||
continue;
|
||||
if (s->vertex.outcome != RAY_OUTCOME_INCOMPLETE &&
|
||||
s->vertex.outcome != RAY_OUTCOME_UNRESOLVED)
|
||||
continue;
|
||||
if (ensure_vertices(mesh, mesh->vertex_count + 1))
|
||||
return -1;
|
||||
LensVertex witness = s->vertex;
|
||||
witness.diagnostic_probe = 1;
|
||||
size_t a = s->edge_vertex[0], b = s->edge_vertex[1];
|
||||
if (a > b) { const size_t swap = a; a = b; b = swap; }
|
||||
witness.probe_edge[0] = a;
|
||||
witness.probe_edge[1] = b;
|
||||
mesh->vertices[mesh->vertex_count++] = witness;
|
||||
++added;
|
||||
}
|
||||
/* Rebuild only when the witness set can have changed; the common no-witness
|
||||
* refinement path stays O(T) without an O(V) scan. */
|
||||
if (added == 0 && mesh->witness_count == 0)
|
||||
return 0;
|
||||
return witness_rebuild(mesh);
|
||||
}
|
||||
|
||||
int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
|
||||
const RefinementConfig *config) {
|
||||
if (mesh == NULL || config == NULL || mesh->sample_count == 0)
|
||||
return -1;
|
||||
/* Serial decision invalidation: installation is an OpenMP bulk loop and
|
||||
* must not write shared triangle flags from individual ray workers. Mark
|
||||
* retried vertex ids once, then make a single pass over the mesh; witness
|
||||
* edges are resolved through the O(1) witness index rather than scanning all
|
||||
* retries for every triangle. */
|
||||
unsigned char *vertex_retried =
|
||||
calloc(mesh->vertex_count ? mesh->vertex_count : 1, 1);
|
||||
if (vertex_retried == NULL)
|
||||
return -1;
|
||||
for (size_t j = 0; j < mesh->sample_count; ++j) {
|
||||
const FrameSample *s = &mesh->samples[j];
|
||||
if (s->kind == FRAME_SAMPLE_RETRY && s->vertex_id < mesh->vertex_count)
|
||||
vertex_retried[s->vertex_id] = 1;
|
||||
}
|
||||
for (size_t i = 0; i < mesh->triangle_count; ++i) {
|
||||
LensTriangle *t = &mesh->triangles[i];
|
||||
int touches = vertex_retried[t->vertex[0]] ||
|
||||
vertex_retried[t->vertex[1]] ||
|
||||
vertex_retried[t->vertex[2]];
|
||||
for (unsigned int side = 0; side < 3 && !touches; ++side) {
|
||||
const size_t wid = witness_find(mesh, t->vertex[side],
|
||||
t->vertex[(side + 1) % 3]);
|
||||
if (wid != SIZE_MAX && wid < mesh->vertex_count &&
|
||||
vertex_retried[wid])
|
||||
touches = 1;
|
||||
}
|
||||
if (touches) {
|
||||
t->evaluated = 0;
|
||||
t->approx_black = 0;
|
||||
}
|
||||
}
|
||||
free(vertex_retried);
|
||||
for (size_t i = 0; i < mesh->sample_count; ++i)
|
||||
if (!mesh->samples[i].vertex.traced &&
|
||||
mesh->samples[i].kind == FRAME_SAMPLE_PROBE)
|
||||
@@ -769,12 +1140,31 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
|
||||
mesh->samples_include_probes = 0;
|
||||
return 0;
|
||||
}
|
||||
for (size_t i = 0; i < mesh->triangle_count; ++i)
|
||||
if (mesh->triangles[i].level < config->max_level)
|
||||
mesh->triangles[i].evaluated = 1;
|
||||
/* A triangle with a completed (or capped) probe can settle; one whose probe
|
||||
* is still being retried must stay pending so the next generation sees the
|
||||
* updated state. */
|
||||
for (size_t i = 0; i < mesh->triangle_count; ++i) {
|
||||
LensTriangle *t = &mesh->triangles[i];
|
||||
const VertexMix mix = triangle_mix(mesh, t);
|
||||
if (mix.u || mix.bad)
|
||||
continue;
|
||||
const unsigned side = longest_side(mesh, t);
|
||||
const size_t probe =
|
||||
find_probe(mesh, t->vertex[side], t->vertex[(side + 1) % 3]);
|
||||
if (probe == SIZE_MAX)
|
||||
continue;
|
||||
const FrameSample *ps = &mesh->samples[probe];
|
||||
if (ps->vertex.outcome == RAY_OUTCOME_UNRESOLVED && !ps->cached)
|
||||
continue; /* retry in flight */
|
||||
t->evaluated = 1;
|
||||
}
|
||||
const size_t edge_count = mesh->triangle_count * 3;
|
||||
MeshEdge *edges = calloc(edge_count, sizeof *edges);
|
||||
unsigned char *requested = calloc(edge_count, sizeof *requested);
|
||||
/* `wanted` records that a triangle asked to split before conformity may
|
||||
* cancel its edges, so a fully blocked triangle can settle instead of
|
||||
* re-requesting the same probes forever. */
|
||||
unsigned char *wanted = calloc(mesh->triangle_count, sizeof *wanted);
|
||||
unsigned char *allowed = calloc(mesh->triangle_count, sizeof *allowed);
|
||||
signed char *parity = calloc(mesh->triangle_count, sizeof *parity);
|
||||
double *jacobians = calloc(mesh->triangle_count, sizeof *jacobians);
|
||||
@@ -782,10 +1172,12 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
|
||||
* Keep that relation in the original triangle-side order so child emission
|
||||
* stays O(T), rather than scanning every sorted edge for every child side. */
|
||||
size_t *side_midpoints = malloc(edge_count * sizeof *side_midpoints);
|
||||
if (edges == NULL || requested == NULL || allowed == NULL || parity == NULL ||
|
||||
jacobians == NULL || side_midpoints == NULL) {
|
||||
free(edges); free(requested); free(allowed); free(parity); free(jacobians);
|
||||
free(side_midpoints);
|
||||
unsigned char *consumed = calloc(mesh->sample_count ? mesh->sample_count : 1, 1);
|
||||
if (edges == NULL || requested == NULL || wanted == NULL || allowed == NULL ||
|
||||
parity == NULL || jacobians == NULL || side_midpoints == NULL ||
|
||||
consumed == NULL) {
|
||||
free(edges); free(requested); free(wanted); free(allowed); free(parity);
|
||||
free(jacobians); free(side_midpoints); free(consumed);
|
||||
return -1;
|
||||
}
|
||||
for (size_t i = 0; i < edge_count; ++i)
|
||||
@@ -814,6 +1206,8 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
|
||||
probe_requires_split(mesh, triangle, side, config);
|
||||
}
|
||||
(void)discrete_jacobian(mesh, triangle, &jacobians[i], &parity[i]);
|
||||
for (unsigned int side = 0; side < 3; ++side)
|
||||
wanted[i] |= requested[3 * i + side] != 0;
|
||||
}
|
||||
}
|
||||
qsort(edges, edge_count, sizeof *edges, compare_mesh_edge);
|
||||
@@ -834,6 +1228,7 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
|
||||
if (allowed[left] && allowed[right]) {
|
||||
requested[3 * left + edges[first].side] = 1;
|
||||
requested[3 * right + edges[first + 1].side] = 1;
|
||||
wanted[left] = wanted[right] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -864,11 +1259,32 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
|
||||
size_t split_edges = 0;
|
||||
for (size_t i = 0; i < edge_count; ++i)
|
||||
split_edges += requested[3 * edges[i].triangle + edges[i].side] != 0;
|
||||
/* A triangle whose requested split was fully cancelled by conformity or a
|
||||
* geometric limit must settle here, or it re-requests the same probes every
|
||||
* generation forever. A UUD/UDD blocked while geometry still allows is
|
||||
* counted budget-incomplete by boundary_stats; at the stop scale it is an
|
||||
* approximate-black boundary. */
|
||||
for (size_t i = 0; i < mesh->triangle_count; ++i) {
|
||||
if (!wanted[i] || !allowed[i])
|
||||
continue;
|
||||
int still = 0;
|
||||
for (unsigned int side = 0; side < 3; ++side)
|
||||
still |= requested[3 * i + side] != 0;
|
||||
if (!still) {
|
||||
mesh->triangles[i].evaluated = 1;
|
||||
mesh->triangles[i].approx_black = 0;
|
||||
}
|
||||
}
|
||||
if (split_edges == 0) {
|
||||
if (promote_witnesses(mesh, NULL)) {
|
||||
free(edges); free(requested); free(wanted); free(allowed); free(parity);
|
||||
free(jacobians); free(side_midpoints); free(consumed);
|
||||
return -1;
|
||||
}
|
||||
mesh->sample_count = 0;
|
||||
mesh->samples_include_probes = 0;
|
||||
free(edges); free(requested); free(allowed); free(parity); free(jacobians);
|
||||
free(side_midpoints);
|
||||
free(edges); free(requested); free(wanted); free(allowed); free(parity);
|
||||
free(jacobians); free(side_midpoints); free(consumed);
|
||||
return 0;
|
||||
}
|
||||
/* Allocate a single stable midpoint vertex for each requested edge group. */
|
||||
@@ -885,8 +1301,8 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
|
||||
first = last;
|
||||
}
|
||||
if (ensure_vertices(mesh, mesh->vertex_count + midpoint_count)) {
|
||||
free(edges); free(requested); free(allowed); free(parity); free(jacobians);
|
||||
free(side_midpoints);
|
||||
free(edges); free(requested); free(wanted); free(allowed); free(parity);
|
||||
free(jacobians); free(side_midpoints); free(consumed);
|
||||
return -1;
|
||||
}
|
||||
size_t next_vertex = mesh->vertex_count;
|
||||
@@ -900,21 +1316,40 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
|
||||
any |= requested[3 * edges[i].triangle + edges[i].side] != 0;
|
||||
if (any) {
|
||||
const size_t probe_id = find_probe(mesh, edges[first].a, edges[first].b);
|
||||
LensVertex midpoint = midpoint_vertex(&mesh->vertices[edges[first].a],
|
||||
&mesh->vertices[edges[first].b]);
|
||||
if (probe_id != SIZE_MAX)
|
||||
midpoint = mesh->samples[probe_id].vertex;
|
||||
mesh->vertices[next_vertex++] = midpoint;
|
||||
size_t midpoint_id;
|
||||
const size_t wid = witness_find(mesh, edges[first].a, edges[first].b);
|
||||
if (wid != SIZE_MAX && wid < mesh->vertex_count &&
|
||||
mesh->vertices[wid].diagnostic_probe) {
|
||||
/* Promote the existing off-mesh witness in place: one physical sample
|
||||
* keeps a single stable vertex id. This must key on the persistent
|
||||
* vertex identity, not on the generating sample being a cached PROBE:
|
||||
* a witness retry is a FRAME_SAMPLE_RETRY and can trigger the split in
|
||||
* the same generation. */
|
||||
midpoint_id = wid;
|
||||
mesh->vertices[wid].diagnostic_probe = 0;
|
||||
if (probe_id != SIZE_MAX)
|
||||
consumed[probe_id] = 1;
|
||||
} else {
|
||||
LensVertex midpoint = midpoint_vertex(&mesh->vertices[edges[first].a],
|
||||
&mesh->vertices[edges[first].b]);
|
||||
if (probe_id != SIZE_MAX) {
|
||||
midpoint = mesh->samples[probe_id].vertex;
|
||||
consumed[probe_id] = 1;
|
||||
}
|
||||
midpoint.diagnostic_probe = 0;
|
||||
midpoint_id = next_vertex;
|
||||
mesh->vertices[next_vertex++] = midpoint;
|
||||
}
|
||||
for (size_t i = first; i < last; ++i)
|
||||
side_midpoints[3 * edges[i].triangle + edges[i].side] = next_vertex - 1;
|
||||
side_midpoints[3 * edges[i].triangle + edges[i].side] = midpoint_id;
|
||||
}
|
||||
first = last;
|
||||
}
|
||||
const size_t old_count = mesh->triangle_count;
|
||||
LensTriangle *children = calloc(old_count * 4, sizeof *children);
|
||||
if (children == NULL) {
|
||||
free(edges); free(requested); free(allowed); free(parity); free(jacobians);
|
||||
free(side_midpoints);
|
||||
free(edges); free(requested); free(wanted); free(allowed); free(parity);
|
||||
free(jacobians); free(side_midpoints); free(consumed);
|
||||
return -1;
|
||||
}
|
||||
size_t child_count = 0;
|
||||
@@ -985,11 +1420,24 @@ int frame_lens_mesh_finish_generation(FrameLensMesh *mesh,
|
||||
mesh->triangle_count = child_count;
|
||||
mesh->triangle_capacity = old_count * 4;
|
||||
mesh->vertex_count = next_vertex;
|
||||
const int promote_failed = promote_witnesses(mesh, consumed);
|
||||
mesh->sample_count = 0;
|
||||
mesh->samples_include_probes = 0;
|
||||
free(edges); free(requested); free(allowed); free(parity); free(jacobians);
|
||||
free(side_midpoints);
|
||||
return (int)midpoint_count;
|
||||
free(edges); free(requested); free(wanted); free(allowed); free(parity);
|
||||
free(jacobians); free(side_midpoints); free(consumed);
|
||||
return promote_failed ? -1 : (int)midpoint_count;
|
||||
}
|
||||
|
||||
void frame_retry_config_defaults(RefinementConfig *config,
|
||||
const GeodesicTraceConfig *trace) {
|
||||
if (config == NULL || trace == NULL)
|
||||
return;
|
||||
if (config->retry_step_increment == 0 && config->max_total_steps == 0) {
|
||||
config->retry_step_increment = trace->max_steps;
|
||||
config->max_total_steps =
|
||||
trace->max_steps > (UINT_MAX / 4u) ? trace->max_steps
|
||||
: trace->max_steps * 4u;
|
||||
}
|
||||
}
|
||||
|
||||
int frame_lens_mesh_refine(FrameLensMesh *mesh,
|
||||
@@ -1009,8 +1457,10 @@ int frame_lens_mesh_refine_with_progress(
|
||||
if (mesh == NULL || spacetime == NULL || observer == NULL || trace == NULL ||
|
||||
config == NULL)
|
||||
return -1;
|
||||
RefinementConfig effective = *config;
|
||||
frame_retry_config_defaults(&effective, trace);
|
||||
for (size_t generation = 0;; ++generation) {
|
||||
const int requested = frame_lens_mesh_prepare_generation(mesh, config);
|
||||
const int requested = frame_lens_mesh_prepare_generation(mesh, &effective);
|
||||
if (requested < 0) return -1;
|
||||
if (requested == 0) return 0;
|
||||
if (callback != NULL)
|
||||
@@ -1019,12 +1469,31 @@ int frame_lens_mesh_refine_with_progress(
|
||||
#pragma omp parallel for schedule(static)
|
||||
for (size_t i = 0; i < mesh->sample_count; ++i) {
|
||||
const FrameSample *sample = &mesh->samples[i];
|
||||
const RayEndpoint endpoint = geodesic_trace_past(
|
||||
spacetime, observer, sample->vertex.camera_direction, trace);
|
||||
if (sample->cached) continue;
|
||||
RayEndpoint endpoint;
|
||||
if (sample->kind == FRAME_SAMPLE_RETRY) {
|
||||
const LensVertex *v = &sample->vertex;
|
||||
GeodesicRayState state = {
|
||||
.coordinate_time = v->continuation_t,
|
||||
.x = {v->continuation_x[0], v->continuation_x[1],
|
||||
v->continuation_x[2]},
|
||||
.Pi = {v->continuation_Pi[0], v->continuation_Pi[1],
|
||||
v->continuation_Pi[2]},
|
||||
.log_alpha_p0 = v->continuation_log_alpha_p0,
|
||||
.log_alpha_p0_0 = v->continuation_log_alpha_p0_0,
|
||||
.steps = v->continuation_steps};
|
||||
GeodesicTraceConfig retry_config = *trace;
|
||||
retry_config.max_steps = sample->step_limit;
|
||||
endpoint = geodesic_trace_past_from_state(spacetime, &state,
|
||||
&retry_config);
|
||||
} else {
|
||||
endpoint = geodesic_trace_past(spacetime, observer,
|
||||
sample->vertex.camera_direction, trace);
|
||||
}
|
||||
/* Each request has a distinct destination vertex or probe slot. */
|
||||
(void)frame_lens_mesh_install_sample(mesh, i, &endpoint);
|
||||
}
|
||||
const int added = frame_lens_mesh_finish_generation(mesh, config);
|
||||
const int added = frame_lens_mesh_finish_generation(mesh, &effective);
|
||||
if (added < 0) return -1;
|
||||
if (callback != NULL)
|
||||
callback(context, generation, 0, mesh->vertex_count, mesh->triangle_count,
|
||||
@@ -1088,7 +1557,7 @@ static int usable_triangle(const FrameLensMesh *mesh,
|
||||
const LensVertex *vertices[3]) {
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
vertices[i] = &mesh->vertices[triangle->vertex[i]];
|
||||
if (vertices[i]->status != RAY_ENDPOINT_ESCAPED)
|
||||
if (vertices[i]->outcome != RAY_OUTCOME_ESCAPED)
|
||||
return 0;
|
||||
}
|
||||
return spherical_area(vertices[0]->n_infinity, vertices[1]->n_infinity,
|
||||
@@ -1989,5 +2458,7 @@ void frame_lens_mesh_destroy(FrameLensMesh *mesh) {
|
||||
free(mesh->triangles);
|
||||
free(mesh->samples);
|
||||
free(mesh->probe_slots);
|
||||
free(mesh->witness_slots);
|
||||
free(mesh->witness_vertices);
|
||||
*mesh = (FrameLensMesh){0};
|
||||
}
|
||||
+73
-2
@@ -14,17 +14,34 @@ typedef struct {
|
||||
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;
|
||||
/* 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];
|
||||
} 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
|
||||
@@ -54,20 +71,51 @@ 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;
|
||||
} 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;
|
||||
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;
|
||||
|
||||
typedef struct {
|
||||
LensVertex *vertices;
|
||||
LensTriangle *triangles;
|
||||
@@ -80,6 +128,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 {
|
||||
@@ -143,6 +203,17 @@ 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);
|
||||
/* 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. */
|
||||
|
||||
+231
-72
@@ -30,14 +30,19 @@ static int invert(double a[3][3], double b[3][3]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Equation (4) and (5) of Bohn et al., arXiv:1410.7775. */
|
||||
static int rhs(const MetricSlab *slab, double t, const State *s,
|
||||
Derivative *out) {
|
||||
/* Equation (4) and (5) of Bohn et al., arXiv:1410.7775. Returns a metric/data
|
||||
* status so the integrator can report why a step failed instead of collapsing
|
||||
* every failure into one generic error. */
|
||||
static SpacetimePointStatus rhs(const MetricSlab *slab, double t,
|
||||
const State *s, Derivative *out) {
|
||||
MetricData m;
|
||||
double inv[3][3], up[3] = {0}, da_pi = 0, k_pi_pi = 0;
|
||||
if (spacetime_slab_eval(slab, t, s->x, &m) || m.alpha <= 0 ||
|
||||
invert(m.gamma, inv))
|
||||
return -1;
|
||||
const SpacetimePointStatus metric_status =
|
||||
spacetime_slab_eval(slab, t, s->x, &m);
|
||||
if (metric_status != SPACETIME_POINT_OK)
|
||||
return metric_status;
|
||||
if (m.alpha <= 0 || invert(m.gamma, inv))
|
||||
return SPACETIME_POINT_INVALID_METRIC;
|
||||
for (int i = 0; i < 3; i++)
|
||||
for (int j = 0; j < 3; j++)
|
||||
up[i] += inv[i][j] * s->Pi[j];
|
||||
@@ -68,7 +73,7 @@ static int rhs(const MetricSlab *slab, double t, const State *s,
|
||||
db_pi - 0.5 * m.alpha * dg_pi_pi;
|
||||
}
|
||||
out->log_alpha_p0 = -da_pi + m.alpha * k_pi_pi;
|
||||
return 0;
|
||||
return SPACETIME_POINT_OK;
|
||||
}
|
||||
|
||||
static State add(const State *s, const Derivative *d, double h) {
|
||||
@@ -81,20 +86,25 @@ static State add(const State *s, const Derivative *d, double h) {
|
||||
return r;
|
||||
}
|
||||
|
||||
static int rk4(const MetricSlab *slab, double t, double h, State *s) {
|
||||
static SpacetimePointStatus rk4(const MetricSlab *slab, double t, double h,
|
||||
State *s) {
|
||||
Derivative a, b, c, d;
|
||||
State q;
|
||||
if (rhs(slab, t, s, &a))
|
||||
return -1;
|
||||
SpacetimePointStatus status = rhs(slab, t, s, &a);
|
||||
if (status != SPACETIME_POINT_OK)
|
||||
return status;
|
||||
q = add(s, &a, h / 2);
|
||||
if (rhs(slab, t + h / 2, &q, &b))
|
||||
return -1;
|
||||
status = rhs(slab, t + h / 2, &q, &b);
|
||||
if (status != SPACETIME_POINT_OK)
|
||||
return status;
|
||||
q = add(s, &b, h / 2);
|
||||
if (rhs(slab, t + h / 2, &q, &c))
|
||||
return -1;
|
||||
status = rhs(slab, t + h / 2, &q, &c);
|
||||
if (status != SPACETIME_POINT_OK)
|
||||
return status;
|
||||
q = add(s, &c, h);
|
||||
if (rhs(slab, t + h, &q, &d))
|
||||
return -1;
|
||||
status = rhs(slab, t + h, &q, &d);
|
||||
if (status != SPACETIME_POINT_OK)
|
||||
return status;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
s->x[i] += h * (a.x[i] + 2 * b.x[i] + 2 * c.x[i] + d.x[i]) / 6;
|
||||
s->Pi[i] += h * (a.Pi[i] + 2 * b.Pi[i] + 2 * c.Pi[i] + d.Pi[i]) / 6;
|
||||
@@ -103,7 +113,7 @@ static int rk4(const MetricSlab *slab, double t, double h, State *s) {
|
||||
(a.log_alpha_p0 + 2 * b.log_alpha_p0 + 2 * c.log_alpha_p0 +
|
||||
d.log_alpha_p0) /
|
||||
6;
|
||||
return 0;
|
||||
return SPACETIME_POINT_OK;
|
||||
}
|
||||
|
||||
int geodesic_initialize_past_ray_metric(const MetricData *metric,
|
||||
@@ -127,6 +137,7 @@ int geodesic_initialize_past_ray_metric(const MetricData *metric,
|
||||
s->Pi[i] /= m->alpha * k[0];
|
||||
}
|
||||
s->log_alpha_p0 = log(m->alpha * k[0]);
|
||||
s->log_alpha_p0_0 = s->log_alpha_p0;
|
||||
s->coordinate_time = o->coordinate_time;
|
||||
s->steps = 0;
|
||||
return isfinite(s->log_alpha_p0) ? 0 : -1;
|
||||
@@ -218,22 +229,104 @@ static AsymptoticStatus localize_worldtube_crossing(const MetricSlab *slab,
|
||||
return ASYMPTOTIC_OK;
|
||||
}
|
||||
|
||||
static GeodesicAdvanceResult legacy_escape_or_capture(const MetricSlab *slab,
|
||||
const State *s,
|
||||
SpacetimeRayStatus status,
|
||||
RayEndpoint *out) {
|
||||
out->status =
|
||||
status == SPACETIME_RAY_ESCAPED ? RAY_ENDPOINT_ESCAPED
|
||||
: RAY_ENDPOINT_CAPTURED;
|
||||
if (out->status == RAY_ENDPOINT_ESCAPED) {
|
||||
if (escaped_direction(slab, s->coordinate_time, s, out->n_infinity) == 0)
|
||||
out->frequency_ratio = exp(-s->log_alpha_p0);
|
||||
else
|
||||
out->status = RAY_ENDPOINT_INTEGRATION_FAILURE;
|
||||
static RayReason reason_from_point_status(SpacetimePointStatus status) {
|
||||
switch (status) {
|
||||
case SPACETIME_POINT_TIME_UNAVAILABLE:
|
||||
return RAY_REASON_TIME_RANGE_EXHAUSTED;
|
||||
case SPACETIME_POINT_OUT_OF_DOMAIN:
|
||||
return RAY_REASON_OUT_OF_DOMAIN;
|
||||
case SPACETIME_POINT_INVALID_METRIC:
|
||||
return RAY_REASON_INVALID_METRIC;
|
||||
case SPACETIME_POINT_INTERNAL_ERROR:
|
||||
return RAY_REASON_PROTOCOL_ERROR;
|
||||
default:
|
||||
return RAY_REASON_INTEGRATION_ERROR;
|
||||
}
|
||||
return out->status == RAY_ENDPOINT_INTEGRATION_FAILURE
|
||||
? GEODESIC_ADVANCE_FAILED
|
||||
: GEODESIC_ADVANCE_TERMINATED;
|
||||
}
|
||||
|
||||
static void record_final_state(RayEndpoint *out, const State *s) {
|
||||
if (s == NULL) {
|
||||
out->stop_coordinate_time = NAN;
|
||||
out->accepted_steps = 0;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
out->final_x[i] = NAN;
|
||||
out->final_Pi[i] = NAN;
|
||||
}
|
||||
out->final_log_alpha_p0 = NAN;
|
||||
out->final_log_alpha_p0_0 = NAN;
|
||||
return;
|
||||
}
|
||||
out->stop_coordinate_time = s->coordinate_time;
|
||||
out->accepted_steps = s->steps;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
out->final_x[i] = s->x[i];
|
||||
out->final_Pi[i] = s->Pi[i];
|
||||
}
|
||||
out->final_log_alpha_p0 = s->log_alpha_p0;
|
||||
out->final_log_alpha_p0_0 = s->log_alpha_p0_0;
|
||||
}
|
||||
|
||||
static void set_incomplete(RayEndpoint *out, RayReason reason,
|
||||
const State *last) {
|
||||
out->outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
out->reason = reason;
|
||||
out->end_id = SPACETIME_END_NONE;
|
||||
record_final_state(out, last);
|
||||
out->threshold_value = NAN;
|
||||
}
|
||||
|
||||
/* Value of the monitored dark-threshold quantity at a trusted state. */
|
||||
static double monitored_threshold_value(const MetricSlab *slab,
|
||||
ThresholdKind kind, const State *s) {
|
||||
if (kind == THRESHOLD_LOG_ALPHA_P0)
|
||||
return s->log_alpha_p0;
|
||||
if (kind == THRESHOLD_LOG_ENERGY_GROWTH)
|
||||
return s->log_alpha_p0 - s->log_alpha_p0_0;
|
||||
if (kind == THRESHOLD_LOG_P0) {
|
||||
MetricData m;
|
||||
if (spacetime_slab_eval(slab, s->coordinate_time, s->x, &m) !=
|
||||
SPACETIME_POINT_OK ||
|
||||
m.alpha <= 0.0)
|
||||
return NAN;
|
||||
return s->log_alpha_p0 - log(m.alpha);
|
||||
}
|
||||
return NAN;
|
||||
}
|
||||
|
||||
/* Check the dark threshold on one trusted state. Returns nonzero and fills a
|
||||
* DARK endpoint when the monitored quantity has reached the threshold. */
|
||||
static int threshold_reached(const MetricSlab *slab,
|
||||
const GeodesicTraceConfig *config, const State *s,
|
||||
RayEndpoint *out) {
|
||||
if (config->threshold.kind == THRESHOLD_DISABLED)
|
||||
return 0;
|
||||
const double value =
|
||||
monitored_threshold_value(slab, config->threshold.kind, s);
|
||||
if (!isfinite(value) || value < config->threshold.value)
|
||||
return 0;
|
||||
out->outcome = RAY_OUTCOME_DARK;
|
||||
out->reason = RAY_REASON_REDSHIFT_LIMIT;
|
||||
out->end_id = SPACETIME_END_NONE;
|
||||
record_final_state(out, s);
|
||||
out->threshold_value = value;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Legacy no-declared-ends backends may still report a region escape. There
|
||||
* is no position-based physical capture; a failed sky direction is reported
|
||||
* as an integration failure, never as capture. */
|
||||
static GeodesicAdvanceResult legacy_escape(const MetricSlab *slab,
|
||||
const State *s, RayEndpoint *out) {
|
||||
if (escaped_direction(slab, s->coordinate_time, s, out->n_infinity) != 0) {
|
||||
set_incomplete(out, RAY_REASON_INTEGRATION_ERROR, s);
|
||||
return GEODESIC_ADVANCE_FAILED;
|
||||
}
|
||||
out->outcome = RAY_OUTCOME_ESCAPED;
|
||||
out->reason = RAY_REASON_NONE;
|
||||
out->frequency_ratio = exp(-s->log_alpha_p0);
|
||||
record_final_state(out, s);
|
||||
out->threshold_value = NAN;
|
||||
return GEODESIC_ADVANCE_TERMINATED;
|
||||
}
|
||||
|
||||
GeodesicAdvanceResult geodesic_advance_past_ray(
|
||||
@@ -241,53 +334,58 @@ GeodesicAdvanceResult geodesic_advance_past_ray(
|
||||
const GeodesicTraceConfig *config, RayEndpoint *out) {
|
||||
if (!slab || !s || !config || !out || config->coordinate_time_step <= 0 ||
|
||||
!config->max_steps || !isfinite(slab_left_time) ||
|
||||
slab_left_time > s->coordinate_time)
|
||||
slab_left_time > s->coordinate_time) {
|
||||
if (out != NULL) {
|
||||
out->outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
out->reason = RAY_REASON_PROTOCOL_ERROR;
|
||||
record_final_state(out, s);
|
||||
out->threshold_value = NAN;
|
||||
}
|
||||
return GEODESIC_ADVANCE_FAILED;
|
||||
}
|
||||
const AsymLifecycleMode mode = asym_lifecycle_mode(slab->source);
|
||||
if (mode == ASYM_LIFECYCLE_PROTOCOL_ERROR) {
|
||||
out->status = RAY_ENDPOINT_INVALID;
|
||||
set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, s);
|
||||
return GEODESIC_ADVANCE_FAILED;
|
||||
}
|
||||
const int directed = mode == ASYM_LIFECYCLE_READY;
|
||||
const size_t end_count =
|
||||
directed ? spacetime_asymptotic_end_count(slab->source) : 0;
|
||||
while (s->coordinate_time > slab_left_time) {
|
||||
if (config->capture_log_alpha_p0 > 0.0 &&
|
||||
s->log_alpha_p0 >= config->capture_log_alpha_p0) {
|
||||
out->status = RAY_ENDPOINT_CAPTURED;
|
||||
/* The threshold is checked only on trusted initial/accepted states. A
|
||||
* trial stage that crosses it does not by itself produce DARK. */
|
||||
if (threshold_reached(slab, config, s, out))
|
||||
return GEODESIC_ADVANCE_TERMINATED;
|
||||
}
|
||||
const SpacetimeRayStatus status =
|
||||
spacetime_slab_classify(slab, s->coordinate_time, s->x);
|
||||
if (status == SPACETIME_RAY_CAPTURED) {
|
||||
out->status = RAY_ENDPOINT_CAPTURED;
|
||||
return GEODESIC_ADVANCE_TERMINATED;
|
||||
}
|
||||
if (!directed && status != SPACETIME_RAY_ACTIVE)
|
||||
return legacy_escape_or_capture(slab, s, status, out);
|
||||
if (!directed &&
|
||||
spacetime_slab_classify(slab, s->coordinate_time, s->x) ==
|
||||
SPACETIME_RAY_ESCAPED)
|
||||
return legacy_escape(slab, s, out);
|
||||
if (s->steps >= config->max_steps) {
|
||||
out->status = RAY_ENDPOINT_MAX_STEPS;
|
||||
/* Trustworthy trajectory, compute budget exhausted: retryable. */
|
||||
out->outcome = RAY_OUTCOME_UNRESOLVED;
|
||||
out->reason = RAY_REASON_BUDGET_EXHAUSTED;
|
||||
out->end_id = SPACETIME_END_NONE;
|
||||
record_final_state(out, s);
|
||||
out->threshold_value = NAN;
|
||||
return GEODESIC_ADVANCE_TERMINATED;
|
||||
}
|
||||
const double h = -fmin(config->coordinate_time_step,
|
||||
s->coordinate_time - slab_left_time);
|
||||
const State before = *s;
|
||||
if (rk4(slab, s->coordinate_time, h, s))
|
||||
const SpacetimePointStatus step_status = rk4(slab, s->coordinate_time, h, s);
|
||||
if (step_status != SPACETIME_POINT_OK) {
|
||||
set_incomplete(out, reason_from_point_status(step_status), &before);
|
||||
return GEODESIC_ADVANCE_FAILED;
|
||||
}
|
||||
s->coordinate_time += h;
|
||||
++s->steps;
|
||||
if (!directed)
|
||||
continue;
|
||||
|
||||
if (spacetime_slab_classify(slab, s->coordinate_time, s->x) ==
|
||||
SPACETIME_RAY_CAPTURED) {
|
||||
out->status = RAY_ENDPOINT_CAPTURED;
|
||||
return GEODESIC_ADVANCE_TERMINATED;
|
||||
}
|
||||
for (size_t i = 0; i < end_count; ++i) {
|
||||
SpacetimeAsymptoticEnd end;
|
||||
if (spacetime_asymptotic_end(slab->source, i, &end)) {
|
||||
out->status = RAY_ENDPOINT_INVALID;
|
||||
set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, &before);
|
||||
return GEODESIC_ADVANCE_FAILED;
|
||||
}
|
||||
double f_before, f_after;
|
||||
@@ -300,14 +398,14 @@ GeodesicAdvanceResult geodesic_advance_past_ray(
|
||||
after_status == ASYMPTOTIC_TIME_RANGE_EXHAUSTED) {
|
||||
/* A first-class terminal reason, matching pre-route exhaustion:
|
||||
* preserve the end id and install it as terminated provenance. */
|
||||
out->status = RAY_ENDPOINT_TIME_RANGE_EXHAUSTED;
|
||||
set_incomplete(out, RAY_REASON_TIME_RANGE_EXHAUSTED, &before);
|
||||
out->end_id = end.end_id;
|
||||
return GEODESIC_ADVANCE_TERMINATED;
|
||||
}
|
||||
if (before_status != ASYMPTOTIC_OK || after_status != ASYMPTOTIC_OK) {
|
||||
/* The backend cannot describe its own worldtube; this is an explicit
|
||||
* failure, not a physical escape. */
|
||||
out->status = RAY_ENDPOINT_INVALID;
|
||||
set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, &before);
|
||||
return GEODESIC_ADVANCE_FAILED;
|
||||
}
|
||||
/* Strict inside->outside: the step must end strictly outside, so a
|
||||
@@ -320,29 +418,37 @@ GeodesicAdvanceResult geodesic_advance_past_ray(
|
||||
const AsymptoticStatus localized = localize_worldtube_crossing(
|
||||
slab, end.end_id, &before, h, &crossing);
|
||||
if (localized == ASYMPTOTIC_TIME_RANGE_EXHAUSTED) {
|
||||
out->status = RAY_ENDPOINT_TIME_RANGE_EXHAUSTED;
|
||||
set_incomplete(out, RAY_REASON_TIME_RANGE_EXHAUSTED, &before);
|
||||
out->end_id = end.end_id;
|
||||
return GEODESIC_ADVANCE_TERMINATED;
|
||||
}
|
||||
if (localized != ASYMPTOTIC_OK) {
|
||||
out->status = RAY_ENDPOINT_INVALID;
|
||||
set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, &before);
|
||||
return GEODESIC_ADVANCE_FAILED;
|
||||
}
|
||||
const AsymptoticStatus transfer = asymptotic_finish_escape(
|
||||
slab->source, end.end_id, crossing.coordinate_time, crossing.x,
|
||||
crossing.Pi, crossing.log_alpha_p0, out);
|
||||
if (transfer == ASYMPTOTIC_OK)
|
||||
if (transfer == ASYMPTOTIC_OK) {
|
||||
record_final_state(out, &crossing);
|
||||
out->threshold_value = NAN;
|
||||
return GEODESIC_ADVANCE_TERMINATED;
|
||||
out->status = transfer == ASYMPTOTIC_TIME_RANGE_EXHAUSTED
|
||||
? RAY_ENDPOINT_TIME_RANGE_EXHAUSTED
|
||||
: RAY_ENDPOINT_INVALID;
|
||||
}
|
||||
if (transfer == ASYMPTOTIC_TIME_RANGE_EXHAUSTED) {
|
||||
set_incomplete(out, RAY_REASON_TIME_RANGE_EXHAUSTED, &before);
|
||||
out->end_id = end.end_id;
|
||||
return GEODESIC_ADVANCE_TERMINATED;
|
||||
}
|
||||
set_incomplete(out, RAY_REASON_PROTOCOL_ERROR, &before);
|
||||
return GEODESIC_ADVANCE_FAILED;
|
||||
}
|
||||
}
|
||||
/* The loop can stop exactly at the slab's left boundary, so the final
|
||||
* accepted step must also be checked before reporting ACTIVE; otherwise a
|
||||
* ray that crossed the dark threshold on its last step would be settled as
|
||||
* budget-unresolved and require another slab/retry. */
|
||||
if (threshold_reached(slab, config, s, out))
|
||||
return GEODESIC_ADVANCE_TERMINATED;
|
||||
return GEODESIC_ADVANCE_ACTIVE;
|
||||
}
|
||||
|
||||
@@ -353,7 +459,12 @@ RayEndpoint geodesic_trace_past(const SpacetimeSource *source,
|
||||
RayEndpoint out = {.frequency_ratio = 0,
|
||||
.magnification = 1,
|
||||
.end_id = SPACETIME_END_NONE,
|
||||
.status = RAY_ENDPOINT_INTEGRATION_FAILURE};
|
||||
.outcome = RAY_OUTCOME_INCOMPLETE,
|
||||
.reason = RAY_REASON_INTEGRATION_ERROR,
|
||||
.stop_coordinate_time = NAN,
|
||||
.accepted_steps = 0,
|
||||
.threshold_value = NAN};
|
||||
record_final_state(&out, NULL);
|
||||
if (!source || !observer || !config || config->coordinate_time_step <= 0 ||
|
||||
!config->max_steps || fabs(dot(n, n) - 1) > 1e-10)
|
||||
return out;
|
||||
@@ -361,16 +472,19 @@ RayEndpoint geodesic_trace_past(const SpacetimeSource *source,
|
||||
const AsymptoticStatus route_status =
|
||||
asymptotic_route_camera(source, observer, n, &route);
|
||||
if (route_status == ASYMPTOTIC_UNSUPPORTED) {
|
||||
out.status = RAY_ENDPOINT_INVALID;
|
||||
out.outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
out.reason = RAY_REASON_UNSUPPORTED;
|
||||
return out;
|
||||
}
|
||||
if (route_status == ASYMPTOTIC_TIME_RANGE_EXHAUSTED) {
|
||||
out.status = RAY_ENDPOINT_TIME_RANGE_EXHAUSTED;
|
||||
out.outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
out.reason = RAY_REASON_TIME_RANGE_EXHAUSTED;
|
||||
out.end_id = route.end_id;
|
||||
return out;
|
||||
}
|
||||
if (route_status != ASYMPTOTIC_OK) {
|
||||
out.status = RAY_ENDPOINT_INVALID;
|
||||
out.outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
out.reason = RAY_REASON_PROTOCOL_ERROR;
|
||||
return out;
|
||||
}
|
||||
if (route.kind == ASYMPTOTIC_ROUTE_ESCAPED) {
|
||||
@@ -378,34 +492,79 @@ RayEndpoint geodesic_trace_past(const SpacetimeSource *source,
|
||||
out.n_infinity[i] = route.n_infinity[i];
|
||||
out.frequency_ratio = route.frequency_ratio;
|
||||
out.end_id = route.end_id;
|
||||
out.status = RAY_ENDPOINT_ESCAPED;
|
||||
out.outcome = RAY_OUTCOME_ESCAPED;
|
||||
out.reason = RAY_REASON_NONE;
|
||||
return out;
|
||||
}
|
||||
if (route.kind == ASYMPTOTIC_ROUTE_TIME_RANGE_EXHAUSTED) {
|
||||
out.status = RAY_ENDPOINT_TIME_RANGE_EXHAUSTED;
|
||||
out.outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
out.reason = RAY_REASON_TIME_RANGE_EXHAUSTED;
|
||||
out.end_id = route.end_id;
|
||||
return out;
|
||||
}
|
||||
if (route.kind != ASYMPTOTIC_ROUTE_INSIDE &&
|
||||
route.kind != ASYMPTOTIC_ROUTE_ENTRY) {
|
||||
out.status = RAY_ENDPOINT_INVALID;
|
||||
out.outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
out.reason = RAY_REASON_PROTOCOL_ERROR;
|
||||
return out;
|
||||
}
|
||||
State state = {.coordinate_time = route.activate_t,
|
||||
.x = {route.x[0], route.x[1], route.x[2]},
|
||||
.Pi = {route.Pi[0], route.Pi[1], route.Pi[2]},
|
||||
.log_alpha_p0 = route.log_alpha_p0,
|
||||
/* Camera-event reference, distinct from the entry-state L for
|
||||
* an external camera. */
|
||||
.log_alpha_p0_0 = route.log_alpha_p0_camera,
|
||||
.steps = 0};
|
||||
const double last_time =
|
||||
route.activate_t - config->coordinate_time_step * config->max_steps;
|
||||
MetricSlab *slab = NULL;
|
||||
if (spacetime_load_slab(source, route.activate_t, last_time - 1.0, &slab)) {
|
||||
out.status = RAY_ENDPOINT_INVALID;
|
||||
out.outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
out.reason = RAY_REASON_IO_ERROR;
|
||||
return out;
|
||||
}
|
||||
if (geodesic_advance_past_ray(slab, &state, last_time, config, &out) ==
|
||||
GEODESIC_ADVANCE_ACTIVE)
|
||||
out.status = RAY_ENDPOINT_MAX_STEPS;
|
||||
GEODESIC_ADVANCE_ACTIVE) {
|
||||
out.outcome = RAY_OUTCOME_UNRESOLVED;
|
||||
out.reason = RAY_REASON_BUDGET_EXHAUSTED;
|
||||
record_final_state(&out, &state);
|
||||
}
|
||||
spacetime_free_slab(slab);
|
||||
return out;
|
||||
}
|
||||
|
||||
RayEndpoint geodesic_trace_past_from_state(const SpacetimeSource *source,
|
||||
const GeodesicRayState *state_in,
|
||||
const GeodesicTraceConfig *config) {
|
||||
RayEndpoint out = {.frequency_ratio = 0,
|
||||
.magnification = 1,
|
||||
.end_id = SPACETIME_END_NONE,
|
||||
.outcome = RAY_OUTCOME_INCOMPLETE,
|
||||
.reason = RAY_REASON_INTEGRATION_ERROR,
|
||||
.stop_coordinate_time = NAN,
|
||||
.accepted_steps = 0,
|
||||
.threshold_value = NAN};
|
||||
record_final_state(&out, NULL);
|
||||
if (!source || !state_in || !config || config->coordinate_time_step <= 0 ||
|
||||
!config->max_steps || state_in->steps >= config->max_steps)
|
||||
return out;
|
||||
State state = *state_in;
|
||||
const double remaining =
|
||||
config->coordinate_time_step * (double)(config->max_steps - state.steps);
|
||||
const double last_time = state.coordinate_time - remaining - 1.0;
|
||||
MetricSlab *slab = NULL;
|
||||
if (spacetime_load_slab(source, state.coordinate_time, last_time, &slab)) {
|
||||
out.outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
out.reason = RAY_REASON_IO_ERROR;
|
||||
return out;
|
||||
}
|
||||
if (geodesic_advance_past_ray(slab, &state, last_time, config, &out) ==
|
||||
GEODESIC_ADVANCE_ACTIVE) {
|
||||
out.outcome = RAY_OUTCOME_UNRESOLVED;
|
||||
out.reason = RAY_REASON_BUDGET_EXHAUSTED;
|
||||
record_final_state(&out, &state);
|
||||
}
|
||||
spacetime_free_slab(slab);
|
||||
return out;
|
||||
}
|
||||
+67
-15
@@ -4,33 +4,75 @@
|
||||
#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,
|
||||
RAY_ENDPOINT_TIME_RANGE_EXHAUSTED,
|
||||
RAY_ENDPOINT_INVALID
|
||||
} 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. */
|
||||
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
|
||||
} RayReason;
|
||||
|
||||
/* 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. */
|
||||
/* Meaningful for RAY_ENDPOINT_ESCAPED and for
|
||||
* RAY_ENDPOINT_TIME_RANGE_EXHAUSTED; SPACETIME_END_NONE otherwise. */
|
||||
/* End this escape belongs to; SPACETIME_END_NONE when not applicable. */
|
||||
SpacetimeEndId end_id;
|
||||
RayEndpointStatus status;
|
||||
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;
|
||||
/* 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;
|
||||
} RayEndpoint;
|
||||
|
||||
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;
|
||||
} GeodesicTraceConfig;
|
||||
|
||||
typedef struct {
|
||||
@@ -38,6 +80,9 @@ typedef struct {
|
||||
double x[3];
|
||||
double Pi[3];
|
||||
double log_alpha_p0;
|
||||
/* 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;
|
||||
} GeodesicRayState;
|
||||
|
||||
@@ -67,4 +112,11 @@ 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
|
||||
+76
-20
@@ -10,7 +10,10 @@
|
||||
/* 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 1 files are rejected: their old captured bit
|
||||
* cannot be upgraded into the new dark/unresolved/error provenance. */
|
||||
enum { LENS_MAP_VERSION = 2, LENS_MAP_ENDIAN = 0x01020304u };
|
||||
|
||||
static uint32_t crc32_update(uint32_t crc, const void *data, size_t size) {
|
||||
const unsigned char *bytes = data;
|
||||
@@ -64,11 +67,14 @@ 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 ||
|
||||
v->reason > RAY_REASON_IO_ERROR || !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 +83,51 @@ 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)
|
||||
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);
|
||||
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);
|
||||
}
|
||||
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 +141,69 @@ 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;
|
||||
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) ||
|
||||
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) ||
|
||||
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 ||
|
||||
prov.threshold_kind > THRESHOLD_LOG_ENERGY_GROWTH ||
|
||||
!isfinite(prov.threshold_value) ||
|
||||
prov.coordinate_time_step < 0.0 || !isfinite(prov.coordinate_time_step) ||
|
||||
count > SIZE_MAX / sizeof *map->frames;
|
||||
if (failed) goto done;
|
||||
map->provenance = prov;
|
||||
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 ||
|
||||
reason > RAY_REASON_IO_ERROR;
|
||||
v->end_id = (SpacetimeEndId)end_id;
|
||||
v->outcome = (RayOutcome)outcome;
|
||||
v->reason = (RayReason)reason;
|
||||
v->traced = 1;
|
||||
}
|
||||
for (size_t i = 0; !failed && i < frame->mesh.triangle_count; ++i) {
|
||||
for (int j = 0; j < 3; ++j) {
|
||||
@@ -163,7 +214,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);
|
||||
}
|
||||
|
||||
+23
-3
@@ -16,17 +16,37 @@ 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;
|
||||
uint32_t integrator; /* 0 = fixed-step RK4 (transitional) */
|
||||
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;
|
||||
} LensMapProvenance;
|
||||
|
||||
typedef struct {
|
||||
int width, height;
|
||||
double horizontal_fov_deg;
|
||||
LensMapProvenance provenance;
|
||||
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
|
||||
+213
-40
@@ -23,9 +23,11 @@ typedef struct {
|
||||
int width, height;
|
||||
int coarse_cell_pixels;
|
||||
int draw_mesh;
|
||||
int allow_incomplete;
|
||||
int psf_direct;
|
||||
int verbose;
|
||||
double horizontal_fov_deg, look_ra_deg, look_dec_deg, exposure;
|
||||
double dark_threshold;
|
||||
double max_magnification;
|
||||
double max_cache_psf_flux;
|
||||
double psf_relative_tail;
|
||||
@@ -98,7 +100,10 @@ static int parse_double(const char *text, double *value) {
|
||||
char *end;
|
||||
errno = 0;
|
||||
*value = strtod(text, &end);
|
||||
return errno || *end || *value <= 0.0 || *value >= 179.0 ? -1 : 0;
|
||||
return errno || *end || !isfinite(*value) || *value <= 0.0 ||
|
||||
*value >= 179.0
|
||||
? -1
|
||||
: 0;
|
||||
}
|
||||
|
||||
static int parse_ra_deg(const char *text, double *value) {
|
||||
@@ -119,7 +124,7 @@ static int parse_positive(const char *text, double *value) {
|
||||
char *end;
|
||||
errno = 0;
|
||||
*value = strtod(text, &end);
|
||||
return errno || *end || *value <= 0.0 ? -1 : 0;
|
||||
return errno || *end || !isfinite(*value) || *value <= 0.0 ? -1 : 0;
|
||||
}
|
||||
|
||||
static int parse_nonnegative(const char *text, double *value) {
|
||||
@@ -379,6 +384,7 @@ static int parse_args(int argc, char **argv, Settings *s,
|
||||
.look_ra_deg = 90.0,
|
||||
.look_dec_deg = -90.0,
|
||||
.exposure = 1e-3,
|
||||
.dark_threshold = 8.0,
|
||||
.max_magnification = INFINITY,
|
||||
.max_cache_psf_flux = 1.0,
|
||||
.psf_relative_tail = 1e-8,
|
||||
@@ -453,6 +459,8 @@ static int parse_args(int argc, char **argv, Settings *s,
|
||||
!parse_positive(argv[++i], &s->refinement.min_area_pixels2)) {
|
||||
} else if (!strcmp(argv[i], "--draw-mesh")) {
|
||||
s->draw_mesh = 1;
|
||||
} else if (!strcmp(argv[i], "--allow-incomplete")) {
|
||||
s->allow_incomplete = 1;
|
||||
} else if (!strcmp(argv[i], "--fov-deg") && i + 1 < argc &&
|
||||
!parse_double(argv[++i], &s->horizontal_fov_deg)) {
|
||||
s->fov_specified = 1;
|
||||
@@ -464,6 +472,8 @@ static int parse_args(int argc, char **argv, Settings *s,
|
||||
s->look_specified = 1;
|
||||
} else if (!strcmp(argv[i], "--exposure") && i + 1 < argc &&
|
||||
!parse_positive(argv[++i], &s->exposure)) {
|
||||
} else if (!strcmp(argv[i], "--dark-threshold") && i + 1 < argc &&
|
||||
!parse_positive(argv[++i], &s->dark_threshold)) {
|
||||
} else if (!strcmp(argv[i], "--tone-map") && i + 1 < argc) {
|
||||
const char *mode = argv[++i];
|
||||
if (!strcmp(mode, "softclip"))
|
||||
@@ -636,6 +646,7 @@ static void print_help(const char *program) {
|
||||
" --look-ra-deg D ICRS look direction right ascension in degrees (default: 90)\n"
|
||||
" --look-dec-deg D ICRS look direction declination in degrees (default: -90)\n"
|
||||
" --exposure E Linear exposure multiplier (default: 1e-3)\n"
|
||||
" --dark-threshold T Camera-relative dark cutoff L-L0 (default: 8)\n"
|
||||
" --tone-map MODE Display transform: softclip or reinhard\n"
|
||||
" (default: softclip)\n"
|
||||
" --tone-map-p P Softclip hardness P >= 1 (default: 2)\n"
|
||||
@@ -706,6 +717,8 @@ static void print_help(const char *program) {
|
||||
fputs(" --draw-mesh Also write the final lens-mesh overlay as <output-stem>_mesh.ppm\n",
|
||||
stdout);
|
||||
#endif
|
||||
fputs(" --allow-incomplete Publish even when unresolved/error rays remain (diagnostic; output is marked incomplete)\n",
|
||||
stdout);
|
||||
#ifdef SPACETIME_ALCUBIERRE
|
||||
fputs(
|
||||
"\nAlcubierre warp bubble (moving x_s(t)=v_s*t; no capture):\n"
|
||||
@@ -806,13 +819,14 @@ static void report_splat_worker_progress(void *context, size_t worker_id,
|
||||
|
||||
static void ray_pool_status_counts(const RayPool *rays, size_t *pending,
|
||||
size_t *active, size_t *terminated,
|
||||
size_t *failed) {
|
||||
*pending = *active = *terminated = *failed = 0;
|
||||
size_t *unresolved, size_t *failed) {
|
||||
*pending = *active = *terminated = *unresolved = *failed = 0;
|
||||
for (size_t i = 0; i < rays->count; ++i)
|
||||
switch (rays->status[i]) {
|
||||
case RAY_POOL_PENDING: ++*pending; break;
|
||||
case RAY_POOL_ACTIVE: ++*active; break;
|
||||
case RAY_POOL_TERMINATED: ++*terminated; break;
|
||||
case RAY_POOL_UNRESOLVED: ++*unresolved; break;
|
||||
case RAY_POOL_FAILED: ++*failed; break;
|
||||
}
|
||||
}
|
||||
@@ -839,7 +853,7 @@ static void report_frame_refinement(void *context, size_t generation,
|
||||
#ifdef SPACETIME_ALCUBIERRE
|
||||
/* Upper bound on the per-ray step budget. Legal parameters whose worst-case
|
||||
* near-comoving ray could need more than this are rejected at startup rather
|
||||
* than silently terminating as RAY_ENDPOINT_MAX_STEPS. */
|
||||
* than silently terminating as UNRESOLVED/BUDGET_EXHAUSTED. */
|
||||
#define ALCUBIERRE_MAX_TRACE_STEPS (1u << 24)
|
||||
|
||||
/* Safety margin over the straight-line worst case: wall-region deflection can
|
||||
@@ -867,16 +881,109 @@ static double alcubierre_step_budget(const Settings *s) {
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Recompute approximate-black provenance and report E/D/U accounting. The
|
||||
* result is diagnostic for now; production failure gating is layered on top
|
||||
* of the same counters. */
|
||||
static void report_boundary_stats(const Settings *s,
|
||||
FrameLensMesh *const *meshes,
|
||||
size_t frame_count,
|
||||
const RefinementConfig *config,
|
||||
FrameBoundaryStats *out_total) {
|
||||
FrameBoundaryStats total = {0};
|
||||
for (size_t i = 0; i < frame_count; ++i) {
|
||||
FrameBoundaryStats frame_stats;
|
||||
frame_lens_mesh_boundary_stats(meshes[i], config, &frame_stats);
|
||||
total.escaped_only += frame_stats.escaped_only;
|
||||
total.dark_only += frame_stats.dark_only;
|
||||
total.eed_edd += frame_stats.eed_edd;
|
||||
total.uud_udd += frame_stats.uud_udd;
|
||||
total.u_with_escape += frame_stats.u_with_escape;
|
||||
total.uuu += frame_stats.uuu;
|
||||
total.error += frame_stats.error;
|
||||
total.approx_black_triangles += frame_stats.approx_black_triangles;
|
||||
total.approx_black_area_pixels2 += frame_stats.approx_black_area_pixels2;
|
||||
total.approx_black_max_edge_pixels = fmax(total.approx_black_max_edge_pixels,
|
||||
frame_stats.approx_black_max_edge_pixels);
|
||||
total.approx_black_max_area_pixels2 = fmax(total.approx_black_max_area_pixels2,
|
||||
frame_stats.approx_black_max_area_pixels2);
|
||||
total.approx_black_level_stops += frame_stats.approx_black_level_stops;
|
||||
total.retry_requests += frame_stats.retry_requests;
|
||||
total.budget_incomplete_triangles += frame_stats.budget_incomplete_triangles;
|
||||
}
|
||||
if (out_total != NULL)
|
||||
*out_total = total;
|
||||
if (s->verbose && frame_count > 0)
|
||||
fprintf(stderr,
|
||||
"Boundary accounting: EEE=%zu DDD=%zu EED/EDD=%zu UUD/UDD=%zu "
|
||||
"U+E=%zu UUU=%zu error=%zu; approx-black=%zu (%.6g px^2), "
|
||||
"retries=%zu, budget-incomplete=%zu.\n",
|
||||
total.escaped_only, total.dark_only, total.eed_edd, total.uud_udd,
|
||||
total.u_with_escape, total.uuu, total.error,
|
||||
total.approx_black_triangles, total.approx_black_area_pixels2,
|
||||
total.retry_requests, total.budget_incomplete_triangles);
|
||||
if (s->verbose && total.approx_black_triangles)
|
||||
fprintf(stderr, "Approx-black achieved scale: max edge=%.6g px, max area=%.6g px^2, level stops=%zu.\n",
|
||||
total.approx_black_max_edge_pixels, total.approx_black_max_area_pixels2,
|
||||
total.approx_black_level_stops);
|
||||
}
|
||||
|
||||
static LensMapProvenance lens_map_provenance(const Settings *s,
|
||||
const GeodesicTraceConfig *trace) {
|
||||
RefinementConfig rc = s->refinement;
|
||||
frame_retry_config_defaults(&rc, trace);
|
||||
return (LensMapProvenance){.threshold_kind = (uint32_t)trace->threshold.kind,
|
||||
.threshold_policy_version =
|
||||
trace->threshold.policy_version,
|
||||
.threshold_value = trace->threshold.value,
|
||||
.retry_step_increment = rc.retry_step_increment,
|
||||
.max_total_steps = rc.max_total_steps,
|
||||
.max_level = rc.max_level,
|
||||
.integrator = 0,
|
||||
.min_edge_pixels = rc.min_edge_pixels,
|
||||
.min_area_pixels2 = rc.min_area_pixels2,
|
||||
.coordinate_time_step = trace->coordinate_time_step,
|
||||
.initial_max_steps = trace->max_steps};
|
||||
}
|
||||
|
||||
/* Refuse to publish silently on true errors or on unresolved triangles that
|
||||
* exhausted the configured total budget. Approximate-black UUD/UDD boundary
|
||||
* triangles are an accepted finite-resolution error and do not block output.
|
||||
* A diagnostic run may override this, but the incompleteness is reported. */
|
||||
static int boundary_allows_publish(const Settings *s,
|
||||
const FrameBoundaryStats *total) {
|
||||
const size_t blocking = total->error + total->budget_incomplete_triangles;
|
||||
if (blocking == 0)
|
||||
return 1;
|
||||
if (s->allow_incomplete) {
|
||||
fprintf(stderr,
|
||||
"WARNING: publishing incomplete render (error triangles=%zu, "
|
||||
"budget-incomplete unresolved triangles=%zu).\n",
|
||||
total->error, total->budget_incomplete_triangles);
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr,
|
||||
"Incomplete render refused: %zu error triangle(s), %zu "
|
||||
"budget-incomplete unresolved triangle(s). Raise the retry budget or "
|
||||
"pass --allow-incomplete for a diagnostic output.\n",
|
||||
total->error, total->budget_incomplete_triangles);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static GeodesicTraceConfig trace_config(const Settings *s) {
|
||||
/* The normal dark terminal is the camera-relative local energy growth
|
||||
* L - L0, independent of the spacetime backend. A constant camera boost
|
||||
* cancels; the photon energy and frequency ratio are never reset. */
|
||||
const ThresholdPolicy threshold = {.kind = THRESHOLD_LOG_ENERGY_GROWTH,
|
||||
.value = s->dark_threshold,
|
||||
.policy_version = 3};
|
||||
#ifdef SPACETIME_SCHWARZSCHILD
|
||||
(void)s;
|
||||
/* The directed worldtube crossing makes an escaping ray traverse the
|
||||
* interior as a round trip from the entry sphere (in, turn, back out),
|
||||
* rather than the old one-way stop at the first outside sample. The step
|
||||
* budget must cover roughly twice the escape sphere plus margin. */
|
||||
return (GeodesicTraceConfig){.coordinate_time_step = 0.1,
|
||||
.max_steps = 65536,
|
||||
.capture_log_alpha_p0 = 8.0};
|
||||
.threshold = threshold};
|
||||
#elif defined(SPACETIME_ALCUBIERRE)
|
||||
const double step = alcubierre_time_step(s);
|
||||
const double budget = alcubierre_step_budget(s);
|
||||
@@ -886,11 +993,12 @@ static GeodesicTraceConfig trace_config(const Settings *s) {
|
||||
if (max_steps < 1024u)
|
||||
max_steps = 1024u;
|
||||
return (GeodesicTraceConfig){.coordinate_time_step = step,
|
||||
.max_steps = max_steps};
|
||||
.max_steps = max_steps,
|
||||
.threshold = threshold};
|
||||
#else
|
||||
(void)s;
|
||||
return (GeodesicTraceConfig){.coordinate_time_step = 1.0,
|
||||
.max_steps = 2048};
|
||||
.max_steps = 2048,
|
||||
.threshold = threshold};
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -947,17 +1055,13 @@ static int build_observer(const Settings *s, const SpacetimeSource *spacetime,
|
||||
camera.position[i] = s->observer_position[i];
|
||||
camera.velocity[i] = s->observer_velocity[i];
|
||||
}
|
||||
const SpacetimeRayStatus camera_status =
|
||||
spacetime_classify(spacetime, camera.coordinate_time, camera.position);
|
||||
if (camera_status == SPACETIME_RAY_CAPTURED) {
|
||||
fputs("Camera position is inside the backend capture cutoff or invalid.\n", stderr);
|
||||
return -1;
|
||||
}
|
||||
/* A camera outside the escape sphere is supported by the asymptotic
|
||||
* exterior module for every declared end kind; unsupported exteriors are
|
||||
* reported through the ray endpoints instead. */
|
||||
/* Camera legality depends only on metric availability, a timelike
|
||||
* four-velocity, time orientation, and an orthonormal tetrad. A camera
|
||||
* inside a horizon or an old spatial cutoff is a normal rendering target;
|
||||
* its position never decides a ray's terminal category. */
|
||||
MetricData metric;
|
||||
if (spacetime_eval(spacetime, camera.coordinate_time, camera.position, &metric)) {
|
||||
if (spacetime_eval(spacetime, camera.coordinate_time, camera.position,
|
||||
&metric) != SPACETIME_POINT_OK) {
|
||||
fputs("Could not evaluate metric at the camera event.\n", stderr);
|
||||
return -1;
|
||||
}
|
||||
@@ -1032,13 +1136,26 @@ static int render_observer_frame(const Settings *s, StarCatalog *catalog,
|
||||
"%zu vertices, %zu triangles.\n",
|
||||
omp_get_wtime() - refinement_start, mesh.vertex_count,
|
||||
mesh.triangle_count);
|
||||
FrameBoundaryStats boundary_totals;
|
||||
{
|
||||
RefinementConfig boundary_config = s->refinement;
|
||||
frame_retry_config_defaults(&boundary_config, &trace);
|
||||
FrameLensMesh *meshes[1] = {&mesh};
|
||||
report_boundary_stats(s, meshes, 1, &boundary_config, &boundary_totals);
|
||||
}
|
||||
if (!boundary_allows_publish(s, &boundary_totals)) {
|
||||
frame_lens_mesh_destroy(&mesh);
|
||||
free(hdr);
|
||||
return -1;
|
||||
}
|
||||
if (s->lens_map_output_path != NULL) {
|
||||
const LensMapFrame map_frame = {.frame_id = 0,
|
||||
.coordinate_time = 0.0,
|
||||
.proper_time = 0.0,
|
||||
.mesh = mesh};
|
||||
const LensMapProvenance provenance = lens_map_provenance(s, &trace);
|
||||
if (lens_map_write(s->lens_map_output_path, s->width, s->height,
|
||||
s->horizontal_fov_deg, &map_frame, 1)) {
|
||||
s->horizontal_fov_deg, &provenance, &map_frame, 1)) {
|
||||
fprintf(stderr, "Failed to write lens map: %s\n", s->lens_map_output_path);
|
||||
frame_lens_mesh_destroy(&mesh);
|
||||
free(hdr);
|
||||
@@ -1113,9 +1230,11 @@ static int trace_movie_generation(Movie *movie, const Settings *s,
|
||||
RayPool rays = {0};
|
||||
size_t ray_count = 0;
|
||||
size_t total_added = 0;
|
||||
RefinementConfig effective = s->refinement;
|
||||
frame_retry_config_defaults(&effective, trace);
|
||||
for (size_t f = 0; f < movie->frame_count; ++f) {
|
||||
const int prepared = frame_lens_mesh_prepare_generation(
|
||||
&movie->frames[f].mesh, &s->refinement);
|
||||
&movie->frames[f].mesh, &effective);
|
||||
if (prepared < 0) return -1;
|
||||
ray_count += (size_t)prepared;
|
||||
}
|
||||
@@ -1128,12 +1247,26 @@ static int trace_movie_generation(Movie *movie, const Settings *s,
|
||||
for (size_t f = 0; f < movie->frame_count; ++f) {
|
||||
size_t count = 0;
|
||||
const FrameSample *samples = frame_lens_mesh_samples(&movie->frames[f].mesh, &count);
|
||||
for (size_t sample = 0; sample < count; ++sample)
|
||||
if (ray_pool_append(&rays, &movie->frames[f].observer,
|
||||
samples[sample].vertex.camera_direction, f, sample)) {
|
||||
for (size_t sample = 0; sample < count; ++sample) {
|
||||
const FrameSample *fs = &samples[sample];
|
||||
if (fs->cached) continue;
|
||||
int rc;
|
||||
if (fs->kind == FRAME_SAMPLE_RETRY) {
|
||||
const LensVertex *v = &fs->vertex;
|
||||
rc = ray_pool_append_continuation(
|
||||
&rays, f, sample, v->continuation_t, v->continuation_x,
|
||||
v->continuation_Pi, v->continuation_log_alpha_p0,
|
||||
v->continuation_log_alpha_p0_0, v->continuation_steps,
|
||||
fs->step_limit);
|
||||
} else {
|
||||
rc = ray_pool_append(&rays, &movie->frames[f].observer,
|
||||
fs->vertex.camera_direction, f, sample);
|
||||
}
|
||||
if (rc) {
|
||||
ray_pool_destroy(&rays);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
ray_pool_preroute(&rays, spacetime);
|
||||
double slab_hi = movie->frames[movie->frame_count - 1].coordinate_time;
|
||||
@@ -1141,29 +1274,35 @@ static int trace_movie_generation(Movie *movie, const Settings *s,
|
||||
while (ray_pool_has_live(&rays)) {
|
||||
const double slab_lo = slab_hi - s->slab_duration;
|
||||
MetricSlab *slab = NULL;
|
||||
size_t pending_before, active_before, terminated_before, failed_before;
|
||||
size_t pending_before, active_before, terminated_before, unresolved_before,
|
||||
failed_before;
|
||||
ray_pool_status_counts(&rays, &pending_before, &active_before,
|
||||
&terminated_before, &failed_before);
|
||||
&terminated_before, &unresolved_before,
|
||||
&failed_before);
|
||||
if (spacetime_load_slab(spacetime, slab_hi, slab_lo, &slab)) {
|
||||
ray_pool_destroy(&rays);
|
||||
return -1;
|
||||
}
|
||||
ray_pool_activate_in_time_range(&rays, slab);
|
||||
size_t pending_active, active_active, terminated_active, failed_active;
|
||||
size_t pending_active, active_active, terminated_active, unresolved_active,
|
||||
failed_active;
|
||||
ray_pool_status_counts(&rays, &pending_active, &active_active,
|
||||
&terminated_active, &failed_active);
|
||||
&terminated_active, &unresolved_active,
|
||||
&failed_active);
|
||||
ray_pool_advance_active(&rays, slab, trace);
|
||||
spacetime_free_slab(slab);
|
||||
size_t pending_after, active_after, terminated_after, failed_after;
|
||||
size_t pending_after, active_after, terminated_after, unresolved_after,
|
||||
failed_after;
|
||||
ray_pool_status_counts(&rays, &pending_after, &active_after,
|
||||
&terminated_after, &failed_after);
|
||||
&terminated_after, &unresolved_after, &failed_after);
|
||||
if (s->verbose)
|
||||
fprintf(stderr,
|
||||
"Ray trace generation %zu, slab %zu [%.6g, %.6g]: activated %zu; "
|
||||
"live %zu -> %zu, terminated %zu, failed %zu.\n",
|
||||
"live %zu -> %zu, terminated %zu, unresolved %zu, failed %zu.\n",
|
||||
generation, ++slab_id, slab_hi, slab_lo,
|
||||
active_active - active_before, pending_before + active_before,
|
||||
pending_after + active_after, terminated_after, failed_after);
|
||||
pending_after + active_after, terminated_after, unresolved_after,
|
||||
failed_after);
|
||||
slab_hi = slab_lo;
|
||||
}
|
||||
for (size_t i = 0; i < rays.count; ++i)
|
||||
@@ -1181,7 +1320,7 @@ static int trace_movie_generation(Movie *movie, const Settings *s,
|
||||
if (movie->frames[f].mesh.sample_count == 0)
|
||||
continue;
|
||||
const int added = frame_lens_mesh_finish_generation(&movie->frames[f].mesh,
|
||||
&s->refinement);
|
||||
&effective);
|
||||
if (added < 0) {
|
||||
fprintf(stderr, "Ray trace generation %zu: frame %zu refinement failed.\n",
|
||||
generation, movie->frames[f].frame_id);
|
||||
@@ -1338,6 +1477,21 @@ static int render_movie(const Settings *s, StarCatalog *catalog,
|
||||
if (traced == 0)
|
||||
break;
|
||||
}
|
||||
{
|
||||
RefinementConfig boundary_config = s->refinement;
|
||||
frame_retry_config_defaults(&boundary_config, &trace);
|
||||
FrameLensMesh **meshes = malloc(movie.frame_count * sizeof *meshes);
|
||||
if (meshes == NULL)
|
||||
goto done;
|
||||
for (size_t i = 0; i < movie.frame_count; ++i)
|
||||
meshes[i] = &movie.frames[i].mesh;
|
||||
FrameBoundaryStats boundary_totals;
|
||||
report_boundary_stats(s, meshes, movie.frame_count, &boundary_config,
|
||||
&boundary_totals);
|
||||
free(meshes);
|
||||
if (!boundary_allows_publish(s, &boundary_totals))
|
||||
goto done;
|
||||
}
|
||||
if (s->lens_map_output_path != NULL) {
|
||||
LensMapFrame *map_frames = calloc(movie.frame_count, sizeof *map_frames);
|
||||
if (map_frames == NULL) goto done;
|
||||
@@ -1346,9 +1500,11 @@ static int render_movie(const Settings *s, StarCatalog *catalog,
|
||||
.coordinate_time = movie.frames[i].coordinate_time,
|
||||
.proper_time = movie.frames[i].proper_time,
|
||||
.mesh = movie.frames[i].mesh};
|
||||
const LensMapProvenance provenance = lens_map_provenance(s, &trace);
|
||||
const int write_failed = lens_map_write(s->lens_map_output_path, s->width,
|
||||
s->height, s->horizontal_fov_deg,
|
||||
map_frames, movie.frame_count);
|
||||
&provenance, map_frames,
|
||||
movie.frame_count);
|
||||
free(map_frames);
|
||||
if (write_failed) {
|
||||
fprintf(stderr, "Failed to write lens map: %s\n", s->lens_map_output_path);
|
||||
@@ -1468,10 +1624,26 @@ done:
|
||||
|
||||
static int render_lens_map(const Settings *s, StarCatalog *catalog) {
|
||||
LensMap map = {0};
|
||||
if (lens_map_read(s->lens_map_input_path, &map)) {
|
||||
if (lens_map_read(s->lens_map_input_path, NULL, &map)) {
|
||||
fprintf(stderr, "Failed to read or validate lens map: %s\n", s->lens_map_input_path);
|
||||
return -1;
|
||||
}
|
||||
RefinementConfig replay_config = {
|
||||
.max_level = map.provenance.max_level,
|
||||
.min_edge_pixels = map.provenance.min_edge_pixels,
|
||||
.min_area_pixels2 = map.provenance.min_area_pixels2,
|
||||
.retry_step_increment = map.provenance.retry_step_increment,
|
||||
.max_total_steps = map.provenance.max_total_steps};
|
||||
int replay_incomplete = 0;
|
||||
/* Replay consumes stored decisions, not current CLI refinement defaults. */
|
||||
for (size_t f = 0; f < map.frame_count; ++f) {
|
||||
FrameBoundaryStats completion;
|
||||
frame_lens_mesh_boundary_stats(&map.frames[f].mesh, &replay_config, &completion);
|
||||
replay_incomplete |= completion.error != 0 || completion.budget_incomplete_triangles != 0;
|
||||
if (!boundary_allows_publish(s, &completion)) {
|
||||
lens_map_destroy(&map); return -1;
|
||||
}
|
||||
}
|
||||
if ((s->width_specified && s->width != map.width) ||
|
||||
(s->height_specified && s->height != map.height) ||
|
||||
(s->fov_specified && fabs(s->horizontal_fov_deg - map.horizontal_fov_deg) > 1e-12)) {
|
||||
@@ -1609,7 +1781,7 @@ static int render_lens_map(const Settings *s, StarCatalog *catalog) {
|
||||
if (prepare_movie_output_job(s, &map.frames[i].mesh, hdr, map.width,
|
||||
map.height, &output_paths,
|
||||
(size_t)map.frames[i].frame_id, images,
|
||||
catalog->count, "; imported lens map",
|
||||
catalog->count, replay_incomplete ? "; INCOMPLETE imported lens map" : "; imported lens map",
|
||||
&psf_stats, &frame_timing, &job)) {
|
||||
free(hdr);
|
||||
result = -1;
|
||||
@@ -1632,7 +1804,7 @@ static int render_lens_map(const Settings *s, StarCatalog *catalog) {
|
||||
const int write_result = write_frame_outputs(
|
||||
s, &map.frames[i].mesh, hdr, map.width, map.height,
|
||||
map.horizontal_fov_deg, &output_paths, images, catalog->count,
|
||||
"; imported lens map", NULL);
|
||||
replay_incomplete ? "; INCOMPLETE imported lens map" : "; imported lens map", NULL);
|
||||
free(hdr);
|
||||
report_psf_splat(s, &psf_stats);
|
||||
if (write_result) { result = -1; break; }
|
||||
@@ -1688,7 +1860,8 @@ int main(int argc, char **argv) {
|
||||
"Usage: %s [--catalog PATH | --all-sky-catalog DIR] [--output PATH] [--width N] [--height "
|
||||
"N] [--fov-deg D] [--look-ra-deg D] [--look-dec-deg D] "
|
||||
"[--lens-map-input FILE | --lens-map-output FILE] "
|
||||
"[--exposure E] [--tone-map softclip|reinhard] [--tone-map-p P] "
|
||||
"[--exposure E] [--dark-threshold T] "
|
||||
"[--tone-map softclip|reinhard] [--tone-map-p P] "
|
||||
"[--sensor-bloom-limit E --sensor-bloom-transfer e] "
|
||||
"[--observer-radius R | --observer-position X Y Z] "
|
||||
"[--observer-velocity VX VY VZ] [--camera-roll-deg ANGLE] "
|
||||
@@ -1696,7 +1869,7 @@ int main(int argc, char **argv) {
|
||||
"[--max-magnification M] [--max-cache-psf-flux F] "
|
||||
"[--psf-relative-tail R] [--psf-min-y Y] "
|
||||
"[--psf-direct] [--fast-mode --fast-supersample N "
|
||||
"--fast-deposit nearest|bilinear] [--verbose] "
|
||||
"--fast-deposit nearest|bilinear] [--verbose] [--allow-incomplete] "
|
||||
#ifdef ENABLE_HDR_OUTPUT
|
||||
"[--hdr-output] "
|
||||
#endif
|
||||
|
||||
@@ -14,7 +14,9 @@ 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(activate_t) && RAY_ALLOC(steps) &&
|
||||
RAY_ALLOC(log_alpha_p0) && RAY_ALLOC(log_alpha_p0_0) &&
|
||||
RAY_ALLOC(activate_t) && RAY_ALLOC(steps) &&
|
||||
RAY_ALLOC(step_limit) && RAY_ALLOC(continuation) &&
|
||||
RAY_ALLOC(frame_id) && RAY_ALLOC(vertex_id) && RAY_ALLOC(status) &&
|
||||
RAY_ALLOC(endpoint))) {
|
||||
ray_pool_destroy(p);
|
||||
@@ -43,7 +45,55 @@ int ray_pool_append(RayPool *p, const ObserverState *observer,
|
||||
p->status[i] = RAY_POOL_PENDING;
|
||||
p->endpoint[i] = (RayEndpoint){.magnification = 1.0,
|
||||
.end_id = SPACETIME_END_NONE,
|
||||
.status = RAY_ENDPOINT_INTEGRATION_FAILURE};
|
||||
.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;
|
||||
p->continuation[i] = 0;
|
||||
p->log_alpha_p0_0[i] = 0.0;
|
||||
++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 (p == NULL || p->count == p->capacity || x == NULL || Pi == NULL)
|
||||
return -1;
|
||||
const size_t i = p->count;
|
||||
p->t[i] = t;
|
||||
p->activate_t[i] = t;
|
||||
p->observer[i] = NULL;
|
||||
p->direction0[i] = p->direction1[i] = p->direction2[i] = 0.0;
|
||||
p->x0[i] = x[0]; p->x1[i] = x[1]; p->x2[i] = x[2];
|
||||
p->p0[i] = Pi[0]; p->p1[i] = Pi[1]; p->p2[i] = Pi[2];
|
||||
p->log_alpha_p0[i] = log_alpha_p0;
|
||||
p->log_alpha_p0_0[i] = log_alpha_p0_0;
|
||||
p->steps[i] = steps;
|
||||
p->step_limit[i] = 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 = steps,
|
||||
.final_x = {NAN, NAN, NAN},
|
||||
.final_Pi = {NAN, NAN, NAN},
|
||||
.final_log_alpha_p0 = NAN,
|
||||
.final_log_alpha_p0_0 = log_alpha_p0_0,
|
||||
.threshold_value = NAN};
|
||||
++p->count;
|
||||
return 0;
|
||||
}
|
||||
@@ -53,7 +103,7 @@ void ray_pool_preroute(RayPool *p, const SpacetimeSource *source) {
|
||||
return;
|
||||
#pragma omp parallel for schedule(static)
|
||||
for (size_t i = 0; i < p->count; ++i) {
|
||||
if (p->status[i] != RAY_POOL_PENDING)
|
||||
if (p->status[i] != RAY_POOL_PENDING || p->continuation[i])
|
||||
continue;
|
||||
AsymptoticRoute route;
|
||||
const AsymptoticStatus status = asymptotic_route_camera(
|
||||
@@ -61,13 +111,17 @@ void ray_pool_preroute(RayPool *p, const SpacetimeSource *source) {
|
||||
(double[]){p->direction0[i], p->direction1[i], p->direction2[i]},
|
||||
&route);
|
||||
if (status == ASYMPTOTIC_UNSUPPORTED || status == ASYMPTOTIC_INVALID) {
|
||||
p->endpoint[i].status = RAY_ENDPOINT_INVALID;
|
||||
p->endpoint[i].outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
p->endpoint[i].reason = status == ASYMPTOTIC_UNSUPPORTED
|
||||
? RAY_REASON_UNSUPPORTED
|
||||
: RAY_REASON_PROTOCOL_ERROR;
|
||||
p->endpoint[i].end_id = route.end_id;
|
||||
p->status[i] = RAY_POOL_FAILED;
|
||||
continue;
|
||||
}
|
||||
if (status == ASYMPTOTIC_TIME_RANGE_EXHAUSTED) {
|
||||
p->endpoint[i].status = RAY_ENDPOINT_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;
|
||||
@@ -77,19 +131,22 @@ void ray_pool_preroute(RayPool *p, const SpacetimeSource *source) {
|
||||
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].status = RAY_ENDPOINT_ESCAPED;
|
||||
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].status = RAY_ENDPOINT_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].status = RAY_ENDPOINT_INVALID;
|
||||
p->endpoint[i].outcome = RAY_OUTCOME_INCOMPLETE;
|
||||
p->endpoint[i].reason = RAY_REASON_PROTOCOL_ERROR;
|
||||
p->status[i] = RAY_POOL_FAILED;
|
||||
continue;
|
||||
}
|
||||
@@ -101,6 +158,8 @@ void ray_pool_preroute(RayPool *p, const SpacetimeSource *source) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +169,9 @@ void ray_pool_activate_in_time_range(RayPool *p, const MetricSlab *slab) {
|
||||
p->activate_t[i] <= slab->t_lo)
|
||||
continue;
|
||||
p->t[i] = p->activate_t[i];
|
||||
p->steps[i] = 0;
|
||||
/* Continuation rays keep the accepted-step count they already consumed. */
|
||||
if (!p->continuation[i])
|
||||
p->steps[i] = 0;
|
||||
p->status[i] = RAY_POOL_ACTIVE;
|
||||
}
|
||||
}
|
||||
@@ -129,16 +190,22 @@ 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],
|
||||
.log_alpha_p0_0 = p->log_alpha_p0_0[i],
|
||||
.steps = p->steps[i]};
|
||||
GeodesicTraceConfig per_ray = *config;
|
||||
if (p->step_limit[i] != 0)
|
||||
per_ray.max_steps = p->step_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;
|
||||
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;
|
||||
}
|
||||
@@ -159,7 +226,9 @@ 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->activate_t); free(p->steps); free(p->frame_id); free(p->vertex_id);
|
||||
free(p->log_alpha_p0_0);
|
||||
free(p->activate_t); free(p->steps); free(p->step_limit);
|
||||
free(p->continuation); free(p->frame_id); free(p->vertex_id);
|
||||
free(p->status);
|
||||
free(p->endpoint);
|
||||
*p = (RayPool){0};
|
||||
|
||||
@@ -10,11 +10,12 @@ 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. */
|
||||
@@ -22,6 +23,11 @@ typedef struct {
|
||||
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;
|
||||
/* 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;
|
||||
@@ -32,6 +38,13 @@ 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.
|
||||
* `limit` is the new total accepted-step budget for this ray. */
|
||||
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);
|
||||
|
||||
+24
-12
@@ -14,10 +14,23 @@ 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
|
||||
@@ -66,15 +79,15 @@ 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
|
||||
@@ -108,8 +121,7 @@ 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);
|
||||
double escape_radius);
|
||||
/* Moving Alcubierre bubble with x_s(t) = vs*t and x_s(0) = 0. Requires
|
||||
* |vs| < 1, R > 0, and sigma > 0. */
|
||||
int spacetime_create_alcubierre(SpacetimeSource *source, double vs,
|
||||
@@ -118,15 +130,15 @@ int spacetime_create_alcubierre(SpacetimeSource *source, double vs,
|
||||
* 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);
|
||||
|
||||
@@ -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,7 +104,7 @@ 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
|
||||
|
||||
+18
-13
@@ -9,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);
|
||||
}
|
||||
|
||||
@@ -47,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);
|
||||
@@ -58,8 +61,10 @@ 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);
|
||||
@@ -102,7 +107,7 @@ 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->classify == NULL || ops->destroy == NULL)
|
||||
if (ops->eval == NULL || ops->destroy == NULL)
|
||||
return -1;
|
||||
const size_t count = spacetime_asymptotic_end_count(source);
|
||||
if (count == 0)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -150,16 +157,13 @@ static const SpacetimeOps schwarzschild_ks_ops = {
|
||||
};
|
||||
|
||||
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)) {
|
||||
@@ -170,5 +174,5 @@ int spacetime_create_schwarzschild_ks(SpacetimeSource *source, double mass,
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -214,8 +214,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 +236,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 +277,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 +305,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) {
|
||||
|
||||
+15
-10
@@ -52,8 +52,9 @@ typedef struct {
|
||||
int sample_call_count;
|
||||
} SyntheticContext;
|
||||
|
||||
static int synthetic_eval(const SpacetimeSource *source, double t,
|
||||
const double x[3], MetricData *metric) {
|
||||
static SpacetimePointStatus synthetic_eval(const SpacetimeSource *source,
|
||||
double t, const double x[3],
|
||||
MetricData *metric) {
|
||||
(void)source;
|
||||
(void)t;
|
||||
(void)x;
|
||||
@@ -61,7 +62,7 @@ static int synthetic_eval(const SpacetimeSource *source, double t,
|
||||
.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 synthetic_classify(const SpacetimeSource *source,
|
||||
@@ -233,7 +234,7 @@ static void test_fixed_sphere(void) {
|
||||
CHECK(asymptotic_finish_escape(&source, 0, 0.0, (double[]){10.0, 0.0, 0.0},
|
||||
(double[]){-1.0, 0.0, 0.0}, 0.0,
|
||||
&endpoint) == ASYMPTOTIC_OK &&
|
||||
endpoint.status == RAY_ENDPOINT_ESCAPED && endpoint.end_id == 0,
|
||||
endpoint.outcome == RAY_OUTCOME_ESCAPED && endpoint.end_id == 0,
|
||||
"finish outward crossing");
|
||||
CHECK(fabs(endpoint.n_infinity[0] - 1.0) < 1e-12 &&
|
||||
fabs(endpoint.frequency_ratio - 1.0) < 1e-12,
|
||||
@@ -493,10 +494,11 @@ static void test_end_protocol_error(void) {
|
||||
RayEndpoint endpoint = {.frequency_ratio = 0.0,
|
||||
.magnification = 1.0,
|
||||
.end_id = SPACETIME_END_NONE,
|
||||
.status = RAY_ENDPOINT_INVALID};
|
||||
.outcome = RAY_OUTCOME_INCOMPLETE};
|
||||
CHECK(geodesic_advance_past_ray(slab, &state, -10.0, &config, &endpoint) ==
|
||||
GEODESIC_ADVANCE_FAILED &&
|
||||
endpoint.status == RAY_ENDPOINT_INVALID,
|
||||
endpoint.outcome == RAY_OUTCOME_INCOMPLETE &&
|
||||
endpoint.reason == RAY_REASON_PROTOCOL_ERROR,
|
||||
"advance rejects a declared-but-broken end without legacy");
|
||||
spacetime_free_slab(slab);
|
||||
}
|
||||
@@ -517,7 +519,8 @@ static void test_interior_crossing_bisection_failure(void) {
|
||||
.max_steps = 2048};
|
||||
const RayEndpoint endpoint = geodesic_trace_past(
|
||||
&source, &observer, (double[]){-1.0, 0.0, 0.0}, &config);
|
||||
CHECK(endpoint.status == RAY_ENDPOINT_TIME_RANGE_EXHAUSTED &&
|
||||
CHECK(endpoint.outcome == RAY_OUTCOME_INCOMPLETE &&
|
||||
endpoint.reason == RAY_REASON_TIME_RANGE_EXHAUSTED &&
|
||||
endpoint.end_id == 0,
|
||||
"interior crossing bisection propagates history exhaustion");
|
||||
}
|
||||
@@ -550,7 +553,8 @@ static void test_interior_history_exhaustion(void) {
|
||||
.max_steps = 2048};
|
||||
const RayEndpoint endpoint = geodesic_trace_past(
|
||||
&source, &observer, (double[]){-1.0, 0.0, 0.0}, &config);
|
||||
CHECK(endpoint.status == RAY_ENDPOINT_TIME_RANGE_EXHAUSTED &&
|
||||
CHECK(endpoint.outcome == RAY_OUTCOME_INCOMPLETE &&
|
||||
endpoint.reason == RAY_REASON_TIME_RANGE_EXHAUSTED &&
|
||||
endpoint.end_id == 0,
|
||||
"interior worldtube history exhaustion on a single trace");
|
||||
|
||||
@@ -567,7 +571,8 @@ static void test_interior_history_exhaustion(void) {
|
||||
ray_pool_activate_in_time_range(&pool, slab);
|
||||
CHECK(pool.status[0] == RAY_POOL_ACTIVE, "exhaustion ray activates");
|
||||
ray_pool_advance_active(&pool, slab, &config);
|
||||
CHECK(pool.endpoint[0].status == RAY_ENDPOINT_TIME_RANGE_EXHAUSTED &&
|
||||
CHECK(pool.endpoint[0].outcome == RAY_OUTCOME_INCOMPLETE &&
|
||||
pool.endpoint[0].reason == RAY_REASON_TIME_RANGE_EXHAUSTED &&
|
||||
pool.endpoint[0].end_id == 0 &&
|
||||
pool.status[0] == RAY_POOL_TERMINATED,
|
||||
"interior worldtube history exhaustion on a RayPool");
|
||||
@@ -700,7 +705,7 @@ static void test_ray_pool_lifecycle(void) {
|
||||
pool.activate_t[0] < observer.coordinate_time - 1.0,
|
||||
"entry ray stays pending until entry time");
|
||||
CHECK(pool.status[1] == RAY_POOL_TERMINATED &&
|
||||
pool.endpoint[1].status == RAY_ENDPOINT_ESCAPED,
|
||||
pool.endpoint[1].outcome == RAY_OUTCOME_ESCAPED,
|
||||
"miss ray escapes during pre-route");
|
||||
|
||||
MetricSlab *early = NULL;
|
||||
|
||||
@@ -26,7 +26,7 @@ static double angle_between(const double a[3], const double b[3]) {
|
||||
|
||||
static void test_round_trip(void) {
|
||||
SpacetimeSource source = {0};
|
||||
CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0, 1.5) == 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");
|
||||
@@ -58,9 +58,9 @@ static void test_round_trip(void) {
|
||||
|
||||
static void test_finish_matches_integration(void) {
|
||||
SpacetimeSource near = {0}, far = {0};
|
||||
CHECK(spacetime_create_schwarzschild_ks(&near, 1.0, 256.0, 1.5) == 0,
|
||||
CHECK(spacetime_create_schwarzschild_ks(&near, 1.0, 256.0) == 0,
|
||||
"create near");
|
||||
CHECK(spacetime_create_schwarzschild_ks(&far, 1.0, 1.0e5, 1.5) == 0,
|
||||
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");
|
||||
@@ -97,12 +97,12 @@ static void test_finish_matches_integration(void) {
|
||||
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,
|
||||
.status = RAY_ENDPOINT_INVALID};
|
||||
.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.status == RAY_ENDPOINT_ESCAPED,
|
||||
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
|
||||
@@ -152,7 +152,7 @@ static double transfer_dphi(double r, const void *context) {
|
||||
|
||||
static void test_preroute_entry(void) {
|
||||
SpacetimeSource source = {0};
|
||||
CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0, 1.5) == 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;
|
||||
@@ -232,6 +232,51 @@ static void test_preroute_entry(void) {
|
||||
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. */
|
||||
@@ -261,7 +306,7 @@ static void test_phi_reference_constants(void) {
|
||||
* measured ~1e-13. */
|
||||
static void test_finish_reference_constants(void) {
|
||||
SpacetimeSource source = {0};
|
||||
CHECK(spacetime_create_schwarzschild_ks(&source, 1.0, 256.0, 1.5) == 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");
|
||||
@@ -313,7 +358,7 @@ static void test_turning_reference(void) {
|
||||
* 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, 1.5) == 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");
|
||||
@@ -365,7 +410,7 @@ static void test_time_reference(void) {
|
||||
* 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, 1.5) == 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");
|
||||
@@ -411,7 +456,7 @@ static void test_grazing_reference(void) {
|
||||
* 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, 1.5) == 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");
|
||||
@@ -480,6 +525,7 @@ 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();
|
||||
|
||||
+39
-10
@@ -91,14 +91,16 @@ def image_payload(path, dimensions=(64, 48), allow_black=False):
|
||||
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('<Q', data, 32)[0] == 1 # frame_count
|
||||
# Header: 40 bytes, then a 60-byte provenance block, then the 48-byte
|
||||
# per-frame header, so vertices start at offset 148 in the v2 format.
|
||||
vertices, triangles = struct.unpack_from('<QQ', data, 124)
|
||||
offset = 148
|
||||
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('<9dIII', data, offset))
|
||||
offset += 84
|
||||
return values, data[offset:offset + triangles * 32]
|
||||
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
|
||||
@@ -113,6 +115,7 @@ with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
|
||||
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.
|
||||
@@ -240,12 +243,20 @@ 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)
|
||||
@@ -271,8 +282,8 @@ 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])))
|
||||
assert x[9:] == y[9:], '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 +300,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, 148 + 76, 3, 5) # INCOMPLETE / INVALID_METRIC
|
||||
struct.pack_into('<I', payload, len(payload)-4,
|
||||
zlib.crc32(payload[148:-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)
|
||||
|
||||
+282
-15
@@ -59,7 +59,105 @@ static int mesh_has_same_winding_shared_edge(const FrameLensMesh *mesh) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int review_probe_regressions(void) {
|
||||
RefinementConfig c = {.max_level=2, .min_edge_pixels=.5,
|
||||
.min_area_pixels2=.25, .angle_absolute_rad=3.14, .angle_relative=1e6,
|
||||
.retry_step_increment=10, .max_total_steps=100};
|
||||
RayEndpoint flat = {.outcome=RAY_OUTCOME_ESCAPED, .frequency_ratio=1,
|
||||
.n_infinity={1,0,0}, .end_id=0};
|
||||
for (int unresolved=0; unresolved<2; ++unresolved) {
|
||||
FrameLensMesh m={0};
|
||||
if (frame_lens_mesh_build_coarse(&m,100,100,100,30)) return -1;
|
||||
for (size_t i=0;i<m.vertex_count;++i) {
|
||||
m.vertices[i].traced=1; m.vertices[i].outcome=RAY_OUTCOME_ESCAPED;
|
||||
m.vertices[i].n_infinity[0]=1;
|
||||
}
|
||||
if (frame_lens_mesh_prepare_generation(&m,&c)!=1) return -1;
|
||||
RayEndpoint bad = {.outcome=unresolved?RAY_OUTCOME_UNRESOLVED:RAY_OUTCOME_INCOMPLETE,
|
||||
.reason=unresolved?RAY_REASON_BUDGET_EXHAUSTED:RAY_REASON_INVALID_METRIC,
|
||||
.accepted_steps=10, .stop_coordinate_time=-1,
|
||||
.final_x={30,0,0}, .final_Pi={1,0,0}};
|
||||
if (frame_lens_mesh_install_sample(&m,0,&bad) ||
|
||||
frame_lens_mesh_finish_generation(&m,&c)<0) return -1;
|
||||
FrameBoundaryStats stats;
|
||||
frame_lens_mesh_boundary_stats(&m,&c,&stats);
|
||||
if ((!unresolved && !stats.error) ||
|
||||
(unresolved && !stats.budget_incomplete_triangles)) return -1;
|
||||
if (unresolved) {
|
||||
const int retry = frame_lens_mesh_prepare_generation(&m,&c);
|
||||
if (retry != 1 || m.samples[0].kind != FRAME_SAMPLE_RETRY) return -1;
|
||||
if (frame_lens_mesh_install_sample(&m,0,&flat) ||
|
||||
frame_lens_mesh_finish_generation(&m,&c)<0) return -1;
|
||||
frame_lens_mesh_boundary_stats(&m,&c,&stats);
|
||||
if (stats.error || stats.budget_incomplete_triangles) return -1;
|
||||
/* The resolved witness and the settled triangle converge. */
|
||||
for (int guard = 0; guard < 8; ++guard) {
|
||||
const int request = frame_lens_mesh_prepare_generation(&m,&c);
|
||||
if (request == 0) break;
|
||||
if (request < 0 || guard == 7) return -1;
|
||||
for (size_t i = 0; i < m.sample_count; ++i)
|
||||
if (frame_lens_mesh_install_sample(&m,i,&flat)) return -1;
|
||||
if (frame_lens_mesh_finish_generation(&m,&c)<0) return -1;
|
||||
}
|
||||
}
|
||||
frame_lens_mesh_destroy(&m);
|
||||
}
|
||||
/* R5: a witness retry that triggers the split in the same generation must
|
||||
* promote the existing witness in place, leaving one vertex and no second
|
||||
* continuation for the same edge. */
|
||||
{
|
||||
FrameLensMesh m={0};
|
||||
if (frame_lens_mesh_build_coarse(&m,100,100,100,30)) return -1;
|
||||
for (size_t i=0;i<m.vertex_count;++i) {
|
||||
m.vertices[i].traced=1; m.vertices[i].outcome=RAY_OUTCOME_ESCAPED;
|
||||
m.vertices[i].n_infinity[0]=1;
|
||||
}
|
||||
if (frame_lens_mesh_prepare_generation(&m,&c)!=1) return -1;
|
||||
RayEndpoint badU={.outcome=RAY_OUTCOME_UNRESOLVED,
|
||||
.reason=RAY_REASON_BUDGET_EXHAUSTED, .accepted_steps=10,
|
||||
.stop_coordinate_time=-1, .final_x={30,0,0}, .final_Pi={1,0,0}};
|
||||
if (frame_lens_mesh_install_sample(&m,0,&badU) ||
|
||||
frame_lens_mesh_finish_generation(&m,&c)<0) return -1;
|
||||
const int retry = frame_lens_mesh_prepare_generation(&m,&c);
|
||||
if (retry != 1 || m.samples[0].kind != FRAME_SAMPLE_RETRY) return -1;
|
||||
RayEndpoint dark={.outcome=RAY_OUTCOME_DARK,.reason=RAY_REASON_REDSHIFT_LIMIT};
|
||||
if (frame_lens_mesh_install_sample(&m,0,&dark) ||
|
||||
frame_lens_mesh_finish_generation(&m,&c)<0) return -1;
|
||||
FrameBoundaryStats stats;
|
||||
frame_lens_mesh_boundary_stats(&m,&c,&stats);
|
||||
if (m.diagnostic_probe_count != 0 || stats.error ||
|
||||
stats.budget_incomplete_triangles) return -1;
|
||||
frame_lens_mesh_destroy(&m);
|
||||
}
|
||||
/* A retry that resolves to DARK must invalidate the settled leaves so the
|
||||
* newly discontinuous EED boundary is refined rather than silently frozen. */
|
||||
{
|
||||
FrameLensMesh m={0};
|
||||
if (frame_lens_mesh_build_coarse(&m,200,100,100,30)) return -1;
|
||||
for (size_t i=0;i<m.vertex_count;++i) {
|
||||
m.vertices[i].traced=1; m.vertices[i].outcome=RAY_OUTCOME_ESCAPED;
|
||||
m.vertices[i].n_infinity[0]=1;
|
||||
}
|
||||
m.vertices[0].outcome=RAY_OUTCOME_UNRESOLVED;
|
||||
m.vertices[0].continuation_limit=10;
|
||||
if (frame_lens_mesh_prepare_generation(&m,&c)<1 || m.retry_requests==0)
|
||||
return -1; /* the retry counter is produced by real retry requests */
|
||||
RayEndpoint dark={.outcome=RAY_OUTCOME_DARK,.reason=RAY_REASON_REDSHIFT_LIMIT};
|
||||
for (size_t i=0;i<m.sample_count;++i)
|
||||
if (frame_lens_mesh_install_sample(&m,i,
|
||||
m.samples[i].kind==FRAME_SAMPLE_RETRY?&dark:&flat)) return -1;
|
||||
if (frame_lens_mesh_finish_generation(&m,&c)<0 ||
|
||||
frame_lens_mesh_prepare_generation(&m,&c)==0) return -1;
|
||||
frame_lens_mesh_destroy(&m);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
if (review_probe_regressions()) {
|
||||
fputs("probe persistence / retry invalidation regression failed\n",stderr);
|
||||
return 1;
|
||||
}
|
||||
const int width = 100, height = 100;
|
||||
const double test_exposure = 1e-3;
|
||||
const double psf_relative_tail = 1e-8;
|
||||
@@ -90,17 +188,39 @@ int main(void) {
|
||||
/* A finalized mesh can be persisted independently of spacetime and then
|
||||
* drive the exact same catalog inverse-map and PSF pass. */
|
||||
const char *lens_map_path = "/tmp/gr_lens_map_test.grlens";
|
||||
mesh.retry_requests = 2; /* cumulative per-frame retry accounting round-trips */
|
||||
const LensMapFrame saved_frame = {.frame_id = 7,
|
||||
.coordinate_time = 3.0,
|
||||
.proper_time = 2.0,
|
||||
.mesh = mesh};
|
||||
LensMap loaded_map = {0};
|
||||
const LensMapProvenance provenance = {.threshold_kind = THRESHOLD_LOG_ALPHA_P0,
|
||||
.threshold_policy_version = 1,
|
||||
.threshold_value = 8.0,
|
||||
.retry_step_increment = 16,
|
||||
.max_total_steps = 64,
|
||||
.max_level = 2,
|
||||
.integrator = 0,
|
||||
.min_edge_pixels = 0.5,
|
||||
.min_area_pixels2 = 0.25,
|
||||
.coordinate_time_step = 0.1,
|
||||
.initial_max_steps = 4096};
|
||||
double *roundtrip_hdr = calloc((size_t)width * height * 3, sizeof *roundtrip_hdr);
|
||||
if (roundtrip_hdr == NULL ||
|
||||
lens_map_write(lens_map_path, width, height, 30.0, &saved_frame, 1) ||
|
||||
lens_map_read(lens_map_path, &loaded_map) || loaded_map.frame_count != 1 ||
|
||||
lens_map_write(lens_map_path, width, height, 30.0, &provenance,
|
||||
&saved_frame, 1) ||
|
||||
lens_map_read(lens_map_path, NULL, &loaded_map) ||
|
||||
loaded_map.frame_count != 1 ||
|
||||
loaded_map.frames[0].frame_id != 7 || loaded_map.width != width ||
|
||||
loaded_map.height != height ||
|
||||
loaded_map.provenance.threshold_kind != THRESHOLD_LOG_ALPHA_P0 ||
|
||||
loaded_map.provenance.threshold_value != 8.0 ||
|
||||
loaded_map.provenance.retry_step_increment != 16 ||
|
||||
loaded_map.provenance.max_total_steps != 64 ||
|
||||
loaded_map.provenance.max_level != 2 ||
|
||||
loaded_map.provenance.coordinate_time_step != 0.1 ||
|
||||
loaded_map.provenance.initial_max_steps != 4096 ||
|
||||
loaded_map.frames[0].mesh.retry_requests != 2 ||
|
||||
loaded_map.frames[0].mesh.vertex_count != mesh.vertex_count ||
|
||||
frame_splat_catalog(&loaded_map.frames[0].mesh, &catalog, roundtrip_hdr,
|
||||
width, height, test_exposure, &psf, NULL, INFINITY,
|
||||
@@ -130,12 +250,39 @@ int main(void) {
|
||||
}
|
||||
if (damaged != NULL && fclose(damaged))
|
||||
damage_failed = 1;
|
||||
if (damage_failed || !lens_map_read(lens_map_path, &loaded_map)) {
|
||||
if (damage_failed || !lens_map_read(lens_map_path, NULL, &loaded_map)) {
|
||||
fputs("lens-map corruption rejection regression failed\n", stderr);
|
||||
lens_map_destroy(&loaded_map); unlink(lens_map_path);
|
||||
goto done;
|
||||
}
|
||||
unlink(lens_map_path);
|
||||
/* A version-1 header must be rejected outright: its captured bit cannot be
|
||||
* upgraded into the new dark/unresolved/error provenance. */
|
||||
{
|
||||
const char *legacy_path = "/tmp/gr_lens_map_v1_test.grlens";
|
||||
FILE *legacy = fopen(legacy_path, "wb");
|
||||
int legacy_failed = legacy == NULL;
|
||||
if (!legacy_failed) {
|
||||
const unsigned char magic[8] = {'G', 'R', 'L', 'E', 'N', 'S', 1, 0};
|
||||
const unsigned char header[32] = {
|
||||
1, 0, 0, 0, /* version 1 */
|
||||
4, 3, 2, 1, /* endian 0x01020304 */
|
||||
8, 0, 0, 0, 8, 0, 0, 0, /* width, height */
|
||||
0, 0, 0, 0, 0, 0, 0, 0, /* fov */
|
||||
1, 0, 0, 0, 0, 0, 0, 0 /* frame_count 1 */
|
||||
};
|
||||
legacy_failed = fwrite(magic, 1, sizeof magic, legacy) != sizeof magic ||
|
||||
fwrite(header, 1, sizeof header, legacy) != sizeof header;
|
||||
}
|
||||
if (legacy != NULL && fclose(legacy))
|
||||
legacy_failed = 1;
|
||||
if (legacy_failed || !lens_map_read(legacy_path, NULL, &loaded_map)) {
|
||||
fputs("lens-map v1 rejection regression failed\n", stderr);
|
||||
lens_map_destroy(&loaded_map); unlink(legacy_path);
|
||||
goto done;
|
||||
}
|
||||
unlink(legacy_path);
|
||||
}
|
||||
memset(hdr, 0, (size_t)width * height * 3 * sizeof *hdr);
|
||||
PsfSplatStats min_y_stats = {0};
|
||||
if (frame_splat_catalog(&mesh, &catalog, hdr, width, height, test_exposure,
|
||||
@@ -475,9 +622,9 @@ int main(void) {
|
||||
{0.99999997228355786, -0.00021216644782556991, -0.00010206998544149922},
|
||||
{0.9999927016945267, -0.0034455730513416835, -0.001650631403158936}};
|
||||
LensVertex thin_vertices[3] = {
|
||||
{.image_x = 40, .image_y = 40, .status = RAY_ENDPOINT_ESCAPED},
|
||||
{.image_x = 48, .image_y = 40, .status = RAY_ENDPOINT_ESCAPED},
|
||||
{.image_x = 40, .image_y = 48, .status = RAY_ENDPOINT_ESCAPED}};
|
||||
{.image_x = 40, .image_y = 40, .outcome = RAY_OUTCOME_ESCAPED},
|
||||
{.image_x = 48, .image_y = 40, .outcome = RAY_OUTCOME_ESCAPED},
|
||||
{.image_x = 40, .image_y = 48, .outcome = RAY_OUTCOME_ESCAPED}};
|
||||
LensTriangle thin_triangle = {.vertex = {0, 1, 2}};
|
||||
FrameLensMesh thin_mesh = {.vertices = thin_vertices, .vertex_count = 3,
|
||||
.triangles = &thin_triangle, .triangle_count = 1};
|
||||
@@ -524,7 +671,7 @@ int main(void) {
|
||||
goto done;
|
||||
for (size_t i = 0; i < adaptive_mesh.vertex_count; ++i) {
|
||||
adaptive_mesh.vertices[i].traced = 1;
|
||||
adaptive_mesh.vertices[i].status = RAY_ENDPOINT_ESCAPED;
|
||||
adaptive_mesh.vertices[i].outcome = RAY_OUTCOME_ESCAPED;
|
||||
adaptive_mesh.vertices[i].n_infinity[0] = 1.0;
|
||||
}
|
||||
if (frame_lens_mesh_prepare_generation(&adaptive_mesh, &refine) != 1) {
|
||||
@@ -534,7 +681,7 @@ int main(void) {
|
||||
}
|
||||
const RayEndpoint bent_probe = {.n_infinity = {0.0, 1.0, 0.0},
|
||||
.frequency_ratio = 1.0,
|
||||
.status = RAY_ENDPOINT_ESCAPED};
|
||||
.outcome = RAY_OUTCOME_ESCAPED};
|
||||
if (frame_lens_mesh_install_sample(&adaptive_mesh, 0, &bent_probe) ||
|
||||
frame_lens_mesh_finish_generation(&adaptive_mesh, &refine) != 1 ||
|
||||
adaptive_mesh.vertex_count != 5 || adaptive_mesh.triangle_count != 4) {
|
||||
@@ -550,10 +697,10 @@ int main(void) {
|
||||
adaptive_mesh.triangle_count = 1;
|
||||
for (size_t i = 0; i < adaptive_mesh.vertex_count; ++i) {
|
||||
adaptive_mesh.vertices[i].traced = 1;
|
||||
adaptive_mesh.vertices[i].status = RAY_ENDPOINT_ESCAPED;
|
||||
adaptive_mesh.vertices[i].outcome = RAY_OUTCOME_ESCAPED;
|
||||
adaptive_mesh.vertices[i].n_infinity[0] = 1.0;
|
||||
}
|
||||
adaptive_mesh.vertices[0].status = RAY_ENDPOINT_CAPTURED;
|
||||
adaptive_mesh.vertices[0].outcome = RAY_OUTCOME_DARK;
|
||||
refine.max_level = 1;
|
||||
refine.angle_absolute_rad = 3.14159265358979323846;
|
||||
refine.angle_relative = 1e6;
|
||||
@@ -584,11 +731,11 @@ int main(void) {
|
||||
adaptive_mesh.triangle_count = 3;
|
||||
for (size_t i = 0; i < adaptive_mesh.vertex_count; ++i) {
|
||||
adaptive_mesh.vertices[i].traced = 1;
|
||||
adaptive_mesh.vertices[i].status = RAY_ENDPOINT_ESCAPED;
|
||||
adaptive_mesh.vertices[i].outcome = RAY_OUTCOME_ESCAPED;
|
||||
adaptive_mesh.vertices[i].n_infinity[0] = 1.0;
|
||||
}
|
||||
adaptive_mesh.vertices[3].status = RAY_ENDPOINT_CAPTURED;
|
||||
adaptive_mesh.vertices[5].status = RAY_ENDPOINT_CAPTURED;
|
||||
adaptive_mesh.vertices[3].outcome = RAY_OUTCOME_DARK;
|
||||
adaptive_mesh.vertices[5].outcome = RAY_OUTCOME_DARK;
|
||||
if (frame_lens_mesh_prepare_generation(&adaptive_mesh, &refine) != 6) {
|
||||
fputs("shadow-boundary blue-neighbour probe setup regression failed\n", stderr);
|
||||
frame_lens_mesh_destroy(&adaptive_mesh);
|
||||
@@ -611,7 +758,7 @@ int main(void) {
|
||||
frame_lens_mesh_destroy(&adaptive_mesh);
|
||||
const RayEndpoint flat_probe = {.n_infinity = {1.0, 0.0, 0.0},
|
||||
.frequency_ratio = 1.0,
|
||||
.status = RAY_ENDPOINT_ESCAPED};
|
||||
.outcome = RAY_OUTCOME_ESCAPED};
|
||||
/* Opposite nonzero discrete-Jacobian signs on the two sides of the shared
|
||||
* diagonal require a sufficiently small magnitude before requesting it. */
|
||||
refine.jacobian_minimum = 10.0;
|
||||
@@ -626,7 +773,7 @@ int main(void) {
|
||||
{sqrt(0.98), 0.1, 0.1}};
|
||||
for (size_t i = 0; i < adaptive_mesh.vertex_count; ++i) {
|
||||
adaptive_mesh.vertices[i].traced = 1;
|
||||
adaptive_mesh.vertices[i].status = RAY_ENDPOINT_ESCAPED;
|
||||
adaptive_mesh.vertices[i].outcome = RAY_OUTCOME_ESCAPED;
|
||||
memcpy(adaptive_mesh.vertices[i].n_infinity, source_directions[i],
|
||||
sizeof source_directions[i]);
|
||||
}
|
||||
@@ -639,6 +786,126 @@ int main(void) {
|
||||
goto done;
|
||||
}
|
||||
frame_lens_mesh_destroy(&adaptive_mesh);
|
||||
/* E/D/U accounting. A UUU triangle must request one merged retry per
|
||||
* unresolved vertex with the next budget increment, and must not be
|
||||
* blackened. */
|
||||
{
|
||||
LensVertex uuu_vertices[3] = {
|
||||
{.image_x = 0, .image_y = 0, .outcome = RAY_OUTCOME_UNRESOLVED,
|
||||
.traced = 1, .continuation_t = -1.0, .continuation_steps = 5,
|
||||
.continuation_limit = 5},
|
||||
{.image_x = 10, .image_y = 0, .outcome = RAY_OUTCOME_UNRESOLVED,
|
||||
.traced = 1, .continuation_t = -1.0, .continuation_steps = 5,
|
||||
.continuation_limit = 5},
|
||||
{.image_x = 0, .image_y = 10, .outcome = RAY_OUTCOME_UNRESOLVED,
|
||||
.traced = 1, .continuation_t = -1.0, .continuation_steps = 5,
|
||||
.continuation_limit = 5}};
|
||||
for (int i = 0; i < 3; ++i)
|
||||
uuu_vertices[i].camera_direction[0] = 1.0;
|
||||
LensTriangle uuu_triangle = {{0, 1, 2}, 0, 0, 0};
|
||||
FrameLensMesh uuu_mesh = {.vertices = uuu_vertices,
|
||||
.vertex_count = 3,
|
||||
.triangles = &uuu_triangle,
|
||||
.triangle_count = 1};
|
||||
RefinementConfig uuu_config = {.max_level = 1,
|
||||
.angle_absolute_rad = 1.0,
|
||||
.angle_relative = 1.0,
|
||||
.jacobian_minimum = 1e-3,
|
||||
.min_edge_pixels = 1.0,
|
||||
.min_area_pixels2 = 1.0,
|
||||
.retry_step_increment = 10,
|
||||
.max_total_steps = 25};
|
||||
if (frame_lens_mesh_prepare_generation(&uuu_mesh, &uuu_config) != 3) {
|
||||
fputs("UUU forced-retry regression failed\n", stderr);
|
||||
free(uuu_mesh.samples);
|
||||
free(uuu_mesh.probe_slots);
|
||||
goto done;
|
||||
}
|
||||
for (size_t i = 0; i < uuu_mesh.sample_count; ++i) {
|
||||
if (uuu_mesh.samples[i].kind != FRAME_SAMPLE_RETRY ||
|
||||
uuu_mesh.samples[i].step_limit != 15) {
|
||||
fputs("UUU retry shape regression failed\n", stderr);
|
||||
free(uuu_mesh.samples);
|
||||
free(uuu_mesh.probe_slots);
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
free(uuu_mesh.samples);
|
||||
free(uuu_mesh.probe_slots);
|
||||
}
|
||||
/* UUD/UDD is red-refined while the geometry can still support children.
|
||||
* At the geometric stop scale it becomes an approximate-black boundary
|
||||
* triangle while its shared U vertex keeps its unresolved outcome. */
|
||||
{
|
||||
LensVertex ud_vertices[3] = {
|
||||
{.image_x = 0, .image_y = 0, .outcome = RAY_OUTCOME_UNRESOLVED,
|
||||
.traced = 1, .continuation_limit = 5},
|
||||
{.image_x = 10, .image_y = 0, .outcome = RAY_OUTCOME_UNRESOLVED,
|
||||
.traced = 1, .continuation_limit = 5},
|
||||
{.image_x = 0, .image_y = 10, .outcome = RAY_OUTCOME_DARK,
|
||||
.traced = 1}};
|
||||
for (int i = 0; i < 3; ++i)
|
||||
ud_vertices[i].camera_direction[0] = 1.0;
|
||||
LensTriangle ud_triangle = {{0, 1, 2}, 0, 0, 0};
|
||||
FrameLensMesh ud_mesh = {.vertices = ud_vertices,
|
||||
.vertex_count = 3,
|
||||
.triangles = &ud_triangle,
|
||||
.triangle_count = 1};
|
||||
RefinementConfig red_config = {.max_level = 1,
|
||||
.angle_absolute_rad = 1.0,
|
||||
.angle_relative = 1.0,
|
||||
.jacobian_minimum = 1e-3,
|
||||
.min_edge_pixels = 0.5,
|
||||
.min_area_pixels2 = 0.5,
|
||||
.retry_step_increment = 10,
|
||||
.max_total_steps = 25};
|
||||
if (frame_lens_mesh_prepare_generation(&ud_mesh, &red_config) != 3) {
|
||||
fputs("UUD red-refinement probe regression failed\n", stderr);
|
||||
free(ud_mesh.probe_slots);
|
||||
goto done;
|
||||
}
|
||||
for (size_t i = 0; i < ud_mesh.sample_count; ++i)
|
||||
if (ud_mesh.samples[i].kind != FRAME_SAMPLE_PROBE) {
|
||||
fputs("UUD red-refinement sample-kind regression failed\n", stderr);
|
||||
free(ud_mesh.probe_slots);
|
||||
goto done;
|
||||
}
|
||||
free(ud_mesh.samples);
|
||||
free(ud_mesh.probe_slots);
|
||||
ud_mesh.samples = NULL;
|
||||
ud_mesh.sample_count = ud_mesh.sample_capacity = 0;
|
||||
ud_mesh.probe_slots = NULL;
|
||||
ud_mesh.probe_slot_capacity = 0;
|
||||
RefinementConfig stop_config = red_config;
|
||||
stop_config.min_edge_pixels = 1e6;
|
||||
stop_config.min_area_pixels2 = 1e6;
|
||||
if (frame_lens_mesh_prepare_generation(&ud_mesh, &stop_config) != 0) {
|
||||
fputs("UUD stop-scale retry regression failed\n", stderr);
|
||||
free(ud_mesh.probe_slots);
|
||||
goto done;
|
||||
}
|
||||
FrameBoundaryStats ud_stats;
|
||||
frame_lens_mesh_boundary_stats(&ud_mesh, &stop_config, &ud_stats);
|
||||
if (ud_stats.uud_udd != 1 || ud_stats.approx_black_triangles != 1 ||
|
||||
ud_stats.approx_black_area_pixels2 <= 0.0 ||
|
||||
ud_stats.escaped_only != 0 || !ud_triangle.approx_black ||
|
||||
ud_vertices[0].outcome != RAY_OUTCOME_UNRESOLVED) {
|
||||
fputs("UUD approximate-black regression failed\n", stderr);
|
||||
free(ud_mesh.probe_slots);
|
||||
goto done;
|
||||
}
|
||||
stop_config = red_config;
|
||||
stop_config.max_level = 0;
|
||||
frame_lens_mesh_boundary_stats(&ud_mesh, &stop_config, &ud_stats);
|
||||
if (ud_stats.approx_black_triangles != 1 ||
|
||||
ud_stats.approx_black_level_stops != 1 ||
|
||||
ud_stats.approx_black_max_edge_pixels < 10 ||
|
||||
ud_stats.approx_black_area_pixels2 != 50) {
|
||||
fputs("max-level approximate-black provenance regression failed\n",stderr);
|
||||
goto done;
|
||||
}
|
||||
free(ud_mesh.probe_slots);
|
||||
}
|
||||
result = 0;
|
||||
done:
|
||||
frame_lens_mesh_destroy(&mesh);
|
||||
|
||||
@@ -14,7 +14,7 @@ static int check_ray(const SpacetimeSource *source,
|
||||
.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]) ||
|
||||
@@ -64,7 +64,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;
|
||||
|
||||
@@ -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,7 +121,7 @@ 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. The
|
||||
* asymptotic exterior transfers the photon to infinity, where
|
||||
@@ -140,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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -54,12 +54,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
|
||||
@@ -108,11 +120,12 @@ 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.
|
||||
would source the bubble is treated as optically transparent and there is no
|
||||
horizon, so in practice rays are active or escaped: the bubble has no causal
|
||||
boundary at which `L - L0` can diverge, and the shared dark policy is not
|
||||
expected to trigger. 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 scope.
|
||||
|
||||
| Option | Meaning / default |
|
||||
| --- | --- |
|
||||
@@ -257,9 +270,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
|
||||
|
||||
Reference in new issue
Block a user