2、MZI基本原理:马赫-曾德尔干涉仪结构、传输矩阵

MZI 这东西,说白了就是让两束光走不同的路,再合到一块儿看看它们还合不合拍。很多刚接触光计算的同学会觉得这跟物理课本上的干涉实验差不多,嗯,本质确实一样——但工程上怎么分析、怎么用它来控制光,才是咱们关心的重点。

我个人习惯,每次讲 MZI 都从结构开始画起。你先别管那些公式,先把器件的样子记牢了。

2.1 标准 MZI 结构长什么样

一个标准的马赫-曾德尔干涉仪,由四个部分拼起来:

  • 输入端口:通常是两个,记作 Input 1 和 Input 2。
  • 第一个 3dB 耦合器:把两路输入光按 50:50 分到两个臂上。当然分束比不一定非得 50:50,但光计算里最常用的是 3dB。
  • 两个波导臂:一臂上有个 相位调制器(热光或电光),另一臂可能不动,也可能加个固定相移。
  • 第二个 3dB 耦合器:把两臂的光再合到一块,从两个输出端口出来。

我在项目里第一次调 MZI 的时候,总觉得相位调制器装在哪个臂上无所谓。后来发现,不同位置对调制的对称性影响很大——你最好把调制臂和参考臂区分清楚,否则后面校准的时候会多花两倍时间。

个人经验:调试时先让两臂完全等长(无电压),看输出端的光功率。如果输出不是理想的最大或最小,多半是分束比偏了或波导有损耗。别上来就调相位,先排除分束器的问题。
标准马赫-曾德尔干涉仪(MZI)结构 Input 1 Input 2 3dB 耦合器1 相位调制 (热/电光) 参考臂 (固定相移) 3dB 耦合器2 Output 1 Output 2 每个3dB耦合器两侧的相位关系决定了最终干涉结果

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 做 光学开关或 可调分束器 的基础。

注意!上面推导假设耦合器是理想无损、分束比精确 50:50。实际上芯片上做的 MZI 总会有分束比误差和额外损耗。我曾经碰过一个批次,3dB 耦合器实际分束比偏差 5%,导致干涉消光比从 30dB 掉到 12dB,花了两周才找到原因。所以量产前必须单独标定每个耦合器。

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 光学单元。传输矩阵把光场和相位的关系变成了线性代数,调芯片时先扫相移,再根据输出功率反推实际相位,百试不爽。