Step-by-step tutorial · imaging

分步教程:从一份 GDS 出五种图

对应 example_template/imaging/ 里的可跑 starter(klink 0.3.3 起随 wheel 打包)。imaging 域是 klink 里专门"出图"的一组出口:process cross-section 剖面、逐步工艺胶片、3D 模型 + 自含网页查看器、SEM 风格顶视、Blender 论文级渲染——全部由你自己的三份文件驱动,klink 机制层零工艺数据。

0.4.0 破坏性变更:四个 imaging 工具现在都必须传 style——因为 klink 是机制层,却曾经攥着一堆"某个人对着某台设备调出来的"数字(太阳能量、镜头焦距、束流模糊、页面配色、连晶格图里每个原子的颜色)。那些现在全部外置,klink 一个都不带、也不猜。升级三步:klink update 刷出四份 style 文件 → python sem_style.py 生成 JSON → 调用时加 style=。完整说明见 v0.4.0 release notes。(截面只要 GDS、不加 render 时不需要 style。)

这是什么

一份 GDS,加上你自己拥有的三份文件,能出五种"图"。驱动它们的只有这三份 EXAMPLE-owned 文件——klink 只提供机制(xsection 引擎绑定、GLB 网格生成、SEM 光栅渲染、Blender 场景搭建),零工艺常数:

你的文件是什么
demo_layout.py示例版图:一整排四晶体管 CMOS 器件(p 衬底两个 NMOS + n-well 两个 PMOS,多晶硅栅极、每个源漏上的接触、金属1加电源轨),并导出切线 CUT_UM。改成你的器件就是改这份文件。
demo.pyxs工艺配方——受信 Python,进程内执行;# klink-step: <名字> 注释标记每个工艺步。改成你的流程就是改这份文件。
demo_stack.py一份 klink_visual_stack_v1 声明:每层的 z0_um/z1_um、颜色、SEM 灰阶(sem_grey)、材质类别(solid / lattice / dielectric)、对应的配方符号(recipe_symbol)。改成你的工艺就是改这份文件。
section_style.py
sem_style.py
viewer_style.py
blender_style.py
四份外观声明,每个出口一份:页面配色、标尺、束流参数、布光、相机、材质配方。klink 自带零个默认值——不给 style 的渲染会被直接拒绝,而不是用别人的审美画一张图给你。每份文件里每个数字都注明了它控制什么、调大调小会怎样。改外观就是改这四份。

这篇教程走一遍随包 starter:example_template/imaging/demo_layout.py、demo.pyxs、demo_stack.py 和四个 demo 脚本(xsection_demo.py / render3d_demo.py / sem_demo.py / blender_demo.py),全部离线跑,输出落进 _generated/。

前置条件

  • 安装 klink:pip install klayout-klink。
  • 四个出口各自的依赖是可选的,按需装——少装了什么,klink 的报错会给出精确的 pip 命令:
pip install klayout klayout-pyxs==0.1.13            # 剖面引擎
pip install trimesh shapely mapbox-earcut           # 3D
pip install numpy scipy pillow                      # SEM / 胶片渲染
pip install bpy                                     # Blender(约 300MB,可选)
这条链路不需要 KLayout 在跑——四个 demo 脚本都是纯 Python,离线读写 GDS/GLB/PNG/HTML 文件。连着 KLayout/klink-mcp 时,同样的功能是四个 MCP 工具:imaging.xsection_run / imaging.render3d / imaging.sem_top / imaging.blender,参数和这些脚本一一对应;klink.find_tools domain=imaging 能看到完整用法。

拿到 starter

klink init myproj
cd myproj/example_template/imaging
python xsection_demo.py

升级 klink 之后用 klink update 刷新 starter(新增/修复的文件会回灌,你自己没碰过的旧文件会清理)。

重要:example_template/ 整个目录归 klink 包所有——klink update 会把它刷回随包状态。所以先把 demo_layout.py / demo.pyxs / demo_stack.py 拷贝到项目根目录或你自己的目录再改(和 pdk.py 一个待遇),不要直接改 example_template/ 里的文件。klink update 完全不碰 demo 的 _generated/ 输出目录。

1剖面 + 逐步工艺胶片

xsection_demo.py 先用 demo_layout.py 画一整排四晶体管 CMOS 器件:p 衬底左半两个 NMOS,n-well 右半两个 PMOS,多晶硅栅极横跨有源区,每个源漏上都有接触,金属1覆盖接触并带上下两条电源轨。再沿一条显式切线 CUT_UM = [[-0.8, 1.9], [11.2, 1.9]](贯穿有源行正中间,切过四个栅极、间隔层和阱边界)剖开:一次单帧剖面,一次 steps=True 的逐步工艺胶片。

python xsection_demo.py
十一帧竖排剖面胶片,从裸衬底经 n-well 注入、LOCOS 场氧、多晶硅栅极、LDD、侧墙、源漏注入、层间介质、钨塞接触,一路叠到金属1完成互连
逐步工艺胶片(film_film.png):demo.pyxs 的 11 个 klink-step 各出一帧剖面,从裸衬底一路叠到金属1互连完成。

demo.pyxs 用 # klink-step: <名字> 注释标记工艺步——每个标记命名的是它后面紧跟的那段代码,胶片按标记数量渲染,一步一帧;这版配方有 11 个标记:p-substrate、n-well implant、LOCOS field oxide、gate oxide、poly gate + silicide、LDD implants、spacer、source/drain implants、ILD deposition、contact etch + W plug + CMP、metal-1 damascene。下面是配方的一段摘录(完整 11 步见 demo.pyxs):

# klink-step: p-substrate
pbulk = bulk()

# klink-step: n-well implant
nwell = mask(lnwell).grow(0.55, -0.05, mode='round', into=pbulk)

# klink-step: LOCOS field oxide
mfield = mask(lfield)
_fox_up = mfield.grow(0.22, 0.22, bias=0.12, mode='round')
_fox_dn = mfield.grow(0.22, 0.22, bias=0.12, mode='round',
                     into=[pbulk, nwell])
fox = _fox_up.or_(_fox_dn)

# ... gate oxide / poly gate + silicide / LDD implants / spacer /
# source-drain implants / ILD deposition 在这里(完整代码见 demo.pyxs)...

# klink-step: contact etch + W plug + CMP
mask(lcontact).etch(0.85, into=ild, taper=5)
tungsten = deposit(0.18, 0.18)
planarize(into=[tungsten, ild], less=0.5)

# klink-step: metal-1 damascene
imd = deposit(0.22)
mask(lmetal1).etch(0.32, into=imd, taper=5)
metal1 = deposit(0.22, 0.22)
planarize(into=[metal1], less=0.22)

被赋值给变量的材质会按变量名自动输出,不需要写 output()——但变量名以 _ 开头的例外:上面 _fox_up / _fox_dn 是 LOCOS 场氧上下两半各自生长出的中间结果,按惯例标成下划线开头,只用来 .or_() 合成最终的 fox,本身不会作为一种材质单独出现在图里。

5 帧最有代表性的截取(完整 11 帧见上面的胶片):

第1步剖面:切线下方是一整块均匀的棕褐色 p 衬底,上方是空气,还没有任何结构
步骤 1/11 · p-substrate
第3步剖面:剖面两端各出现一个圆角的蓝灰色场氧化物凸起,中间偏右有一块颜色略浅的 n-well 区域
步骤 3/11 · LOCOS field oxide
第5步剖面:硅表面均匀分布着四个深红色的小方块状多晶硅栅极,场氧化物凸起仍在两端
步骤 5/11 · poly gate + silicide
第10步剖面:浅蓝灰色的层间介质覆盖全表面,若干深灰色梯形钨塞垂直穿过它连到下方绿色和紫色的源漏区
步骤 10/11 · contact etch + W plug + CMP
第11步(最后一步)剖面:在钨塞上方又出现一层灰蓝色金属层,通过下方的钨塞与源漏区相连
步骤 11/11 · metal-1 damascene

MCP 工具:imaging.xsection_run——cut_um 必须显式给(µm),steps=true 配合配方里的 # klink-step: 标记出胶片;z_window_um 给每帧定一个统一的 z 窗口(这里是 (-1.1, 1.5)——engine 的衬底本身有好几微米深,不框一下画面大半都是衬底),axis=true 会在每帧左边画一条带刻度的 z / µm 标尺、右下角配一条水平比例尺(上面每张图左边的刻度和角落的 "1 µm" 线段就是它画的)。输出确定性(GDS 不带时间戳),overwrite=false 是默认值,每次运行都写一份 klink_imaging_result_v1 机读 sidecar。

23D + 自含网页查看器

render3d_demo.py 把同一份四晶体管 CMOS 版图出成三样东西:完整模型的查看器、同一模型切开一角的 cutaway 查看器,外加同一个 GLB 换一套配色的 light 页面。这个 3D 模型是版图掩膜的忠实绘图:把 klink_visual_stack_v1 里每一层的多边形在它自己的 z0_um/z1_um 之间拉伸出来,一个圆还是一个光滑的棱柱,不发明任何东西。一次拉伸永远不会包含工艺曲率——LOCOS 的 bird's beak、锥形接触孔、CMP 碟形,这些都不在 GDS 里,自然也不会出现在这个模型里;它们属于 imaging.xsection_run 的 2D 剖面,是引擎精确的。0.5.8 移除了曾经存在的"process 扫描"3D 模式(把 1D 引擎的切片堆成一个体,结果处处都是台阶),换成两个诚实声明、绝不瞎猜的能力:

  • sidewall_deg(层声明里的字段,默认 0 = 竖直)把一层的墙 loft 成一个平滑的倾角——顶面按 thickness*tan(angle) 向内收。这个 demo 在 contact 和 metal-1 上都声明了 5 度,跟 demo.pyxs 刻蚀这两层用的角度一致。窄到撑不住这个倾角的多边形会被画成竖直,并记进结果的 warnings。
  • cutaway_um=[x0,y0,x1,y1] 把建好的整个模型布尔切开,只保留这个区域(µm):先建完整模型,再切割,所以截面只出现在被切开的那两个面上(需要 pip install manifold3d)。
python render3d_demo.py
# ---- 1. the model: a 3D drawing of the masks ---------------------
glb = OUT / "render3d_fast.glb"
r = build_glb_fast(str(GDS), STACK, str(glb), STYLE)
build_viewer_html(str(glb), str(OUT / "render3d_fast.html"),
                  STYLE, title="demo fast", overwrite=True)

# ---- 2. the same model, cut open ---------------------------------
cut_glb = OUT / "render3d_cutaway.glb"
rc = build_glb_fast(str(GDS), STACK, str(cut_glb), STYLE,
                    cutaway_um=[-0.8, 1.9, 11.2, 4.7])
build_viewer_html(str(cut_glb), str(OUT / "render3d_cutaway.html"),
                  STYLE, title="demo cutaway", overwrite=True)

生成的三个 .html 都是完全自含、离线的(查看器 JS + 模型数据都编码进同一个文件),双击直接在浏览器打开,拖动旋转,右侧面板能调每层的颜色/金属度/粗糙度/透明度、导出 PNG。

0.5.2 版按两条真实用户反馈修了两处:GLB 导出现在遵循 glTF 规范的 +Y-up 朝向,查看器还放开了 model-viewer 默认 22.5°–157.5° 的俯仰钳制、变成完整的 0°–180°——旋转不再被墙挡住,模型的每一个面都能转到;查看器面板每种材质还新增一条透明度滑条(A,挨着金属度 M、粗糙度 R),把某一层调淡来突出或弱化它——低于 1 材质会自动切到 BLEND 混合模式,回到 1 恢复它声明的模式,Reset 还原成声明时的样子。下面嵌的就是这份 cutaway 查看器——拖满一整圈试试,也拉一下 A 滑条。

这就是 render3d_demo.py 生成的 cutaway 查看器本身,原样嵌在这里:模型先整个建好,再做一次布尔切割,保留 y ≥ 1.9 µm 的一侧,暴露面正好穿过四个栅极;拖动旋转、滚轮缩放,右侧面板调背景/曝光/色调映射/阴影、逐材质改颜色/金属度/透明度,还能导出 PNG。它同样是单文件、离线的——单独打开或拷给同事双击都一样能跑,不连网、不装东西。
render3d_fast GLB 的渲染:一整条矩形 die,顶面均匀分布着四个深红色多晶硅栅极,横跨浅色的有源区条带,栅极之间是接触塞和悬浮在其声明高度上的金属1梯子,全部立在 n-well/衬底板上,透明背景带柔和阴影
这不是自含网页查看器的截图(那是一个你要自己双击打开的离线 HTML 页面)——这是同一个 render3d_fast.glb 模型被 blender_demo.py 的 mode="die" 重新摆到透明薄膜 + 阴影承接面上渲染出的静态图:悬浮在其声明高度上的金属1梯子,架在横跨浅色有源区条带的红色多晶硅栅极之上,栅极之间是接触塞,全部立在 n-well/衬底板上;contact 和 metal-1 都带着这份 stack 声明的 5 度侧壁倾角。

MCP 工具:imaging.render3d——把 klink_visual_stack_v1 声明的每一层拉伸成掩膜实体;旧的 process 扫描相关参数(mode/recipe/exclude 等)在 0.5.8 里已经不存在,换成层声明里的 sidewall_deg(平滑倾角)和调用参数 cutaway_um(先建整模型再布尔切开)。

0.5.8 起,每一次读取 3D 图层都遵守同一条 slit 契约:GDS 里那种0 宽的 keyhole 切割线(画出来的孔存进 GDS 后变成的存储产物)会被静默溶解;宽度 ≥ 1 dbu 的缝隙可能是故意画出来的意图(比如纳米间隙),会被保留并写进结果的 warnings;weld_slits_dbu(默认 0)是显式的焊接开关,用来把确认是假象的窄缝焊死。这条契约同样管着 imaging.xsection_run 和 imaging.blender(figure 模式)读到的每个 3D 图层。新 starter example_template/imaging/keyhole_3d_demo.py 完整走一遍这三条规则。

3SEM 风格顶视

sem_demo.py 把版图渲染成"看起来像"二次电子显微图的顶视图:按 VisualStack 里每层的 sem_grey/edge_glow 出灰阶发射强度和边缘亮边,加光刻圆角、束斑模糊、带种子的胶片颗粒、扫描线、暗角——同一个种子永远出同一张图。

python sem_demo.py
灰度 SEM 顶视图:上下各一条粗的亮灰色电源轨,中间九根亮灰色竖条高度交替排列,横跨一条稍亮的水平有源区带,背景左半接近纯黑、右半是较浅的深灰色,分界线出现在正中间
灰度版(sem_top.png):顶部/底部两条电源轨,中间九根竖条是四个多晶硅栅极和五个金属1接触列交替排列,横跨有源区;背景左半(p 衬底)比右半(n-well)暗——per-layer sem_grey 发射强度决定了这些明暗差。
假彩色 SEM 顶视图:红色竖条(多晶硅栅极)和浅蓝灰色竖条(金属1)交替排列,横跨绿色的有源区带,上下各有一条浅蓝灰色电源轨,背景左侧深蓝黑、右侧橄榄棕色
假彩色版(sem_top_color.png):同一份数据换一种看法——红色是多晶硅栅极,浅蓝灰色是金属1,绿色带是有源区,背景颜色区分 p 衬底(深蓝黑)和 n-well(橄榄棕)。

脚本还按 demo_stack.py 的层顺序累积渲染"目前已印出哪些掩膜"的序列——配合配方的 # klink-step: 顺序,就是顶视版的逐步工艺胶片:

只印过 n-well 掩膜后的顶视:画面几乎是空白的,只能看到一条竖直分界线把左侧较深的区域和右侧略浅的区域分开,没有任何其他可见结构
只印过掩膜 1/0(n-well)之后的顶视(sem_after_1_0.png)——n-well 本身是一次没有自身形貌的注入,这一步画面只是一块被 well 边界分成深浅两块的平坦灰场;有源/多晶/接触/金属这些之后才印的掩膜,此刻完全看不到任何结构。layers= 参数限制到目前已印出的掩膜子集。

MCP 工具:imaging.sem_top——灰度 + 假彩色两个 PNG 一次出,layers=[...] 限制到某一步已印出的掩膜子集,确定性渲染(同种子同结果)。

4Blender 论文级渲染

blender_demo.py 用无头 bpy 出两张图,来自同一批声明:

python blender_demo.py
  • mode="figure":一个二维材料器件,按版图坐标 1:1 摆放——10/0 层是 MoS2 flake,声明 kind="lattice", motif="mos2" 后直接长出真实原子晶格(也支持 motif="graphene");20/0 层是 Au 电极,画成精确的版图棱柱;外加显式的 Si/SiO2 衬底 slab。
  • mode="die":把 render3d_fast.glb(第②节的产物)重新摆到透明薄膜 + 阴影承接面上,就是上面已经看过的那张 die 渲染图。
MoS2 场效应器件的论文级渲染:中间是不规则六边形的原子晶格薄片,两端各连一个金色矩形电极,下方是灰蓝色的 SiO2/Si 衬底切块
blender_figure.png:MoS2 flake 长成真实原子晶格(kind="lattice", motif="mos2"),Au 电极是精确的版图棱柱,衬底是显式的 SiO2/Si slab。

两种模式都额外写一份 .blend(blender_figure.blend / blender_die.blend)——在桌面版 Blender 里打开就能手动调相机、灯光、材质再重新渲染,不是一次性黑盒输出。

MCP 工具:imaging.blender——子进程隔离跑 headless bpy;mode="die" 需要先跑过 imaging.render3d(或 render3d_demo.py)产出的 GLB;mode="figure" 直接吃 gds + stack,需要 bpy(外加 trimesh/klayout-pyxs==0.1.13,如果要用 die 模式)。

物理对不对?

这不是营销话术,是一个可核对的结果:同一份版图,把 .pyxs 配方里接触工序的那几行代码换三种写法,钨塞是否真的出现在生成的 3D 模型里就完全不一样——因果关系在配方顺序里,不在版图坐标里。

变体配方改动模型里的 W plug
A(随包 demo.pyxs 就是这个顺序) ild = deposit(...) → mask(lcontact).etch(0.85, into=ild, taper=5) → tungsten = deposit(...) → planarize(...) z = 0.00 – 0.49 µm:钨塞从硅表面一路灌到 CMP 平坦化后的顶面。
B(删掉接触刻蚀那一行) mask(lcontact).etch(...) 那一行整个删掉 模型里完全没有 W plug 这种材质——没有孔可填,CMP 直接把整层钨磨掉了。
C(把接触刻蚀挪到 ILD 沉积之前) 同样的 etch 那一行,挪到 ild = deposit(...) 之前执行 同样完全没有——是在一层还没沉积出来的膜上开孔,相当于在空气里刻蚀。

三种情况下版图逐字节相同,只是配方顺序不同:B 和 C 都是开路——每个晶体管都断在接触层,整个器件不导通。而且版图视图里什么都看不出来:普通的版图检查工具只看 GDS 里的多边形,这三个变体的 GDS 是同一份文件。只有剖面(imaging.xsection_run)能看见这个断路,因为它真正跑了配方顺序;render3d 的 3D 现在只拉伸 GDS 里的掩膜多边形,跟配方顺序无关,三种变体会拉伸出同一个模型——这正是 0.5.8 把 3D 和 2D 职责拆干净之后的效果。

顺带说一句诚实话:三种情况下金属1都稳稳地待在 z = 0.45 – 0.93 µm——上面几层看起来完全正常,因为金属1的 damascene 工序根本不关心下面的钨塞在不在。失效只在接触层可见,光看版图或只看金属层视图都发现不了。

常见坑:deposit ≠ patterned

动词语义(最容易踩的坑):带图案的沉积是 mask(l).grow(...)——没有 mask(l).deposit() 这种写法;deposit(...) 永远是整面覆盖(blanket)操作。etch(...) 同理也是整面刻蚀;带图案的刻蚀要写 mask(l).etch(...)。demo.pyxs 就是活例子:nwell 用 mask(lnwell).grow(...)(图案化生长),gox/ild/tungsten/imd/metal1 这些用整面的 deposit(...),接触孔和金属1沟槽分别用 mask(lcontact).etch(...) 和 mask(lmetal1).etch(...)(图案化刻蚀)。

确定性 · 产物契约

  • 永不静默覆盖:目标文件已存在时默认拒绝,必须显式传 overwrite=True 才会替换。
  • 每次运行都写机读 sidecar:*.klink_imaging.json,格式 klink_imaging_result_v1——里面有输入的 cut_um/参数、每个输出文件的 sha256、GDS/配方输入的 sha256,脚本或 CI 可以直接读这份 JSON 而不是解析人读的日志。
  • GDS 输出字节可复现:不写时间戳,同样的输入两次跑出字节相同的 GDS。

引擎与查看器归属

剖面由 klayout-pyxs 引擎驱动——klink 把它当依赖导入,固定版本 ==0.1.13,从不 vendor 进仓库;3D 查看器里嵌的是 Google 的 <model-viewer>(vendor 了一份打包好的 JS,这样导出的 .html 才能完全离线打开)。klink 自己写的是 GLB 网格生成、SEM 光栅渲染和 Blender 场景搭建这几层胶水,不是剖面引擎或查看器本身。

下一步

把 demo_layout.py、demo.pyxs 和 demo_stack.py 拷到你自己的项目里,换成你真实的版图、层号/z 范围/颜色和真实的工艺步骤(沉积厚度、刻蚀深度、掩膜层),四个脚本原样重跑。想看物理对不对,就照着上面「物理对不对?」节的做法,在配方里挪一行代码的顺序(比如把某道刻蚀挪到它该在的沉积之前),对比剖面(imaging.xsection_run)跑出来的材质有没有整个消失——材质凭空消失说明配方在真实生效,不是装饰;3D(imaging.render3d)现在只拉伸 GDS 掩膜多边形,看不出配方顺序的差别。四个出口共用同一套 VisualStack 声明,改一次层颜色,剖面/3D/SEM/Blender 四张图都会跟着换。