第 10 章:OPD 与波前误差
一个点列图告诉我们光线落在哪里。
这很有用。非常有用。在第 7 章中,点列图是我们第一个真正的分析图。在第 9 章中,光线扇形图帮助我们看清实际光线如何偏离近轴参考。我们可以观察横向光线误差,开始识别球差、彗差、像散、场曲、畸变和色差效应。
但这种观察方式存在局限。
一条光线可以落在接近正确位置的地方,而仍然带着错误的相位到达。
这句话是进入本书下一部分的门。
几何光线追踪跟踪光线去向。衍射成像关心光场如何叠加。要用傅里叶方法计算 PSF,我们不仅需要一组光线交点坐标,还需要一个复瞳函数。要建立这个瞳函数,我们需要瞳面上的相位。要得到相位,我们需要光程差。
这就是桥梁:
实际光线追迹
→ 光程长度
→ 光程差
→ 波前误差
→ 瞳面相位
→ PSF
→ OTF
→ MTF
本章是关于这座桥梁的中间部分。
我们将探讨:
什么是光程长度?
什么是 OPD?
什么是参考波前?
为什么 OPD 通常显示在瞳面上?
我们如何在 Python 中创建一个简单的 OPD 图?
这如何连接到 Optiland 的波前分析?
本章不会从完整的光线追迹中计算具有生产质量的 OPD。这需要谨慎的参考几何、出瞳坐标、光线瞄准、焦点约定以及逐面的光程记录。真正的程序库会处理这些细节。例如,当前 Optiland 的波前工具包括 OPD 和波前分析模块,其 Wavefront 类文档记录为根据所选策略计算出瞳光线交点、OPD、光线强度和参考球半径。(Optiland)
我们在这里的工作是理解这些工具在计算什么。
因此,我们将做两件事:
1. 小心地构建概念。
2. 自己生成简化的 OPD 图。
一旦你理解了 OPD 图,第 11 章就可以将其转换为复瞳函数。
那就是几何光学变成傅里叶光学的时刻。
为什么光线落点不够
想象两条光线到达像平面上同一点。
仅从点列图来看,它们看起来完美。它们落在一起。
但假设一条光线走过的光程比另一条稍长。那么当波干涉时,它们的相位可能不匹配。如果相位差很小,它们大部分是相长干涉。如果相位差很大,它们可能部分抵消或在衍射图样中产生结构。
点列图无法显示这一点。
点列图记录横向位置:
x_image, y_image
波前分析记录瞳面上的光程差:
OPD(Px, Py)
这些是不同种类的数据。
光线交点坐标回答:
光线去了哪里?
OPD 回答:
每个瞳面点相对于参考携带了多少光学相位延迟?
两者都很重要。
对于有严重像差的镜头,点列图可以提供很多信息。对于一个接近衍射极限的镜头,点列图可能变得不那么揭示性。几何光斑可能很小,但波前相位仍然决定详细的衍射图样。
这就是为什么我们需要 OPD。
它是光线追迹和 PSF 之间缺失的量。
光程长度
一束光在空气和玻璃中传播时,不仅覆盖了几何距离,还累积了光程长度。
对于折射率为 n 的介质中长度为 ds 的一小段,光程贡献是:
dOPL = n ds
沿整个路径:
OPL = ∫ n ds
在有离散分段的光线追迹器中,这变为:
OPL = n1 s1 + n2 s2 + n3 s3 + ...
其中:
s1, s2, s3 = 几何段长度
n1, n2, n3 = 这些段上的折射率
这正是为什么我们的 Ray 类从第 4 章开始就一直携带 opl。
当我们写:
ray.propagate(distance, refractive_index=n)
我们更新了:
ray.opl += n * distance
当时,这看起来可能像是多余的簿记。现在它变得至关重要。
光线的最终位置告诉我们它落在哪里。
光线累积的 OPL 告诉我们它走了多长的光学距离。
这两条信息导致不同的分析。
一个小小的 OPL 示例
让我们重温一个最简单的路径。
一条光线经过:
空气中 20 mm
玻璃中 5 mm
空气中 20 mm
如果玻璃折射率是 1.5,那么:
几何距离 = 20 + 5 + 20 = 45 mm
OPL = 1.0×20 + 1.5×5 + 1.0×20 = 47.5 mm
用代码:
segments = [
{"distance_mm": 20.0, "n": 1.0},
{"distance_mm": 5.0, "n": 1.5},
{"distance_mm": 20.0, "n": 1.0},
]
opl = sum(seg["distance_mm"] * seg["n"] for seg in segments)
geometric = sum(seg["distance_mm"] for seg in segments)
print("几何距离 [mm]:", geometric)
print("光程长度 [mm]:", opl)
光线物理上走了 45 mm,但在光学上走了 47.5 mm。
这种差异不是表面功夫。它决定相位。
如果另一条光线走了略微不同的光程,即使它们的几何终点很接近,两条光线也可能以不同的相位到达。
从 OPL 到 OPD
单纯的光程长度还不是波前误差。
OPL 是沿一条射线累积的绝对路径。OPD 是相对于参考的差值。
一个简单的定义是:
OPD = OPL_ray - OPL_reference
但参考的选择很关键。
对于成像,理想的参考通常不是平面。一个朝向像点汇聚的完美会聚波具有以该像点为中心的球波前。所以参考波前通常是一个球面。
这意味着 OPD 被更准确地理解为:
实际波前与选定的参考波前之间的光程差
或者,用射线术语来说:
每条光线的光程与理想参考路径之间相差多少
这就是波前分析变得有些微妙的地方。我们必须决定“理想”意味着什么。
可能的参考选择包括:
主光线参考球
质心参考球
最佳拟合参考球
近轴参考球
像平面参考
不同的选择会去除或保留不同的低阶项,如平移、倾斜和离焦。这不是作弊,而是选择 OPD 图应该回答什么问题。
当前 Optiland 的文档直接反映了这一思想:其 Wavefront 分析接受策略选择,如 chief_ray、centroid_sphere 和 best_fit_sphere,并包含 OPD 数据的 remove_tilt 选项。(Optiland)
这告诉我们一个重要的事情:
OPD 不只是一个原始数值。它是相对于参考几何的数值。
在解释 OPD 图之前,必须先知道参考是什么。
以长度表示的 OPD 与以波长表示的 OPD
OPD 可以用长度单位表达:
OPD = 0.00025 mm
它也可以用波长来表示:
OPD_waves = OPD_length / wavelength
这对于相位通常更有用。
如果波长为 0.5 µm,那么 OPD 为 0.25 µm 就是半个波长:
0.25 µm / 0.5 µm = 0.5 waves
相位与以波长表示的 OPD 的关系为:
phase = 2π × OPD_waves
或使用长度单位:
phase = 2π × OPD_length / wavelength
这种单位转换是错误容易隐藏的地方。
如果 OPL 以毫米存储,波长以微米存储,我们必须转换:
wavelength_mm = wavelength_um × 1e-3
然后:
OPD_waves = OPD_mm / wavelength_mm
让我们编写辅助函数:
import numpy as np
import math
import matplotlib.pyplot as plt
def wavelength_um_to_mm(wavelength_um):
return wavelength_um * 1e-3
def opd_mm_to_waves(opd_mm, wavelength_um):
wavelength_mm = wavelength_um_to_mm(wavelength_um)
return opd_mm / wavelength_mm
def opd_waves_to_phase(opd_waves):
return 2.0 * np.pi * opd_waves
测试:
opd_um = 0.25
wavelength_um = 0.5
opd_mm = opd_um * 1e-3
waves = opd_mm_to_waves(opd_mm, wavelength_um)
print("OPD [waves]:", waves)
print("phase [radians]:", opd_waves_to_phase(waves))
结果应该是:
0.5 waves
π radians
这一步将在第 11 章中直接被使用。
瞳面坐标
OPD 通常表现为瞳面上的函数。
我们不按像平面坐标绘制 OPD,而是在归一化瞳面坐标上绘制:
Px, Py
其中瞳面圆为:
Px² + Py² <= 1
瞳面坐标标识了采样光线通过瞳面的位置。对于瞳面上的每个点,波前相对于参考有某个光程差。
所以 OPD 图是:
OPD(Px, Py)
这就是为什么波前图看起来像彩色圆盘。圆盘是瞳面。颜色是 OPD。
让我们创建一个瞳面网格。
def make_normalized_pupil_grid(N):
"""创建一个带有圆形瞳面掩膜的方形网格。
Returns
-------
X, Y:
从 -1 到 +1 的网格坐标。
R:
半径 sqrt(X^2 + Y^2)。
mask:
布尔圆形孔径掩膜。
"""
u = np.linspace(-1.0, 1.0, N)
X, Y = np.meshgrid(u, u)
R = np.sqrt(X**2 + Y**2)
mask = R <= 1.0
return X, Y, R, mask
绘制掩膜:
X, Y, R, mask = make_normalized_pupil_grid(256)
plt.imshow(mask, extent=[-1, 1, -1, 1], origin="lower")
plt.xlabel("Pupil X")
plt.ylabel("Pupil Y")
plt.title("Circular pupil mask")
plt.colorbar(label="inside pupil")
plt.show()
这还不是 OPD。这是 OPD 将要存在的域。
一个简化的波前函数
现在我们将直接生成一个 OPD 图。
这还不是从完整光线追迹计算出来的,而是一个受控的教学模型。我们在瞳面上选择一个波前误差函数,然后将其可视化。
这很有用,因为本章的目标是表示方法:
OPD over pupil → phase → pupil function
OPD 的来源可以是实际光线追迹、拟合的 Zernike 模型、测量的波前传感器,或简化的多项式。表示方法是相同的。
让我们以波长为单位构建一个玩具波前。
使用归一化瞳面坐标:
X, Y ∈ [-1, 1]
R² = X² + Y²
添加简单项:
类离焦项: 2R² - 1
类像散项: X² - Y²
类彗差项: (3R² - 2)Y
类球差项: 6R⁴ - 6R² + 1
这些不是严格归一化的 Zernike 多项式,而是教学用的形状。这对于理解 OPD 图如何工作来说足够了。
def toy_opd_waves(X, Y, mask, defocus=0.0, astig=0.0, coma=0.0, spherical=0.0):
"""在归一化圆形瞳面上创建一个以波长为单位的玩具 OPD 图。
各项为简单教学多项式,非归一化 Zernike 多项式。
"""
R2 = X**2 + Y**2
R4 = R2**2
W = np.zeros_like(X, dtype=float)
W += defocus * (2.0 * R2 - 1.0)
W += astig * (X**2 - Y**2)
W += coma * (3.0 * R2 - 2.0) * Y
W += spherical * (6.0 * R4 - 6.0 * R2 + 1.0)
W = np.where(mask, W, np.nan)
return W
现在创建一个图:
X, Y, R, mask = make_normalized_pupil_grid(256)
W = toy_opd_waves(
X, Y, mask,
defocus=0.25,
astig=0.10,
coma=0.08,
spherical=0.05,
)
这里 W 是以波长为单位的 OPD。
这意味着:
W = 1.0 表示一个波长的 OPD
W = 0.5 表示半个波长
W = 0.0 表示等于参考
绘制它:
def plot_opd_map(W, title="OPD map [waves]"):
plt.imshow(W, extent=[-1, 1, -1, 1], origin="lower")
plt.xlabel("Pupil X")
plt.ylabel("Pupil Y")
plt.title(title)
plt.colorbar(label="OPD [waves]")
plt.gca().set_aspect("equal", adjustable="box")
plt.show()
plot_opd_map(W, title="Toy OPD map")
现在我们有了一个 OPD 图。
它不是一个点列图,不是一个图像,也不是一个 PSF。
它是瞳面上的波前误差。
解读 OPD 图
一个 OPD 图是光程中的高度图。
高区和低区意味着瞳面的不同部分相对于参考具有不同的相位延迟。
一个完美校正的波前在去除不相关的平移和倾斜后将是平坦的:
OPD(Px, Py) ≈ 0
离焦的波前呈碗状或圆顶状。
像散沿着正交瞳面方向改变符号。
彗差具有与视场方向相关的不对称形状。
球差是旋转对称的,但以更高阶的方式随瞳面半径变化。
同样,不要过度记忆这些图片。计算意义更为重要:
每个瞳面点都有一个相位延迟。
OPD 图存储了那个延迟。
瞳函数将把那个延迟转换成复相位因子。
OPD 图不是最终的像质指标,而是一个中间表示。
但它是让衍射得以进入的表示。
平移、倾斜和离焦
一些 OPD 项会以可能对不同分析而言重要或不重要的方式平移波前。
平移
平移是加到各处的常数 OPD:
W_new(Px, Py) = W(Px, Py) + constant
这为整个瞳面添加相同的相位。对于强度 PSF,全局相位因子通常不改变结果。所以平移常常被移除。
倾斜
倾斜是贯穿瞳面的线性相位斜坡:
W(Px, Py) = a Px + b Py
倾斜会移动 PSF 的位置。如果你想独立于指向或主光线偏移来分析像质,可以移除倾斜。如果你关心图像位移,可以保留它。
离焦
离焦是二次波前项。移除离焦相当于改变参考焦点。有时你想移除它以评估最佳焦点。有时你想保留它以了解当前像平面离焦点有多远。
这就是波前软件通常提供移除或保留低阶项选项的原因。
去除了平移的图并不“更真实”。它回答了不同的问题。
让我们编写一个简单的平移移除:
def remove_piston(W, mask):
"""减去瞳面上的平均 OPD。"""
W2 = W.copy()
mean = np.nanmean(W2[mask])
W2[mask] -= mean
return W2
使用它:
W_no_piston = remove_piston(W, mask)
print("mean before:", np.nanmean(W[mask]))
print("mean after:", np.nanmean(W_no_piston[mask]))
对于倾斜移除,进行平面拟合:
def remove_piston_and_tilt(W, X, Y, mask):
"""从瞳面上的 W 中移除最佳拟合平面 a + bX + cY。"""
valid = mask & np.isfinite(W)
A = np.column_stack([
np.ones(np.count_nonzero(valid)),
X[valid],
Y[valid],
])
b = W[valid]
coeffs, *_ = np.linalg.lstsq(A, b, rcond=None)
plane = coeffs[0] + coeffs[1] * X + coeffs[2] * Y
W2 = W.copy()
W2[valid] = W[valid] - plane[valid]
return W2, coeffs
使用它:
W_flat, coeffs = remove_piston_and_tilt(W, X, Y, mask)
print("removed plane coefficients:", coeffs)
plot_opd_map(W_flat, title="Toy OPD map after piston and tilt removal")
这段代码有意简化了。真正的光学波前工具可能使用参考球、光线瞄准、Zernike 拟合或最佳拟合策略。但思路是相同的:
移除代表参考选择的项
检查残余波前误差
RMS 和峰谷波前误差
两种常见的 OPD 摘要是 RMS 波前误差和峰谷波前误差。
RMS 波前误差为:
RMS = sqrt(mean((W - mean(W))²))
在瞳面上。
峰谷值为:
PV = max(W) - min(W)
在瞳面上。
代码实现:
def wavefront_rms(W, mask):
valid = mask & np.isfinite(W)
values = W[valid]
values = values - np.mean(values)
return math.sqrt(float(np.mean(values**2)))
def wavefront_pv(W, mask):
valid = mask & np.isfinite(W)
values = W[valid]
return float(np.max(values) - np.min(values))
计算:
print("RMS [waves]:", wavefront_rms(W, mask))
print("PV [waves]:", wavefront_pv(W, mask))
这些数字很有用,但它们不是全部。
具有相同 RMS 的波前可能具有不同的空间结构。一个可能主要是离焦,另一个可能有高频波纹。它们的 PSF 可能不同,它们的 MTF 可能不同。
所以 RMS 是一个摘要,而不是像质神谕。
这一课现在应该感觉很熟悉了。光斑 RMS 并没有取代点列图。波前 RMS 没有取代 OPD 图或 PSF。
OPD 以毫米、微米和波长表示
让我们将玩具波前从波长转换为物理长度。
假设波长为:
λ = 0.5876 µm
那么:
OPD_um = W_waves × λ_um
并且:
OPD_mm = OPD_um × 1e-3
代码:
def opd_waves_to_um(W_waves, wavelength_um):
return W_waves * wavelength_um
def opd_waves_to_mm(W_waves, wavelength_um):
return W_waves * wavelength_um * 1e-3
示例:
wavelength_um = 0.5876
W_rms_waves = wavefront_rms(W, mask)
W_rms_um = opd_waves_to_um(W_rms_waves, wavelength_um)
print("RMS [waves]:", W_rms_waves)
print("RMS [µm]:", W_rms_um)
这种转换很重要,因为不同工具以不同方式报告 OPD。一些报告波长,一些报告长度。一些在纳米尺度标记图。一些按分析波长归一化。
例如,Optiland 的 WavefrontData 文档描述了 opd 字段作为归一化到波长的光程差数据。(Optiland)
因此,如果你在工具之间比较输出,务必检查单位。
在 0.5 µm 下 0.1 waves 的波前误差与在 10 µm 下 0.1 waves 的波前误差物理长度不同。
以波长表示时,它们看起来相等。但以微米表示时,它们不相等。
一个线轮廓:OPD 扇形图
OPD 图是一个二维瞳面图。
OPD 扇形图是光线扇形图的波前版本:它显示沿瞳面上一条线的 OPD。
例如:
OPD(Px, 0)
或:
OPD(0, Py)
让我们提取一条经过中心的水平线。
def center_line_profile(W, axis="x"):
N = W.shape[0]
c = N // 2
if axis == "x":
coord = np.linspace(-1.0, 1.0, N)
values = W[c, :]
elif axis == "y":
coord = np.linspace(-1.0, 1.0, N)
values = W[:, c]
else:
raise ValueError("axis must be 'x' or 'y'")
return coord, values
绘制:
px, wx = center_line_profile(W_no_piston, axis="x")
plt.plot(px, wx)
plt.axhline(0.0, linewidth=0.8)
plt.axvline(0.0, linewidth=0.8)
plt.xlabel("Pupil X")
plt.ylabel("OPD [waves]")
plt.title("OPD fan through pupil center")
plt.show()
这看起来在精神上类似于光线扇形图,但纵轴不同。
光线扇形图:
transverse image error [mm]
OPD 扇形图:
optical path difference [waves or length]
两者都是瞳面坐标诊断工具。它们回答不同的问题。
光线像差与波像差相关但不相同
很容易说:
光线扇形图是波前的斜率
这个想法中有真理,但需要谨慎。在经典光学理论中,在某些近似下,横向光线像差与波像差的导数相关。在大数值孔径或有复杂波前结构的情况下,这种关系可能需要更谨慎的处理。
对于本书,实际信息是:
光线扇形图显示光线在像平面上偏离的位置。
OPD 图显示瞳面上的光程误差。
它们相互关联,但在所有情况下都不能简单替代。
这对我们的计算链来说足够了。
我们不需要在这里推导完整的数学关系。我们需要理解为什么下一个表示是必要的。
光线落点引导我们得到点列图和光线扇形图。
光程差引导我们得到瞳面相位和衍射。
这就是过渡。
从 OPD 到相位
现在来读本章最重要的一行。
如果 W 是以波长表示的 OPD,那么瞳面相位为:
φ(Px, Py) = 2π W(Px, Py)
代码:
phase = 2.0 * np.pi * W_no_piston
如果 OPD 以毫米为单位,那么:
phase = 2.0 * np.pi * opd_mm / wavelength_mm
这个相位就是复瞳函数所使用的:
P(Px, Py) = A(Px, Py) exp(i φ(Px, Py))
其中:
A(Px, Py) = 孔径振幅
φ(Px, Py) = 来自 OPD 的相位
不要匆忙略过这一点。这是第 10 章与第 11 章之间的交接。
OPD 图是实数值。
瞳函数是复数值。
转换是通过相位进行的。
这就是波前误差如何变成傅里叶光学可以使用的东西。
瞳面相位预览
让我们绘制相位包裹在 -π 和 π 之间的图。
def wrap_phase(phase):
"""将相位包裹到 [-pi, pi]。"""
return (phase + np.pi) % (2.0 * np.pi) - np.pi
phase = opd_waves_to_phase(W_no_piston)
phase_wrapped = np.where(mask, wrap_phase(phase), np.nan)
plt.imshow(phase_wrapped, extent=[-1, 1, -1, 1], origin="lower")
plt.xlabel("Pupil X")
plt.ylabel("Pupil Y")
plt.title("Wrapped pupil phase")
plt.colorbar(label="phase [radians]")
plt.gca().set_aspect("equal", adjustable="box")
plt.show()
这个图可能看起来比 OPD 图更突兀,因为相位包裹在 ±π 处跳跃。这并不意味着波前在那里物理跳跃,而是相位显示以 2π 为模进行包裹。
这是另一个常见的混淆来源。
对于平滑的波前误差,OPD 通常更容易阅读。
包裹的相位对于观察复场的周期性很有用。
两者都是相同底层延迟的视图。
一个特意简化的 OPD 到 PSF 预览
我们还没有进入完整的 PSF 章节,但一个小小的预览有助于展示为什么 OPD 很重要。
给定:
孔径振幅 A
相位 φ
瞳面场为:
pupil = A exp(iφ)
一个简化的夫琅禾费 PSF 可以从瞳场傅里叶变换的模平方计算出来。
现在不必担心缩放。第 12 章将妥善处理这一点。这里的目的仅仅是展示改变 OPD 会改变衍射图样。
def quick_psf_from_opd(W_waves, mask):
"""根据以波长表示的 OPD 生成一个最小化、未缩放的 PSF 预览。
这只是为了直观理解。第 12 章将处理采样和缩放。
"""
phase = 2.0 * np.pi * np.nan_to_num(W_waves, nan=0.0)
pupil = mask.astype(float) * np.exp(1j * phase)
field = np.fft.fftshift(np.fft.fft2(np.fft.ifftshift(pupil)))
psf = np.abs(field)**2
psf = psf / psf.sum()
return psf
比较一个平坦波前和一个有像差的波前:
W_flat = np.where(mask, 0.0, np.nan)
psf_flat = quick_psf_from_opd(W_flat, mask)
psf_aberrated = quick_psf_from_opd(W_no_piston, mask)
绘制一个:
def plot_psf_preview(psf, title):
plt.imshow(np.log10(psf + 1e-12), origin="lower")
plt.title(title)
plt.colorbar(label="log10 intensity")
plt.show()
plot_psf_preview(psf_flat, "Preview PSF: flat wavefront")
plot_psf_preview(psf_aberrated, "Preview PSF: aberrated wavefront")
这只是一个预览,但它使桥梁可见。
点列图来自光线交点坐标。
PSF 来自复瞳场。
复瞳场来自孔径振幅和 OPD 导出的相位。
因此,PSF/MTF 不能仅通过光线落点来完全解释。
它们需要波前相位。
实际光线追迹如何提供 OPD
我们的玩具 OPD 图是从多项式项创建的。真正的波前分析必须从光学系统计算 OPD。
概念上的步骤如下:
1. 选择视场和波长。
2. 在瞳面上采样光线。
3. 通过光学系统追迹光线。
4. 为每条光线累积光程长度。
5. 确定一个参考波前或参考球。
6. 将每条光线的光程与参考比较。
7. 在瞳面坐标上存储 OPD。
8. 可选择移除平移、倾斜或焦点项。
第 5 步是微妙的一步。参考波前是使 OPD 有意义的东西。没有它,你只有 OPL 值,没有波前误差。
这也是为什么生产工具会暴露策略选项。策略定义了参考几何是如何选择的。
在 Optiland 文档记载的 Wavefront 类中,参数包括视场、波长、光线数量、分布模式如 hexapolar、策略选择、无焦模式以及倾斜移除。文档说明该类计算出瞳光线交点、OPD、光线强度和参考球半径。(Optiland)
这就是上述概念列表的程序库级别版本。
一个简化的从 OPL 到 OPD 的草图
让我们编写一个草图来澄清 OPL 和 OPD 之间的区别。
假设我们有几条光线,每条有:
瞳面坐标
OPL
参考 OPL
那么:
def compute_opd_from_opl(opl_values, reference_opl_values):
"""将 OPD 计算为 OPL 差值。"""
return np.asarray(opl_values) - np.asarray(reference_opl_values)
如果参考只是主光线 OPL,那么:
def compute_opd_relative_to_chief(opl_values, chief_index):
opl_values = np.asarray(opl_values, dtype=float)
return opl_values - opl_values[chief_index]
这很简单,但不是一个完整的成像 OPD 计算。仅基于主光线 OPL 的差值可能不包括到像点的正确球面参考。它可能根据几何结构混入倾斜或离焦。
这就是为什么这段代码只是一个草图。
真正要点是:
OPL 沿光线累积。
OPD 是 OPL 与选定参考的比较。
波前误差是在瞳面上解释的 OPD。
将这三者区分开。
Optiland OPD 图比较
Optiland 提供了更高级别的 OPD 工具,因此实际工作流程可以比我们的教学模型短得多。
当前文档库展示了一个 OPD 图示例,使用:
from optiland.samples.infrared import InfraredTripletF4
from optiland.wavefront import OPD
lens = InfraredTripletF4()
lens.draw()
opd = OPD(lens, field=(0, 1), wavelength=4.2)
opd.view()
该示例将输出标记为在瞳面坐标上的 OPD 图,并报告以波长表示的 RMS。(Optiland)
对于一个可见光单透镜,概念性工作流程看起来类似:
from optiland import optic
from optiland.wavefront import OPD
lens = optic.Optic()
lens.surfaces.add(index=0, radius=float("inf"), thickness=float("inf"))
lens.surfaces.add(index=1, radius=50.0, thickness=5.0, material="N-BK7", is_stop=True)
lens.surfaces.add(index=2, radius=-50.0, thickness=45.0)
lens.surfaces.add(index=3)
lens.set_aperture(aperture_type="EPD", value=10.0)
lens.fields.set_type("angle")
lens.fields.add(y=0.0)
lens.wavelengths.add(value=0.5876, is_primary=True)
opd = OPD(lens, field=(0, 0), wavelength=0.5876)
opd.view()
请将此视为程序库级别的比较代码。具体行为可能取决于安装的 Optiland 版本和光学模型。重要的计算意义是稳定的:
程序库对瞳面进行采样
追迹光线
相对于参考几何计算 OPD
在瞳面坐标上显示 OPD
本章中的教学代码直接将 OPD 生成为多项式图。Optiland 从实际光学系统计算 OPD。
两者产生相同类型的对象:
OPD(Px, Py)
那个对象正是第 11 章所需要的。
波前分析和 Zernike 系数
OPD 图经常被分解为 Zernike 多项式。
Zernike 是一组定义在单位圆上的函数。它们很有用,因为瞳面通常是圆形的,而常见的像差类形状可以用低阶 Zernike 项表示。
例如,类 Zernike 项可以表示:
平移
倾斜
离焦
像散
彗差
球差
这就是为什么光学软件经常在 OPD 图旁边报告 Zernike 系数。
我们不会在这里推导 Zernike 多项式。本章的目的更基本:理解 OPD 图本身。
但了解 Zernike 的位置是有帮助的:
OPD 图:
瞳面上原始或计算得出的波前误差
Zernike 拟合:
该 OPD 图的紧凑系数表示
Zernike 表不是理解 OPD 的替代品。它是描述它的一种紧凑方式。
Optiland 的波前 API 概览包括与 OPD、波前、参考几何以及 PSF/MTF 模块并列的 Zernike 相关 OPD 模块。(Optiland)
这种放置是合理的:Zernike 是波前分析的一部分,而不是一个独立的宇宙。
OPD 能告诉你什么,不能告诉你什么
OPD 很强大,但我们不应该夸大它。
OPD 可以告诉你:
瞳面上的相位误差
RMS 波前误差
峰谷波前误差
主要的低阶波前形状
瞳面相位是否接近平坦
波前误差如何随视场或波长变化
OPD 本身不直接告诉你:
完整的 PSF 强度分布
MTF 曲线
传感器采样效应
杂散光
镀膜鬼影
制造公差
偏振效应
散射
OPD 是衍射计算的输入,而不是最终答案。
这与我们之前看到的模式相同。
处方不是光线追迹。
光线追迹不是点列图。
点列图不是波前。
波前不是 PSF。
PSF 不是 MTF。
每个表示都保留了一些信息,转换了一些信息,并丢弃或摘要了一些信息。
技能在于知道哪个表示回答哪个问题。
一个有用的心理模型
这是记住这种过渡的最简单方法:
光线位置误差:
“光线落在了哪里?”
波前误差:
“波在这个瞳面点是迟到还是早到?”
光线扇形图将第一种误差相对于瞳面坐标绘制。
OPD 扇形图将第二种误差相对于瞳面坐标绘制。
点列图显示光线落点。
OPD 图显示瞳面上的相位延迟。
PSF 将显示当所有这些瞳面贡献干涉时会发生什么。
最后一个词很重要:干涉。
光线图不干涉。波会干涉。
OPD 是光线追迹世界将相位信息交给波动光学世界的方式。
OPD 常见错误
在我们继续之前,让我们指出陷阱。
错误 1:混淆 OPL 和 OPD
OPL 是沿光线累积的光程长度。
OPD 是该路径与参考之间的差值。
一个 OPL 值列表不会自动成为波前图。
错误 2:忘记参考几何
OPD 总是取决于参考。主光线球、质心球、最佳拟合球、近轴焦点以及像平面参考可能给出不同的图。
询问使用了什么参考。
错误 3:混淆长度单位和波长
如果 OPD 以毫米为单位,波长以微米为单位,在相除之前进行转换。
wavelength_mm = wavelength_um × 1e-3
错误 4:将平移视为图像劣化
常数相位偏移通常不会改变强度 PSF。平移在干涉测量或分段系统中可能很重要,但对于普通的单瞳 PSF 强度,它经常被移除。
错误 5:不清楚移除倾斜的含义就移除
移除倾斜会使波前相位重新居中,并常常移动 PSF 参考。这可能是可取的,但它改变了所问的问题。
错误 6:总是把离焦视为不好或总是可移除的
离焦可能代表错误的像平面、有意的焦点移动,或者你想优化去掉的项。不要盲目移除它。
错误 7:将包裹的相位解读为物理不连续
相位在 ±π 处包裹。包裹相位中的跳变可能只是显示包裹,而非物理不连续。
错误 8:认为 RMS 波前误差讲述全部故事
RMS 很有用,但具有相同 RMS 的不同 OPD 形状可能产生不同的 PSF 和 MTF。
错误 9:忘记波长依赖性
相同的物理 OPD 长度在不同波长下对应不同的波长数。
错误 10:期望 OPD 取代光线图
OPD 是波前表示。光线扇形图和点列图仍然是有用的几何诊断工具。
本章给了我们什么
我们现在已经跨越了从几何光线追迹到波动光学的桥梁。
我们可以解释:
OPL 作为 n ds 累积
OPD 作为相对于参考的光程差
为什么 OPD 绘制在瞳面坐标上
为什么 OPD 可以用长度或波长表示
OPD 如何变成相位
为什么平移、倾斜和离焦依赖于分析惯例
如何生成玩具 OPD 图
如何计算 RMS 和 PV 波前误差
OPD 图如何不同于点列图和光线扇形图
Optiland 的波前工具如何融入同一链条
核心变换是:
以波长表示的 OPD(Px, Py)
→ phase φ(Px, Py) = 2π OPD(Px, Py)
这就是与下一章的确切交接。
前方的路
现在我们有了一个实值波前误差图。
但衍射计算需要瞳面上的复场。
该场有两部分:
振幅:光线在哪里通过以及有多强
相位:每个瞳面点携带多少光学延迟
孔径掩膜提供振幅。
OPD 图提供相位。
它们一起构成瞳函数:
P(Px, Py) = A(Px, Py) exp(i 2π W(Px, Py))
其中 W 是以波长表示的 OPD。
第 11 章将仔细构建那个瞳函数。一旦我们有了它,PSF 就不再是一个神秘的像质图,而是一个复瞳场的傅里叶变换,随之而来的所有关于采样、缩放和归一化的常见警告也在附近等待。
我们快要理解了。
光线追迹器告诉我们光去了哪里。
OPD 图告诉我们波前如何被延迟。
瞳函数将把那个延迟转化为每个瞳面点上的复数。
那就是衍射计算开始的地方。
生成的验证图

来源和验证说明
OPD 的讨论有意考虑了约定。OPD 只有在定义了参考波前或参考几何之后才有意义。本章中的玩具 OPD 图验证了表示方法和相位转换;它不能替代生产级的光线瞄准、出瞳分析、参考球拟合或 Zernike 报告。