2. MZI基本原理
MZI 这东西,说白了就是让两束光走不同的路,再合到一块儿看看它们还合不合拍。很多刚接触光计算的同学会觉得这跟物理课本上的干涉实验差不多,嗯,本质确实一样——但工程上怎么分析、怎么用它来控制光,才是咱们关心的重点。
我个人习惯,每次讲 MZI 都从结构开始画起。你先别管那些公式,先把器件的样子记牢了。
2.1 标准 MZI 结构长什么样
一个标准的马赫-曾德尔干涉仪,由四个部分拼起来:
- 输入端口:通常是两个,记作 Input 1 和 Input 2。
- 第一个 3dB 耦合器:把两路输入光按 50:50 分到两个臂上。当然分束比不一定非得 50:50,但光计算里最常用的是 3dB。
- 两个波导臂:一臂上有个 相位调制器(热光或电光),另一臂可能不动,也可能加个固定相移。
- 第二个 3dB 耦合器:把两臂的光再合到一块,从两个输出端口出来。
我在项目里第一次调 MZI 的时候,总觉得相位调制器装在哪个臂上无所谓。后来发现,不同位置对调制的对称性影响很大——你最好把调制臂和参考臂区分清楚,否则后面校准的时候会多花两倍时间。
2.2 传输矩阵——用一个方程看懂干涉
你想想看,两路光经过两个耦合器再加一个相移,最终输出能不能算出来?当然能。传输矩阵就是干这事儿的。
先回忆一下 3dB 定向耦合器的传输矩阵。对于理想无损的 50:50 分束器:
[ 1/√2 j/√2 ]
T_coupler = [ j/√2 1/√2 ]
注意那个虚数单位 j,它表示耦合器内部有一个 π/2 的相移。很多新手算不对干涉结果,就是漏了这个 j。
两臂之间的相移用对角矩阵表示:
[ e^(jφ) 0 ]
T_arm = [ 0 1 ]
其中 φ 是调制臂相对参考臂引入的相位差。有些设计会在参考臂上额外加一个固定相移(比如 π/2),那就在第二个元素上也乘一个 e^(jθ)。
整个 MZI 的传输矩阵就是三个矩阵的乘积:
T_mzi = T_coupler2 · T_arm · T_coupler1
我刚开始做仿真时,总习惯把两个耦合器的矩阵写成一模一样的。其实它们是在不同的位置,但好在如果是相同的 3dB 耦合器,矩阵确实一样。不过注意坐标系——端口顺序别搞反了。
2.3 完整推导:从输入到输出
设输入光场为 [E_in1, E_in2]^T,经过第一个耦合器:
[E_a] [ 1/√2 j/√2 ] [E_in1]
[E_b] = [ j/√2 1/√2 ] [E_in2]
经过两臂相移:
[E_a'] [ e^(jφ) 0 ] [E_a]
[E_b'] = [ 0 1 ] [E_b]
最后经过第二个耦合器:
[E_out1] [ 1/√2 j/√2 ] [E_a']
[E_out2] = [ j/√2 1/√2 ] [E_b']
把这三步合在一起,就得到了输出光场关于 φ 和输入的关系。如果只从 Input 1 输入(E_in1 = 1, E_in2 = 0),输出端的相对光强为:
|E_out1|² = cos²(φ/2)
|E_out2|² = sin²(φ/2)
你看,通过改变 φ,光功率可以从一个端口完全切换到另一个端口。这就是 MZI 做 光学开关或 可调分束器 的基础。
2.4 传输矩阵的 Python 实现
写几行代码跑一下,感觉就实在多了。我一般在调芯片前先用这种脚本扫一遍相位,心里有个底。
import numpy as np
def coupler_3dB():
"""理想3dB定向耦合器传输矩阵"""
c = 1/np.sqrt(2)
return np.array([[c, 1j*c],
[1j*c, c ]], dtype=complex)
def arm_phase(phi):
"""相位调制臂传输矩阵,phi为相对相移"""
return np.array([[np.exp(1j*phi), 0],
[0, 1]], dtype=complex)
def mzi_transfer(phi):
"""整个MZI传输矩阵"""
T1 = coupler_3dB()
T_arm = arm_phase(phi)
T2 = coupler_3dB()
# 矩阵乘法: T2 * T_arm * T1
return T2 @ T_arm @ T1
# 只从端口1输入
E_in = np.array([[1], [0]], dtype=complex)
phis = np.linspace(0, 2*np.pi, 100)
out1_power = []
out2_power = []
for phi in phis:
E_out = mzi_transfer(phi) @ E_in
out1_power.append(np.abs(E_out[0,0])**2)
out2_power.append(np.abs(E_out[1,0])**2)
# 取几个典型相位看一下
for i, phi in enumerate([0, np.pi/2, np.pi, 3*np.pi/2]):
print(f"phi = {phi:.2f} : out1={out1_power[i*25]:.3f}, out2={out2_power[i*25]:.3f}")
这个脚本跑出来后你会看到典型的余弦平方和正弦平方曲线。我记得第一次用这个脚本扫相位时,发现 3π/2 的输出跟 π/2 对称,当时还楞了一下——其实是因为余弦平方的周期是 π。
| 相位 φ | Output 1 功率 | Output 2 功率 | 工作状态 |
|---|---|---|---|
| 0 | 1.000 | 0.000 | 直通(Bar 态) |
| π/2 | 0.500 | 0.500 | 3dB 分束 |
| π | 0.000 | 1.000 | 交叉(Cross 态) |
| 3π/2 | 0.500 | 0.500 | 3dB 分束(另一相位) |
你可能会问:为什么从 Bar 态切换到 Cross 态只需要 π 的相位差?对,这就是 MZI 作为 2×2 可调光开关的核心。但注意,实际芯片因为波导损耗和耦合器非理想,消光比不会那么完美。
2.5 我踩过的几个坑
- 相位调制的方向搞反了:有的设计是加热下臂,有的是加热上臂。我曾经接反过电极,扫相位扫出来完全反着走,查了半天才发现。
- 忽略了热串扰:密集阵列中,相邻 MZI 的热调制会互相影响。我建议你在测试时先把所有不调用的 MZI 设置到一个固定偏置,不要悬空。
- 忘记校准零相位点:传输矩阵里 φ=0 对应 Bar 态,但实际芯片的零偏置点可能不在 Bar 态(因为波导长度差)。所以每次上电都要先做一次 “归零”扫描,找到真正的 Bar 态对应的电压。
一句话记住 MZI 的核心:一个 MZI 就是一个可以连续调节分束比的 2×2 光学单元。传输矩阵把光场和相位的关系变成了线性代数,调芯片时先扫相移,再根据输出功率反推实际相位,百试不爽。