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

第11章 瞳孔函数:用复数表示波前

在上一章末尾,光程差(OPD)为我们提供了一种新的信息。

点列图告诉我们会聚点的位置。光线扇面图告诉我们实际光线与参考光线的偏离。OPD则告诉我们更细微的信息:对于瞳孔平面上的每个点,该处的波前与理想参考波前相比,超前或滞后了多少。

这已经是一大步了。但OPD本身仍不足以生成图像。

为了计算衍射点扩散函数(PSF),计算机需要知道整个瞳孔的光在像面上如何干涉。这意味着每个瞳孔点需要两种信息:

  1. 透过的光量。
  2. 该光线具有的相位。

这两方面正是瞳孔函数所存储的内容。

瞳孔函数是波前误差转化为复数数组的关键步骤。它是几何光学路径误差与傅里叶光学之间的桥梁。

本章在概念上篇幅不长,但在整个处理链条中地位重要:

OPD图
→ 瞳孔振幅
→ 瞳孔相位
→ 复瞳孔函数
→ PSF

下一章将对这个复瞳孔函数进行傅里叶变换,并将其转化为PSF。这里,我们将仔细准备输入数据。

不要跳过这一步。许多错误的PSF和MTF计算都源于瞳孔函数构建时使用了错误的单位、错误的掩膜或错误的符号约定。


1. 我们正在打开的黑箱

衍射计算在书中常以简洁的等式呈现:

PSF与瞳孔函数的傅里叶变换相关。

在通常的夫琅禾费近似下,这个说法是正确的,但它掩盖了一个实际问题:

代码中瞳孔函数究竟是什么?

它不只是孔径形状。

它不只是OPD图。

它不只是相位图。

它是瞳孔平面上一个复值函数:

P(x,y)=A(x,y)exp(i2πW(x,y)λ) P(x, y) = A(x, y)\exp\left(i\frac{2\pi W(x, y)}{\lambda}\right)

其中:

  • P(x,y)P(x, y) 是复瞳孔函数。
  • A(x,y)A(x, y) 是瞳孔振幅。
  • W(x,y)W(x, y) 是OPD或波前误差。
  • λ\lambda 是波长。
  • ii 是虚数单位。
  • x,yx, y 是瞳孔坐标。

这个公式可以概括本章的全部内容。

但仅有这一行是不够的。我们需要知道每个部分的意义、如何采样,以及如何在不自欺欺人的情况下编写代码。


2. 孔径掩膜:光存在的地方

从最简单的部分开始:孔径。

对于一个理想的圆形瞳孔,光通过圆内各点,并在圆外被阻挡。在归一化瞳孔坐标中,我们可以写:

ρ=x2+y2 \rho = \sqrt{x^2 + y^2}

瞳孔内部:

ρ1 \rho \leq 1

瞳孔外部:

ρ>1 \rho > 1

因此,最简单的孔径掩膜是:

M(x,y)={1,ρ10,ρ>1 M(x, y) = \begin{cases} 1, & \rho \leq 1 \ 0, & \rho > 1 \end{cases}

这个掩膜还不是完整的瞳孔函数。它只说明了哪些地方允许光线通过。

在代码中,这变成一个布尔数组:

mask = rho <= 1.0

这行代码很重要。没有它,你的FFT将包含来自瞳孔外部的光线。结果可能看起来像个图案,但它不再是你打算建模的光学系统的PSF。

在计算光学中,这是一个有益的习惯:

错误的数组仍可能产生一幅美丽的图像。

计算机并不知道你的瞳孔是否物理上合理。它只执行你给出的数字。


3. 瞳孔振幅:并不总是只有1

对于一个清晰、理想、均匀照明的圆形孔径,振幅通常写为:

A(x,y)=M(x,y) A(x, y) = M(x, y)

这意味着:

  • 瞳孔内部的振幅为1;
  • 瞳孔外部的振幅为0。

这是最简单的情况,也是正确的起点。

但在实际光学系统中,振幅可能更复杂。它可能包括:

  • 渐晕;
  • 中心遮挡;
  • 望远镜中的蜘蛛支架;
  • 透过率变化;
  • 变迹;
  • 偏振相关效应;
  • 镀膜或材料的透过率差异。

例如,带有中心遮挡的望远镜,其掩膜如下:

M(x,y)={1,rinner<ρ10,其它情况 M(x, y) = \begin{cases} 1, & r_\text{inner} < \rho \leq 1 \ 0, & \text{其它情况} \end{cases}

带有变迹的系统,其孔径内部可能具有平滑的振幅,例如:

A(ρ)=exp(αρ2) A(\rho) = \exp(-\alpha \rho^2)

在本章中,我们将主要使用简单的圆形瞳孔。但重要的是要清楚地区分:

掩膜:允许光通过的区域
振幅:通过的场振幅大小
强度:振幅的平方

振幅不是强度。如果强度透过率为TT,那么场振幅通常与以下值成比例:

A=T A = \sqrt{T}

这个平方根是常见的错误来源。如果你将强度直接放入场振幅数组中,PSF的能量将是错误的。

目前,我们将保持:

amplitude = mask.astype(float)

这足以构建第一个瞳孔函数。


4. OPD变成相位

OPD图告诉我们每个瞳孔点具有多少光程差。

如果某一点的OPD为W(x,y)W(x,y),那么对应的相位为:

ϕ(x,y)=2πW(x,y)λ \phi(x, y) = \frac{2\pi W(x, y)}{\lambda}

这是关键的转换。

单位规则很简单,但很严格:

W和λ必须使用相同的长度单位。

如果WW以米为单位,λ\lambda也必须以米为单位。

如果WW以微米为单位,λ\lambda也必须以微米为单位。

如果WW以纳米为单位,而λ\lambda以米为单位,代码仍然会运行,但相位将因10910^9的因子而出错。不会有警告,只会有一个错误的结果。

让我们把物理意义具体化。

假设波长为:

λ=550 nm \lambda = 550\text{ nm}

如果OPD为一个整波长:

W=λ W = \lambda

则:

ϕ=2πλλ=2π \phi = \frac{2\pi\lambda}{\lambda} = 2\pi

该点完成了一个完整的相位周期。

如果OPD为半个波长:

W=λ2 W = \frac{\lambda}{2}

则:

ϕ=π \phi = \pi

该点的相位差了半个周期。

这就是为什么很小的OPD会产生如此大的影响。几百纳米在机械尺度上很小,但在光学上,它可能是可见光波长中的很大一部分。

瞳孔函数将此相位以复数形式存储:

exp(iϕ)=cosϕ+isinϕ \exp(i\phi) = \cos\phi + i\sin\phi

因此,完整的瞳孔函数变为:

P(x,y)=A(x,y)exp(iϕ(x,y)) P(x, y) = A(x, y)\exp(i\phi(x, y))

或者,代入相位:

P(x,y)=A(x,y)exp(i2πW(x,y)λ) P(x, y) = A(x, y)\exp\left(i\frac{2\pi W(x, y)}{\lambda}\right)

这是最简洁的记忆形式。


5. 为什么这里复数不是装饰品

人们很容易将复数视为数学上的装饰。但在这里,它们不是装饰。它们是我们存储相位的方式。

一个实值的OPD图说明:

这个瞳孔点超前或滞后了这么多光程

一个复瞳孔函数说明:

这个瞳孔点以这个振幅和这个相位作出贡献

当我们稍后计算PSF时,来自不同瞳孔点的光将发生干涉。干涉取决于相位。

瞳孔的两个部分可能具有相同的振幅但相反的相位。它们可能会部分抵消。两个部分也可能相位对齐。它们可能会相互加强。

这就是为什么点列图不能完全替代衍射计算的原因。点列图将光线视为会聚点。瞳孔函数则准备光的波动特性,以便进行相干求和。

这里有一个重要的转变:

几何光线追迹问的是:
这条光线射向哪里?

波动光学问的是:
所有瞳孔贡献如何作为复场叠加在一起?

瞳孔函数正是我们准备第二个问题答案的地方。


6. 最小的瞳孔网格

现在我们用Python编写第一个版本。

我们将使用归一化瞳孔坐标。这意味着瞳孔半径为1,坐标范围大致为:

x 从 -1 到 1
y 从 -1 到 1

这还不是以毫米为单位的物理长度。它是一个归一化坐标系。这对本章来说是可以的,因为我们关注的是转换:

OPD → 相位 → 复瞳孔

这是生成网格的代码:

import numpy as np
import matplotlib.pyplot as plt


def make_pupil_grid(n=256):
    """
    创建带有归一化瞳孔坐标的正方形采样网格。

    返回
    -------
    x, y : 二维数组
        归一化瞳孔坐标。
    rho : 二维数组
        径向瞳孔坐标。
    theta : 二维数组
        角度瞳孔坐标。
    mask : 二维布尔数组
        在单位圆形瞳孔内为True。
    """
    coord = np.linspace(-1.0, 1.0, n)
    x, y = np.meshgrid(coord, coord)

    rho = np.sqrt(x**2 + y**2)
    theta = np.arctan2(y, x)
    mask = rho <= 1.0

    return x, y, rho, theta, mask

输出数组的形状都是:

(n, n)

正方形网格便于进行FFT,尽管物理瞳孔是圆形的。圆形孔径用掩膜表示。

这是数值光学中最早的小妥协之一:

计算用的数组是正方形的。
光学孔径可能不是。
掩膜告诉数组什么是物理上真实的。

这种区别对下一章非常重要。


7. 一个简单的OPD图

在实际的光学设计程序中,OPD是根据光学系统计算得到的。光线追迹穿过表面,累积光程,然后将波前与参考球面或参考波前进行比较。

为了学习,我们可以从一个合成的OPD图开始。这样我们可以在每个例子中测试瞳孔函数机制,而不需要完整的镜头模型。

我们将创建一个具有两个成分的简单波前:

  1. 类似离焦的变化。
  2. 类似像散的变化。

这并非完整的Zernike实现。它只是一种简洁的方式,生成一个可识别的OPD模式。

def synthetic_opd(rho, theta, mask, wavelength):
    """
    创建一个以米为单位的合成OPD图。

    这是一个教学用波前,而非完整的光学设计结果。
    系数是波长的几分之一。
    """
    opd = np.zeros_like(rho)

    # 以米为单位的系数
    defocus = 0.50 * wavelength
    astigmatism = 0.25 * wavelength

    # 简单的波前项
    opd += defocus * (2.0 * rho**2 - 1.0)
    opd += astigmatism * rho**2 * np.cos(2.0 * theta)

    # 在瞳孔外部,OPD在物理上没有意义。
    # 在数值上我们保留为零,但掩膜会阻挡它。
    opd[~mask] = 0.0

    return opd

重要的不是具体用了什么多项式。重要的是opd数组与瞳孔网格具有相同的形状。

每个采样的瞳孔点现在都有一个波前误差。

网格点 → OPD值 → 相位值 → 复瞳孔值

这就是我们想要明确表达的转换。


8. 构建复瞳孔函数

现在我们可以编写完成本章主要任务的函数。

def pupil_function_from_opd(opd, mask, wavelength, amplitude=None):
    """
    将OPD图转换为复瞳孔函数。

    参数
    ----------
    opd : 二维数组
        光程差,以米为单位。
    mask : 二维布尔数组
        在孔径内为True。
    wavelength : 浮点数
        以米为单位的波长。
    amplitude : 二维数组或None
        场振幅透过率。如果为None,则使用清晰孔径。

    返回
    -------
    pupil : 二维复数组
        复瞳孔函数。
    phase : 二维数组
        以弧度为单位的相位。
    """
    if amplitude is None:
        amplitude = mask.astype(float)

    phase = 2.0 * np.pi * opd / wavelength

    pupil = amplitude * np.exp(1j * phase)

    # 确保孔径外部的场为零。
    pupil[~mask] = 0.0 + 0.0j

    return pupil, phase

这个函数很小,但它执行了一个非常重要的转换。

让我们运行它:

wavelength = 550e-9  # 550 nm,以米为单位

x, y, rho, theta, mask = make_pupil_grid(n=256)
opd = synthetic_opd(rho, theta, mask, wavelength)

pupil, phase = pupil_function_from_opd(
    opd=opd,
    mask=mask,
    wavelength=wavelength
)

此时:

  • opd 是以米为单位的实值数组。
  • phase 是以弧度为单位的实值数组。
  • pupil 是复值数组。

你可以检查:

print(opd.dtype)
print(phase.dtype)
print(pupil.dtype)

第三个应当是复数类型。

将这一点作为健全性检查。如果你的瞳孔函数不是复数,那么你还没有以能够发生干涉的形式存储相位。

一个非常常见的Python 3笔误看起来像这样:

print phase.dtype

这行在Python 3中无效。应该是:

print(phase.dtype)

这是一个小的提醒:在计算光学中,许多错误并不是重大的理论错误。有些仅仅是语法、形状、单位或约定上的错误。要尽早发现它们。


9. 可视化OPD、相位和振幅

在进行PSF计算之前,一定要先查看一下瞳孔数据。

一个好规则是:

永远不要对你没有检查过的瞳孔函数进行傅里叶变换。

我们可以绘制三样东西:

  1. 以波长为单位的OPD。
  2. 包裹相位。
  3. 振幅掩膜。

以波长为单位的OPD是:

W(x,y)λ \frac{W(x,y)}{\lambda}

这通常比米更容易理解。

def show_pupil_maps(opd, phase, pupil, mask, wavelength):
    opd_waves = opd / wavelength

    wrapped_phase = np.angle(pupil)

    opd_plot = np.where(mask, opd_waves, np.nan)
    phase_plot = np.where(mask, wrapped_phase, np.nan)
    amp_plot = np.abs(pupil)

    plt.figure(figsize=(5, 4))
    plt.imshow(opd_plot, origin="lower", extent=[-1, 1, -1, 1])
    plt.colorbar(label="OPD [waves]")
    plt.title("OPD map")
    plt.xlabel("Normalized pupil x")
    plt.ylabel("Normalized pupil y")
    plt.tight_layout()
    plt.show()

    plt.figure(figsize=(5, 4))
    plt.imshow(phase_plot, origin="lower", extent=[-1, 1, -1, 1])
    plt.colorbar(label="Wrapped phase [rad]")
    plt.title("Pupil phase")
    plt.xlabel("Normalized pupil x")
    plt.ylabel("Normalized pupil y")
    plt.tight_layout()
    plt.show()

    plt.figure(figsize=(5, 4))
    plt.imshow(amp_plot, origin="lower", extent=[-1, 1, -1, 1])
    plt.colorbar(label="Field amplitude")
    plt.title("Pupil amplitude")
    plt.xlabel("Normalized pupil x")
    plt.ylabel("Normalized pupil y")
    plt.tight_layout()
    plt.show()


show_pupil_maps(opd, phase, pupil, mask, wavelength)

有几点需要注意。

OPD图可能看起来平滑而连续。

相位图可能会出现突然的跳跃。这并不一定意味着波前不连续。这可能只是相位在π-\piπ\pi之间发生了包裹。

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

复指数并不关心相位写为:

0 0

还是:

2π 2\pi

因为它们在单位圆上是同一点。

所以当你用np.angle绘制相位时,通常看到的是包裹相位。包裹相位很有用,但它可能让平滑的波前看起来像是含有尖锐的跳跃。

OPD图通常更适合理解物理波前。包裹相位图更适合检查即将进入FFT的复场。


10. 相位包裹并不自动意味着错误

让我们暂停一下,因为这种错误非常普遍。

假设OPD在整个瞳孔上从0逐渐增加到一个波长。相位从:

0 0

增加到:

2π 2\pi

np.angle通常将复相位显示在如下范围:

πϕπ -\pi \leq \phi \leq \pi

因此,当相位经过π\pi时,绘图值会跳转到π-\pi

这个跳跃看起来很剧烈,但它可能只是一个显示约定。

复数本身在单位圆上是连续的:

exp(iπ)=1 \exp(i\pi) = -1

以及:

exp(iπ)=1 \exp(-i\pi) = -1

所以显示的跳跃边缘可能几乎代表相同的复数值。

这就是为什么你应该同时检查:

以波长为单位的OPD
包裹相位

OPD告诉你物理波前误差。

包裹相位告诉你复瞳孔中存储的相位。

它们回答的是相关但不同的问题。


11. 无像差的清晰圆形孔径

在引入有像差的波前之前,先构建理想情况会有所帮助。

如果瞳孔内各处OPD为零:

W(x,y)=0 W(x,y)=0

那么:

P(x,y)=A(x,y)exp(0)=A(x,y) P(x,y)=A(x,y)\exp(0)=A(x,y)

对于一个清晰的圆形孔径:

P(x,y)=M(x,y) P(x,y)=M(x,y)

因此瞳孔函数只是一个复数值为1+0i1+0i的平坦圆盘。

zero_opd = np.zeros_like(rho)

perfect_pupil, perfect_phase = pupil_function_from_opd(
    opd=zero_opd,
    mask=mask,
    wavelength=wavelength
)

print(np.max(np.abs(perfect_pupil)))
print(np.min(np.abs(perfect_pupil[mask])))

在瞳孔内部,振幅应为1。

在瞳孔外部,场应为0。

这个完美的瞳孔将在下一章中产生熟悉的衍射受限艾里斑图样。

在这一步,我们还没有计算艾里斑。我们只是在准备产生它的场。

整个过程是这样的:

平坦的圆形瞳孔
→ 傅里叶变换
→ 类似艾里斑的PSF

像差会改变瞳孔相位。它们不一定会改变瞳孔振幅。

因此,一个有像差的瞳孔可能在孔径内部振幅仍为1,但其相位不再是平坦的。

核心思想是:

一个有像差的清晰系统可以具有均匀的振幅但非均匀的相位。

12. 添加中心遮挡

现在让我们只修改振幅。

这个例子很有用,因为它分离了两种效应:

  • 孔径形状影响振幅;
  • OPD影响相位。

中心遮挡可以通过阻挡瞳孔中间部分来表示。

def annular_amplitude(rho, inner_radius=0.3):
    """
    环形瞳孔的场振幅。
    """
    return ((rho >= inner_radius) & (rho <= 1.0)).astype(float)


amplitude_annular = annular_amplitude(rho, inner_radius=0.3)

annular_pupil, annular_phase = pupil_function_from_opd(
    opd=zero_opd,
    mask=mask,
    wavelength=wavelength,
    amplitude=amplitude_annular
)

这里OPD仍为零。相位仍为平坦。但瞳孔振幅改变了。

这意味着即使没有像差,PSF也会发生变化。

这是一个重要的教训:

并非所有的PSF变化都来自波前误差。
有些PSF变化来自孔径形状和振幅透过率。

如果你为一个望远镜建模,却忘记了中心遮挡,即使你的OPD是完美的,衍射图样也会是错误的。

如果你错误地模拟了渐晕,PSF和MTF可能会因为瞳孔振幅的改变而改变。

再次强调,漂亮的输出并不能证明输入是正确的。


13. Optiland在这一步的贡献

我们的最小代码使用了一个合成的OPD图。这对学习很有用,但它并不是真实镜头分析通常开始的方式。

在一个真实的光学系统中,OPD应该来自光学处方:

表面
→ 材料
→ 波长
→ 视场
→ 瞳孔采样
→ 光线追迹
→ 参考波前
→ OPD图

这就是像Optiland这样的工具变得有用的地方。教学的模式应该是:

使用Optiland或其他光学设计库从真实系统计算OPD。
然后使用相同的瞳孔函数公式来理解接下来会发生什么。

具体的API可能随版本而变化,所以在书籍项目中,最安全的方法是在配套代码中锁定库的版本。从概念上讲,工作流程如下所示:

# 版本检查工作流程。
# 在使用具体的类和方法的名称之前,
# 检查已安装的Optiland版本和文档。

# 1. 构建或加载一个光学系统。
# optic = ...

# 2. 选择视场和波长。
# field = ...
# wavelength = 550e-9

# 3. 运行波前 / OPD分析。
# wavefront = ...
# opd = wavefront.opd_map(...)

# 4. 获取或定义瞳孔掩膜。
# mask = wavefront.pupil_mask(...)

# 5. 将OPD转换为复瞳孔函数。
# pupil, phase = pupil_function_from_opd(opd, mask, wavelength)

这并非要取代Optiland内部的PSF工具。相反,它解释了背后的桥梁。

如果Optiland给你一个OPD图,你现在知道接下来的数学步骤是什么:

P(x,y)=A(x,y)exp(i2πW(x,y)λ) P(x, y) = A(x, y)\exp\left(i\frac{2\pi W(x, y)}{\lambda}\right)

而如果Optiland直接给你一个PSF,现在你也知道了该PSF计算所依赖的隐藏输入之一。

这就是本书双轨方法的意义所在。

库执行完整的工程工作流程。最小代码让你看到计算的骨架。


14. 符号约定:不统一意见的静默源头

有一个细节我们应该小心处理:指数中的符号。

你可能会看到瞳孔函数被写成:

P(x,y)=A(x,y)exp(i2πW(x,y)λ) P(x,y)=A(x,y)\exp\left(i\frac{2\pi W(x,y)}{\lambda}\right)

或者:

P(x,y)=A(x,y)exp(i2πW(x,y)λ) P(x,y)=A(x,y)\exp\left(-i\frac{2\pi W(x,y)}{\lambda}\right)

这两种约定都出现在光学和信号处理领域,通常取决于傅里叶变换的约定和OPD的定义。

这很烦人,但可以处理。

规则不是“某一符号总是正确的”。规则是:

在OPD、瞳孔函数、傅里叶变换和坐标定义中使用一致的符号约定。

仅对于强度PSF,改变一个实值相位函数的符号通常会产生一个相关或镜像的结果。但当你关心相位敏感的量、非对称像差、相干成像,或与某个软件包进行比较时,约定就会很重要。

对于本书的最小代码,我们将使用:

P(x,y)=A(x,y)exp(i2πW(x,y)λ) P(x, y)=A(x,y)\exp\left(i\frac{2\pi W(x,y)}{\lambda}\right)

如果你的软件包使用相反的约定,不要慌张。检查其文档,然后在你的比较代码中调整符号。

一个实用的比较检查清单是:

  1. 使用相同的波长。
  2. 使用相同的瞳孔坐标方向。
  3. 使用相同的OPD参考。
  4. 使用相同的相位符号约定。
  5. 在转到PSF时使用相同的FFT约定。

当两个程序不一致时,首先怀疑的不应是“物理错了”。首先怀疑的应是单位、符号、采样和归一化。


15. 瞳孔坐标:物理的还是归一化的?

在本章中我们使用了归一化坐标:

1x1 -1 \leq x \leq 1

1y1 -1 \leq y \leq 1

圆形瞳孔定义为:

x2+y21 x^2+y^2 \leq 1

这对教学很方便。

在一个真实的光学系统中,瞳孔坐标也可以用物理单位(如毫米)表示。瞳孔半径可能是:

Rpupil=5 mm R_\text{pupil}=5\text{ mm}

那么物理瞳孔坐标(X,Y)(X,Y)可以归一化为:

x=XRpupil x = \frac{X}{R_\text{pupil}}

y=YRpupil y = \frac{Y}{R_\text{pupil}}

OPD本身并不会自动归一化。OPD仍然是一个长度。

所以你可能会遇到:

x, y: 归一化瞳孔坐标
W: 米
λ: 米
phase: 弧度

这种组合完全没问题。

坐标系描述的是你在瞳孔中的位置。OPD和波长描述的是该处存在多少相位延迟。

将这些角色分开:

瞳孔坐标:位置
OPD:光程差
波长:相位转换的尺度
相位:以弧度为单位的角度

当这种区分清晰了,代码的调试会变得容易得多。


16. 瞳孔函数不是PSF

这一点值得直截了当地说明。

瞳孔函数不是点光源的图像。

瞳孔函数存在于瞳孔平面。它描述的是孔径上的复场。

PSF存在于像平面。它描述的是衍射和像差之后的强度分布。

下一章将通过傅里叶变换将它们连接起来,但它们不是同一个对象。

一个粗略的图示是:

瞳孔平面:
孔径上的复场

像平面:
像点周围的强度分布

这很重要,因为一个相位图可能看起来很严重,而得到的PSF可能仍然很紧凑;或者一个看似很小的瞳孔遮挡就可能产生可见的衍射环。

你不能总是从一张瞳孔图直接读出最终的图像质量。瞳孔函数是输入。PSF是经过相干传播后的输出。

这就是为什么我们正在小心地构建整个链条,而不是直接跳到最终的图。


17. 一个完整的最小脚本

下面是本章整个最小实现的紧凑版本。

它创建:

  • 一个瞳孔网格;
  • 一个圆形掩膜;
  • 一个合成的OPD图;
  • 一个复瞳孔函数;
  • OPD、相位和振幅的图。
import numpy as np
import matplotlib.pyplot as plt


def make_pupil_grid(n=256):
    coord = np.linspace(-1.0, 1.0, n)
    x, y = np.meshgrid(coord, coord)

    rho = np.sqrt(x**2 + y**2)
    theta = np.arctan2(y, x)
    mask = rho <= 1.0

    return x, y, rho, theta, mask


def synthetic_opd(rho, theta, mask, wavelength):
    opd = np.zeros_like(rho)

    defocus = 0.50 * wavelength
    astigmatism = 0.25 * wavelength

    opd += defocus * (2.0 * rho**2 - 1.0)
    opd += astigmatism * rho**2 * np.cos(2.0 * theta)

    opd[~mask] = 0.0

    return opd


def pupil_function_from_opd(opd, mask, wavelength, amplitude=None):
    if amplitude is None:
        amplitude = mask.astype(float)

    phase = 2.0 * np.pi * opd / wavelength
    pupil = amplitude * np.exp(1j * phase)

    pupil[~mask] = 0.0 + 0.0j

    return pupil, phase


def plot_map(data, title, colorbar_label):
    plt.figure(figsize=(5, 4))
    plt.imshow(data, origin="lower", extent=[-1, 1, -1, 1])
    plt.colorbar(label=colorbar_label)
    plt.title(title)
    plt.xlabel("Normalized pupil x")
    plt.ylabel("Normalized pupil y")
    plt.tight_layout()
    plt.show()


wavelength = 550e-9

x, y, rho, theta, mask = make_pupil_grid(n=256)
opd = synthetic_opd(rho, theta, mask, wavelength)

pupil, phase = pupil_function_from_opd(
    opd=opd,
    mask=mask,
    wavelength=wavelength
)

opd_waves = np.where(mask, opd / wavelength, np.nan)
wrapped_phase = np.where(mask, np.angle(pupil), np.nan)
amplitude = np.abs(pupil)

plot_map(opd_waves, "OPD map", "OPD [waves]")
plot_map(wrapped_phase, "Wrapped pupil phase", "Phase [rad]")
plot_map(amplitude, "Pupil amplitude", "Field amplitude")

这个脚本此时还没有计算PSF。这是故意的。

在这一点上,正确的问题不是:

图像在哪里?

正确的问题是:

我们是否正确地构建了将形成图像的复场?

一旦答案是肯定的,下一章的傅里叶变换就说得通了。


18. 常见错误

让我们在继续之前总结最常见的错误。

错误1:单位混用

这是最危险的一个。

错误:

opd = 200      # 纳米,但未注明
wavelength = 550e-9  # 米
phase = 2 * np.pi * opd / wavelength

正确:

opd = 200e-9
wavelength = 550e-9
phase = 2 * np.pi * opd / wavelength

始终确保OPD和波长使用相同的单位。

错误2:忘记孔径掩膜

如果你计算:

pupil = np.exp(1j * phase)

而没有应用掩膜,你就允许光通过整个正方形数组。

正确:

pupil = mask.astype(float) * np.exp(1j * phase)
pupil[~mask] = 0.0

FFT并不知道你的孔径应该是圆形的,除非你告诉它。

错误3:混淆振幅和强度

如果一个滤光片透过25%的强度,场振幅因子不是0.25。而是:

0.25=0.5 \sqrt{0.25}=0.5

错误:

amplitude = intensity_transmission

通常正确:

amplitude = np.sqrt(intensity_transmission)

这很重要,因为PSF强度是根据复场幅值的平方计算的。

错误4:认为包裹相位中的跳跃总是物理上的跳跃

np.angle(pupil)中的突然跳跃可能只是相位包裹。

在断定波前不连续之前,先检查以波长为单位的OPD。

错误5:未检查约定就与软件比较

如果你的PSF与某个软件包不同,请检查:

  • 波长;
  • 瞳孔采样;
  • OPD参考;
  • 相位符号;
  • FFT移位;
  • 归一化;
  • 坐标方向。

在做改变物理模型的决定之前,先做这些检查。

错误6:将瞳孔函数当作最终结果

瞳孔函数是一个中间表示。它极其重要,但它不是PSF,也不是MTF。

它的作用是将振幅和相位信息带入衍射计算中。


19. 本章为完整链条添加了什么

我们现在可以更新本书的主要计算链条了:

镜头处方
→ 表面和材料
→ 光线追迹
→ 光程
→ OPD图
→ 瞳孔振幅
→ 瞳孔相位
→ 复瞳孔函数

本章执行了一次精确的转换:

以长度单位表示的OPD
→ 以弧度表示的相位
→ 孔径上的复场

这个转换小到几行代码就能容纳。但从概念上讲,它是本书中最重要的步骤之一。

没有它,OPD仅仅是一个波前误差图。

有了它,波前变成了能够干涉、衍射并形成图像的东西。

这就是为什么瞳孔函数是进入PSF计算的大门。

在下一章中,我们将把这个复瞳孔函数送入傅里叶变换。结果将不再存在于瞳孔平面。它将成为一个点图像的强度分布:PSF。

而从那个PSF开始,我们最终将接近开启这段旅程的MTF曲线。


章节总结

瞳孔函数存储了孔径上的复场。

它结合了:

振幅
相位
孔径掩膜

核心公式是:

P(x,y)=A(x,y)exp(i2πW(x,y)λ) P(x, y) = A(x, y)\exp\left(i\frac{2\pi W(x, y)}{\lambda}\right)

其中OPD W(x,y)W(x,y)必须与波长λ\lambda使用相同的长度单位。

孔径掩膜告诉代码光在哪里。振幅告诉我们有多少场穿透。相位告诉我们OPD如何改变复场。

一个完美的清晰圆形瞳孔具有平坦的相位和均匀的振幅。一个有像差的瞳孔可能具有均匀的振幅但非均匀的相位。一个被遮挡或变迹的瞳孔即使OPD为零,也可能具有非均匀的振幅。

本章中的主要代码动作是:

phase = 2.0 * np.pi * opd / wavelength
pupil = amplitude * np.exp(1j * phase)
pupil[~mask] = 0.0

这就是从OPD到傅里叶光学的桥梁。

接下来,我们将使用这个瞳孔函数来计算PSF。

生成验证图

由第11章瞳孔函数模型生成的瞳孔振幅和相位