Files
GR-raytracing/nr_spacetime_movie_renderer_design.md
T
wyj e31e1ed27e Feat: Add soft-clip tone mapping with legacy Reinhard
Replace the per-channel Reinhard display transform with a parameterized
soft clip T_p(x) = tanh(x^p)^(1/p), default softclip p=2, exposed through
--tone-map and --tone-map-p. Keep --tone-map reinhard bit-compatible with
the previous x/(1+x) curve for existing images and reject combining it
with an explicit --tone-map-p.

Route the primary image and the mesh overlay through the same
ToneMapSettings; the linear HDR FITS writer stays pre-tone-map. Add a
focused optics-linked tone-map test target, CLI success/error coverage,
and document the display operator versus the Moffat effective PSF.
2026-09-27 00:44:19 -04:00

1615 lines
48 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Numerical-Relativity Spacetime Movie Renderer
## 1. Repo 目标
本项目的目标是开发一个**离线、尽可能物理正确的数值相对论时空视频渲染器**,最终用于渲染双黑洞(BBH)并合等动态数值时空的 4K 视频。
核心目标不是实时渲染,也不是构造视觉上“像双黑洞”的近似度规,而是:
- 直接消费数值相对论演化输出的真实 4D 时空;
- 对每一帧的相机光线做完整的 time-dependent backward null ray tracing;
- 支持真实巡天恒星数据作为无穷远天球背景;
- 正确处理恒星点源的多像、放大、频移和最终 PSF;
- 允许相机沿一般 4D worldline 运动,并使用预先生成的 tetrad/标架轨迹;
- 让平直时空、解析时空、数值时空在同一渲染框架中作为可替换 backend;
- 最终能够“看到”每次 NR 代码实际跑出来的时空,而不是只看 waveform 或标量诊断。
第一阶段不考虑物质辐射、吸积盘、流体、等离子体等局域发射源。每条 ray 的终点暂时只有两类:
1. 被黑洞捕获;
2. 到达无穷远天球。
---
## 2. 明确的非目标
当前 repo 不以以下内容为主要目标:
- 实时渲染;
- ShaderToy / 游戏式视觉近似;
- frozen-snapshot 作为最终物理方案;
- 直接渲染吸积盘、喷流或其他 matter emission;
- GPU 加速;
- 通过 OO/class hierarchy 构建复杂抽象层;
- 将恒星背景预烘焙为 RGB 天球纹理;
- 依赖单个固定坐标位置的“相机”。
可以保留 frozen-snapshot 模式作为 nmesh 内部的廉价在线 diagnostic,但它与本 repo 的最终 4D renderer 是不同层次的工具。
---
## 3. 语言与总体实现风格
首选 **C**。
C++ 并不是当前项目的必要条件。需要的抽象主要可以通过:
- `struct`
- 函数指针表
- 显式 ownership
- 大块连续数组
- thread-local workspace
完成。
性能敏感部分尽量采用:
- bulk array processing;
- SoA(structure of arrays);
- 避免 per-ray malloc;
- 避免大量虚调用/函数指针 dispatch;
- 避免细粒度锁;
- 避免每个 triangle/ray 建 task;
- 尽量顺序读取 metric 数据;
- OpenMP 做 CPU 并行。
核心实现原则:
> **bulk arrays + time-slab streaming + coarse-grained parallelism**
---
# 4. 总体物理与数据流
整个程序不是“逐帧、逐像素独立 trace”,而是:
1. 预先生成相机 worldline 与 tetrad 轨迹;
2. 建立所有视频帧的初始 image-plane adaptive mesh;
3. 从所有帧收集当前 refinement level 需要的新 ray samples;
4. 将这些 rays 组成一个全局 `RayPool`;
5. 从视频结束时刻向过去,按 time slab 顺序加载数值时空;
6. 在每个 slab 内,把所有 active rays 一起推进到 slab 左边界;
7. ray 若到达无穷远或进入黑洞,则立即终止;
8. 一轮 ray tracing 完成后,把 endpoint 数据回填到各帧 image mesh;
9. 根据局部 lens mapping 误差判断哪些 image-plane triangles 需要进一步细分;
10. 生成下一批新增 rays;
11. 重复若干 refinement passes;
12. 最终用局部 inverse lens maps 查询恒星 catalog;
13. 计算每个恒星像的位置、放大率和频移;
14. 将对应黑体 PSF 加到 HDR framebuffer;
15. tone mapping / 编码输出视频。
总结构:
```text
Observer Track
z^μ(t), e_(a)^μ(t)
│
▼
Movie / Frame Set
│
▼
Adaptive image-plane meshes
│
▼
Refinement analyzer
│
▼
new ray samples
│
▼
RayPool
│
▼
Spacetime time-slab stream
(newest → oldest)
│
▼
ray endpoint:
n∞, frequency shift, captured/escaped
│
├──────────► next refinement pass
│
▼
Final local lens maps
│
▼
Star catalog
(n∞, T, amplitude)
│
▼
inverse mapping + μ + g + PSF
│
▼
HDR video frames
```
---
# 5. 为什么必须按 time slab 组织,而不是按帧组织
最终 BBH 4D 数据很可能超过单机内存。
因此不能对每帧独立:
```text
for frame:
load all required 4D metric data
trace all rays
```
否则同一段时空会被反复从硬盘读取。
更合理的是:
```text
for time_slab = latest -> earliest:
load metric data for this slab
activate all rays whose camera time enters this slab
evolve every active ray backward through this slab
remove terminated rays
discard slab
```
这样:
- metric I/O 主要取决于 NR 数据量;
- 不随 4K 分辨率、帧数、supersampling 线性重复;
- 数据访问可以是大块顺序读;
- 适合本地 NVMe;
- 适合未来超过 128 GB 的数据集。
---
# 6. Ray 生命周期与跨帧整合
不应一开始生成整个视频所有 rays 并常驻内存。
随着倒序时间扫描,在经过某个 frame 的 `t_camera` 时才激活该帧当前 refinement pass 的新 rays。
因此 active-ray pool 大小由以下因素决定:
```text
单位相机时间产生的 ray 数
×
ray 在 NR domain 内的典型 coordinate-time 停留长度
```
而不是整个视频总 ray 数。
对于长时间停留在 strong-field region 的 rays,可以一直跨多个 slabs 保留。
---
# 7. Adaptive image-plane mesh
最终点源渲染不适合简单的“每 pixel backward lookup sky color”。
恒星在无穷远天球上是点源:
\[
I(\hat n) \sim \sum_s F_s \delta(\hat n-\hat n_s)
\]
因此需要先通过 backward ray tracing 得到局部 lens mapping:
\[
F:\ (x,y)_\text{image} \to \hat n_\infty
\]
然后在每个局部可逆 patch 上建立 inverse:
\[
F^{-1}:\ \hat n_\infty \to (x,y)_\text{image}
\]
### 基本单元:triangle
每个 image-plane triangle 的三个顶点都保存:
- image-plane 坐标 `(x,y)`;
- ray 是否 escaped/captured;
- 若 escaped:无穷远方向 `n_inf`;
- frequency shift / redshift accumulator。
示意:
```c
typedef struct {
double x, y;
double n_inf[3];
double log_g;
uint8_t ray_status;
} LensVertex;
typedef struct {
uint32_t v[3];
uint8_t level;
uint8_t flags;
} LensTriangle;
```
---
# 8. 局部 adaptive refinement
局部 inverse map 的球面面积权重必须满足凸组合约束。当前实现以无符号
子面积除以总面积得到权重,在插值位置和频移前检查权重和有限、为正,且
`abs(sum(weights) - 1) <= 1e-8`;不满足时只跳过当前恒星在当前 triangle
中的像。通过检查后,将权重除以其总和,消除小的归一化误差。整 tile
包含的 catalog 快速路径也必须执行这项检查。
这是反演有效性保护,不是 refinement 阈值:细长 source triangle 的带容差
边测试可能误接纳外部点,无符号子面积之和便大于总面积,直接插值会把像
放到 image triangle 外。不能先归一化明显无效的权重来掩盖误接纳。
固定的无量纲容差 `1e-8` 依据 Schwarzschild 3840×2160、45° FOV、R=100、
coarse cell=32、level=4、J=0.2 网格上的诊断选定:正对中心及偏离
0.00161°、方位为 1°/44°/23° 的单星测试中,严格包含样本的 double
权重和误差不超过约 `2.5e-13`,正对中心的越界样本最小误差约 `5.9e-4`。
这不保证临界曲线附近的离散映射已经收敛,也不保证数值亮环无缺口。
每个 triangle 只关心自身局部映射。
不需要先把整个像平面映射到天球、再全局分类“第几阶像”。
对一个 triangle:
1. 已有三个 corner rays;
2. 计算或已有 edge midpoint / center rays;
3. 比较真实 mapping 与低阶插值;
4. 检查 orientation consistency;
5. 检查 Jacobian 是否接近奇异;
6. 若误差过大则 subdivide。
这意味着:
- Schwarzschild 下无需显式按 winding number 分类;
- BBH 下也不需要定义全局“第几阶像”;
- 每个局部 patch 只需保持局部单值可逆;
- 多像会自然表现为同一个 source direction 被多个 image patches 覆盖。
---
# 9. 为什么 refinement 需要多 pass
adaptive refinement 与 out-of-core time streaming 存在天然冲突。
只有当一条 ray 已经完整追到无穷远后,才知道某个 triangle 是否需要细分。
但此时早期 metric slab 已经释放。
因此不能在单次 time sweep 中随时创建新 ray 并从相机时刻重新追。
采用:
```text
Pass 0:
trace coarse samples through whole spacetime
analyze maps
Pass 1:
trace only newly requested midpoint/center rays
analyze maps
Pass 2:
trace only newly requested rays
...
until converged
```
每个 pass:
- metric data 仍然只顺序扫描;
- 只追踪新增样本;
- refinement 通常若干轮即可完成。
---
# 10. 相机不是固定坐标点
相机不应由:
```c
double x[3];
```
定义。
否则结果强依赖某个 NR 坐标系中的“固定点”。
应由独立 trajectory generator 预先生成:
\[
z^\mu(\tau), \qquad e_{(a)}{}^\mu(\tau)
\]
其中:
- \(z^\mu\):observer worldline;
- \(e_{(0)}{}^\mu=u^\mu\):observer 四速度;
- \(e_{(i)}{}^\mu\):相机空间 tetrad。
trajectory generator 可以采用不同物理规则:
- timelike geodesic;
- Fermi-Walker transport;
- parallel transport;
- 人为给定加速度;
- 人为叠加 camera pointing/rotation。
电影 renderer 本身只读取轨迹;单张入口可以用下述参数直接构造一个事件处的完整 tetrad。
示意:
```c
typedef struct {
double t;
double tau;
double x[3];
double tetrad[4][4];
} ObserverSample;
typedef struct {
ObserverSample *samples;
size_t n;
} ObserverTrack;
```
每帧根据 `t_camera` 插值得到完整 observer state。
## 10.1 单张的瞬时相机
单张与电影共享 `ObserverState` 和 ray 初始化。单张不是只指定三维位置:
由事件、坐标速度、指向与 roll 生成完整四速度和 tetrad。
目前解析单张事件取 $t=0$;此构造不要求静态时空或静止观测者。
`--observer-position X Y Z` 与 `--look-ra-deg` / `--look-dec-deg` 独立。
只有位置时取朝原点的坐标方向;只有指向时令 $\mathbf x=-R\mathbf d$。
`--observer-radius R` 仅用于缺省位置的推导(默认 30),与显式位置互斥。
几何参数全部省略时保留 Minkowski 原点、Schwarzschild 半径 30 的默认相机。
原点无法推导指向,要求显式 look;由位置推导方向且在极轴上时固定 RA=0。
RA 或 Dec 只给一个时,另一个取默认值(90°、−90°)。
`--observer-velocity VX VY VZ` 表示 $v^i=dx^i/dt$,默认零。
由事件处的 $\alpha,\beta^i,\gamma_{ij}$ 计算
$$
Q=-\alpha^2+\gamma_{ij}(v^i+\beta^i)(v^j+\beta^j),\qquad
u^\mu=\frac{(1,v^i)}{\sqrt{-Q}}.
$$
要求 $Q<0$,未来指向选择 $u^t>0$。不能用欧氏 $|\mathbf v|<1$ 或
$r>2M$ 代替类时性检查;删除旧的 `--observer-inward-speed` 参数。
构造失败直接报错,不修改速度或退回静止相机。
采用标准 ICRS 坐标种子
$$
\mathbf d=(\cos\delta\cos\alpha_{\rm RA},\cos\delta\sin\alpha_{\rm RA},\sin\delta),
$$
将 $D^\mu=(0,\mathbf d)$ 投影为
$F^\mu=D^\mu+u^\mu(u_\nu D^\nu)$,归一化得到 forward。
随后把天球北向和西向的坐标种子投影、正交化为 up、right,保留标准画面定向。
因此 look 表示坐标方向在相机静止空间内的投影,不保证特定远方物体位于画面中心。
`--camera-roll-deg` 默认零,正角按
$e'_2=\cos\rho\,e_2+\sin\rho\,e_3$、
$e'_3=-\sin\rho\,e_2+\cos\rho\,e_3$ 定义。
observer 构造只接收当地 metric 和已补全的参数,不加载 slab、不分类 ray。
调用方在昂贵的 catalog/PSF 初始化前验证相机及 backend 数据域。
现有 Schwarzschild cutoff 为 $r=1.5M$;相机必须在 cutoff 外,但允许在视界内。
此功能不改变捕获 cutoff 或向过去追踪的高红移终止条件。
单张相机参数与轨迹输入、lens-map 导入互斥;导入仍跳过 metric 与 observer 初始化。
验证包括 tetrad 正交归一和 null 初始化、平直时空平移不变性与解析光行差/多普勒、
KS 视界处和视界内的向过去径向逃逸及频移,以及相同 observer 的单张/电影单帧一致性。
---
# 11. Ray 初始化
给定相机 film coordinate `(x,y)`:
1. camera projection 得到 observer local frame 中的 photon direction;
2. 与 observer tetrad 相乘得到 spacetime 中的 \(k^\mu\);
3. 转换到 geodesic integrator 所使用的 3+1 ray variables。
例如:
\[
k^\mu = e_{(a)}{}^\mu k^{(a)}
\]
film model 只负责:
\[
(x,y) \to \hat n^{(i)}_\text{camera}
\]
它与 gauge、metric backend、observer worldline 都解耦。
---
# 12. Ray 状态
采用 coordinate time \(t\) 作为 ODE 自变量,以便天然和 time slabs 对齐。
单 ray 概念结构:
```c
typedef struct {
double t;
double x[3];
double Pi[3];
double log_E;
double h;
uint32_t sample_id;
uint32_t frame_id;
uint8_t status;
} Ray;
```
真正批量计算时建议 SoA:
```c
typedef struct {
double *restrict t;
double *restrict x0;
double *restrict x1;
double *restrict x2;
double *restrict p0;
double *restrict p1;
double *restrict p2;
double *restrict log_E;
double *restrict h;
uint32_t *sample_id;
uint32_t *frame_id;
uint8_t *status;
size_t n;
size_t capacity;
} RayPool;
```
目的:
- cache locality;
- SIMD;
- OpenMP;
- stream compaction;
- 避免 per-ray allocation。
---
# 13. Spacetime backend
所有上层渲染逻辑都不应该知道 metric 来自哪里。
需要支持至少:
- Minkowski;
- analytic Schwarzschild;
- 未来可加 analytic Kerr;
- numerical nmesh 4D spacetime。
核心不是“单点 metric 对象”,而是**当前已加载 time slab 的 evaluator**。
概念接口:
```c
typedef struct SpacetimeSource SpacetimeSource;
typedef struct MetricSlab MetricSlab;
typedef struct {
int (*load_slab)(
SpacetimeSource *src,
double t_hi,
size_t memory_budget,
MetricSlab **out);
void (*free_slab)(MetricSlab *slab);
int (*eval)(
const MetricSlab *slab,
double t,
const double x[3],
void *workspace,
MetricData *out);
int (*classify)(
const MetricSlab *slab,
double t,
const double x[3]);
} SpacetimeOps;
```
解析 backend 的 `load_slab()` 可以近似 no-op。
nmesh backend 则真正加载一段时间的 3D outputs。
---
# 14. 统一的 3+1 metric 数据
初步采用:
\[
\alpha,\quad
\beta^i,\quad
\gamma_{ij},\quad
K_{ij}
\]
作为主要 3+1 变量。
空间导数:
\[
\partial_i \alpha,\quad
\partial_i\beta^j,\quad
\partial_i\gamma_{jk}
\]
由 backend evaluator 提供。
示意:
```c
typedef struct {
double alpha;
double beta[3];
double gamma[6];
double K[6];
double d_alpha[3];
double d_beta[3][3];
double d_gamma[3][6];
} MetricData;
```
---
# 15. 时间导数
时间导数不能简单假设全部从文件直接输出。
应按量的性质选择来源。
例如:
\[
\partial_t \gamma_{ij}
=
-2\alpha K_{ij}
+
\mathcal L_\beta \gamma_{ij}
\]
可以直接由 3+1 内部量构造。
对于 lapse/shift:
- 若 gauge evolution equations 已知,可从 gauge RHS 构造;
- 或从 temporal interpolant 解析求导。
原则:
> 若已经建立 \(u(t,\mathbf x)\) 的时间插值,就从同一个 interpolation polynomial 解析求 \(\partial_tu\),而不是另做 finite difference。
且不要每个 geodesic RHS 都计算用不到的时间导数。
---
# 16. nmesh 数值时空 backend
nmesh 当前 3D 输出格式已经非常适合 renderer:
对每个需要输出的 timestep:
- 每个 AMR leaf node 一个文件;
- 文件名按 node 的树状 refinement 命名;
- 从文件名可恢复 refinement tree;
- 文件内保存每个 nodal point 的 global coordinates;
- 保存所选物理量。
nmesh:
- DG;
- node 内用 local coordinates 上 Legendre nodes 对应的 Lagrange basis;
- production 中每边一般最多约 12 个 nodal points;
- AMR 为 h-refinement;
- 典型网格为中心六面体 + 外层 cubic-sphere shells。
因此读入一个 node 文件后,可以直接构造 node 内:
\[
u(\xi,\eta,\zeta)
=
\sum_{ijk}
u_{ijk}
L_i(\xi)L_j(\eta)L_k(\zeta)
\]
空间导数直接对 Lagrange polynomial 求导。
不需要:
- resample 到 Cartesian uniform grid;
- 额外输出 derivative fields。
---
# 17. nmesh time interpolation
h-AMR 可能使相邻 timestep 的树结构不同。
但不要求时间方向上 node 一一对应。
对任意固定 global point \(\mathbf x\):
\[
u_n(\mathbf x)
=
I_{\mathcal M_n}[u](\mathbf x)
\]
\[
u_{n+1}(\mathbf x)
=
I_{\mathcal M_{n+1}}[u](\mathbf x)
\]
先分别在各自空间 mesh 上求值,再做:
\[
u(t,\mathbf x)
=
T[u_n(\mathbf x),u_{n+1}(\mathbf x),\ldots]
\]
因此:
- 每个 time slice 可独立恢复空间函数;
- AMR topology 不需要跨时间匹配;
- time slab 只负责保存足够多邻近 slices 供时间插值。
slab 边界需要少量 overlapping temporal ghost slices。
---
# 18. 黑洞捕获判据
目标使用 moving-puncture BBH,而不是 excision。
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 做判断。
形式:
\[
|\mathbf x-\mathbf x_p(t)|<r_\text{cut}
\Rightarrow \text{captured}
\]
未来 BBH merger 后若仅用两个 puncture-centered 小球导致大量 doomed rays 继续积分,可以再加入 common-horizon-derived termination 优化。
---
# 19. 恒星 catalog 的内部表示
不要预存 RGB。
恒星应保留:
- 无穷远天球方向;
- 温度 \(T\);
- 辐射 normalization / amplitude。
概念结构:
```c
typedef struct {
float n[3];
float temperature;
float amplitude;
} Star;
```
原因:
ray tracing 会给每个 image 一个频率比:
\[
g=
\frac{\nu_\text{obs}}
{\nu_\text{emit}}
\]
对于黑体谱:
\[
T_\text{obs}=gT_\text{emit}
\]
因此温度是更自然的源数据。
最终根据:
- intrinsic temperature;
- frequency shift \(g\);
- lensing magnification \(\mu\);
- source amplitude;
得到 observed blackbody spectrum,再转换为 linear RGB。
---
# 20. 点源渲染
不将恒星 catalog 插值为天球亮度纹理。
最终 frame mesh 已知局部:
\[
(x,y)
\to
\hat n_\infty
\]
对每个局部可逆 triangle,反过来得到:
\[
\hat n_\infty
\to
(x,y)
\]
并查询 source triangle 内的 catalog stars。
### 固定 2MASS tile 的基准结论(2026-08-27)
在 `5d80bb1` 上,以 54,160-source 的 2MASS `1 deg` tile、完全位于该 tile
内的 `0.6 deg` Minkowski 视场、1920 x 1920 输出和 **6 个 catalog render
worker** 测得:每个 source triangle 扫描完整 tile 的有效墙钟成本约为
`105.9 us`,并且 wall time 对 triangle 数的线性拟合为
`11.855 s + 105.88 us * T`(`R^2 = 0.99955`)。完整记录、命令和原始时间见
[`benchmarks/2mass_fixed_tile_triangle_scan_2026-08-27.md`](benchmarks/2mass_fixed_tile_triangle_scan_2026-08-27.md)。
这说明局部稠密 tile 的全扫本身可以接受;catalog spatial index 的首要职责是
让每个 source triangle 只枚举相交的固定 1-degree tile,而不是扫描全天。不要
仅为把叶子降至数百颗星而预先激进细分;是否需要对极稠密 tile 再做浅层细分,
应由实际 Schwarzschild/BBH source-triangle 覆盖和负载 profile 决定。
每颗星获得:
- image-plane 连续位置 `(x,y)`;
- frequency shift \(g\);
- magnification \(\mu\)。
然后:
\[
T_\text{obs}=gT
\]
最终用一个 PSF:
\[
P(x-x_s,y-y_s)
\]
直接 splat 到 HDR framebuffer。
不要先把星压进最终 pixel 再卷积,因为那会丢失亚像素位置。
`--fast-mode` 是显式预览近似:把每个星像作为 delta 累积进 `N×N` 超采样
buffer(`--fast-deposit nearest` 只写 1 个超采样像素,`bilinear` 写 4 个
相邻像素),最后对整张 buffer 用单个全局离散核做一次卷积,再以 `N×N`
box 平均降采样。核取目标 Moffat 在超采样尺度(宽度 `Nα`、同一 `beta`)下
的 pixel-area 积分且不做额外归一化;`1/N²` 的 box 平均正好重建最终
pixel-area 积分,所以核本身不改变指定的 FWHM/beta。`nearest` 在吸附到的
超采样格点上精确保持 PSF 形状,代价是至多 `1/(2N)` 个输出像素的位置量化;
`bilinear` 精确保持亚像素质心,但会略微展宽 FWHM,因此默认是 `nearest`。
沉积误差测量、`N` 与取舍见
[`benchmarks/fast_mode_deposit_2026-09-18.md`](benchmarks/fast_mode_deposit_2026-09-18.md)。
该模式只支持 CPU PSF 后端;`--max-cache-psf-flux` 不适用(所有事件共用同一
个全局核半径)。CPU 构建中该全局卷积实现为零填充的 double 精度 FFTW 线性
卷积(`fftw3`/`fftw3_omp`):核频谱、FFT plans 与 planar scratch 在初始化时
一次性建立并跨帧复用,`resolve()` 只做 zero/pack、批量 R2C、频域乘、批量
C2R,以及在 `(R,R)` 处的裁剪、`1/(Pwidth·Pheight)` 与 `1/N²` 归一化和
`+=` 到 HDR。旧的嵌套空间循环作为测试/基准参考保留,不是运行时 fallback。
依赖与 plan 模式取舍见 `build.md` 与
[`benchmarks/fast_mode_fftw_2026-09-25.md`](benchmarks/fast_mode_fftw_2026-09-25.md)。
fast mode 的单星精度由 deposit 模式与 `N` 决定:`nearest` 的格点间距是
每轴 `1/N` 个输出像素,单帧瞬时舍入误差至多是 `1/(2N)`;在
`--psf-fwhm-pixels` 不变时它与图像分辨率无关,只有增大 `N`(或使用保持
质心、但会展宽 profile 的 `bilinear`)才会降低。
但单星偏移不等于一张稠密静态图的整图误差。生产密度 4K 银心单帧对照实测
fast 对标准路径的 HDR relative L2 为 `N=2` 时 `10.44%`、`N=4` 时 `4.37%`,
线性 RGB 总和偏差 `+0.018%`。把 FITS 三通道展平后,按 `|fast-base|` 排序
的前 `0.01%` RGB channel samples 占平方误差能量的 `95.7%`(`N=2`)或
`94.5%`(`N=4`),其 baseline linear-RGB 总和约占 `9%`。不重叠的 base-value
分区中,承载 flux 的暗值区间(`[1e-3,0.06)`、`[0.06,0.1)`、`[0.1,0.23)`,
合计约 `47%` 的 linear-RGB 总和)给出约 `2-5%` local relative RMS 与不高于
`0.19%` 的 signed bias。这一分布与大量暗星 PSF 的位置误差部分相消、已分辨
亮星核心保留亚像素位移的解释一致。因此只能说这次高密度 4K 单帧的银河纹理
明显好于全局 L2 数字所暗示,不能据此外推时间连续性或逐像素精度。
对 movie,平滑移动的星像跨越 nearest 格点边界时会跳变完整的 `1/N` 输出
像素:`N=2` 每轴 `0.5 px`、`N=4` 每轴 `0.25 px`。即使在 4K,这种不连续
跳变也可能形成可见闪烁,因此 nearest fast mode 尚未获得保持视频视觉特征的
资格。`bilinear` 可消除这一质心跳变,但存在上述 profile 展宽,也尚未作为
最终 movie 路径验收。定位上仍是近似路径:对单帧稠密星场,`N=2` 是吞吐
默认、`N=4` 是更高保真预览;per-event 参考 PSF 路径仍是定量与 movie 基准。
单帧误差分解可用
[`scripts/fast_mode_error_decomposition.py`](scripts/fast_mode_error_decomposition.py)
复现。
PSF 第一版可用 Gaussian;
以后可换成 Airy 或其他相机模型。
---
# 21. Lens magnification
局部 mapping:
\[
(x,y)
\leftrightarrow
\hat n_\infty
\]
自然给出 Jacobian。
点源每阶像的总 flux 应乘:
\[
\mu
=
\left|
\frac{d\Omega_\text{image}}
{d\Omega_\text{source}}
\right|
\]
可由:
- triangle area ratio;
- 或局部 interpolation Jacobian;
计算。
critical curve / caustic 附近需要更多 adaptive refinement。
---
# 22. 并行策略
## 22.1 ray evolution
使用 OpenMP 对当前 active ray pool 做 coarse-grained parallel loop。
优先:
```c
#pragma omp parallel for schedule(static, CHUNK)
for (...) {
evolve_ray_through_current_slab(...);
}
```
若 strong-field rays 工作量差异明显,再测试:
```c
schedule(dynamic, CHUNK)
```
当前 movie `RayPool` 使用 `schedule(dynamic, 32)`:samples 按 frame
排列,连续静态分块会把同一 time slab 的 active rays 集中到少数 worker。
32 条一块的动态分配让空闲 worker 继续领取工作,也允许不同光线的积分开销
不一致。每条 ray 仍由一个 worker 推进,pool slot 与 frame/sample ID 不移动;
没有新增共享可变缓存或 per-ray allocation。
这一 chunk 大小来自 [393 帧 Schwarzschild 对照实验](benchmarks/ray_pool_scheduling_2026-09-06.md),
并非所有 backend 的最优参数。实验同时比较连续静态、轮转静态和动态分块,
用完整 lens-map 文件校验调度前后的结果一致性。当前仍遍历整个 pool;
active-index compaction、activation 和 mesh analysis 的并行化属于后续优化。
不要:
- 一 ray 一个 OpenMP task;
- 每 ray malloc;
- 细粒度 mutex。
---
## 22.2 adaptive mesh analysis
recursive refinement 逻辑用**迭代 work queue**实现,不用真正的递归 task。
概念:
```c
while (!queue_empty()) {
tri = queue_pop();
if (triangle_is_good(tri)) {
accept(tri);
} else {
request_missing_samples(tri);
}
}
```
并行粒度应是:
```text
(frame, root_tile)
```
或至少按 frame。
每线程使用自己的 local queue 和 local request buffer。
最后 merge:
- sort;
- deduplicate;
- assign vertex IDs;
- bulk append。
避免多个 threads 同时修改一个 global mesh。
---
## 22.3 可选 HIP PSF 后端
CPU reference 验证后,HIP 只替换最终 PSF 累积;catalog/source-triangle 查询、
inverse mapping、频移和黑体积分仍由 OpenMP worker 按 triangle 粗粒度并行完成。
每 worker 独占最多 16,384 项的 event chunk,不创建 per-star task。GPU sink 独占
一份 immutable cache 和 double RGB HDR;仅在 chunk 提交和 direct fallback 边界
使用共享锁。direct fallback 的完成 GPU 工作、下载、CPU 累积、上传必须在同一锁内。
每个 PSF 用 32 个线程协作访问相邻像素,仍使用原来的四点 phase cache 插值、圆形
support 和 double atomic 累积。两个 pinned host/device staging slot 只有在先前
kernel 完成后才能复用;所有 GPU 工作保持单 stream 顺序,不要求整个帧事件驻留内存。
计时 event 随 slot 复用,覆盖所有 batch,不再只测前 64 批。
以上参数由 [RX 9070 有界实验](benchmarks/hip_psf_2026-09-07.md) 支持,尚不是跨设备
最优值。该实验未运行完整全天渲染,不能用缩小样本直接宣称整帧加速比。分桶规约、
降低 HDR 精度或近似黑体积分均未作为此次优化的一部分。
2026-09-07 后续实验提供了独立的有界事件捕获/回放与 producer 诊断工具,详见
[回放记录](benchmarks/hip_psf_replay_2026-09-07.md)。诊断钩子仅编入测试程序,
不进入 renderer;输入限制为小型 CSV、指定 triangle 范围与事件上限,按既有
catalog/inverse-map/频移/黑体/cache/direct/min-Y 路径准备事件,不能输出伪 HDR。
索引诊断将有限子集预载进现有一度 tile 数据结构,在 worker 启动前完成,保留只读查询。
实验回放与 production 32-thread/event kernel 并存,比较 16×16 与 32×32 像素归约。
CPU 按完整、经 cache 限制的 support 矩形建立 tile 引用;GPU 仍按原来的圆形行边界
过滤贡献。phase/行边界在每个 chunk 内预计算并复用;每个像素线程使用 double RGB。
单列表 tile 独占写回 HDR;超过 256 项的列表拆成部分和,随后按固定 task 顺序合并。
引用上限 32 MiB、task 上限 2 MiB、部分和上限 128 MiB;缓冲由回放 sink 独占,
单 stream 顺序复用,超限明确失败。wing-clipped 事件保留原物理 support,不能在读取时
强制截短;只有 cache 遍历范围受限。direct 边界完整执行 finish/download/CPU/upload。
上述参数是实验配置,不是新的生产默认值。是否集成必须看包含初始化、分桶、预计算、
合并、清理和下载的完整成本及分布依赖,不能只引用无 atomic kernel 的单项耗时。
本轮生产 HIP 路径保持原实现,尚未进行新的完整全天渲染。
2026-09-11 的后续有界实验加入了按 chunk 的分布选择器:先统计 32px 中心 tile 占用,
再在高中心密度 chunk 使用 16×16 像素归约,其余保留 atomic。RX 9070 的配对回放中,
它在银心密集输入接近固定 tile16,同时在同贡献分散输入选择 atomic,详情见上述回放记录。
此选择器仍仅存在于测试程序;阈值来自固定样本,尚未验证 production producer/sink 锁竞争
或完整全天事件序列,因此不能成为 renderer 默认策略。
2026-09-11 又加入了 `PSF_BACKEND=dummy` 的只统计后端。它要求导入 `.grlens`,复用
production 的 OpenMP `schedule(dynamic,1)`、每 worker 16,384-event chunk,以及完整的
catalog/inverse-map/颜色/flux/cache 分类,但不分配完整 HDR,也绝不写 PNG/FITS。对银心
45 度 Schwarzschild 全天输入的一次 16-worker 全量运行中,598,264,924 个 cache event
形成 36,508 个满 chunk;满 chunk 跨越的 event-producing triangle 数为 p50=2、p75=3、
p90=10、p99=31、max=59,32px 中心 tile 占用为 p50=1、p75=2、p90=5、p99=30、
max=45。全部满 chunk 都满足当前测试选择器的 tile16 条件。
这个结果支持“真实宽视场下一个 worker 的满 chunk 通常集中在少数三角形/少数局部
tile”这一调度假设,但也揭示了稀有的远距离簇:满 chunk 的三角形质心包围盒对角线
p99 约 3744px,最大相邻三角形跳跃 p99 也约 3744px。因而不能把“少数三角形”等同于
“全 chunk 单个连续矩形”;production tile 原型仍应按稀疏 occupied-tile 列表处理,并用
真实流式端到端数据验证分桶成本。完整命令、hash、输出保护和原始记录见
[dummy chunk 统计](benchmarks/dummy_psf_chunks_2026-09-11/README.md)。
2026-09-12 在 production HIP sink 中加入了显式可选的 adaptive tile16 路径。默认仍为
原来的 atomic;设置 `HIP_PSF_ACCUMULATION=adaptive` 后,每个至少 8,192-event 的 chunk
先统计 32px 中心 tile,占用密度达到每 tile 32 events 时才使用 tile16,否则继续 atomic。
tile 路径按完整 cache-limited support 构建稀疏 touched-tile 引用,让一个线程拥有一个像素,
在寄存器中完成 double RGB 累积后写回 HDR。超过 256 项的 tile 列表使用有界部分和;
references、tasks、partials 的 device 上限分别为 32 MiB、2 MiB、128 MiB,任一容量不足时
该 chunk 明确回退 atomic。event、metadata 和 partial 缓冲由 sink 持有并复用;所有工作仍在
同一 stream 中保持顺序,direct fallback 仍使用完整 download/CPU/upload 边界。
RX 9070 上,固定 65,536-event 银心真实输入连续回放 128 轮时,production atomic/adaptive
完整 replay wall 为 7.164/4.631 s(1.55×),其中 GPU 阶段为 7.132/3.875 s;adaptive 另含
0.607 s CPU 分桶和 0.091 s 上传。同贡献分散输入会选择 atomic。三 tile renderer 小样中
adaptive 选择 14 个 tile chunk、2 个 atomic chunk,splat wall 为 0.236 s,对照 atomic 为
0.285 s;两张 PNG 的 SHA-256 相同。这些有界结果随后被完整全天测试否定:相同
598,264,924 events 和 36,530 batches 下,atomic/adaptive 的 splat wall 为
480.500/586.763 s,process wall 为 533.62/640.35 s。tile kernel 虽从 477.302 s 降至
416.265 s,但 support 分桶串行花费 155.302 s,且 36,515 batches 被中心密度规则选为
tile16。中心位置的 32px tile 占用不能代表 47px PSF support 扩张后的 references、tasks、
列表长度和成本;固定事件样本也没有代表全天 tile kernel 的工作量分布。atomic 继续作为
默认值;上述旧串行 adaptive 是历史负结果,不能作为新版并行预分桶的结论。当时提出度量完整
support 工作量,并先证明分桶可并行隐藏或移至 GPU,再考虑完整全天复测。原始日志和误差检查见
[HIP 回放记录](benchmarks/hip_psf_replay_2026-09-07.md)。
随后实现的 producer 私有并行预分桶原型保留 GPU HDR/stream 的单 sink 所有权,且 mixed
direct fallback 与小型 renderer 的图像正确性通过。固定银心事件 512-batch 回放中,sink 内
串行分桶和 16 worker 预分桶的完整 wall 分别为 3.886/3.958 s,后者没有收益;此输入又明显
低估全天真实的分桶工作,因此当时证据不足以推荐 adaptive。新诊断记录完整 support
生成的 references、tasks 和 merges。下一步需先获取真实全天 chunk 的这些指标及并行阶段
wall,而不是继续调中心密度阈值或直接重复昂贵的全天 HIP 渲染。
`DUMMY_PSF_SUPPORT_STATS=1` 可在无 HDR/GPU 的相同 16-worker chunk 流上报告这些指标的
分位数及第一遍 support 计数耗时;该耗时不包含 references 写入、task 构建或上传。
完整全天诊断的 36,508 个满 chunk 中,16px support references 的 p50/p90 为
746,749/802,816,touched tiles 的 p50/p90 为 49/168,最大 tile 列表的 p50 已达整批
16,384 个事件;全部 chunk 第一遍计数 worker 时间之和为 29.347 s。旧固定银心回放的
references 总量相近,但展开到更多 tile、最大列表更短。即使构造恰好 49 tiles、最大列表
16,384 的反事实热点回放,tile GPU 阶段仍约 7.3 ms/batch,远低于真实全天均值
11.40 ms/batch;15 个忙等 CPU 线程也未复现该差距。真实全天进度显示中段连续数万批
约 12–13 ms/batch、首尾约 7–8 ms/batch,因此尚不能把性能损失归因于某一个聚合几何量
或单纯主机抢占。当时提出采集旧版中段真实 chunk;该追溯现已搁置,不作为新版推进前提。
2026-09-13 用户重新构建并行预分桶 adaptive 后完成全天渲染:splat 282.944 s、GPU event
阶段 256.184375 s、完整进程 335.75 s。stash 回 `2d01c2c` 重编译后的原 atomic 为
481.417/478.336515/534.37 s,与历史 atomic 相差约 0.2%;该 HEAD 忽略 adaptive 环境变量。
相同 598,264,924 events 下新版 splat 快约 1.70×,但新版全天 HDR 尚未在本次文档工作中
比较。新版 bin 134.374 s 是 worker 累加时间,不能作为串行成本;旧 adaptive GPU 阶段
416.265 s 到新版 256.184 s 的差异仍未定位。**保留历史证据,搁置旧 adaptive 专项追溯,
后续以新版为优化基础;atomic 默认和参考路径本次不变。**
下一步目标是相同物理/数值参数、输入与 HDR/PNG 输出下完整进程达到 240 s/张以内,
这是待验证目标而非承诺。用户在 btop 连续观察到新版 GPU 约 94%、反复降至约 80%,
CPU 满载,并报告原 atomic 能吃满 GPU;旧串行 adaptive 未观测,不能推断其利用率。
GPU busy 不代表净算力利用率,CPU 忙等需通过锁等待和线程 CPU time 定量确认。
仅消除当前 splat 与 GPU event 区间的约 26.8 s 差距仍需约 309 s/张;若其他阶段约
52.8 s 不变,目标要求 splat 约 187.2 s,需要进一步降低 GPU 执行或区间内间隙成本。
先利用已有全天输出完成 HDR 正确性比较,再在新版有界真实流式样本上拆分分桶、锁等待、
GPU 完成等待、staging/上传与 prepare/accumulate/merge,辅以同步设备遥测。根据证据
评估双缓冲重叠准备/上传,必要时对照专用提交线程和有界队列;均为待实验方案,保留单
sink HDR ownership、完成后复用、有界背压及 direct fallback 顺序。随后按实测占比优化
主要 kernel 或输入阶段,保留 double RGB 和现有误差阈值。先完成有界配对、正确性及
完整 provenance,再另行安排全天确认,不以恢复旧版慢因或 btop 达到 100% 为验收条件。
具体目标、计时口径、用户原始证据与限制见
[2026-09-13 全天复测与下一步目标](benchmarks/hip_psf_replay_2026-09-07.md#2026-09-13-全天复测与下一步目标)。
2026-09-12,黑洞渲染的黑体颜色计算改为以仓库内数值 LUT 为默认实现,取代原先
`optics.c` 中的 Wyman 解析 CIE 拟合。正常区使用仓库唯一的
`assets/blackbody/cie1931_2deg_xyz_1024.grbblut`:以 40 位精度把 Planck 谱和
`assets/CIE_xyz_1931_2deg.csv` 的 360--830 nm、1 nm 分段线性 CIE 数据积分,生成覆盖
`[670.146556,101408.88] K` 的 1024 个 `log(T)` 等距 XYZ 节点。`GRBBLUT3` header
固定标识该 reference,loader 校验 magic/version/endianness/物理常数/范围/长度与 payload
checksum,并拒绝旧的 Wyman/5 nm 表与 runtime 造表。表内使用四点三次 Lagrange 插值,
在全部 1023 个区间中点相对独立 Gauss-16 积分的最大 XYZ channel 相对误差为
`5.4633e-5`,没有负 XYZ。由 1 nm 线性插值自身约 `3.2e-4` 的最坏误差估计,正常区采用
`1e-4` LUT 内部限与 `5e-4` 整体误差预算,避免继续追逐缺乏物理意义的 `1e-6`。
两端使用同一 CSV reference 的表外模型。高温端使用连续积分得到的
`A T+B+C/T` Rayleigh--Jeans 展开,在 `[Tmax,10Tmax]` 最大 XYZ 相对误差为 `1.6635e-5`。
低温端针对 X/Y 的 830 nm 端点和 Z 的 650 nm 零端点分别展开:在 `4.097321 K` 以下使用
crossover 锚定、在 `T -> 0` 趋于极限的端点式,其上用六段八次
`1/T -> log(scaled XYZ)` Chebyshev--Lobatto 插值接到 LUT。低温端点段/过渡段的最大 XYZ
相对误差分别为 `6.69e-5`/`8.16e-5`,因而三段均满足 `1e-4` 内部限;log 输出避免把极端
低温的线性下溢误解为拟合硬下限。
最终 legacy Wyman/5 nm 与 CSV LUT 的 1920x1080 double-HDR 比较显示,每通道峰值归一化
最大绝对变化为 R `9.00e-3`、G `7.02e-3`、B `8.12e-4`;这是 reference 升级本身造成的
预期变化,而不是 LUT 插值误差。旧 Wyman/5 nm 积分实现、可切换的 integral/fixed/LUT
A/B 后端、有界性能证据与观测温度采样器保留在实验分支
`codex/blackbody-cost-experiment`(tag `blackbody-cost-experiment-2026-09-12`)。
---
# 23. Metric evaluator 的 thread-local workspace
nmesh 单点查询需要:
```text
global x
→ locate AMR leaf
→ global/local coordinate inversion
→ Lagrange interpolation
→ derivatives
```
应给每线程一个 workspace:
```c
typedef struct {
uint32_t last_node[...];
double lx[12];
double ly[12];
double lz[12];
/* scratch arrays */
} MetricWorkspace;
```
连续 RK stage 中 ray 大概率仍在同一 node,因此优先:
```text
is x still inside last_node?
yes: reuse
no: tree / neighbor lookup
```
不要在 evaluator 内维护共享 mutable cache。
---
# 24. 顶层主要数据结构
```c
typedef struct ObserverTrack ObserverTrack;
typedef struct SpacetimeSource SpacetimeSource;
typedef struct MetricSlab MetricSlab;
typedef struct RayPool RayPool;
typedef struct StarCatalog StarCatalog;
typedef struct {
double t_camera;
ObserverState observer;
FrameMesh mesh;
HDRImage image;
} Frame;
typedef struct {
Frame *frames;
size_t nframes;
} Movie;
```
---
# 25. 顶层执行逻辑
概念 `main()`:
```c
int main(void)
{
ObserverTrack observer;
SpacetimeSource spacetime;
StarCatalog stars;
Movie movie;
load_observer_track(&observer, ...);
open_spacetime(&spacetime, ...);
load_star_catalog(&stars, ...);
init_movie(&movie, &observer, ...);
movie_init_coarse_meshes(&movie);
for (;;) {
SampleRequest *req = NULL;
size_t nreq =
movie_collect_refinement_requests(
&movie,
&req
);
if (nreq == 0)
break;
RayPool rays;
rays_build_generation(
&rays,
&movie,
req,
nreq
);
rays_trace_generation(
&rays,
&spacetime
);
movie_install_ray_results(
&movie,
&rays
);
free_ray_pool(&rays);
free(req);
}
movie_render_star_catalog(
&movie,
&stars
);
movie_write_output(&movie);
return 0;
}
```
---
# 26. `rays_trace_generation()` 的核心流程
```c
void rays_trace_generation(
RayPool *rays,
SpacetimeSource *st)
{
while (spacetime_has_previous_slab(st)) {
MetricSlab *slab =
spacetime_load_previous_slab(st);
activate_rays_for_slab(
rays,
slab
);
#pragma omp parallel
{
MetricWorkspace ws;
metric_workspace_init(&ws);
#pragma omp for schedule(static)
for (size_t i = 0; i < rays->n; ++i) {
if (!ray_is_active(rays, i))
continue;
evolve_ray_to_slab_left_boundary(
rays,
i,
slab,
&ws
);
}
metric_workspace_free(&ws);
}
compact_ray_pool(rays);
spacetime_free_slab(slab);
}
}
```
---
# 27. repo 的几个主要模块
这里讨论的是代码职责,不强调具体文件树。
## `observer`
负责:
- 读取 worldline;
- 插值 observer event;
- 插值/正交化 tetrad;
- 给每个 frame 提供 observer state。
不负责:
- geodesic;
- metric;
- image rendering。
---
## `movie / frame`
负责:
- frame 时间表;
- 每帧 adaptive image mesh;
- vertex / triangle;
- refinement passes;
- ray sample request;
- 安装 ray endpoint results。
---
## `ray`
负责:
- 批量 ray state;
- ray 初始化;
- active/inactive 状态;
- stream compaction;
- endpoint 保存。
---
## `geodesic`
负责:
- 3+1 null geodesic RHS;
- frequency shift evolution;
- ODE stepper;
- 在给定 metric slab 中推进 ray。
不负责:
- disk I/O;
- frame mesh;
- star rendering。
---
## `spacetime`
负责统一 backend:
- Minkowski;
- analytic Schwarzschild;
- nmesh 4D numerical spacetime。
负责:
- time slab;
- metric interpolation;
- spatial derivatives;
- 必要的 temporal derivatives;
- capture/infinity classification。
---
## `nmesh_backend`
负责:
- 读取 nmesh node 文件;
- 从 filename 恢复 AMR tree;
- node spatial lookup;
- global/local coordinate conversion;
- Lagrange interpolation;
- Lagrange derivatives;
- temporal interpolation;
- puncture trajectory。
---
## `catalog`
负责:
- 读取 Gaia / 2MASS 等 catalog;
- 转换为内部 `(direction, temperature, amplitude)`;
- 天球 spatial index;
- triangle/source-region 查询。
---
## `optics / psf`
负责:
- blackbody frequency shift;
- temperature → linear RGB;
- flux normalization;
- PSF splatting;
- HDR accumulation。
---
## `output`
负责:
- HDR frame;
- exposure;
- 线性 HDR → 可选 display operator → sRGB encoding;
- tone mapping;
- PNG/EXR;
- 最终视频编码接口。
当前实现决策:预览用的 tone-mapped PNG/PPM 路径为
线性 RGB → display operator → sRGB 传输曲线 → 8-bit。默认 display operator
是逐通道 soft clip \(T_2(x)=\sqrt{\tanh(x^2)}\)(`--tone-map softclip
--tone-map-p 2`);历史 Reinhard \(x/(1+x)\) 以 `--tone-map reinhard`
保留,用于复现旧输出。FITS `--hdr-output` 绕过 tone mapping,始终写
pre-tone-map 线性 RGB。这里的 operator 只是当前简化显示管线,不是最终相机/
传感器模型。
---
# 28. 开发顺序
## Phase 0 — 平直时空 + 星表
目的:
- 验证 observer/camera projection;
- 验证天球方向;
- 验证 Gaia/2MASS catalog;
- 验证 temperature/amplitude;
- 验证 PSF;
- 验证 adaptive image mesh 数据结构。
平直时空:
\[
(x,y)\leftrightarrow \hat n_\infty
\]
可以作为严格 benchmark。
---
## Phase 1 — analytic Schwarzschild
建议使用 horizon-penetrating coordinates。
验证:
- capture;
- Einstein ring;
- multiple images;
- adaptive refinement;
- local inverse maps;
- magnification;
- frequency shift。
---
## Phase 2 — numerical single Schwarzschild
使用当前已能跑的 FOCCZ4 moving-puncture Schwarzschild。
特点:
- 开始存在 gauge relaxation;
- 随后 metric 趋于稳定;
- 可真实测试 4D metric streaming;
- 可和 analytic Schwarzschild 做 regression comparison。
比较:
\[
\Delta \hat n_\infty,
\quad
g,
\quad
\text{captured/escaped classification}
\]
---
## Phase 3 — FOZ4c BBH
等 FOZ4c:
- 双曲性;
- constraint damping;
- 数值实现;
- BBH 稳定演化;
成熟后接入。
renderer 顶层架构原则上不应为 BBH 重新设计。
---
# 29. 尚未决定的问题
需要以后通过 prototype / convergence test 决定:
- 时间输出 cadence;
- time interpolation order;
- time slab 最佳内存大小;
- BBH production node 数;
- 4D 输出总数据量;
- AH calibration 后 puncture cutoff;
- adaptive triangle refinement criterion;
- critical curve 附近最大 refinement level;
- Gaia 与 2MASS 的最终组合;
- PSF 模型;
- ODE integrator;
- observer trajectory 标准;
- 当前预览/PNG 默认采用逐通道 \(T_2(x)=\sqrt{\tanh(x^2)}\) soft clip,
Reinhard 作为兼容模式保留;最终 production color management、传感器模型、
曝光标定和 HDR 视频编码规则仍未决定;
- 是否需要 diffuse Milky Way background;
- 是否将 ray redshift 变量定义为 `log(alpha p^0)` 或其他更方便的量。
---
# 30. 当前最核心的设计原则
整个 repo 可以压缩成以下几条:
1. **最终目标是动态 4D NR 视频渲染,不是单帧 ray tracer。**
2. **所有帧的 rays 按 coordinate time 整合,通过 time slabs 倒序推进。**
3. **metric backend 可替换,但顶层 scheduler 与具体时空无关。**
4. **相机是 worldline + tetrad track,不是固定三维坐标。**
5. **恒星是点源 catalog,不是 RGB 天球纹理。**
6. **恒星存温度与 amplitude,频移通过温度缩放自然处理。**
7. **image-plane 使用 adaptive local triangles 构造局部 inverse lens maps。**
8. **adaptive refinement 采用多 pass,避免和 out-of-core metric streaming 冲突。**
9. **ray evolution 用 SoA + OpenMP bulk loops。**
10. **recursive refinement 用 thread-local iterative queues,不用细粒度 OpenMP tasks。**
11. **nmesh 原生 DG node 输出直接作为空间插值数据,不做 Cartesian resampling。**
12. **时间导数优先由 3+1 identities、gauge RHS 或 temporal interpolant 解析获得。**
13. **所有高开销 mutable cache 都 thread-local。**
14. **第一版 CPU-only,先把物理与数据流做正确,再谈 GPU。**
15. **从 Minkowski → analytic Schwarzschild → numerical Schwarzschild → BBH 逐级验证。**
## Schwarzschild 自由落体轨迹辅助脚本
`scripts/schwarzschild_camera_track.py` 在入射 Cartesian Kerr–Schild 坐标中使用
$g_{\mu\nu}=\eta_{\mu\nu}+(2/r)\ell_\mu\ell_\nu$、$\ell_\mu=(1,x_i/r)$,
固定 $G=c=M=1$ 与解析 backend 一致。解析微分 metric 构造四维 Christoffel,
以本征时联合积分
$dz^\mu/d\tau=u^\mu$ 与
$de_{(a)}^\mu/d\tau=-\Gamma^\mu_{\alpha\beta}u^\alpha e_{(a)}^\beta$。
$e_{(0)}=u$ 同时满足自由落体方程;四加速度为零时费米–沃克输运就是平行输运。
初始坐标速度及指向构造沿用 10.1 的约定,也接受经验证的完整标架。
不在积分期间重新正交化以隐藏误差;输出正交归一误差超过 $10^{-6}$ 时失败。
DOP853 默认 rtol=$10^{-10}$、atol=$10^{-12}$,可配置并通过解析径向自由落体、
圆轨道和圆轨道平行输运的收敛回归验证。视界不终止相机;默认 $r=10^{-3}M$
只是可配置的奇点数值保护边界,不等于精确撞击奇点。它独立于光线的 $1.5M$
捕获 cutoff;该 cutoff 内的轨迹可输出,但当前 renderer 的光线会立即被捕获。
CSV 保持 21 列不变,记录 $\tau=k/\mathrm{fps}$ 与积分得到的真实坐标时间。
只保留不超过请求持续本征时或提前终止时刻的规则采样,包含 $\tau=0$。
`--movie-track-samples` 选择每个 CSV 样本直接生成一帧,绕过坐标时间均匀插值,
忽略 renderer 的 start-time/duration/fps。默认 movie 路径不变;两条路径随后共用
按真实 coordinate time 的 time-slab 调度,不把本征时冒充坐标时间。