Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第 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_raycentroid_spherebest_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 图更突兀,因为相位包裹在 ±π 处跳跃。这并不意味着波前在那里物理跳跃,而是相位显示以 为模进行包裹。

这是另一个常见的混淆来源。

对于平滑的波前误差,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 图告诉我们波前如何被延迟。

瞳函数将把那个延迟转化为每个瞳面点上的复数。

那就是衍射计算开始的地方。

生成的验证图

来自第 10 章波前模型的 OPD 图

来源和验证说明

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