几何检查器也要被测:先让空圆性判据失败一次

举报
岚枫 发表于 2026/10/31 07:26:11 2026/10/31
【摘要】 一个永远返回 0 违规的检查器,看起来最稳定,却什么也没证明。空圆性判据就是例子:5×5 网格曾报 32 处误报,复现后最大残差约 1.8×10⁻¹²。这篇讲怎么用已知错误的剖分给检查器做负例,以及为什么容差要跟着尺度走。

写几何代码时,检查器通常比算法本身写得快:算个面积、数个数、比个距离,几十行就能跑。也正因为写得快,它很少被当成「需要测试的代码」。

一个永远返回「0 处违规」的检查器,看起来最稳定。但它和「什么都不检查」在输出上完全一样。要证明它有检验力,最直接的办法就是:喂给它一个已知错误的输入,看它能不能报出来。

一次真实的误报

5×5 网格,25 个点,间距均匀。把空圆性判据的容差临时改成固定的 1e-12,会报出 32 处违规。

如果真有 32 处违规,剖分应该已经乱套了。但网格看起来规整,每个方格被一条对角线分成两个直角三角形。把「三角形 + 点」逐对打印出来,每一对里的点都恰好落在那个三角形的外接圆上:方格的四个格点共圆,两条对角线选哪条都满足空圆条件,检查器却把圆周上的点算成了圆内点。

这不是几何错,是判据的容差错。按当前代码、把容差设成 0 复现这 32 对,每一对的距离平方都比 r² 小一点,最大 1.819×10⁻¹²,相对 1.289×10⁻¹⁶,约一个双精度 ulp;阈值放到 1e-11,全部消失。这些「违规」的深度只有 1 个 ulp 量级,完全是浮点舍入。

固定绝对容差的问题不在于「一定太大」或「一定太小」,而在于它和圆的大小没有关系:同一个 1e-12,在半径平方 1.4×10⁴ 的网格上太小,在半径平方 1 的小三角形上又可能太松。阈值要跟着被比较的量走,而不是跟着某个习惯小数走。

检查器现在怎么判

空圆性检查的核心只有几行:

const r2 = cc.r * cc.r;
const tol = r2 * 1e-6;
for (let i = 0; i < pts.length; i++) {
  if (i === t[0] || i === t[1] || i === t[2]) continue;
  const dx = pts[i].x - cc.cx, dy = pts[i].y - cc.cy;
  if (dx * dx + dy * dy < r2 - tol) {
    bad.push({ triangle: ti, point: i });
  }
}

tol = r2 * 1e-6 是相对容差。半径平方 1.4×10⁴ 时,tol 约 0.014;半径平方 1 时,tol 是 1e-6。代码判的是 d² < r² − tol,也就是 d²/r² < 1 − 1e-6:只有明显进到圆内、相对深度超过百万分之一才算违规,更靠近圆周的一律放过。

这个阈值会不会太松?要看真实违规有多深。两条不同对角线造成的空圆性违规,通常是某个点明显落在另一个三角形的圆内,相对深度远大于 1e-6;只有接近共圆的情况才会落进容差带,而那种情况本来就剖分不唯一,判成「不算违规」是合理的。

负例一:故意连错对角线

容差调好,只说明误报少了。要证明检查器能报出真错,得构造一个已知错误的剖分。

拿四个点:

A(0, 0)   B(10, 0)   C(10, 10)   D(1, 9)

以 ABC 为一个三角形,它的外接圆圆心在 (5, 5),半径平方 50。点 D 到圆心的距离平方是 (-4)² + 4² = 32,明显小于 50,所以 D 落在 ABC 的外接圆内。用对角线 AC 剖分,两侧都违规:ABC 的圆包含 D;ACD 的外接圆(圆心约 (6.125, 3.875),半径平方约 52.53)也包含 B(距离平方约 30.03)。检查器应该报 2 处。

换另一条对角线 BD,三角形变成 ABD 和 BCD。BCD 的外接圆圆心在 (6, 5),半径平方 41;点 A 到圆心的距离平方是 (-6)² + (-5)² = 61,在圆外。ABD 的外接圆圆心在 (5, 4),半径平方 41,C 的距离平方是 5² + 6² = 61,也在圆外。这一组没有任何违规。

同一个四边形,只换一条对角线,检查器必须从「2 处违规」变成「0 处」。如果它对两种剖分都返回 0,那它测的不是几何,是自己。

这个四边形是给检查器的建议负例:把它写进测试,检查器才能证明自己抓得住这类错。

负例二:删掉一个三角形

第二个负例更粗暴:拿一个正确的三角网,删掉中间一个三角形,然后看各个检查器能不能发现。

  • 空圆性检查:大概率发现不了。剩下的三角形仍然满足空圆条件,少一个三角形不会让它们变得不合法;
  • 面积和 = 凸包面积:能从变异后的三角形列表重新算的检查器会发现,面积直接少了一块;如果它读的是构造时缓存的总面积,就会漏掉——「检查器不能只信构造器给的汇总值」本身就是一条教训;
  • 覆盖面:按「每个点至少属于一个三角形」这个定义,发现不了。被删三角形的三个顶点,通常还被周围的三角形使用着;
  • 3F = 2E − h:也抓不到。这里的 h 必须是边表里 count === 1 的边数:删掉内部三角形后,它的三条边从共享两次变成一次,h 加 3、F 减 1,3F 和 2E − h 同时减 3,等式照样成立。如果换成凸包边数当 h,等式会立刻失败——这其实是另一种用法:h 小于凸包边数,说明网格边界和凸包边界对不上,中间大概率有洞。现有检查用的是边表统计值,所以这一步抓不到。

这个负例说明:没有哪一个检查器能抓住所有错误。空圆性管的是「三角形是否满足 Delaunay 条件」,不管「网格是否覆盖了整个凸包」;面积管覆盖,不管局部合法性。它们必须组合使用。

负例三:重复一个三角形

再做一个变异:把某个内部三角形复制一份,插进三角形列表。

  • 空圆性检查:重复的三角形和原来完全一样,外接圆也一样,检查器不会报;
  • 面积和:面积多算了一块,会报;
  • 3F = 2E − h:三条边从共享 2 次变成 3 次,会报。唯一的例外是整个网格只有一个三角形:重复后 h 从 3 掉到 0,3F 和 2E − h 同为 6,等式就漏过去了。

同一个错误,三个检查器的反应不同。这不是坏事,正好说明它们的职责不同。测试要做的是:每个变异至少被一个检查器抓住;如果某个变异谁都没反应,说明检查器组合有盲区。

构造器和检查器用的是两套公式

还有一点值得说明:剖分内部判断「点是否在外接圆内」用的是行列式,而独立检查用的是圆心距离。两套公式在数学上等价,在浮点下不一定给出完全相同的判断。

构造器里的容差是 1e-12 * dmax^4(dmax 是点集包围盒的长边),因为那个行列式对坐标是四次的;检查器里的容差是 r² × 1e-6,因为比较的是平方距离。两个容差各自有各自的量纲和尺度,不能互相替代,也不应该指望它们在共圆附近逐点一致。

这意味着:「检查器报 0 违规」不等于「构造器一定用了正确的容差」。检查器能证明的是「在当前判据下没有发现明显违规」;要证明构造器稳,还得靠外部实现对拍、面积恒等式、退化算例这些不同来源的检查。

一个够用的检查器测试集

如果要把这套做法落到测试里,我会分成两组。

正向算例(必须零违规):

  • 随机点集,多个固定种子;
  • 网格,大量四点共圆;
  • 正圆:自己给一组严格共圆的坐标(扰动过的圆环只能当近似共圆算例);
  • 三簇,密度不均;
  • 近共线,退化路径;
  • 凸包边上的共线点;
  • 重复点去重后的点集;
  • 全共线退化(不产生三角形,单独断言退化标记)。

负向变异(每个都必须被至少一个检查器抓住):

  • 四边形连错对角线(空圆性);
  • 删掉一个内部三角形(面积和);
  • 重复一个内部三角形(面积和、欧拉关系);
  • 把某个点从三角形列表里排除(覆盖面)。

正向算例证明「不误报」,负向变异证明「不漏报」。只有正向算例的测试集,等于只测了一半。

适用范围与局限

  • 相对容差 1e-6 是这套实现的选择,换一套浮点策略可能需要调整;写清楚阈值,别人才知道「通过」是在什么标准下通过;
  • 坐标跨度极端的点集没有测;半径平方到 1e12 时,r² × 1e-6 是否仍然合适,需要重新评估;
  • 近共圆点集的剖分本身不唯一,检查器只能判断「是否明显违规」,不能判断「哪一条对角线才是唯一正确的」;
  • 空圆性检查不检查覆盖,面积和与覆盖面检查不检查局部合法性;它们必须一起用。

检查器的价值在于它能拒绝什么

一个检查器报「通过」的次数,不能说明它有多可靠;它能拒绝哪些已知错误,才能说明它的检验力。

空圆性判据的这次误报,表面上是容差问题,实际上是「检查器没有被测试」的问题:如果一开始就有一组「必须零违规」的网格算例,32 处误报当场就会暴露;如果同时还有一个「必须报错」的连错对角线算例,容差调过头也能被发现。正向算例管误报,负向算例管漏报,缺哪一边都不行。

写检查器和写算法一样,都要先想清楚:它应该拒绝什么,然后拿一个真的该被拒绝的输入,看它会不会拒绝。

(完)

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。