对话式分步教程 · 硅光 · Port 与布线

gdsfactory 硅光:三种拿到端口的方式 + 拖动重连

要布线,先得让 klink 知道每个器件的端口在哪。这一篇走三条拿到 klink Port 的路:手绘三角形转换(自定义器件)、标准 gdsfactory 自动端口PDK 黑箱按 stub 约定 harvest,中间放一个"三角形底边画错"的反面教材;拿到端口后,再走一遍 SEND 选区 → 连线 → 移动器件 → 重新布线的交互闭环。每一步都写清楚:agent 说了什么、调了哪个工具、返回什么结果,配当时的真实 KLayout 截图。

这是给人和 agent 同时看的:每一轮先用人话说清在做什么,再给出具体的工具调用和返回值。所有截图都是本次会话在实时 KLayout 里真跑出来的——版图画布用 view.screenshot 离屏渲染,只有需要展示工具栏按钮(SEND)那一张是整窗口截图。演示只在一次性 tab 里进行,不碰任何已有工作 tab;用到的是几个普通图层(1/0 波导、12/0 布线、999/99 端口标记),不含任何真实工艺 PDK。本篇全部场景也打包成了可跑 starter:python example_template/photonics/gf_ports.py --port <端口>,拖动器件后 --reroute 只重连不重建。

第一部分 · 三种拿到 Port 的方式

用户
我画了一个自定义器件(不是 PDK 里的),左右各有一条 0.8 µm 的接入波导。帮我在两个波导口上标出端口——我在口上各画了一个小三角形指着外面。
klink agent

手绘端口的约定很简单:一个三角形(3 个点的多边形)就是一个端口——三角形的尖端指向就是端口朝向,三角形的底边长度就是端口宽度。所以底边要和波导等宽(这里 0.8 µm),不能随手乱画。你画的两个三角形底边都正好压在 0.8 µm 波导口上、尖端朝外,标准。下面是转换之前的样子(黄色就是你手绘的三角形):

shape.insert_boxes(器件体 + 两条 0.8µm 波导 → 1/0) shape.insert_many(两个三角形 marker → 999/99,底边=0.8µm,尖端朝外) view.screenshot(bbox_um=[-13,-4,13,4])
一个粉色自定义器件,左右各一条细波导,波导末端各有一个黄色三角形,尖端分别指向左和右
Turn 1 · 自定义器件 + 两个手绘三角形端口标记(黄色)。三角形底边 = 波导宽度 0.8 µm,尖端指向端口朝向。

然后一次调用把三角形转成标准 klink Port——它读每个三角形的几何,推出朝向、宽度、位置,标成正式 Port,再把原始三角形删掉:

recognize_handdrawn_ports(cell="GFPORTS", layer="999/99", port_type="optical") → P0 center=[-10,0] orient=180° width=0.8 attached=True → P1 center=[ 10,0] orient=0° width=0.8 attached=True
同一个器件,两个黄三角形不见了,波导末端变成标注 klink_port.P0 和 klink_port.P1 的红色标准端口标记
Turn 1(续)· 转换后:两个标准 Port P0 / P1,宽度精确 = 0.8 µm,朝向 180°/0°。因为底边正好压在波导边上,klink 还把它们识别成贴边端口attached=True),路由时可以沿边滑动。
用户
如果三角形底边画得不对会怎样?比如底边画得比波导宽很多。
klink agent反面教材

好问题——正因为底边=宽度、尖端=朝向,随手乱画就会翻车。这里在一条 0.8 µm 波导上画一个底边 2.5 µm(比波导宽三倍多)的三角形,转换结果就全错了:宽度被读成 1.95 µm(和波导对不上)、朝向被"长边启发式"带偏成 270°(指向下,而不是朝外的 0°)、而且因为底边压不到波导边,端口是悬空的(attached=False)。

shape.insert_many(三角形 marker,底边=2.5µm ← 画错了) recognize_handdrawn_ports(cell="GFERR", layer="999/99") → P0 center=[0.75,0] orient=270°(✗应为0°) width=1.95(✗应为0.8) attached=False(✗悬空)
一条细波导,末端画着一个明显过大的三角形
底边 2.5 µm 的错误三角形(比 0.8 µm 波导宽太多)。
转换后端口是一个远大于波导的方块,悬在波导末端,明显不匹配
转出来的 Port 是个 ~2.5 µm 的大方块,悬在 0.8 µm 波导口——尺寸、位置、朝向全错。

结论:手绘端口时,三角形底边一定要和波导等宽、尖端明确朝外。klink 不会替你猜你想要的宽度——它如实地把你画的几何变成端口。画得准,端口就准。

用户
如果我用的是标准 gdsfactory 器件呢?比如一个 1×2 MMI 分束器。
klink agent

标准 gdsfactory 器件自带端口定义,就不用手绘了。放置组件时,klink 直接从 gdsfactory 的 Port 对象把端口自动标成 klink Port,位置、朝向、宽度都跟着组件走。一个 mmi1x2 有 3 个口:输入 o1 朝左、两个输出 o2/o3 朝右。

place_gdsfactory_components(cell="GFAUTO", [{component:"mmi1x2", center_um:[0,0], port_nets:{o1:"in",o2:"out0",o3:"out1"}}]) → SPL1.o1 center=[-10,0] orient=180° width=0.5 → SPL1.o2 center=[15.5,0.62] orient=0° width=0.5 → SPL1.o3 center=[15.5,-0.62] orient=0° width=0.5
一个 gdsfactory MMI 1x2 分束器:左侧输入锥形波导、中间 MMI 体、右侧两个输出锥形波导,三个端口自动标记
Turn 3 · 标准 gdsfactory mmi1x2:3 个端口自动标成 klink Port(o1 输入 / o2 o3 输出),宽度 0.5 µm = gdsfactory 默认波导宽。没有一笔手绘。
用户
那 PDK 给的黑箱器件呢?它就是一坨不透明几何,端口在哪 klink 怎么知道?
klink agent

foundry PDK 的黑箱器件不给你 gdsfactory Port 对象,但通常遵守一个约定:在波导层的 cell 边界上放一个个小 stub 方块(一种常见约定是 0.5×0.5 µm 的方块),每个 stub 就是一个光学口。klink 的做法是:你把约定告诉它(波导层是哪层、stub 多大),它就从活的实例几何里 harvest 出端口——端口是派生数据,器件在 GUI 里挪了,重新 harvest 一下就刷新。下面用一个合成的"代工厂风格"黑箱演示(不透明体在 60/0,两个 0.5×0.5 stub 在波导层 1/0):

port.harvest_blackbox(cell="GFBB", tags={"SYNTH_BB":"bb"}, wg_layer="1/0", stub_size_um=0.5) → bb0_0 center=[-4.25,0] orient=180° → bb0_1 center=[ 4.25,0] orient=0°
一个方形黑箱器件轮廓,标注 SYNTH_BB,左右边界各有一个红色端口标记 bb0_0 和 bb0_1
Turn 4 · 合成黑箱 SYNTH_BB:按 stub 约定 harvest 出 2 个端口(bb0_0 / bb0_1),朝向从 stub 位置推出。
这套 harvest 机制是按约定、逐 PDK 配置的——不同 foundry 的 stub 层号和尺寸不一样,所以约定由你从自己的 pdk.py 传入,klink 本身不带任何默认。真实 foundry PDK 的黑箱走的是完全相同的这条路,只是把 wg_layer / stub_size_um 换成那家 PDK 的真实值。(本站不发布任何具体 foundry 的 PDK 内容。)

第二部分 · SEND → 连线 → 移动 → 重连

拿到端口之后,就能走 klink 的交互闭环了。下面把上面的自定义器件(手绘端口 P0)和一个 gdsfactory mmi1x2 摆在一起,让它们的两个口对着、共一个 net link0,然后连起来、挪一个、再连。

用户
这两个口我在 KLayout 里框选中了,用工具栏的 SEND 发给你——把它们连起来。
klink agent

你点 SEND 之后,这次选中的两个端口就进了我的会话记忆,拿到一个持久 id——之后你说"我刚发的这两个口",我就能准确对应到它们,不用你再报坐标。这两个口都在 net link0 上(左边是自定义器件的 P0,右边是 MMI 的输入 o1)。

selection.send_context(source="gf_tutorial") → {status:"sent", count:2, send_seq:2} # 两个 link0 端口进入 agent 记忆
KLayout 工具栏,SEND 按钮被红框圈出并有箭头指向说明;左侧 cell 树列出 klink_port.P0 (link0) 和 MMI 的端口
Turn 5 · 点 SEND(红框为标注)把两个 link0 端口发给 agent。左侧 cell 树能看到 klink_port.P0 (link0) 和 MMI 的三个端口。
用户
好,连线吧。
klink agent

两个口都在 link0、正好相对,直接布一条波导连上。用 gdsfactory 的 bundle 路由后端,写回 12/0 布线层。这一步之前它们中间是空的(见下图左),布完就接上了(见下图右的深色波导)。

route_gdsfactory_ports(cell="GFLOOP", route_layer="12/0", all_two_port_nets=True) → {ok:true, MMI.o1 → P0, length_um:40, crossings:0}
左边自定义器件、右边 MMI,两个端口相对,中间有一段间隙没有连接
连线前:两个 link0 端口相对,中间是空的。
同样的两个器件,中间的间隙现在被一条直波导连上了
连线后:一条 40 µm 直波导(12/0)把两口接上,0 交叉。
用户
我把左边那个器件往上挪了 20 µm。
klink agent

器件挪了,但刚才那条布线还钉在旧坐标——现在它一端悬空了,和挪走的器件对不上。这就是"刚拖完、还没重新布线"那一刻:布线过期了。(这里用一次有记录的 exec.python 复现"拖动"这个 GUI 动作——只改器件位置,不动布线、不动 net。)

exec.python(把器件 A 的几何 + 它的 P0 端口标记 上移 20µm) # 布线仍停在 y=0,没有跟着动 → 过期
左边器件明显移到了上方,原来的水平布线还停在下方,一端悬空没有连到器件
Turn 7 · 器件 A 上移 20 µm 后:旧布线(底部深色横线)还停在原处,一端悬空——布线已经过期,肉眼可见。
用户
重新连一下。
klink agent

同一个布线调用重跑一次——它从活的端口位置重新读(P0 这时已经在新坐标上了),把旧布线换成一条跟到新位置的波导。器件挪了,线跟上了,一条平滑的 S 弯接上,其它什么都没动。

route_gdsfactory_ports(cell="GFLOOP", route_layer="12/0", all_two_port_nets=True) → {ok:true, MMI.o1 → P0(新位置), length_um:60} # 从 40µm 直线变成 60µm S 弯
左边上移的器件现在通过一条平滑的 S 形波导连回到右边的 MMI,版图重新完整
Turn 8 · 重新布线后:一条平滑 S 弯波导从上移后的器件连回 MMI 输入——组件挪了,波导跟着重连。这套"标端口 → 布线 → 拖动 → 重连"就是硅光日常迭代版图的核心循环。

验证,不是截图

核心思路一样,截图是给人看的,真正的完成依据是结构化返回。这一篇里每一步都有可核对的数字:手绘端口转换后 width=0.8(精确 = 波导宽)、反面教材里 width=1.95 / orient=270° / attached=False(如实反映画错的三角形)、自动端口 3 个 / 黑箱 harvest 2 个、连线 length=40 → 重连 length=60crossings=0。图只是让人一眼看懂,判断成没成看的是这些返回值。

下一步

三种拿端口的方式按场景选:自己画的器件用手绘三角形(记住底边=宽度);标准 gdsfactory 器件自动就有端口;foundry 黑箱按它的 stub 约定 harvest。拿到端口后,SEND / 连线 / 拖动 / 重连的闭环对三种端口一视同仁。想看一个完整 gdsfactory 脚本被一次接管进这个闭环(含热光 MZI、倾斜光栅耦合器、电学 net),看 gdsfactory MZI 接管教程;想看 agent 把一句话变成器件 / DRC / LVS 的更多实战对话,看 教程页顶部的实战对话案例