GDS里的圆弧为什么总是多边形:从弦高误差到顶点数控制
在版图软件里画一个直径 $20\,\mu\text{m}$ 的圆,看起来只是一次鼠标操作;但把文件写成 GDS 再放大,你看到的通常不是“真正的曲线”,而是一圈首尾相接的短直线。
这件事很容易被简化成一句话:**GDS 不支持圆,所以只能拿多边形凑。**方向大体没错,但工程上不够准确。真正决定结果的至少有四件事:曲线逼近容差、版图数据库单位(database unit,DBU)、单个多边形的顶点策略,以及下游工具如何解释 PATH 或 fracture 后的图形。
本文不讨论圆角究竟能降低多少衍射损耗,而是回答一个更基础、也更容易复现的问题:给定半径和允许误差,一个圆到底需要多少个顶点?
图 1 理想圆、内接多边形与最大弦高误差 $e$。示意图为本文自制。
先澄清:版图里的“圆”可能有三种命运
GDSII 的核心几何对象包括边界(BOUNDARY)和路径(PATH),它们最终都依赖一系列坐标点描述形状。KLayout 的几何文档也把与 GDS2 兼容的 SimplePolygon 定义为点构成的闭合轮廓。这就是为什么多边形是跨工具交换曲线时最常见、也最稳妥的形式。
但“GDS 里绝对没有任何圆的表达技巧”也不严谨。KLayout 文档提到,可以利用单点、圆端 PATH 表达圆,可它同时警告:其他系统不一定能正确读取,PATH 的一些边角行为在不同软件中也可能不同。工程上遇到这种兼容性不确定时,官方建议是转成 polygon。
如果换成 OASIS,情况又不同。gdstk 文档指出,OASIS 有圆形记录,也不采用与 GDS 输出相同的多边形顶点限制。于是,同一段 Python 代码产生的“圆”,写入 GDS 时可能经历多边形拆分,写入 OASIS 时却可能被识别并转换成圆形对象。
因此,更准确的说法是:
为了获得稳定的 GDS 跨工具兼容性,曲线通常会在导出前离散成多边形;格式和工具链不同,最终表示也可能不同。
一个圆需要多少条边?
把半径为 $R$ 的圆近似成内接正 $N$ 边形。每条边对应的圆心角是 $2\pi/N$。误差最大的地方不在顶点——顶点本来就在圆上——而在每条弦的中点。
从圆心到弦中点的距离为 $R\cos(\pi/N)$,所以圆弧与弦之间的最大径向偏差,也就是弦高误差(sagitta),为:
$$
e=R\left[1-\cos\left(\frac{\pi}{N}\right)\right].
$$
如果设计允许的最大误差是 $e_{\max}$,反解得到:
$$
N\geq
\frac{\pi}{\arccos\left(1-e_{\max}/R\right)}.
$$
最后把结果向上取整。这个公式给出的是完整圆所需的最小边数;对于张角为 $\Theta$ 的圆弧,可以按角度比例计算所需线段数。
当 $e\ll R$ 时,利用 $1-\cos x\approx x^2/2$,还能得到一个便于心算的近似:
$$
N\approx\pi\sqrt{\frac{R}{2e_{\max}}}.
$$
它揭示了一个很有用的尺度关系:将容差缩小到原来的 $1/4$,顶点数大约增加到原来的 2 倍,而不是 4 倍;在绝对误差不变时,圆越大,需要的顶点也越多。
1 nm 容差并不总是“精细得刚刚好”
把公式代入几组数值:
| 半径 $R$ | 最大弦高误差 $e_{\max}$ | 最小顶点数 $N$ | 代回后的实际误差 |
|---|---|---|---|
| $1\,\mu\text{m}$ | $1\,\text{nm}$ | 71 | $0.9788\,\text{nm}$ |
| $10\,\mu\text{m}$ | $1\,\text{nm}$ | 223 | $0.9923\,\text{nm}$ |
| $10\,\mu\text{m}$ | $5\,\text{nm}$ | 100 | $4.9344\,\text{nm}$ |
| $10\,\mu\text{m}$ | $10\,\text{nm}$ | 71 | $9.7877\,\text{nm}$ |
| $100\,\mu\text{m}$ | $1\,\text{nm}$ | 703 | $0.9985\,\text{nm}$ |
这里最值得注意的不是“223”这个数字,而是它可能触发某些工具的多边形拆分策略。例如,gdstk 写 GDS 时默认使用 max_points=199,超过该值会被 fracture 成多个较小多边形。它们合起来仍然描述原来的形状,但对象数量、接缝结构、后续布尔运算和检查日志都可能发生变化。
需要强调:199 是 gdstk 的默认兼容策略,也是旧版 GDS 流程常见的顶点选择,不是自然定律。LayoutEditor 的格式说明区分了旧版常见的 200 点限制与较新记录允许的更高技术上限。最终应以你的完整工具链——版图编辑器、DRC、数据准备和制造端——共同支持的设置为准。
tolerance、DBU 和制造精度不是一回事
假设曲线离散程序把最大弦高误差设置为 0.001,而长度单位是微米。这只是在告诉几何生成器:多边形与理想曲线之间的逼近误差目标约为 $1\,\text{nm}$。
它不等于:
- 所有顶点坐标都能以任意精度写入 GDS;
- 光刻或电子束设备一定能复制出 $1\,\text{nm}$ 的轮廓变化;
- 仿真网格能分辨这一级别的差异;
- fracture 和布尔运算不会引入新的边界变化。
KLayout 将 GDS 兼容图形的整数坐标表示为 DBU。浮点多边形转成整数多边形时,坐标会舍入到最近的网格点。如果 DBU 本身就是 $1\,\text{nm}$,继续把曲线 tolerance 压到 $0.1\,\text{nm}$,通常只会制造更多顶点,写出时仍要落到同一个整数网格上。
所以,误差预算至少应拆成两层:
$$
e_{\text{total}}\quad\text{由曲线逼近误差、坐标网格误差和后续处理误差共同决定。}
$$
它们不一定能简单线性相加,但必须分别检查。只调小一个 tolerance,并不能证明最终版图更接近设计意图。
用 KLayout 生成并检查,而不是只相信参数
下面的代码使用 KLayout 的 Python 包计算理论顶点数,显式构造 DPolygon,再写出和回读 GDS。这样,曲线离散规则直接体现在代码里,DBU 也由版图库统一管理。
1 | |
layout.dbu = 0.001 表示一个数据库单位是 $0.001\,\mu\text{m}$,也就是 $1\,\text{nm}$。它与 max_error_um = 0.001 数值上相同,但语义不同:前者控制坐标量化,后者控制我们构造多边形时允许的弦高误差。
在本文的 Windows 临时环境中使用 KLayout 0.30.10 实测,理论计算得到 223 个顶点;写出并重新读入后仍是 1 个、223 点的多边形。这说明“是否 fracture”是具体工具和写出配置的行为,不能把 gdstk 的默认值套用到 KLayout。完整可运行版本保存在本文素材包的 create_circle_klayout.py 中。
一套更稳妥的圆弧离散流程
实际项目中,可以按下面的顺序做:
- 先确定误差预算。 依据仿真网格、最小特征尺寸和器件对边界的敏感度决定,而不是机械地写成 $1\,\text{nm}$。
- 用弦高公式估算顶点数。 如果顶点数异常大,先判断容差是否已经小于 DBU 或实际工艺分辨能力。
- 检查顶点与 fracture 策略。 确认
max_points以及下游 DRC、数据准备软件的兼容范围。 - 导出后重新读入。 检查多边形数量、顶点数、包围盒、面积和关键尺寸,不要只检查写文件是否报错。
- 在物理仿真中做收敛测试。 选择粗、中、细三档曲线容差;如果目标光学量已经收敛,就没有必要继续增加顶点。
- 最后再决定格式。 如果工具链完整支持 OASIS,包含大量曲线或重复结构的版图可能有更合适的表达方式;不要仅因为习惯就默认所有中间文件都必须是 GDS。
结语:精度不是顶点越多越好
曲线离散本质上是一个误差分配问题。顶点太少,几何偏差可能进入仿真和制造;顶点过多,又会增加 fracture、布尔运算、文件处理和兼容性成本。
真正稳妥的做法不是寻找一个适用于所有项目的固定边数,而是把半径、弦高误差、DBU、工具上限和物理敏感度放进同一套检查流程。这样,GDS 中那一圈肉眼几乎看不见的短线段,才是经过验证的工程近似,而不是“看起来够圆”。
