Runnable code
完整项目见 GitHub 仓库,代码按职责组织如下:
- 完整可运行实验:
autograd-inverse-design.ipynb,包含可微 ASM、Gaussian 输入与目标场、mode-overlap loss、Autograd 梯度、Adam 优化,以及最终光场和三张相位图的可视化。- 正文中的代码片段均摘自该 notebook,并按计算链路拆分讲解。
最近在学习论文 TorchOptics: An open-source Python library for differentiable Fourier optics simulations。
一、问题场景:用三块相位调制器完成光束变换
优化目标
给定一个输入光场和目标光场,不再人工设计每一块相位板,而是把相位分布当成待优化参数,通过光场传播、loss 和梯度下降,自动寻找合适的 phase profile。
论文 Figure 4 给出了一个很小但非常完整的例子:
$$
\text{Gaussian beam}
\rightarrow
M_1
\rightarrow P
\rightarrow
M_2
\rightarrow P
\rightarrow
M_3
\rightarrow P
\rightarrow
\text{four Gaussian beams}.
$$
光学系统布局
其中三块 phase modulator 分别位于
$$
z=0,\quad 0.2,\quad 0.4\ {\rm m},
$$
目标平面位于
$$
z=0.6\ {\rm m}.
$$
数值参数
论文使用 $250\times250$ 的采样网格、$10,\mu{\rm m}$ pixel spacing、$700,{\rm nm}$ 波长和 $150,\mu{\rm m}$ Gaussian waist,并用 Adam、$lr=0.1$ 训练 400 iterations。
初始条件与待优化参数
输入是 waist 为 $150,\mu{\rm m}$ 的单束 Gaussian,目标是在四个指定位置形成四束 Gaussian。三块调制器的 phase profile
$$
\phi_1(x,y),\quad \phi_2(x,y),\quad \phi_3(x,y)
$$
均从零相位开始,因此初始系统只是让输入 Gaussian 在自由空间传播 $0.6,{\rm m}$。优化要寻找这三张 phase map,使输出 complex field 与目标 mode 尽可能重合。
二、整体思路:把传播模型接入梯度优化
TorchOptics 用 PyTorch 把这些步骤全部封装好了。但如果只调用:
1 | loss.backward() |
其实很难真正理解 inverse design 到底发生了什么。
所以这次我不用 TorchOptics,而是只使用:
1 | NumPy |
自己搭出完整链路:
$$
\text{phase}
\rightarrow
\text{optical propagation}
\rightarrow
\text{output field}
\rightarrow
\text{loss}
\rightarrow
\text{gradient}
\rightarrow
\text{Adam}
$$
这里真正想验证的并不是能不能重新造一个 TorchOptics,而是:
一个普通的 Fourier optics simulator,到底怎样变成一个可以做 inverse design 的 simulator?
从 forward simulation 到 inverse design
以前写光场传播时,我解决的问题通常是:
$$
U_0(x,y)
\overset{P}{\longrightarrow}
U_z(x,y).
$$
输入场、传播距离和系统参数都是已知的,我们只计算输出。
例如 Angular Spectrum Method:
$$
U_z
\mathcal F^{-1}
\left[
\mathcal F(U_0)
H_z
\right],
$$
其中 transfer function 为
$$
H_z(k_x,k_y)
\exp
\left[
iz\sqrt{k^2-k_x^2-k_y^2}
\right].
$$
这属于典型的 forward problem:
$$
\text{已知 optical system,求 output}
$$
inverse design 则反过来了。
我们知道输入:
$$
U_{\rm in},
$$
也知道希望得到的目标:
$$
U_{\rm target},
$$
但是不知道中间的 phase modulator 应该长什么样。
于是:
$$
\phi_1(x,y),\quad
\phi_2(x,y),\quad
\phi_3(x,y)
$$
变成待求参数。
问题就变成:
$$
\text{寻找 }
\phi_1,\phi_2,\phi_3
\text{,使 }
U_{\rm out}
\approx U_{\rm target}
$$
这已经和训练神经网络非常像了。
只是我们训练的不是 neural-network weight,而是一个真实光学系统中的 phase profile。
三、建立可优化的光学问题
三平面 phase modulator 的 forward model
每块 phase modulator 的 transmission function 为
$$
M_j(x,y)
e^{i\phi_j(x,y)}.
$$
所以通过一块 phase-only SLM 后:
$$
U’(x,y)
U(x,y)e^{i\phi_j(x,y)}.
$$
三块调制器组成的完整 forward model 为:
$$
U_1
U_{\rm in}e^{i\phi_1},
$$
$$
U_2
P_{z}(U_1)e^{i\phi_2},
$$
$$
U_3
P_{z}(U_2)e^{i\phi_3},
$$
最后
$$
U_{\rm out}
P_{z}(U_3).
$$
用代码写出来反而非常简单:
1 | def forward(U_in, phi1, phi2, phi3, H): |
这里的 H 是传播距离为 $0.2,{\rm m}$ 的 ASM transfer function;三段自由空间传播使用同一个 H。phase_modulate() 与 propagate() 会在后文分别定义。这个函数就是整个 inverse-design forward model。
输入 Gaussian beam
输入使用 Gaussian field:
$$
U(x,y)
C
\exp
\left[
-\frac{
(x-x_0)^2+(y-y_0)^2
}{w_0^2}
\right].
$$
注意这里定义的是 field amplitude,而不是 intensity。
因此:
$$
I(x,y)=|U(x,y)|^2.
$$
代码:
1 | def gaussian_beam(w0, C, X, Y, x0=0.0, y0=0.0): |
采用论文参数:
1 | wavelength = 700e-9 |
所以整个 simulation window 为
$$
L=N\Delta x
250\times10,\mu{\rm m}
2.5,{\rm mm}.
$$
构造 four-Gaussian target
论文 Listing 2 并没有直接生成 target,而是:
1 | target_field = Field(torch.load("target.pt"), z=0.6) |
也就是说,论文没有在 Listing 2 中给出四个 Gaussian 的具体位置参数。
因此这里并不是逐像素恢复论文的 target.pt,而是自己构造一个等价任务:
$$
U_{\rm target}
\sum_{j=1}^{4}
G_j(x,y).
$$
四个 Gaussian 分别位于:
$$
(-d,-d),\quad
(-d,d),\quad
(d,-d),\quad
(d,d).
$$
这里取:
$$
d=0.5,{\rm mm}.
$$
代码:
1 | def four_gaussian_target(X, Y, offset, waist, C): |
因此这里的目标是:
$$
\text{single Gaussian}
\rightarrow
\text{four Gaussian beams}
$$
归一化输入场与目标场
如果直接比较两个 field,它们的总功率也会影响 loss。
但现在真正关心的是:
输出 field 的 spatial mode 是否和 target 一样?
因此先定义 optical power:
$$
P
\iint |U(x,y)|^2dxdy.
$$
离散化以后:
$$
P
\approx
\sum_{m,n}
|U_{mn}|^2
\Delta x\Delta y.
$$
于是做 normalization:
1 | def normalize(U): |
这样:
$$
\iint |U|^2dxdy=1.
$$
输入场和 target 都进行归一化。
Phase modulator 与零相位初始化
Phase-only modulator 不改变 amplitude,只改变 phase:
$$
U_{\rm out}
U_{\rm in}
e^{i\phi}.
$$
所以代码只有一行:
1 | def phase_modulate(U, phi): |
一开始我们完全不知道 phase profile 应该是什么,因此初始化:
1 | phi1 = np.zeros((Ny, Nx)) |
此时:
$$
e^{i\phi}=1.
$$
三块 SLM 什么都没有做。
因此最初的 optical system 实际上只是:
$$
\text{Gaussian}
\rightarrow
0.6,{\rm m\ free\ space}.
$$
显然不可能自动变成四个 Gaussian。
接下来就需要优化。
四、构建可微的前向传播
Differentiable ASM
我这里使用 Angular Spectrum Method 作为 propagation backend。
先把输入场变换到 spatial-frequency domain:
$$
A(k_x,k_y)
\mathcal F{U(x,y)}.
$$
自由空间传播会给每个 plane-wave component 加上不同 phase:
$$
A_z
A_0H_z,
$$
其中
$$
H_z
\exp(i k_z z),
$$
以及
$$
k_z
\sqrt{
k^2-k_x^2-k_y^2
}.
$$
最后:
$$
U_z
\mathcal F^{-1}(A_z).
$$
关键不在 ASM 本身。
关键在于现在:
$$
U
$$
依赖:
$$
\phi_1,\phi_2,\phi_3.
$$
因此 FFT 也必须存在于 automatic differentiation 的 computational graph 中。
这里将固定的 frequency grid 和 transfer function 用普通 NumPy 预先计算,而依赖 phase 的传播路径使用 autograd.numpy:
1 | import numpy as np |
而不能在 differentiable path 中调用普通:
1 | numpy.fft.fft2 |
否则 grad() 会把 phase 包装成 ArrayBox,普通 NumPy FFT 并不知道怎样处理它。
Autograd 官方文档说明它支持大部分 NumPy FFT 操作以及 complex number differentiation,因此这种包含 complex FFT 的 real-valued objective 可以进行 reverse-mode differentiation。
传播距离、波长和采样网格在优化过程中保持不变,因此先在 computational graph 外构造 $H$:
1 | def make_asm_transfer_function( |
这里的 Nx、Ny、dx、dy、wavelength 和 propagation_distance 就是问题场景中已经给出的数值参数。随后才定义可微的 FFT 传播:
1 | def fft2c(U): |
于是 computational graph 变成:
$$
\phi
\rightarrow
e^{i\phi}
\rightarrow
FFT
\rightarrow
H
\rightarrow
IFFT
\rightarrow
U_{\rm out}.
$$
这一步完成以后,普通 Fourier-optics simulator 就变成了一个 differentiable simulator。
五、定义目标函数
Mode-overlap loss
接下来需要回答一个问题:
怎么判断 output 和 target 有多接近?
最直接的方法可能是 intensity MSE:
$$
|I_{\rm out}-I_{\rm target}|^2.
$$
但论文并不是这样做的。
它优化的是两个 complex fields 的 mode overlap。论文 loss 为:
$$
L
1-
\left|
\iint
U_{\rm out}(x,y)
U_{\rm target}^*(x,y)
dxdy
\right|^2.
$$
为了让实现对 field power 更稳健,我这里显式写成 normalized overlap:
$$
\eta
\frac{
|\langle U_t,U_o\rangle|^2
}{
\langle U_o,U_o\rangle
\langle U_t,U_t\rangle
}.
$$
然后:
$$
L=1-\eta
$$
其中:
$$
0\le\eta\le1.
$$
当 output 完全等于 target mode:
$$
\eta=1,
$$
因此:
$$
L=0.
$$
代码:
1 | def mode_overlap(U_out, U_target, dx, dy): |
这里优化的不是简单的 intensity pattern。
而是 complex optical mode:
$$
\text{amplitude + phase}
$$
这也是 mode overlap 相比 intensity MSE 更重要的一点。
Objective function
整个 optical system 现在已经可以看成一个函数:
$$
U_{\rm out}
F(
\phi_1,\phi_2,\phi_3
).
$$
再把 output 送进 loss:
$$
L
L(
U_{\rm out},
U_{\rm target}
).
$$
于是最终:
$$
L=L(\phi_1,\phi_2,\phi_3)
$$
代码:
1 | def objective(phi1, phi2, phi3, U_in, U_target, H, dx, dy): |
这里把输入场、目标场、传播传输函数和积分采样间隔全部显式列出,避免 objective 依赖正文中没有交代的隐式全局状态。
六、从梯度到参数更新
Automatic differentiation
我们需要:
$$
\frac{\partial L}{\partial\phi_1},
\qquad
\frac{\partial L}{\partial\phi_2},
\qquad
\frac{\partial L}{\partial\phi_3}.
$$
每个 $\phi$ 都是一个:
$$
250\times250
$$
matrix。
也就是说单块 phase modulator 就有:
$$
62500
$$
个 optimization variables。
三块一共:
$$
187500
$$
个变量。
手推这样的 gradient 显然没有必要。
Autograd 可以直接根据 computational graph 使用 reverse-mode automatic differentiation。
1 | grad_phi1 = grad(objective, 0) |
grad(objective, 0)、grad(objective, 1) 和 grad(objective, 2) 分别指定对前三个 phase 参数求导;其余参数只作为 forward calculation 所需的固定量传入。
$$
g_1
\nabla_{\phi_1}L,
$$
$$
g_2
\nabla_{\phi_2}L,
$$
$$
g_3
\nabla_{\phi_3}L.
$$
至此最重要的链路已经跑通:
$$
\phi
\rightarrow
e^{i\phi}
\rightarrow
FFT
\rightarrow
propagation
\rightarrow
U_{\rm out}
\rightarrow
L
\rightarrow
\nabla_\phi L
$$
Adam 更新 phase
最后只剩 optimizer。
我没有再引入 PyTorch,而是简单实现了一遍 Adam。
对于 gradient $g_t$:
$$
m_t
\beta_1m_{t-1}
+
(1-\beta_1)g_t,
$$
$$
v_t
\beta_2v_{t-1}
+
(1-\beta_2)g_t^2.
$$
经过 bias correction:
$$
\hat m_t
\frac{m_t}{1-\beta_1^t},
$$
$$
\hat v_t
\frac{v_t}{1-\beta_2^t}.
$$
最后:
$$
\phi_{t+1}
\phi_t
\alpha
\frac{
\hat m_t
}{
\sqrt{\hat v_t}+\epsilon
}.
$$
代码:
1 | class Adam: |
三块 phase modulator 分别维护自己的 Adam state:
1 | adam1 = Adam(phi1.shape, lr=0.1) |
Optimization loop
最终训练循环其实已经非常接近 neural-network training:
1 | objective_args = (U_in, U_target, H, dx, dy) |
其中 num_iterations=400,三个 Adam 实例分别保存三张 phase map 的一阶矩和二阶矩状态。
$$
\text{Gaussian}
$$
经过当前三块 phase masks,
得到:
$$
U_{\rm out}.
$$
计算它与:
$$
U_{\rm target}
$$
之间的 mode mismatch,
再沿着:
$$
-\nabla_\phi L
$$
方向调整所有 SLM pixels。
然后重新传播。
不断重复。
七、理解优化结果
怎样理解训练后的三张 phase map
最终得到:
$$
\phi_1(x,y),
\qquad
\phi_2(x,y),
\qquad
\phi_3(x,y).
$$
它们通常看起来并不像某种简单 lens 或 grating。
这是正常的。
因为 optimizer 并没有被告知:
第一块负责分束,第二块负责聚焦,第三块负责修正。
它只知道最终 objective:
$$
U_{\rm out}
\rightarrow
U_{\rm target}.
$$
因此三块 phase mask 会联合工作。
可以理解为:
$$
M_1,M_2,M_3
$$
共同构成了一个 spatial-mode transformation。
第一块产生新的 spatial-frequency components,传播过程中这些频率分量积累不同 phase,第二和第三块继续重新组织 amplitude 和 phase,最终让目标 plane 上的 complex field 尽可能接近指定 target。
这就是 multi-plane light conversion 背后的基本思想之一。
八、边界、差异与实现注意点
与原论文的差异:这里固定使用 ASM
需要特别说明一点。
论文给出的 propagation-method selection criterion 为:
$$
z_{\rm critical}
\frac{L\Delta x}{\lambda}.
$$
当前:
$$
L=2.5,{\rm mm},
$$
$$
\Delta x=10,\mu{\rm m},
$$
$$
\lambda=700,{\rm nm}.
$$
因此:
$$
z_{\rm critical}
\approx35.7,{\rm mm}.
$$
而每次 propagation distance 为:
$$
z=200,{\rm mm}.
$$
也就是说:
$$
z>z_{\rm critical}.
$$
按照论文的规则,TorchOptics 在这种情况下默认更倾向使用 Direct Integration,而不是 ASM。论文 Listing 2 没有显式指定 propagation method,而是让 System 自己处理。
所以这篇文章更准确的表述是:
用 NumPy + Autograd 重写 TorchOptics Figure 4 的 inverse-design architecture,并使用自己实现的 ASM 作为 differentiable propagation backend。
它不是 Figure 4 的逐像素数值复刻。
如果之后把 propagate() 换成 differentiable DI,其余 optimization pipeline 完全不需要改变。
这一点实际上也很好地体现了系统设计:
$$
\text{propagation model}
\quad\text{和}\quad
\text{optimization framework}
$$
是两个可以独立替换的模块。
为什么优化中的 phase 可以超过 $2\pi$
训练过程中我没有执行:
1 | phi = phi % (2 * np.pi) |
因为真正进入 forward model 的是:
$$
e^{i\phi}.
$$
而:
$$
e^{i(\phi+2\pi n)}
e^{i\phi}.
$$
所以数学上:
$$
\phi
$$
和:
$$
\phi+2\pi
$$
完全等价。
因此 optimization 时允许 phase 自由变化没有问题。
最终显示或映射到实际 SLM 时,再使用:
$$
\phi_{\rm SLM}
\phi\bmod2\pi.
$$
九、总结:从 forward optics 到 inverse design
以前学习 Fourier optics 时,重点通常是:
$$
U_0
\rightarrow
U_z.
$$
也就是:
光经过一个系统以后会变成什么?
但 inverse design 把问题变成了:
$$
\text{target}
\rightarrow
\text{system parameters}.
$$
也就是:
我想得到这个结果,中间的光学系统应该长什么样?
真正连接这两个问题的并不是某个新的 diffraction formula。
Forward physics 仍然是:
$$
FFT
\rightarrow
H
\rightarrow
IFFT.
$$
新增的其实只有三个东西:
$$
\text{objective}
+
\text{gradient}
+
\text{optimizer}
$$
也就是说:
$$
\text{Differentiable Optics}
\text{Forward Optics}
+
\text{Automatic Differentiation}
$$
forward model 决定:
$$
\text{一个设计会产生什么结果};
$$
loss 定义:
$$
\text{什么结果算好};
$$
gradient 告诉我们:
$$
\text{参数往哪个方向修改};
$$
optimizer 则不断执行这个修改。
这次真正从头写完之后,TorchOptics 里面:
1 | loss.backward() |
这两行代码背后的物理和计算过程也就不再是黑盒了。