半导体与微纳制造
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_{\max}\),反解得到:
最后把结果向上取整。这个公式给出的是完整圆所需的最小边数;对于张角为 \(\Theta\) 的圆弧,可以按角度比例计算所需线段数。
当 \(e\ll R\) 时,利用 \(1-\cos x\approx x^2/2\),还能得到一个便于心算的近似:
它揭示了一个很有用的尺度关系:将容差缩小到原来的 \(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}\),通常只会制造更多顶点,写出时仍要落到同一个整数网格上。
所以,误差预算至少应拆成两层:
它们不一定能简单线性相加,但必须分别检查。只调小一个 tolerance,并不能证明最终版图更接近设计意图。
用 KLayout 生成并检查,而不是只相信参数
下面的代码使用 KLayout 的 Python 包计算理论顶点数,显式构造 DPolygon,再写出和回读 GDS。这样,曲线离散规则直接体现在代码里,DBU 也由版图库统一管理。
import math
from pathlib import Path
import klayout.db as db
def vertices_for_circle(radius_um: float, max_error_um: float) -> int:
"""返回内接正多边形满足弦高误差上限所需的最小顶点数。"""
if not 0 < max_error_um < radius_um:
raise ValueError("max_error_um 必须满足 0 < max_error_um < radius_um")
return math.ceil(
math.pi / math.acos(1 - max_error_um / radius_um)
)
radius_um = 10.0
max_error_um = 0.001 # 1 nm
vertex_count = vertices_for_circle(radius_um, max_error_um)
points = [
db.DPoint(
radius_um * math.cos(2 * math.pi * index / vertex_count),
radius_um * math.sin(2 * math.pi * index / vertex_count),
)
for index in range(vertex_count)
]
output_path = Path("curve-test.gds")
layout = db.Layout()
layout.dbu = 0.001 # µm/DBU,即 1 nm
top = layout.create_cell("CURVE_TEST")
layer = layout.layer(1, 0)
top.shapes(layer).insert(db.DPolygon(points))
layout.write(str(output_path))
# 用新的 Layout 回读,检查实际写入的对象和顶点数。
check = db.Layout()
check.read(str(output_path))
shapes = list(check.top_cell().shapes(check.layer(1, 0)).each())
print(vertex_count) # 223
print(len(shapes), [shape.polygon.num_points() for shape in shapes])
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 中那一圈肉眼几乎看不见的短线段,才是经过验证的工程近似,而不是“看起来够圆”。
参考来源
CONTINUE EXPLORING
DISCUSSION
留言与讨论
分享你的理解,或留下一个值得继续讨论的问题。
还没有留言,欢迎成为第一个参与讨论的人。