☰
光束传输分析入门:用LightPipes模拟高斯光束、透镜与孔径的衍射效应
2026/10/8 3:31:44 网站建设 项目流程

我做光学系统设计最烦的不是搭光路,而是搭光路之前脑子里没个“数”。比如客户问:“你这束激光过完那个透镜,放多远光斑最小?透镜前面加个圆孔,光斑边缘会不会毛?”这类问题靠几何光学公式只能估个大概,真要把光束传输分析算到能信的程度,还是得老老实实做衍射计算。LightPipes正是做这件事的库——一个基于标量衍射的轻量级光束传输分析Python工具,用来模拟光场从一个截面到另一个截面的演化。这篇文章我会从零开始,带你装好环境、跑通第一段高斯光束传播、理解它背后的傅里叶逻辑,最后搭一个有透镜和孔径的完整模拟系统,顺便把我实际使用中踩过的坑也一并倒出来。

这套内容适合谁?如果你是研究生、光学工程师,或者做激光器/光路设计时经常要回答“光斑多大、焦点在哪、衍射之后什么样”这类问题,LightPipes会比Zemax轻量得多,也比COMSOL上手快得多。不用懂复杂的电磁场求解,只需要知道振幅、相位、孔径和透镜,就能完成大部分概念级的光束传输验证。

1. 光束传输分析到底要算哪些东西

1.1 光场不是一根线,而是一张二维复振幅图

很多人先入为主地以为光束就是“一条线”,但实际做光束传输分析时,核心对象是一个二维复振幅分布:场中每个点都有一个复数值,实部虚部或者振幅和相位都能描述它。强度就是振幅的平方,而这团复振幅在空间里往前传播的过程中,会因为衍射效应发生横向扩散或重新分布。

LightPipes把这件事简化为一个纯数值过程:先把一个二维网格上的复振幅初始化,然后用“传播算子”作用在它上面,得到新位置上的复振幅。整个过程不需要解微分方程,只需要做傅里叶变换和相位乘法。这也是它速度快、代码短、容易入门的原因。

1.2 LightPipes和Zemax、COMSOL到底有什么区别

我在带新人时,经常被问“有Zemax了,为什么还要用LightPipes?”坦白说,它们解决的问题层次不一样。

工具基本方法适合场景学习成本
Zemax几何光线追迹为主,衍射为辅成像镜头设计、公差分析、照明光路偏高,有专门软件界面和序列/混合模式
COMSOL求解麦克斯韦方程组亚波长结构、波导、表面等离激元、严格电磁场结果高,需要有限元思想和网格收敛概念
LightPipes标量衍射、角谱传播激光束传播、衍射、聚焦、干涉、整形方案验证低,几行代码就能出图

这么说吧:你不需要知道透镜边角的千分之几波长的波前误差,只想看高斯光束过透镜之后焦点大概在哪个位置、焦斑多大、加个孔径会不会出现衍射环,用LightPipes是最快的路径。它是“概念验证”阶段的利器,不是工业镜头设计的最终答案。

1.3 我什么时候会真正掏出LightPipes

我的典型使用场景包括:激光扩束后光斑有多大,能否匹配后级光学口径;光束通过一个硬边孔径后远场衍射图案是什么样;迈克尔逊干涉仪双臂光程差变化时条纹变化规律;还有透镜焦距不匹配时,焦点漂移了多少。这些事情在实验之前先在电脑上跑一遍,能避免大量在光学平台上“瞎调”的时间。

2. 跑通第一段仿真:从高斯光束开始

2.1 环境安装与第一个可运行代码

LightPipes的安装比想象中简单,一条pip命令就能解决。建议新建一个虚拟环境,避免跟其他包版本打架。

python -m venv lp_env source lp_env/bin/activate # Windows下是 lp_env\Scripts\activate pip install lightpipes matplotlib numpy

这里lightpipes会自动把numpy等核心依赖带上来,matplotlib是拿来画图的。装完可以用一句导入验证:

from LightPipes import * print(Begin)

如果能看到Begin函数对象而不是报错,说明环境OK。

接下来是入门必跑的“Hello World”——一束高斯光束在自由空间里传播:

from LightPipes import * import matplotlib.pyplot as plt # 参数:波长、网格宽度、像素数、束腰、传播距离 wavelength = 632.8*nm size = 12.0*mm N = 256 w0 = 1.0*mm z = 0.5*m # 初始化光场,生成高斯光束,传播 F = Begin(size, wavelength, N) F = GaussHermite(F, w0) F = Propagate(F, z) # 取强度并画图 I = Intensity(F) plt.figure(figsize=(6,5)) plt.imshow(I, cmap='jet', extent=[-size/2, size/2, -size/2, size/2]) plt.colorbar(label='Intensity (a.u.)') plt.xlabel('x (mm)') plt.ylabel('y (mm)') plt.title('Intensity after 0.5 m') plt.show()

这七行代码干的事,相当于你在实验台上放了一个激光器、一个扩束器(把束腰弄成1mm),然后拿探测器看0.5米后的光斑强度分布。

2.2 几个最容易造成困惑的“基本单位”问题

LightPipes里最让我一开始不适应的是单位。它不是像matplotlib那样直接按像素坐标处理,而是希望你用nm、mm、m这样的预定义常量和具体数值相乘。也就是说,632.8*nm实际是一个浮点数,只是利用常量把纳米换算成了内部单位制(通常所有长度最终都要统一成米)。

这个设计看起来多此一举,实际上很省心。我在代码里写波长用632.8*nm,写口径用5*mm,写传播距离用20*cm,一眼就知道对应物理量,也避免算错量纲。如果你不习惯,也可以直接写0.0006328(毫米),结果一样,但可读性差很多。

Begin(size, wavelength, N)里的三个参数也很关键:size是网格的物理宽度,wavelength是波长,N是网格像素数。像素尺寸dx = size / N,这会直接影响后续传播是否出现混叠或数值误差,第3节我会详细讲。

2.3 GaussHermite是啥,为什么入门用它

GaussHermite这个名字看着高级,其实它生成的就是一个高斯光束横模。基模实际上就是最常见的TEM00高斯光束:横向振幅分布服从高斯函数,束腰半径w0定义的是光强下降到峰值1/e²处的半径。之所以起名叫GaussHermite,是因为高阶横模(TEM01、TEM10等)在数学上对应厄米高斯函数,而TEM00就是它的最低阶。

初学者做光束传输分析,高斯光束几乎是必选起点:它有解析解,有瑞利长度概念,方便和仿真结果互相验证。用LightPipes生成一束高斯光,只要指定束腰w0即可,它会在网格中心生成一束对称的高斯分布场。

3. 传播原理与网格参数:不搞懂这些后面全是坑

3.1 为什么Propagate能模拟光束往前走

LightPipes的传播逻辑是经典的角谱法。简单理解:任意一束光可以被看作无数个不同方向传播的平面波的叠加。这就像你把一个复杂的声波拆成不同频率的简谐波一样,不过这里拆的是“方向”。

当光场在一个平面上时,横向的复杂分布被FFT分解为不同空间频率的分量,频率就对应平面波传播方向偏离光轴的程度。比如一个非常细的针孔衍射出来的光,横向变化剧烈,那么它的空间频率谱会很宽,也就是说它“包含”了各种传播方向。而一束很粗的准直高斯光,横向变化平缓,频率谱窄,主要能量都集中在接近光轴的方向。

自由空间传播一段距离,各平面波在z方向积累的相位不同。沿光轴的平面波走得“最短”,斜着走的平面波等效路程长一些,相位变化快一些。在频域里把这个相位因子乘上去,再逆傅里叶变换回空间域,就得到了传播后的光场。这个过程在LightPipes里被封装成了Propagate。

3.2 近轴近似在哪一步发生了

上面说的精确角谱传播,频率和传播方向的关系是圆形的余弦关系。但LightPipes默认采用近轴近似:假设光束的主要传播方向是z轴,所有平面波与z轴的夹角都不大。这种近似下,频域传递函数可以化简为带二次相位的形式,也就是菲涅耳衍射积分对应的频域版本。

这意味着:如果你模拟一束发散角特别大的光,或者在很短的传播距离内看非常陡的相位变化,近轴近似的误差会明显增大。实际工程里,绝大多数激光光束是近轴的,发散角通常只有毫弧度到几度,LightPipes完全够用。真到了强聚焦或大角度衍射的情况,我会先做个粗略分析:如果模拟结果出现不合理的振荡或条纹,首先怀疑是不是近轴近似失效,而不是直接怪代码。

3.3 网格大小、像素数和混叠的三角关系

这是LightPipes所有数值问题里最核心的一个。网格宽度size决定你观察窗口有多大,像素数N决定空间分辨率,两者共同决定频域的最大空间频率。FFT能表示的最高横向空间频率是1/(2dx),其中dx=size/N。如果光场在频域里的分量超过了这个范围,就会发生频率混叠,表现为仿真图上出现周期性的虚假干涉条纹。

我通常用几条经验来判断参数是否合理:

  • 网格宽度至少要是光束直径的3倍。如果生成一个束腰1mm的高斯光,网格宽度8mm到12mm是合理的,太小则光斑边缘会被网格边界截断。
  • 传播距离越大,衍射越严重,光斑在输出面可能很宽,所以网格宽度还要考虑衍射后的扩展尺度。
  • N不是越大越好。每增大一倍像素数,内存和计算时间大约变成四倍。对于常见仿真,256到512已经能给很干净的图。

举个实际的判断例子:一束束腰0.5mm、波长632.8nm的高斯光,瑞利长度约1.24m。如果你让它传播10m,光斑会扩到约4mm半径,此时网格宽度至少得给到20mm以上,否则光会冲出窗口。

3.4 传播距离过大时,分段传播是一个更稳的做法

Propagate不是魔法,它默认一次走完整段距离。当传播距离很大时,简单的一次傅里叶变换可能造成较大的数值误差。工程上有一个很实用的技巧:把总距离分成多段,每段调用一次Propagate,这样减少单步的相位变化范围,能显著提高稳定性。

total_z = 5*m steps = 20 dz = total_z / steps F = Begin(size, wavelength, N) F = GaussHermite(F, w0) for _ in range(steps): F = Propagate(F, dz)

我在模拟长距离大气传或公里级光传播时一定会这么做。至于分多少步,没有绝对标准,通常看结果是否随分段数增加而变化,如果分段数加倍以后结果变化很小,就说明步长够了。

4. 在光路上加元件:孔径、透镜和组合操作

4.1 孔径命令:用硬边切掉光斑

实验光路几乎总有孔径元件,不管是镜架口径、光阑还是光学件本身的通光口径。LightPipes里切圆孔最简单的是CircAperture:

F = Begin(size, wavelength, N) F = GaussHermite(F, w0) F = CircAperture(F, 1.5*mm) # 通光半径1.5mm的圆孔 F = Propagate(F, 0.3*m)

孔径的本质是对光场振幅乘一个二值函数:孔内乘1,孔外乘0。这个操作非常符合物理直觉——硬边孔径会带来衍射效应,孔边缘会把能量散射到旁边,远场出现一系列交替的亮暗环。

如果你做矩形光阑或方形狭缝,用RectAperture(F, width, height)即可。孔径半径千万不能设得比光束还宽太多,否则等于没加;也不要设得太小,否则衍射太强,可能很快超出你设定的网格窗口。

4.2 透镜命令:本质上只是乘法

薄透镜在近轴光学中可以等效为一个纯相位调制器件:光穿过透镜后,各点的相位被重新分布,中心相位落后最多,边缘相位改变较小,这种二次相位分布使得平面波会聚到焦点。

LightPipes中的Lens命令就是这个相位调制:

f = 500*mm F = Lens(F, f)

需要注意,Lens只改变相位,不改变振幅。所以如果你只看强度,透镜命令本身什么效果都看不到,必须在透镜后加一段Propagate,聚焦效果才会出现。很多新手问“为什么加了透镜强度没变化”,就是这个原因。

4.3 组合操作:构建一个真实光路

把元件按顺序串起来,是LightPipes最自然的用法。因为每个操作都接收一个光场对象并返回新的光场,所以可以直接链式调用。比如一个典型的“高斯光束 -> 圆孔 -> 透镜 -> 传播”系统:

F = Begin(size, wavelength, N) F = GaussHermite(F, w0) F = CircAperture(F, 2.0*mm) F = Lens(F, f=300*mm) F = Propagate(F, 400*mm) I = Intensity(F)

这段代码对应的物理过程是:束腰1mm的高斯光先被半径2mm的光阑挡掉边缘能量,然后经过焦距300mm的透镜,在透镜后400mm处观察光斑。

组合操作的顺序非常重要,必须按照光在空间里实际通过的先后顺序来写。比如先透镜再孔径,和先孔径再透镜,结果完全不同。前者是透镜聚焦后受孔径限制,后者是光束先被切边再聚焦,衍射效应更明显。每写完一长串命令,我会在纸上画出光路逻辑,再对照代码检查顺序。

5. 集成仿真:单透镜聚焦系统的焦距验证

5.1 设计一个可验证的仿真目标

只看单步命令没意思,一定要串成一个有物理结论的案例。我选了一个最典型也最适合验证的:高斯光束经透镜聚焦,找焦点位置和焦斑大小。

仿真参数设置如下:

  • 波长:632.8nm
  • 网格宽度:16mm
  • 像素:512
  • 入射高斯束腰:1mm
  • 透镜焦距:500mm
  • 透镜前传播距离:400mm
  • 透镜后传播距离:从300mm到800mm扫描

理论预期有两个:一是焦点应该在透镜后某个位置,二是在焦点处光斑会有一个最小值。用高斯光束的q参数或ABCD矩阵可以算出:入射高斯光束的q = 400mm + iz_R,z_R = πw0²/λ ≈ 4.96m。经过焦距500mm的透镜后,新的束腰位置大约在透镜后1m附近。这个值超出了扫描范围也没关系,我们更关注仿真是否能体现“有一个聚焦极小值”。

5.2 完整代码:扫描传播距离并记录光斑状态

from LightPipes import * import numpy as np import matplotlib.pyplot as plt wavelength = 632.8*nm size = 16.0*mm N = 512 w0 = 1.0*mm f_lens = 500*mm z_before_lens = 400*mm z_list = np.linspace(300*mm, 800*mm, 31) peak_intensity_list = [] spot_radius_list = [] for z_after_lens in z_list: F = Begin(size, wavelength, N) F = GaussHermite(F, w0) F = Propagate(F, z_before_lens) F = Lens(F, f_lens) F = Propagate(F, z_after_lens) I = Intensity(F) peak_intensity_list.append(I.max()) # 用二阶矩或简单找50%峰值半径近似 # 这里取强度最大点所在行,找到半高全宽对应的半径 ix, iy = np.unravel_index(np.argmax(I), I.shape) line = I[ix, :] half_max = line.max() / 2 xpix = np.where(line > half_max)[0] radius_pix = (xpix[-1] - xpix[0]) / 2 dx = size / N spot_radius_list.append(radius_pix * dx / mm) plt.figure(figsize=(10,4)) plt.subplot(1,2,1) plt.plot(z_list/mm, peak_intensity_list, 'o-') plt.xlabel('Distance after lens (mm)') plt.ylabel('Peak intensity (a.u.)') plt.grid(True) plt.subplot(1,2,2) plt.plot(z_list/mm, spot_radius_list, 's-') plt.xlabel('Distance after lens (mm)') plt.ylabel('Spot radius (mm)') plt.grid(True) plt.tight_layout() plt.show()

运行后你会看到两条趋势相反的曲线:峰值光强在某个距离达到最大值,光斑半径在同一个距离达到最小值。这个位置就是系统的“有效焦点”。在LightPipes里这个结果非常直观,你能同时看到强度大小和光斑宽度两个指标。

5.3 怎么判断仿真是否可信

判断仿真结果不是“画出来了”就完了。我建议至少做三件事:

第一,峰值光强曲线应该光滑,如果出现剧烈抖动,说明网格尺寸或者传播步长有问题。第二,焦点位置的量级应该符合理论估算,如果偏离很大,先看是不是网格宽度太小导致高频衍射成分被截断。第三,焦斑尺寸应该是亚毫米或毫米量级,如果焦点处光斑仍然和入射一样宽,说明透镜焦距或传播距离设置计算可能不对。

更严格的做法是更换网格像素数:把N从256改成512再跑一遍,结果如果差不多,说明数值解已经收敛;如果变化巨大,说明原参数不够,需要加大网格宽度或减少单步传播距离。

5.4 进阶一点:模拟中加入孔径以观察衍射环

下一个很容易扩展的方向是在透镜前加一个孔径。比如加一个半径1.5mm的圆孔,原本高斯光束的边缘已经被孔径切掉一部分,透镜聚焦后焦平面的光斑就不是纯高斯形状,而是会出现旁瓣。这在实际工程里非常常见,比如激光通过变倍扩束镜的镜架口径、经过光阑后聚焦,光斑周围总是带着一点点光环。

你可以用放大模式观察焦点附近的强度图,或者在imshow时用norm=LogNorm()把旁瓣细节显示出来。这会让你对“孔径衍射 + 透镜聚焦”叠加的效果有很直观的理解。

6. 我踩过的坑和一张检查表

6.1 版本差异和函数调用顺序

LightPipes发展到现在,不同小版本之间的API发生过变化。同一个Propagate,有些文档示例是Propagate(F, z),早期版本则是Propagate(z, F)。我第一次跑别人代码时就被这个坑过。最稳妥的办法是装好之后立刻看本地帮助:

from LightPipes import * help(Propagate)

以你本机的签名和示例为准。后面再上网搜代码时,遇到老版本写法不要生搬硬套,先确认自己版本里函数的参数顺序。

6.2 Phase图的一万个坑

Phase(F)函数返回的相位值在(-π, π]之间,画出来会有很多彩虹色跳变条纹,看起来好像光场相位的空间变化非常剧烈,其实那只是相位卷绕造成的假象。

真实物理上的相位是连续的,数值上只是被“折叠”进了一个周期区间。如果你需要分析波前形状,用np.unwrap在单行或单列方向展开相位会更接近真实情况。对于二维相位,正式的光学相位解包裹算法比较复杂,但做入门分析时,沿中心行画一维相位曲线已经能看出很多问题。

6.3 性能:别把N堆到无限大

我曾经为了让图“看起来更细腻”把N设为2048,结果一次传播算了好久,而且内存占用相当大,其实808或者512画出来已经足够光滑。LightPipes的核心运算主要是FFT,复杂度是O(N² logN),像素数翻倍,计算时间可能翻三到四倍。

如果多次扫描大量传播距离,我建议先N=256跑通流程,确认物理结果合理后,再用N=512或1024做最终精度验证。这样迭代快,也不容易在调参时干等。

6.4 常见问题速查表

现象原因解法
图上有规律斜条纹混叠,网格宽度或像素数不合理增大size,或增大N,或分段传播
光斑边缘被截断网格窗口小于光束实际扩散范围增大size
加了Lens但强度没变化透镜只改相位,需要Propagate后才显现透镜后接传播
传播后光斑变形严重近轴近似失效或网格太小缩短单步距离,验证近轴条件
焦点位置和理论差很远透镜前传播距离、焦距单位/量级错误检查单位,减少分段步长
图像中心出现NaN或异常值某些命令生成了负振幅平方或网格异常检查是否用了非物理参数

这张表是我调代码时最常翻的。你可以把它贴在手边,遇到异常先排查这几项,大部分问题都能找到方向。

说回开头那个“激光过透镜焦点在哪”的问题——用LightPipes跑一遍,你得到的不是一个粗略估计,而是一整套强度分布图和数据。它能帮你直观看到孔径的影响、衍射的边缘、焦点的位置,而且全部在几分钟内完成。我的个人体会是,做光束系统设计的人工具箱里值得常备这么一个小工具:它不见得能替代专业光学仿真软件,但绝对能让你在第一步走得又快又稳。最后分享一个小习惯:所有仿真参数写进代码时都带上单位常量,哪怕多敲几次键盘,也比某天因为单位换算错误多折腾俩小时划算。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询