Fix: reject invalid inverse lens-map weights

Check spherical area weight sums against a fixed 1e-8 tolerance and normalize accepted weights before interpolation. Skip invalid images in both catalog paths.

Add a thin-triangle regression for both parities, document the measured tolerance margin, and refresh HDR fixtures with strict comparisons restored.
This commit is contained in:
wyj committed 2026-09-06 17:47:09 -04:00
1 parent 8741597d9d
commit 265d7b95d5
6 files changed
+72 -5

No files matched your search

+1 -4
View File
@@ -13,7 +13,4 @@ test-reference-images: $(FLOATDIFF_SCRIPT) $(REFERENCE_DIR)/minkowski_ra1_dec1_6
python3 $(FLOATDIFF_SCRIPT) $(REFERENCE_DIR)/minkowski_ra1_dec1_640x360_HDR.fits $(REFERENCE_TMP_DIR)/minkowski_ra1_dec1_640x360_HDR.fits
$(MAKE) SPACETIME=schwarzschild ENABLE_HDR=1 backend
OMP_NUM_THREADS=16 $(BUILD_DIR)/schwarzschild_sky --catalog assets/sky_grid_5deg.csv --output $(REFERENCE_TMP_DIR)/schwarzschild_ra1_dec1_fov60_640x360.png --hdr-output --width 640 --height 360 --fov-deg 60 --look-ra-deg 1 --look-dec-deg 1 --exposure 0.1 --observer-radius 30 --observer-velocity 0 0 0 --psf-fwhm-pixels 2.7 --psf-moffat-beta 4.5 --max-magnification 1e300 --max-cache-psf-flux 1 --psf-relative-tail 1e-8 --psf-min-y 0 --coarse-cell-pixels 16 --refine-max-level 0 --refine-angle-abs-deg 0.001 --refine-angle-rel 0.1 --refine-jacobian-min 1e-3 --refine-min-edge-pixels 0.5 --refine-min-area-pixels2 0.25 --catalog-load-workers 4
# The general metric-based tetrad changes four float32 samples by one ULP
# versus the legacy analytic static tetrad. Keep the original fixture and
# allow at most float32 relative rounding (2^-23), with zero abs tolerance.
python3 $(FLOATDIFF_SCRIPT) $(REFERENCE_DIR)/schwarzschild_ra1_dec1_fov60_640x360_HDR.fits $(REFERENCE_TMP_DIR)/schwarzschild_ra1_dec1_fov60_640x360_HDR.fits --rel-tolerance 1.1920928955078125e-7
python3 $(FLOATDIFF_SCRIPT) $(REFERENCE_DIR)/schwarzschild_ra1_dec1_fov60_640x360_HDR.fits $(REFERENCE_TMP_DIR)/schwarzschild_ra1_dec1_fov60_640x360_HDR.fits
+15
View File
@@ -250,6 +250,21 @@ typedef struct {
# 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 只关心自身局部映射。
不需要先把整个像平面映射到天球、再全局分类“第几阶像”。
+12 -1
View File
@@ -930,6 +930,17 @@ static int spherical_barycentric_weights(const double point[3], const double a[3
weights[0] = spherical_area(point, b, c) / area;
weights[1] = spherical_area(point, c, a) / area;
weights[2] = spherical_area(point, a, b) / area;
/* A thin source triangle can admit an exterior point through the tolerant
* edge test. Unsigned subareas then do not partition the total area, and
* interpolating with their ratios can move an image outside its triangle.
* Reject that case before normalizing roundoff in valid convex weights.
* The 1e-8 bound is dimensionless; see the inverse-map design notes. */
const double weight_sum = weights[0] + weights[1] + weights[2];
if (!isfinite(weight_sum) || weight_sum <= 0.0 ||
fabs(weight_sum - 1.0) > 1e-8)
return -1;
for (int i = 0; i < 3; ++i)
weights[i] /= weight_sum;
return 0;
}
@@ -1055,7 +1066,7 @@ static int splat_catalog_tile(const Star *stars, size_t count,
context->vertex[0]->n_infinity,
context->vertex[1]->n_infinity,
context->vertex[2]->n_infinity, weights))
return -1;
continue;
}
if (!owns_source_boundary(context->triangle, weights))
continue;
+44
View File
@@ -330,6 +330,50 @@ int main(void) {
goto done;
}
frame_lens_mesh_destroy(&fine_mesh);
/* Schwarzschild level-4/J=0.2 ring triangle: the old edge tolerance accepts
* (1,0,0) although it is outside, producing unsigned weights summing to
* 1.09608. Keep a genuine interior source after it to check that rejecting
* one source does not discard subsequent stars. Exercise both parities. */
const double thin_directions[3][3] = {
{0.99999998891116071, 0.00013431364716360775, 6.4323579270168807e-05},
{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}};
LensTriangle thin_triangle = {.vertex = {0, 1, 2}};
FrameLensMesh thin_mesh = {.vertices = thin_vertices, .vertex_count = 3,
.triangles = &thin_triangle, .triangle_count = 1};
Star thin_stars[2] = {
{.direction = {1, 0, 0}, .temperature_K = 7000, .amplitude = 1},
{.temperature_K = 7000, .amplitude = 1}};
for (int i = 0; i < 3; ++i) {
memcpy(thin_vertices[i].n_infinity, thin_directions[i],
sizeof thin_directions[i]);
memcpy(thin_vertices[i].camera_direction, thin_directions[i],
sizeof thin_directions[i]);
for (int axis = 0; axis < 3; ++axis)
thin_stars[1].direction[axis] += thin_directions[i][axis];
}
const double thin_norm = hypot(hypot(thin_stars[1].direction[0],
thin_stars[1].direction[1]),
thin_stars[1].direction[2]);
for (int axis = 0; axis < 3; ++axis)
thin_stars[1].direction[axis] /= thin_norm;
StarCatalog thin_catalog = {.stars = thin_stars, .count = 2};
for (int parity = 0; parity < 2; ++parity) {
thin_triangle.vertex[1] = parity ? 2 : 1;
thin_triangle.vertex[2] = parity ? 1 : 2;
memset(hdr, 0, (size_t)width * height * 3 * sizeof *hdr);
if (frame_splat_catalog(&thin_mesh, &thin_catalog, hdr, width, height,
test_exposure, &psf, NULL, 1.0, 1.0,
psf_relative_tail, 0.0, 0, 1, NULL, NULL, NULL) != 1 ||
hdr[3 * (43 * width + 43)] <= 0.0) {
fputs("thin source-triangle inverse-map regression failed\n", stderr);
goto done;
}
}
/* Refinement probes are temporary until their generation is complete. A
* shared diagonal probe must produce one stable midpoint and conforming
* children only after its endpoint has been installed. */