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

第九章:为什么真实光线会偏离近轴光线

点列图是诚实的,但它并不总能解释原因。

它告诉你光线落点在哪里。它显示光束是紧密的还是发散的。它可能呈现一个圆形模糊、彗星状的拖尾、一条线、色分离或轴外畸变。但点列图本身并不总能告诉你光线为何落成那样。

要理解原因,我们需要一个参考基准。

第八章给了我们这个基准:近轴光学。

近轴模型告诉我们光学系统在一阶近似下的表现。在轴附近,小角度时,光线遵循简单的线性规则。一个场点映射到一个近轴像点。一个准直的轴上光束聚焦在近轴焦平面上。基于薄透镜的估算可以给出焦距的大致比例。F 数和数值孔径告诉我们光线锥的宽窄。

真实光线并不保证遵循那个理想情况。

它们撞击实际表面,计算实际的局部法线,根据矢量斯涅耳定律折射,穿过实际的厚度和材料。在较大的光瞳高度和轴外视场下,实际的光线路径开始偏离近轴预测。

这种偏离并非模糊的缺陷。

它是可以测量的。

本章就是关于测量和解读这种偏离。

我们将用到三个概念:

近轴参考基准
真实光线落点
横向光线误差

然后我们将这些误差转化为光线扇面图,并用它们来介绍主要的像差:

球差
彗差
像散
场曲
畸变
色差

我们不会进行完整的三阶像差推导。那是另一本书的内容。这里的目标是计算上的理解:

追踪真实光线,将它们与参考基准比较,并解读误差的模式。

这是一条更温和的了解像差的途径。我们不是从名称开始,而是从数据开始。

像差是对参考基准的偏离

“像差”这个词听起来像是一堆需要死记硬背的缺陷名称。

球差、彗差、像散、场曲、畸变、色差。

这些名称很重要。光学设计师使用它们,因为它们描述了可识别的模式。但一旦理解了其底层概念,这些名称就变得更容易了。

像差是对理想成像的偏离。

本章中,我们将使用一个基于光线的实用定义:

真实光线落点 - 参考落点 = 横向光线误差

如果一条真实光线恰巧落在参考基准预计的位置,那么横向光线误差为零。

如果它落在别处,误差就是像平面坐标的差值。

对于一条落在:

(x_real, y_real)

的光线,以及一个参考点:

(x_ref, y_ref)

横向误差为:

εx = x_real - x_ref
εy = y_real - y_ref

这就是光线像差图背后的基本计算。

微妙之处不在于减法,而在于如何选择参考基准。

常见的参考基准包括:

近轴像点
主光线落点
光斑质心
最佳焦点位置
波前相关分析中的最佳拟合球面中心

不同的参考基准回答不同的问题。如果使用主光线作为参考,你移除了像移,通常也从绘制的误差中移除了畸变。如果使用近轴像点,你就能看到畸变和视场相关的像位移。如果使用最佳焦点作为参考,你可能只关注重新聚焦后的剩余模糊。

这就是为什么软件中的光线扇面图不仅仅是“光线扇面”。它们是具有参考约定的光线扇面图。

我们将从一个简单的参考基准开始:主光线落点或质心。然后,当畸变重要时,我们会讨论近轴参考基准。

从点列图到横向误差

在第七章中,我们的点列图数据点就是像平面落点坐标:

points = [(x1, y1), (x2, y2), ...]

要将其转换为横向误差,需要选择一个参考基准。

对于居中后的点列图,我们使用质心:

center = mean(points)

然后:

errors = points - center

这已经是横向误差的计算。

在代码中:

import numpy as np
import math
import matplotlib.pyplot as plt

def transverse_errors(points, reference=None):
    """Return transverse ray errors relative to a reference point.

    points:
        Array of shape (N, 2), image-plane x, y intercepts.

    reference:
        Optional array-like [x_ref, y_ref]. If None, use centroid.
    """
    points = np.asarray(points, dtype=float)

    if len(points) == 0:
        return points, np.array([np.nan, np.nan])

    if reference is None:
        reference = points.mean(axis=0)
    else:
        reference = np.asarray(reference, dtype=float)

    return points - reference, reference

在收集了点列图数据后使用它:

errors, reference = transverse_errors(spot_points)

print("reference:", reference)
print("first errors:", errors[:5])

如果我们对 errors[:, 0]errors[:, 1] 做散点图,就会得到居中后的点列图。

但光线扇面图有所不同。

光线扇面图不仅显示误差的二维分布,它还将误差作为光瞳坐标的函数来绘制。

这正是其诊断价值所在。

为什么光线扇面图能揭示结构

点列图回答:

光线落在了哪里?

光线扇面图问的是:

落点误差如何随光瞳位置变化?

这是一个更具结构化的问题。

对于子午扇面,我们可以让一个光瞳坐标沿一条线变化:

Px from -1 to +1, Py = 0

然后绘制像误差随 Px 的变化。

对于弧矢扇面,我们改变另一个光瞳坐标:

Py from -1 to +1, Px = 0

然后绘制像误差随 Py 的变化。

因此,光线扇面图是通过对光瞳进行线性采样构建的,不一定需要完整的二维网格。

这就是为什么光线扇面图比密集的点列图更能清晰地显示像差模式。点列图可能看起来像一团云,而光线扇面图展示了函数关系:

光瞳坐标 → 横向误差

如果误差主要是一条直线,通常表明有离焦。如果它以三次曲线形状弯曲,可能存在球差。如果轴外扇面不对称,可能涉及彗差。如果子午和弧矢扇面表现不同,像散或场曲可能参与其中。

我们不会仅凭一张图就下结论,但我们可以开始解读这些形状。

在光瞳内进行线性采样

让我们创建光瞳内的线性采样点。

对于 x 扇面:

x varies from -R to +R
y = 0

对于 y 扇面:

x = 0
y varies from -R to +R

在归一化的光瞳坐标中,我们使用:

P = coordinate / pupil_radius

这样 P 的范围是 -1+1

def sample_pupil_line(axis, num_points, pupil_radius):
    """Sample a line through the circular pupil.

    axis:
        "x" for x-line samples with y = 0.
        "y" for y-line samples with x = 0.
    """
    P = np.linspace(-1.0, 1.0, num_points)
    points = []

    for p in P:
        if axis == "x":
            points.append((p * pupil_radius, 0.0))
        elif axis == "y":
            points.append((0.0, p * pupil_radius))
        else:
            raise ValueError("axis must be 'x' or 'y'")

    return P, np.array(points, dtype=float)

如果我们希望包含中心光线,就强制点数设为奇数:

def odd_count(n):
    return n if n % 2 == 1 else n + 1

现在构建一个线性光束:

def make_line_bundle(
    axis,
    num_points,
    pupil_radius,
    z_start,
    theta_x_deg=0.0,
    theta_y_deg=0.0,
    wavelength_um=0.5875618,
):
    num_points = odd_count(num_points)
    P, pupil_points = sample_pupil_line(axis, num_points, pupil_radius)

    rays = make_ray_bundle(
        pupil_points=pupil_points,
        z_start=z_start,
        theta_x_deg=theta_x_deg,
        theta_y_deg=theta_y_deg,
        wavelength_um=wavelength_um,
    )

    return P, rays

这样我们同时得到了归一化光瞳坐标和光线。

利用我们的单片透镜追踪器构建光线扇面图

现在我们可以通过前面章节中的简单单片透镜来追踪线性光束。

假设我们仍保留了:

front
back
image
N_BK7
trace_bundle_through_singlet(...)
collect_image_points(...)

现在编写一个光线扇面函数。

def ray_fan_for_singlet(
    axis,
    num_points,
    pupil_radius,
    z_start,
    theta_x_deg,
    theta_y_deg,
    wavelength_um,
    front,
    back,
    image,
    glass_material,
    reference="centroid",
):
    """Compute a simple transverse ray fan for the singlet.

    Returns:
        P: normalized pupil coordinates
        errors: transverse errors, shape (N, 2), for valid rays
        points: image-plane intercepts
        valid_mask: Boolean mask for rays that reached the image plane
        ref: reference point used for transverse error
    """
    P, rays = make_line_bundle(
        axis=axis,
        num_points=num_points,
        pupil_radius=pupil_radius,
        z_start=z_start,
        theta_x_deg=theta_x_deg,
        theta_y_deg=theta_y_deg,
        wavelength_um=wavelength_um,
    )

    traced = trace_bundle_through_singlet(
        rays,
        front=front,
        back=back,
        image=image,
        glass_material=glass_material,
    )

    points = []
    valid_P = []

    for p, ray in zip(P, traced):
        if ray.alive:
            points.append((ray.position[0], ray.position[1]))
            valid_P.append(p)

    points = np.array(points, dtype=float)
    valid_P = np.array(valid_P, dtype=float)

    if len(points) == 0:
        return valid_P, points, points, np.array([], dtype=bool), np.array([np.nan, np.nan])

    if reference == "centroid":
        errors, ref = transverse_errors(points)
    elif reference == "chief":
        # With odd line sampling, the center point is the closest chief-like sample
        center_index = np.argmin(np.abs(valid_P))
        ref = points[center_index]
        errors, ref = transverse_errors(points, reference=ref)
    else:
        raise ValueError("reference must be 'centroid' or 'chief'")

    return valid_P, errors, points, ref

现在绘制扇面图。

对于轴上视场,使用 x 扇面:

P, errors, points, ref = ray_fan_for_singlet(
    axis="x",
    num_points=51,
    pupil_radius=5.0,
    z_start=-20.0,
    theta_x_deg=0.0,
    theta_y_deg=0.0,
    wavelength_um=0.5875618,
    front=front,
    back=back,
    image=image,
    glass_material=N_BK7,
    reference="chief",
)

plt.plot(P, errors[:, 0], marker=".")
plt.axhline(0.0, linewidth=0.8)
plt.axvline(0.0, linewidth=0.8)
plt.xlabel("normalized pupil coordinate P_x")
plt.ylabel("transverse error εx [mm]")
plt.title("On-axis x ray fan")
plt.show()

这个图就是一个光线扇面图。

它不是什么新的物理原理。它只是对同样的追踪光线数据进行了不同的组织方式。

光线追踪 → 像面落点 → 减去参考点 → 绘制误差与光瞳坐标的关系

这就是整个构建过程。

离焦:最简单的光线扇面形状

在命名复杂的像差之前,先从离焦开始。

假设一个光束理想上会在某个平面聚焦,但我们在焦前或焦后的位置进行测量。那么光线落点会大致随光瞳坐标线性偏移。边缘光线落在一边,中心光线靠近参考点,另一侧边缘光线落在另一边。

光线扇面图近似为一条直线。

通过移动像平面很容易模拟这一点。

def make_image_plane(z_image):
    return IntersectableSurface(
        radius=float("inf"),
        z_vertex=z_image,
        semi_diameter=None,
        surface_type="plane",
        comment="image plane",
    )

现在比较不同像面位置下的扇面图:

for z_image in [40.0, 50.0, 60.0]:
    image_test = make_image_plane(z_image)

    P, errors, points, ref = ray_fan_for_singlet(
        axis="x",
        num_points=51,
        pupil_radius=5.0,
        z_start=-20.0,
        theta_x_deg=0.0,
        theta_y_deg=0.0,
        wavelength_um=0.5875618,
        front=front,
        back=back,
        image=image_test,
        glass_material=N_BK7,
        reference="chief",
    )

    plt.plot(P, errors[:, 0], marker=".", label=f"z={z_image:.0f} mm")

plt.axhline(0.0, linewidth=0.8)
plt.axvline(0.0, linewidth=0.8)
plt.xlabel("normalized pupil coordinate P_x")
plt.ylabel("transverse error εx [mm]")
plt.title("Defocus changes the slope of the ray fan")
plt.legend()
plt.show()

主要变化通常是扇形图中斜率的倾斜。

这很有用,因为离焦会掩盖其他模式。如果像平面不在一个有意义的焦平面上,光线扇面图可能主要由直线项主导。一旦重新聚焦,剩下的曲率就更容易检查了。

这就是为什么光学分析软件通常允许你改变焦点或减去某些参考项。这个图不是一个纯粹的原始真相,而是一个诊断视图。

球差:边缘光线与近轴光线的聚焦位置不同

用我们目前的代码最容易观察到的一种像差就是球差。

对于一个轴上物点,完美的近轴模型会使所有入射的平行光线聚焦在同一点。而一个真实的球面通常不会这样。靠近边缘的光线可能与中心光线在轴上交叉的位置不同。

在光线扇面图中,球差通常表现为一种弯曲的模式,常见的形状是横向光线误差与光瞳坐标呈近似的三次方关系。

让我们计算单片透镜后不同入射高度的光线大致在何处与轴相交。

def trace_simple_singlet_to_exit(ray, front, back, glass_material):
    n_air = 1.0
    n_glass = glass_material.n(ray.wavelength_um)

    interact_refractive_surface(ray, front, n1=n_air, n2=n_glass)

    if ray.alive:
        interact_refractive_surface(ray, back, n1=n_glass, n2=n_air)

    return ray

def z_axis_crossing_xz(ray):
    """Estimate where a ray in the x-z plane crosses x=0 after exit."""
    x0 = ray.position[0]
    z0 = ray.position[2]
    dx = ray.direction[0]
    dz = ray.direction[2]

    if abs(dx) < 1e-12:
        return math.inf

    t = -x0 / dx

    if t < 0:
        return math.inf

    return z0 + t * dz

现在扫描光线高度:

heights = np.linspace(0.5, 8.0, 16)
crossings = []

for h in heights:
    ray = Ray(
        position=[h, 0.0, -20.0],
        direction=[0.0, 0.0, 1.0],
        wavelength_um=0.5875618,
    )

    trace_simple_singlet_to_exit(ray, front, back, N_BK7)
    crossings.append(z_axis_crossing_xz(ray))

plt.plot(heights, crossings, marker=".")
plt.xlabel("input ray height [mm]")
plt.ylabel("axis crossing z [mm]")
plt.title("Longitudinal focus shift with ray height")
plt.show()

如果光线交叉位置随光线高度变化,则说明在这种设置下透镜存在球差。

这个结论不是从记忆中贴上的标签,而是从数据中得出的:

近轴光线聚焦在此处
边缘光线聚焦在别处

这就是计算意义上的球差。

固定像平面上的横向球差

轴向交叉点是一个纵向视角。而在固定像平面上的光线扇面图则提供了横向视角。

选择一个像平面。追踪通过光瞳的光线。绘制横向误差。

image_focus = make_image_plane(50.0)

P, errors, points, ref = ray_fan_for_singlet(
    axis="x",
    num_points=101,
    pupil_radius=5.0,
    z_start=-20.0,
    theta_x_deg=0.0,
    theta_y_deg=0.0,
    wavelength_um=0.5875618,
    front=front,
    back=back,
    image=image_focus,
    glass_material=N_BK7,
    reference="chief",
)

plt.plot(P, errors[:, 0])
plt.axhline(0.0, linewidth=0.8)
plt.axvline(0.0, linewidth=0.8)
plt.xlabel("normalized pupil coordinate P_x")
plt.ylabel("transverse error εx [mm]")
plt.title("On-axis transverse ray fan")
plt.show()

对于一个旋转对称的轴上系统,x 扇面和 y 扇面应该表现相似。如果绘图中的曲线弯曲并以预期的方式对称,你看到的就是光线高度如何改变焦点和像面落点。

不必担心给每个系数都起个名字。计算层面的理解已经足够:

误差不是恒定的
误差不仅仅是一个线性的离焦项
误差随光瞳坐标变化

这种变化就是光线追踪暴露出的像差。

彗差:轴外不对称性

现在转到轴外。

彗差出现在轴外视场点,当来自不同光瞳区域的光线产生不对称的模糊时。在点列图中,彗差可能看起来像彗星:一个密集的头部和扩散的尾巴。在光线扇面图中,彗差会引入在轴上对称情况下不存在的非对称项。

让我们追踪一个轴外视场:

image_focus = make_image_plane(50.0)

P, errors, points, ref = ray_fan_for_singlet(
    axis="y",
    num_points=101,
    pupil_radius=5.0,
    z_start=-20.0,
    theta_x_deg=0.0,
    theta_y_deg=5.0,
    wavelength_um=0.5875618,
    front=front,
    back=back,
    image=image_focus,
    glass_material=N_BK7,
    reference="chief",
)

plt.plot(P, errors[:, 1])
plt.axhline(0.0, linewidth=0.8)
plt.axvline(0.0, linewidth=0.8)
plt.xlabel("normalized pupil coordinate P_y")
plt.ylabel("transverse error εy [mm]")
plt.title("Off-axis ray fan")
plt.show()

现在也绘制轴外点列图:

pupil_points = sample_pupil_grid(samples_per_axis=17, pupil_radius=5.0)

rays = make_ray_bundle(
    pupil_points=pupil_points,
    z_start=-20.0,
    theta_x_deg=0.0,
    theta_y_deg=5.0,
    wavelength_um=0.5875618,
)

traced = trace_bundle_through_singlet(rays, front, back, image_focus, N_BK7)
spot_points, valid = collect_image_points(traced)
spot_errors, ref = transverse_errors(spot_points)

plot_spot(spot_errors, title="Off-axis spot, centered")

如果居中的光斑不对称,那是一个线索。如果随着光瞳坐标变化,光线扇面图显示出不对称的偏离,那是另一个线索。

这就是以实用方式认识彗差的方法:

不是先接触它的名称,
而是先通过追踪光线数据中观察到的轴外不对称误差模式。

简单的单片透镜并未校正轴外像差,因此它是一个很好的教学对象。其糟糕表现足以展示问题所在。

子午与弧矢特性

轴外系统有一个特别的复杂之处。

存在两个重要的平面:

子午面
弧矢面

如果视场在 y 方向,子午面包含光轴和视场方向。弧矢面与之垂直。

从实用的光线扇面图角度来看,我们经常检查两个扇面:

子午扇面
弧矢扇面

确切的命名取决于坐标约定。重要的计算思想是:

沿两条正交的光瞳线采样光线,并比较它们的横向误差。

如果子午扇面和弧矢扇面表现不同,系统正在告诉你,轴外成像在两个平面上并不相同。

这种差异与像散和场曲相关。

让我们为同一个轴外视场计算两个线性的风扇图:

field_angle = 5.0
image_focus = make_image_plane(50.0)

P_x, errors_x, points_x, ref_x = ray_fan_for_singlet(
    axis="x",
    num_points=101,
    pupil_radius=5.0,
    z_start=-20.0,
    theta_x_deg=0.0,
    theta_y_deg=field_angle,
    wavelength_um=0.5875618,
    front=front,
    back=back,
    image=image_focus,
    glass_material=N_BK7,
    reference="chief",
)

P_y, errors_y, points_y, ref_y = ray_fan_for_singlet(
    axis="y",
    num_points=101,
    pupil_radius=5.0,
    z_start=-20.0,
    theta_x_deg=0.0,
    theta_y_deg=field_angle,
    wavelength_um=0.5875618,
    front=front,
    back=back,
    image=image_focus,
    glass_material=N_BK7,
    reference="chief",
)

plt.plot(P_x, errors_x[:, 0], label="x fan: εx")
plt.plot(P_y, errors_y[:, 1], label="y fan: εy")
plt.axhline(0.0, linewidth=0.8)
plt.axvline(0.0, linewidth=0.8)
plt.xlabel("normalized pupil coordinate")
plt.ylabel("transverse error [mm]")
plt.title("Two orthogonal ray fans for an off-axis field")
plt.legend()
plt.show()

这种比较比任何单一曲线都更有信息量。

如果一个平面的聚焦方式与另一个不同,移动像平面可能改善一个扇面却使另一个恶化。这就是存在像散现象的实用标志。

像散:两个线焦点而不是一个点焦点

像散并非泛指“一切都很模糊”。

对于一个轴外视场,光束两个正交截面的光线可能在轴向不同位置达到最佳焦点。系统不是拥有一个清晰的点焦点,而是有两个倾向于线状的焦点:子午焦点和弧矢焦点。

从计算上观察这一点的简单方法是扫描像平面位置并测量两个方向上的 RMS 宽度。

让我们使用轴外光束,随着像平面移动测量光斑的散布。

def spot_statistics_for_plane(z_image, field_angle, wavelength_um=0.5875618):
    image_test = make_image_plane(z_image)
    pupil_points = sample_pupil_grid(samples_per_axis=17, pupil_radius=5.0)

    rays = make_ray_bundle(
        pupil_points=pupil_points,
        z_start=-20.0,
        theta_x_deg=0.0,
        theta_y_deg=field_angle,
        wavelength_um=wavelength_um,
    )

    traced = trace_bundle_through_singlet(rays, front, back, image_test, N_BK7)
    points, valid = collect_image_points(traced)

    if len(points) == 0:
        return np.nan, np.nan, np.nan

    centered, ref = transverse_errors(points)
    rms_x = math.sqrt(float(np.mean(centered[:, 0]**2)))
    rms_y = math.sqrt(float(np.mean(centered[:, 1]**2)))
    rms_r = rms_spot_radius(points)

    return rms_x, rms_y, rms_r

扫描焦点:

z_values = np.linspace(35.0, 70.0, 71)
field_angle = 5.0

rms_x_values = []
rms_y_values = []
rms_r_values = []

for z in z_values:
    rms_x, rms_y, rms_r = spot_statistics_for_plane(z, field_angle)
    rms_x_values.append(rms_x)
    rms_y_values.append(rms_y)
    rms_r_values.append(rms_r)

plt.plot(z_values, rms_x_values, label="RMS x")
plt.plot(z_values, rms_y_values, label="RMS y")
plt.plot(z_values, rms_r_values, label="RMS radius")
plt.xlabel("image plane z [mm]")
plt.ylabel("RMS spot size [mm]")
plt.title("Focus scan for an off-axis field")
plt.legend()
plt.show()

如果 RMS x 和 RMS y 的最小值出现在不同的 z 位置,说明系统在横向两个方向上的最佳聚焦行为不同。

这就是计算层面的像散特征。

再次强调,我们不是让你记忆图表,而是展示光线追踪如何揭示这种效应。

场曲:最佳焦点随视场变化

一个平坦的传感器是平的,但实际光学系统可能偏好一个弯曲的像面。

场曲意味着最佳焦点位置随视场变化。中心视场可能在一个像平面上聚焦,而边缘视场希望另一个像平面。

这很容易与普通的离焦混淆,除非你比较多个视场点。

让我们为几个视场计算最佳 RMS 焦点。

def best_focus_by_rms(field_angle, z_min=35.0, z_max=70.0, count=71):
    z_values = np.linspace(z_min, z_max, count)
    rms_values = []

    for z in z_values:
        _, _, rms_r = spot_statistics_for_plane(z, field_angle)
        rms_values.append(rms_r)

    rms_values = np.array(rms_values)
    idx = np.nanargmin(rms_values)

    return z_values[idx], rms_values[idx], z_values, rms_values

运行它:

for field_angle in [0.0, 2.0, 4.0, 6.0]:
    z_best, rms_best, _, _ = best_focus_by_rms(field_angle)
    print(
        f"field={field_angle:.1f} deg, "
        f"best z≈{z_best:.2f} mm, "
        f"best RMS≈{rms_best:.6f} mm"
    )

如果最佳焦点随视场变化,那在计算上就是场曲。

如果最佳焦点面是弯曲的,平坦的像平面无法完美匹配所有视场。设计者可能接受它、校正它,或将其与其他像差平衡。相机镜头、显微物镜、目镜或投影镜头对此的重视程度各不相同。

关键点很简单:

场曲不仅仅是边缘模糊。
它是一种随视场变化的最佳焦点位置。

通过焦点扫描就能看到这一点。

畸变:即使光斑清晰,像的位置也是错的

畸变与模糊不同。

一个透镜可以形成一个相对清晰的像点,但位置错误。这就是畸变。

这就是为什么居中的点列图会隐藏畸变。如果你减去质心或主光线落点,你就移除了像的位移,只关注散布。这对于分析模糊很有用,但对于评估映射精度则不然。

要看到畸变,需比较真实像高和近轴像高。

根据第八章:

y_paraxial ≈ f tan θ

对于我们的简单单片透镜,我们可以使用薄透镜焦距估算作为粗略参考。真实的软件库会从实际光学系统提供更好的近轴参考,但薄透镜估算足以展示方法。

def real_chief_ray_image_height(field_angle_deg, z_image, wavelength_um=0.5875618):
    image_test = make_image_plane(z_image)

    chief = make_chief_ray(
        z_start=-20.0,
        theta_x_deg=0.0,
        theta_y_deg=field_angle_deg,
        wavelength_um=wavelength_um,
    )

    trace_simple_singlet_ray(chief, front, back, image_test, N_BK7)

    if not chief.alive:
        return np.nan

    return chief.position[1]

现在比较:

f_est = thin_lens_focal_length_from_radii(
    N_BK7.n(0.5875618),
    R1=50.0,
    R2=-50.0,
)

z_image = 50.0

for field_angle in [1.0, 2.0, 3.0, 5.0, 7.0]:
    y_real = real_chief_ray_image_height(field_angle, z_image)
    y_para = paraxial_image_height_from_field(f_est, field_angle)
    error = y_real - y_para

    print(
        f"field={field_angle:.1f} deg, "
        f"real y={y_real:.4f} mm, "
        f"paraxial y={y_para:.4f} mm, "
        f"distortion-like error={error:.4f} mm"
    )

这并不是一个精细的畸变分析。焦距参考是粗略的,我们简化后的主光线模型也比较简单。但结构是正确的:

视场角
→ 近轴像高
→ 真实主光线像高
→ 差异

这个差异就是映射误差。

光斑可能很小,但映射仍然可能畸变。这就是为什么畸变属于像差家族,但其表现却与模糊不同。

色差:不同波长,不同的光线

色差源自材料模型。

在第三章中,我们了解到玻璃的折射率取决于波长:

n = n(λ)

在第六章中,我们在斯涅耳定律中使用了那个折射率。

因此,不同波长的光线通过同一个透镜时,路径并不完全相同。

有两个常见的色差效应需要注意:

轴向色差:
    不同波长聚焦在轴向不同的位置

倍率色差:
    轴外成像时,不同波长对应不同的像高

让我们估算一个简单的轴向色差焦点位置。

for wl in [0.4861327, 0.5875618, 0.6562725]:
    z_best, rms_best, _, _ = best_focus_by_rms(
        field_angle=0.0,
        z_min=35.0,
        z_max=70.0,
        count=71,
    )
    print(wl, z_best, rms_best)

这个版本不小心在 spot_statistics_for_plane 中使用了默认波长,所以让我们明确指定波长。

def best_focus_by_rms_for_wavelength(
    field_angle,
    wavelength_um,
    z_min=35.0,
    z_max=70.0,
    count=71,
):
    z_values = np.linspace(z_min, z_max, count)
    rms_values = []

    for z in z_values:
        rms_x, rms_y, rms_r = spot_statistics_for_plane(
            z_image=z,
            field_angle=field_angle,
            wavelength_um=wavelength_um,
        )
        rms_values.append(rms_r)

    rms_values = np.array(rms_values)
    idx = np.nanargmin(rms_values)

    return z_values[idx], rms_values[idx]

现在计算:

for wl in [0.4861327, 0.5875618, 0.6562725]:
    z_best, rms_best = best_focus_by_rms_for_wavelength(
        field_angle=0.0,
        wavelength_um=wl,
    )

    print(
        f"λ={wl:.7f} µm, "
        f"best focus z≈{z_best:.2f} mm, "
        f"best RMS≈{rms_best:.6f} mm"
    )

如果不同波长的最佳焦点不同,你看到的就是轴向色差。

对于倍率色差,比较轴外主光线像高随波长的变化:

field_angle = 5.0
z_image = 50.0

for wl in [0.4861327, 0.5875618, 0.6562725]:
    y_real = real_chief_ray_image_height(
        field_angle_deg=field_angle,
        z_image=z_image,
        wavelength_um=wl,
    )

    print(f"λ={wl:.7f} µm, chief ray image y={y_real:.6f} mm")

如果像高随波长变化,在这个简化分析中就是倍率色差。

单片透镜看起来不会好,也确实如此。它之所以有用,是因为色差来源于同样的光线追踪机制。

在概念层面上,不需要一个专门的“色差模块”。

链条是:

波长
→ 折射率
→ 折射方向
→ 像面落点
→ 随波长变化的焦点或像高

从计算出发的分类,而非死记硬背

现在我们可以用计算术语总结经典像差。

球差

轴上光线在不同光瞳半径处聚焦不同。

计算迹象:

轴向交点 z 随光瞳高度变化
轴上光线扇面图显示误差与光瞳坐标呈弯曲关系

彗差

轴外光线形成不对称的模糊。

计算迹象:

轴外光斑变得不对称
轴外光线扇面图显示出不对称的误差项

像散

子午和弧矢截面聚焦不同。

计算迹象:

两个正交方向的扇面行为不同
RMS x 和 RMS y 可能在不同的像平面处达到最小值

场曲

最佳焦点随视场变化。

计算迹象:

最佳 RMS 焦点 z 随视场角改变而移动
平坦像平面无法同样好地匹配所有视场

畸变

像点与近轴映射位置相比有位移,即使光斑是清晰的。

计算迹象:

真实主光线或质心像高与近轴像高不同
居中的点列图可能掩盖这一点

色差

不同波长成像不同。

计算迹象:

最佳焦点随波长移动
轴外像高随波长变化
复色光光斑按颜色分离

这种学习像差的方法比背诵表格要慢,但也更扎实。

现在,这些名称都与可测量的行为相关联了。

为什么真实光线扇面图不是波前图

光线扇面图功能强大,但它仍然基于光线。

它绘制的是横向像误差与光瞳坐标的关系。它不直接显示光程差。它本身不给出 PSF 或 MTF。它仍然是几何光学的一部分。

这一局限性是重要的。

两个具有相似点列图的系统,可能波前相位行为不同。当几何光斑很小时,衍射会占据主导地位。接近衍射极限的镜头需要进行波前分析,而不仅仅是光线落点分析。

所以,本章是一座桥梁,但不是最后一座桥梁。

我们已经从:

光线落在哪里?

进入到:

真实光线如何偏离参考基准?

下一章将进入到:

光线路径如何转化为光瞳上的光程差?

那将是连接几何光线追踪与波动光学的桥梁。

Optiland 的 RayFan 作为一个更高层级的版本

一个真正的光学库已经具备光线扇面图工具。

在当前的 Optiland 文档中,RayFan 分析接受一个光学系统、视场、波长和采样点数;它追踪光瞳内的线性分布采样,并存储 x 和 y 方向的光线扇面数据。源码还显示,标准视图会将光线扇面误差相对于归一化光瞳坐标绘图,并分 x、y 扇面面板展示。(Optiland)

一个简洁的 Optiland 风格工作流程如下:

from optiland.samples.objectives import CookeTriplet
from optiland.analysis import RayFan

lens = CookeTriplet()

fan = RayFan(
    lens,
    fields="all",
    wavelengths="all",
    num_points=101,
)

fan.view()

对于自定义的单片透镜,设置方式遵循之前使用的光学系统构建风格:

from optiland import optic
from optiland.analysis import RayFan

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.fields.add(y=5.0)
lens.wavelengths.add(value=0.5876, is_primary=True)

fan = RayFan(lens, fields="all", wavelengths="all", num_points=101)
fan.view()

将此视为库层面的对比代码。确切的导入路径和选项可能随项目发展而变化,但其计算含义是稳定的:

选择视场和波长
通过光瞳追踪线性采样
测量像面横向误差
绘制误差与归一化光瞳坐标的关系图

当前的 Optiland 源码还包含一个 BestFitRayFan 类,它将光线扇面数据参考到一个最佳拟合球面中心,而不是通常的主光线中心参考。这很好地提醒了我们,光线扇面图依赖于参考约定,而不仅仅是追踪到的光线。(Optiland)

这正是我们先自己构建一个小版本的原因。

当一个库给你一张漂亮的扇面图时,你现在可以问:

采样了哪条光瞳线?
使用了什么视场和波长?
减去了哪个参考点?
畸变是否被移除?
无效光线是否被抑制?

这些问题并非吹毛求疵,它们决定了图表的含义。

一个小型诊断工作流程

解读光线扇面结果时,应遵循一个有纪律的顺序。

首先,检查设置:

视场
波长
孔径
像平面
参考约定
采样密度

其次,检查是否有光线丢失:

被孔径遮挡
错过了某个表面
发生全内反射
无效的像面落点

第三,将离焦与残留形状分开。如果扇面图主要是一条直线,移动像平面或减去离焦项后,再诊断更细微的像差。

第四,比较 x 和 y 扇面。不要假设一个扇面能讲述完整的故事。

第五,比较视场。轴上和轴外行为通常完全不同。

第六,比较波长。如果曲线因波长而分离,那是材料模型在说话。

最后,将扇面图与点列图联系起来。扇面图解释结构,光斑展示二维落点模式。两者不能互相替代。

这个工作流程比仅凭视觉记忆去识别每种像差更为可靠。

解读像差时的常见错误

让我们明确列出这些陷阱。

错误 1:将所有模糊都称为“球差”

球差特指光线行为如何随光瞳半径变化,且是针对轴上视场而言的。轴外模糊可能涉及彗差、像散、场曲和其他效应。

错误 2:忘记像平面

如果像平面选择不当,光线扇面图可能被离焦主导。在诊断细微像差之前,应先检查焦点。

错误 3:居中操作消除了畸变

居中的光斑或参考主光线的扇面图可能移除了像位移。这对于模糊分析有用,但可能会隐藏畸变。

错误 4:比较不同参考基准的扇面图

参考主光线的扇面图与参考近轴像点或最佳拟合焦点的扇面图是不同的。

错误 5:忽略波长

单色光扇面图无法显示色差分离。复色光扇面图可能包含多个重叠的故事。

错误 6:过于自信地解读稀疏的扇面图

采样密度很重要。点太少会隐藏曲率,或使曲线看起来比实际更平滑。

错误 7:将光线扇面图当作衍射分析

光线扇面图属于几何光学。它们不能直接告诉你衍射 PSF 或 MTF。

错误 8:忘记单位

扇面图中的误差 0.01 mm 可能大也可能小,取决于像素大小、艾里斑尺寸、焦距和预期用途。

错误 9:期望一个简单单片透镜像一个校正良好的物镜那样工作

一个双凸的单片透镜是一个教学系统。它会清晰地显示像差,因为它未经校正。这在这里是很有用的。

错误 10:仅凭一幅图就下诊断

应结合使用点列图、光线扇面图、焦点扫描、视场比较和波长比较。一幅图很少揭示全部真相。

本章带给我们的收获

我们现在有了一种讨论像差的计算方法。

我们可以:

追踪真实光线通过镜头
选择一个参考落点
计算横向光线误差
跨光瞳采样线性扇面
绘制光线扇面曲线
扫描焦点位置
比较视场
比较波长
将光线扇面形状与经典像差联系起来

重要的转变是:

像差不再仅仅是名字,它们是计算出的光线误差中的模式。

这是本书后续部分恰当的理解水平。

当我们以后计算 OPD、PSF、OTF 和 MTF 时,我们不会忘记光线的故事。波动光学并没有抹去几何光学,而是扩展了它。光瞳上的波前建立在光程差之上,而这些光程来自系统中追踪的光线路径。

光线扇面图告诉我们光线如何偏离。

波前图将告诉我们相位如何偏离。

PSF 将展示一个点如何扩散。

MTF 将显示对比度如何保持。

这条链正在变长,但每个环节现在都清晰可见。

前方的道路

光线扇面图是一个强大的诊断工具,但它仍然停留在像平面坐标中。它告诉我们横向误差。

要进入衍射分析,我们需要一个不同的量。

我们需要光程差。

光线不仅仅落在某个地方,它在穿过空气和玻璃时也会累积光程。穿过光瞳的不同光线可能携带着相对于某个参考波前不同的光程长度。这种差异就成为了相位误差。

这就是从光线追踪到波动光学的桥梁。

因此,下一章将问:

我们如何将追踪到的光线路径转化为光瞳上的 OPD?

一旦我们有了 OPD,后续章节就可以构建光瞳函数、PSF、OTF,最后是 MTF。

点列图来自光线落点。

波前图来自光线携带的光程。

我们现在知道了第一个,接下来去学习第二个。

生成的验证图

第九章像差检查生成的光线扇面图样本