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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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. 先确定误差预算。 依据仿真网格、最小特征尺寸和器件对边界的敏感度决定,而不是机械地写成 $1\,\text{nm}$。
  2. 用弦高公式估算顶点数。 如果顶点数异常大,先判断容差是否已经小于 DBU 或实际工艺分辨能力。
  3. 检查顶点与 fracture 策略。 确认 max_points 以及下游 DRC、数据准备软件的兼容范围。
  4. 导出后重新读入。 检查多边形数量、顶点数、包围盒、面积和关键尺寸,不要只检查写文件是否报错。
  5. 在物理仿真中做收敛测试。 选择粗、中、细三档曲线容差;如果目标光学量已经收敛,就没有必要继续增加顶点。
  6. 最后再决定格式。 如果工具链完整支持 OASIS,包含大量曲线或重复结构的版图可能有更合适的表达方式;不要仅因为习惯就默认所有中间文件都必须是 GDS。

结语:精度不是顶点越多越好

曲线离散本质上是一个误差分配问题。顶点太少,几何偏差可能进入仿真和制造;顶点过多,又会增加 fracture、布尔运算、文件处理和兼容性成本。

真正稳妥的做法不是寻找一个适用于所有项目的固定边数,而是把半径、弦高误差、DBU、工具上限和物理敏感度放进同一套检查流程。这样,GDS 中那一圈肉眼几乎看不见的短线段,才是经过验证的工程近似,而不是“看起来够圆”。


参考来源


GDS里的圆弧为什么总是多边形:从弦高误差到顶点数控制
https://time-frame.cloud/2026/08/02/2026-08-02-gds-curve-polygon-tolerance/
作者
Time Frame
发布于
2026年8月2日
许可协议