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
2
loss.backward()
optimizer.step()

其实很难真正理解 inverse design 到底发生了什么。

所以这次我不用 TorchOptics,而是只使用:

1
2
3
NumPy
Autograd
Matplotlib

自己搭出完整链路:

$$
\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
2
3
4
5
6
7
8
9
10
11
def forward(U_in, phi1, phi2, phi3, H):
U = phase_modulate(U_in, phi1)

U = propagate(U, H)
U = phase_modulate(U, phi2)

U = propagate(U, H)
U = phase_modulate(U, phi3)

U = propagate(U, H)
return U

这里的 H 是传播距离为 $0.2,{\rm m}$ 的 ASM transfer function;三段自由空间传播使用同一个 Hphase_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
2
3
def gaussian_beam(w0, C, X, Y, x0=0.0, y0=0.0):
R2 = (X - x0)**2 + (Y - y0)**2
return C * np.exp(-R2 / w0**2)

采用论文参数:

1
2
3
4
5
6
7
8
9
wavelength = 700e-9

Nx = 250
Ny = 250

dx = 10e-6
dy = 10e-6

w0 = 150e-6

所以整个 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def four_gaussian_target(X, Y, offset, waist, C):
centers = [
(-offset, -offset),
(-offset, +offset),
(+offset, -offset),
(+offset, +offset),
]

U = np.zeros_like(X, dtype=np.complex128)

for x0, y0 in centers:
U += gaussian_beam(
waist,
C,
X,
Y,
x0,
y0,
)

return U

因此这里的目标是:

$$
\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
2
3
4
5
6
7
8
def normalize(U):
power = (
np.sum(np.abs(U)**2)
* dx
* dy
)

return U / np.sqrt(power)

这样:

$$
\iint |U|^2dxdy=1.
$$

输入场和 target 都进行归一化。


Phase modulator 与零相位初始化

Phase-only modulator 不改变 amplitude,只改变 phase:

$$
U_{\rm out}

U_{\rm in}
e^{i\phi}.
$$

所以代码只有一行:

1
2
def phase_modulate(U, phi):
return U * anp.exp(1j * phi)

一开始我们完全不知道 phase profile 应该是什么,因此初始化:

1
2
3
phi1 = np.zeros((Ny, Nx))
phi2 = np.zeros((Ny, Nx))
phi3 = 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
2
import numpy as np
import autograd.numpy as anp

而不能在 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def make_asm_transfer_function(
Nx, Ny, dx, dy, wavelength, z
):
fx = np.fft.fftshift(np.fft.fftfreq(Nx, d=dx))
fy = np.fft.fftshift(np.fft.fftfreq(Ny, d=dy))
FX, FY = np.meshgrid(fx, fy)

k = 2 * np.pi / wavelength
KX = 2 * np.pi * FX
KY = 2 * np.pi * FY
KZ = np.sqrt(
(k**2 - KX**2 - KY**2).astype(np.complex128)
)

return np.exp(1j * z * KZ)


H = make_asm_transfer_function(
Nx=Nx,
Ny=Ny,
dx=dx,
dy=dy,
wavelength=wavelength,
z=propagation_distance,
)

这里的 NxNydxdywavelengthpropagation_distance 就是问题场景中已经给出的数值参数。随后才定义可微的 FFT 传播:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def fft2c(U):
return anp.fft.fftshift(
anp.fft.fft2(
anp.fft.ifftshift(U)
)
)


def ifft2c(A):
return anp.fft.fftshift(
anp.fft.ifft2(
anp.fft.ifftshift(A)
)
)


def propagate(U, H):
A = fft2c(U)
return ifft2c(A * H)

于是 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def mode_overlap(U_out, U_target, dx, dy):
inner = (
anp.sum(
anp.conj(U_target)
* U_out
)
* dx
* dy
)

power_out = (
anp.sum(anp.abs(U_out)**2)
* dx
* dy
)

power_target = (
anp.sum(anp.abs(U_target)**2)
* dx
* dy
)

eta = (
anp.abs(inner)**2
/ (power_out * power_target)
)

return anp.real(eta)

这里优化的不是简单的 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
2
3
4
def objective(phi1, phi2, phi3, U_in, U_target, H, dx, dy):
U_out = forward(U_in, phi1, phi2, phi3, H)
eta = mode_overlap(U_out, U_target, dx, dy)
return 1.0 - eta

这里把输入场、目标场、传播传输函数和积分采样间隔全部显式列出,避免 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
2
3
4
5
6
7
8
9
grad_phi1 = grad(objective, 0)
grad_phi2 = grad(objective, 1)
grad_phi3 = grad(objective, 2)

objective_args = (U_in, U_target, H, dx, dy)

g1 = grad_phi1(phi1, phi2, phi3, *objective_args)
g2 = grad_phi2(phi1, phi2, phi3, *objective_args)
g3 = grad_phi3(phi1, phi2, phi3, *objective_args)

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class Adam:
def __init__(
self,
shape,
lr=0.1,
beta1=0.9,
beta2=0.999,
eps=1e-8,
):
self.lr = lr
self.beta1 = beta1
self.beta2 = beta2
self.eps = eps

self.m = np.zeros(shape)
self.v = np.zeros(shape)
self.t = 0

def update(self, x, g):
self.t += 1

self.m = (
self.beta1 * self.m
+ (1 - self.beta1) * g
)

self.v = (
self.beta2 * self.v
+ (1 - self.beta2) * g**2
)

m_hat = (
self.m
/ (1 - self.beta1**self.t)
)

v_hat = (
self.v
/ (1 - self.beta2**self.t)
)

return (
x
- self.lr
* m_hat
/ (
np.sqrt(v_hat)
+ self.eps
)
)

三块 phase modulator 分别维护自己的 Adam state:

1
2
3
adam1 = Adam(phi1.shape, lr=0.1)
adam2 = Adam(phi2.shape, lr=0.1)
adam3 = Adam(phi3.shape, lr=0.1)

Optimization loop

最终训练循环其实已经非常接近 neural-network training:

1
2
3
4
5
6
7
8
9
10
11
12
objective_args = (U_in, U_target, H, dx, dy)

for i in range(num_iterations):
loss = objective(phi1, phi2, phi3, *objective_args)

g1 = grad_phi1(phi1, phi2, phi3, *objective_args)
g2 = grad_phi2(phi1, phi2, phi3, *objective_args)
g3 = grad_phi3(phi1, phi2, phi3, *objective_args)

phi1 = adam1.update(phi1, g1)
phi2 = adam2.update(phi2, g2)
phi3 = adam3.update(phi3, g3)

其中 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
2
loss.backward()
optimizer.step()

这两行代码背后的物理和计算过程也就不再是黑盒了。