第16章:库克三片式镜头:贯穿整个计算链的实例演示
库克三片式镜头看起来并不复杂。
三个镜片。前方一个正透镜,中间一个负透镜,后方再跟一个正透镜。与现代多镜片相机镜头相比,它几乎显得很简单。
而这种简单,恰恰是它在这里的价值所在。
库克三片式镜头复杂到足以展现出真实的光学行为:
- 球差;
- 彗差;
- 像散;
- 场曲;
- 畸变;
- 色差校正;
- 视场相关的像质;
- 优化中的权衡取舍。
但它又足够小巧,让我们能将整个设计一览无余。
本章将库克三片式镜头作为一个全链条的实例。我们并不打算巨细无遗地讲解三片式镜头的每一处历史与商业细节,而是将其作为一个测试台。
在这里,我们将走完本书一直在构建的路线:
光学处方
→ 表面与材料
→ 光线追迹
→ 点列图
→ 光扇图
→ 光程差与波前
→ 光瞳函数
→ 点扩散函数(PSF)
→ 光学传递函数(OTF)
→ 调制传递函数(MTF)
→ 评价函数
→ 优化
→ 优化前后对比
如果前面各章是工作台上摆放的单件工具,那么本章就是用它们进行流水作业的地方。
这是一次检验:当你看到最终的 MTF 曲线时,你能说清楚在这条曲线出现之前,经历了哪些步骤吗?
到本章结尾,答案应当是肯定的。
在开始之前,有一点需要说明:本章采用一套固定的教学用光学处方,而不是将库克三片式镜头作为一个模糊的占位符。数据经过有意四舍五入,并用于教学目的,并非声称来自某个历史专利或量产镜头。这一区别很重要。一套固定的教学处方足以让计算链得到检视。而量产处方还需要玻璃库验证、公差分析、可制造性检查以及版本锁定的工程评审。
1. 为什么库克三片式镜头是良好的测试案例
单片薄透镜过于简单,无法承载完整的光学设计故事。它能展示焦距和基本像差,但无法承载太多的设计权衡。
复杂的现代摄影镜头则面临相反的问题。它可能包含众多镜片、非球面、特殊玻璃、浮动组、机械约束和专有设计选择。它很真实,但对于首次全链条演练来说,信息量太大。
库克三片式镜头位于两者之间,恰到好处。
它拥有足够的自由度,使优化有意义:
曲率半径
厚度
空气间隙
玻璃选择
光阑位置
像面位置
它拥有足够的像差行为,使分析有意义:
轴上与轴外点列图各不相同
光扇图展现出结构
波前图随视场变化
PSF 随视场和焦面变化
MTF 因视场和方向而异
而且它足够小巧,我们仍能以表格形式理解其处方。
这就是光学设计教育常常回过头来研究三片式镜头的原因。它们不是玩具,而是真实设计逻辑的紧凑范例。
对本书而言,库克三片式镜头还有另一个优势:它能让我们展示开放式计算工作流的教育价值。
我们不仅能问:
MTF 是什么?
还能问:
是哪一份处方产生的 MTF?
追迹的是哪些光线?
使用的是哪个视场点?
使用的是哪个波长?
哪个波前成为了光瞳函数?
哪个 PSF 产生了 OTF?
哪个评价函数改变了设计?
这才是关键所在。
2. 我们将做什么,不做什么
本章是一次贯穿流程的演示,而不是一个完整的工业级镜头设计项目。
我们不会假装几页纸的代码能替代专业的光学设计软件。我们不会推导每一个三级像差项。我们不会宣称一个在教学脚本中优化过的三片式镜头就可以投产。
我们要做的更加专注:
选择一个完整的镜头
执行主要分析
将每一幅图与其计算起源联系起来
进行一次小型优化
比较优化前后
这就足以证明本书的主要承诺。
现在我们可以跟踪一个镜头,从处方数据一直走到像质指标。
库克三片式镜头并非主角,计算链才是。
3. 本章的双轨方法
我们将沿用贯穿全书的双轨方法。
A轨:极简 Python 理解
A轨用于理解骨架。它采用小段代码片段,展示正在传递何种数据。
A轨不在此处实现一个完整的生产级光线追迹器。我们在此前的章节中已经构建了许多组件:
- 光线表示;
- 表面相交;
- 矢量折射;
- 光瞳采样;
- 光程差到光瞳函数;
- 通过 FFT 计算 PSF;
- 从 PSF 计算 OTF 和 MTF;
- 评价函数优化。
在本章中,A轨主要提醒我们每个阶段意味着什么。
B轨:Optiland 适配器边界
B轨不允许成为一堆半拉子代码。它需要一个清晰的边界。本书正文展示工作流;配套代码应锁定安装的库版本,并围绕实际 API 实现一个小型适配层。
阅读本章中的 Optiland 示例时,请按以下两种方式之一理解:
- 纯 Python 代码块 旨在作为语法有效的教学代码。
- 适配器草图 描述的是必须在配套仓库中为固定的 Optiland 版本实现的调用。它们被标记为草图,而非可直接运行的片段。
实用的配套仓库应包含:
pyproject.toml
notebooks/16_cooke_triplet_full_chain.ipynb
src/optics_examples/cooke_triplet.py
src/optics_examples/analysis_adapters.py
tests/test_chapter_code_syntax.py
tests/test_cooke_triplet_smoke.py
适配器的约定很简单:
make_cooke_triplet() -> 光学系统对象
plot_layout(system) -> 布局图形或坐标轴
run_spot_diagram(system, ...) -> 点列图数据及图形
run_ray_fan(system, ...) -> 光扇图数据及图形
run_wavefront(system, ...) -> OPD图、光瞳掩膜、元数据
run_psf(system, ...) -> PSF数组、坐标元数据
run_mtf(system, ...) -> 频率轴与MTF曲线
optimize_triplet(system, ...) -> 优化后系统及优化日志
这个适配器并不隐藏计算过程。它防止印刷本书籍依赖于不稳定的方法名,同时仍要求配套代码证明工作流可行。
4. 光学处方:作为数据的镜头
在绘制图形之前,先有处方。
光学处方不仅仅是文档,它是计算用的输入数据。
一个简化的三片式镜头处方可以用表格表示:
| 面序号 | 曲率半径 | 到下一面的厚度 | 材料 | 半孔径 | 注释 |
|---|---|---|---|---|---|
| 0 | 无穷大 | 物距 | 空气 | — | 物体 |
| 1 | 正 | 玻璃厚度 | 冕牌玻璃 | 孔径 | 前方镜片第一面 |
| 2 | 负 | 空气间隙 | 空气 | 孔径 | 前方镜片第二面 |
| 3 | 负 | 玻璃厚度 | 火石玻璃 | 孔径 | 中间镜片第一面 |
| 4 | 正 | 空气间隙 | 空气 | 孔径 | 中间镜片第二面 |
| 5 | 正 | 玻璃厚度 | 冕牌玻璃 | 孔径 | 后方镜片第一面 |
| 6 | 负 | 后焦距 | 空气 | 孔径 | 后方镜片第二面 |
| 7 | 无穷大 | — | 像面 | — | 像面 |
在本章中,我们使用一套固定的教学处方。下面的数值是经四舍五入的教学用值,单位为毫米。它们不作为历史专利处方展示,也不是用于生产的处方。它们的作用是让整个计算链变得具体。
在代码中,它可能如下所示:
from dataclasses import dataclass
from typing import Optional
@dataclass
class SurfaceRecord:
radius: float # mm; 平面用 np.inf
thickness: float # mm 到下一表面
material_after: str # 该面之后的材料
semi_aperture: float # mm
comment: str = ""
cooke_triplet_prescription = [
SurfaceRecord(radius= 23.7, thickness=4.0, material_after="CROWN", semi_aperture=8.0, comment="前方镜片 S1"),
SurfaceRecord(radius= -80.0, thickness=6.0, material_after="AIR", semi_aperture=8.0, comment="前方镜片 S2"),
SurfaceRecord(radius= -22.0, thickness=2.0, material_after="FLINT", semi_aperture=6.5, comment="中间镜片 S1"),
SurfaceRecord(radius= 22.0, thickness=7.0, material_after="AIR", semi_aperture=6.5, comment="中间镜片 S2"),
SurfaceRecord(radius= 80.0, thickness=4.0, material_after="CROWN", semi_aperture=7.5, comment="后方镜片 S1"),
SurfaceRecord(radius= -23.7, thickness=38.0, material_after="AIR", semi_aperture=7.5, comment="后方镜片 S2"),
]
这些数值现在就是本章固定的教学处方。配套仓库应以机器可读形式保存同一张表格,这样本章中的每一幅图都可以从同一组初始数据重新生成。
关键在于:
所有后续图形都起源于这样一张表格。
点列图不是凭空画出的。波前图不是猜出来的。MTF曲线不是软件生成的魔法。
它们全都起源于表面、距离、材料、孔径、波长、视场和离焦设置。
5. 在 Optiland 中构建或加载三片式镜头
在固定的 Optiland 环境中,您可以加载一个示例三片式镜头,也可以逐面构建。
已验证的配套工作流如下:
# 已验证的封装工作流。
# 在配套包中为 optiland==0.6.0 实现。
optic = make_cooke_triplet()
plot_layout(optic)
如果手动构建,概念步骤如下:
# 概念上的构建步骤。
optic = OpticalSystem()
optic.add_surface(radius=23.7, thickness=4.0, material="CROWN", semi_aperture=8.0)
optic.add_surface(radius=-80.0, thickness=6.0, material="AIR", semi_aperture=8.0)
optic.add_surface(radius=-22.0, thickness=2.0, material="FLINT", semi_aperture=6.5)
optic.add_surface(radius=22.0, thickness=7.0, material="AIR", semi_aperture=6.5)
optic.add_surface(radius=80.0, thickness=4.0, material="CROWN", semi_aperture=7.5)
optic.add_surface(radius=-23.7, thickness=38.0, material="AIR", semi_aperture=7.5)
optic.set_aperture(...)
optic.set_fields(...)
optic.set_wavelengths(...)
optic.set_image_plane(...)
本手稿配套包针对 optiland==0.6.0 实现了封装调用。若将示例迁移到其他版本,请在信任这些图形之前重新运行验证测试。
重要的是结构:
先定义表面
再定义孔径
然后定义视场
然后定义波长
然后定义像面
然后进行分析
不要从 MTF 开始。从镜头本身开始。
6. 选择视场和波长
一个镜头没有一个通用的性能值。
性能取决于视场点和波长。
对于一个简单的三片式镜头示例,我们可以使用三个视场:
轴上视场
中等视场
边缘视场
以及三个波长:
蓝光
绿光
红光
例如:
fields = [
{"name": "axis", "angle_deg": 0.0},
{"name": "mid", "angle_deg": 7.0},
{"name": "edge", "angle_deg": 14.0},
]
wavelengths = [
{"name": "blue", "wavelength_m": 486.1e-9, "weight": 1.0},
{"name": "green", "wavelength_m": 587.6e-9, "weight": 1.0},
{"name": "red", "wavelength_m": 656.3e-9, "weight": 1.0},
]
这些是手稿配套包中使用的验证用波长。不同的设计目标可能需要不同的波长或权重。
单色设计问题与多色成像问题不同。
仅轴上优化与全视场优化不同。
慢速镜头与快速镜头不同。
因此,在进行分析之前,请写下配置:
哪些视场点?
哪些波长?
什么孔径?
哪个对焦平面?
什么性能目标?
如果没有这些答案,“这个镜头的 MTF”便是一个不完整的表述。
更好的表述是:
该镜头在此视场、此波长组、此孔径、此对焦设定、此方向下的 MTF。
虽然更长,但它是诚实的。
7. 第一幅图:布局图
布局图是第一项合理性检查。
optic = make_cooke_triplet()
plot_layout(
optic,
fields=fields,
wavelengths=wavelengths
)
布局图应当回答以下基本问题:
- 表面是否按预期顺序排列?
- 各镜片是否正确地分隔?
- 光阑在你认为的位置吗?
- 光线是否通过了通光孔径?
- 像面位置看起来大致合理吗?
- 是否存在明显的光线追迹失败?
布局图不是性能图,它不能证明镜头是否优良。
但它能及早发现严重的建模错误。
如果布局图是错的,后续的每一项分析都值得怀疑。
一个好习惯是:
在查看点列图之前,永远不要轻信布局图。
在查看处方和布局图之前,永远不要轻信 MTF 曲线。
这听起来可能有点慢,但比从一个错误模型中调试出一幅漂亮的 MTF 曲线要快得多。
8. 近轴检查:焦距与 F 数
在进行实际光线分析之前,先检查近轴骨架。
三片式镜头是一个实际镜头,但近轴量仍然重要:
- 有效焦距;
- 后焦距;
- F 数;
- 入瞳;
- 像距;
- 若为有限共轭,近似的放大倍率。
已验证的封装调用:
# 适用于 optiland==0.6.0 的已验证封装工作流。
paraxial = optic.paraxial_analysis()
print("有效焦距:", paraxial.efl)
print("后焦距:", paraxial.bfl)
print("F数:", paraxial.f_number)
print("入瞳直径:", paraxial.entrance_pupil_diameter)
如果您想要的镜头焦距约为 50 mm,而近轴分析显示为 5000 mm,那就不必继续计算 PSF 了。
近轴检查是系统的骨架。
它告诉您一阶行为是否大致正确,然后您再去询问具体的像差。
这与第8章直接相关:
近轴光学不是过时的装饰;
它是整个系统的第一项一致性检查。
9. 实际光线追迹:几何在此变得真实
现在追迹实际光线。
在一个完整的三片式镜头中,实际光线并不严格遵循简单的近轴模型。它们在弯曲表面上的实际点处相交,使用局部表面法线进行折射,并积累误差。
已验证的封装调用:
# 适用于 optiland==0.6.0 的已验证封装工作流。
ray_bundle = optic.trace_rays(
field=fields[1],
wavelength=wavelengths[1],
pupil_sampling="grid",
num_rays=21,
)
print(ray_bundle.summary())
实际光线追迹是后续许多图形的引擎。
从追迹到的光线我们可以计算:
- 点列图;
- 光扇图;
- 横向像差;
- 主光线行为;
- 渐晕;
- 光程长度;
- 波前误差。
这就是本书中间部分花了大量笔墨讲解光线的原因。
如果光线追迹是错误的,后续的波动光学将不可信。
计算链依赖于它:
处方
→ 表面交点
→ 折射
→ 像面光线束
这便是几何基础。
10. 点列图:像质初览
点列图通常是第一项可视化的性能检查。
已验证的封装调用:
# 适用于 optiland==0.6.0 的已验证封装工作流。
spot_results = run_spot_diagram(
optic,
fields=fields,
wavelengths=wavelengths,
pupil_sampling="grid",
num_rays=21,
)
spot_results.plot()
spot_results.report()
点列图回答了:
追迹光线落在像面上的什么位置?
对于三片式镜头,您应该预料到视场依赖性。
轴上光斑可能相对紧凑。轴外光斑可能会拉伸、倾斜或增大。不同波长的光可能落在略微不同的位置,或者散布程度不同。
这告诉我们几件事:
- 球差影响轴上光斑;
- 彗差和像散在轴外表现得更明显;
- 当波长分离时,出现了色差;
- 焦面选择改变了分布。
但点列图仍然是基于光线的。
它不显示衍射环。它不直接显示 MTF。它不能替代 PSF。
它在计算链中的角色是:
实际光线追迹
→ 像面光线拦截点
→ 点列图
这是基于几何光学的像质视图。
很有用,但不完整。
11. 解读点列图,不过度解读
一个紧凑的光斑通常意味着几何成像效果更好。一个大光斑意味着更多的模糊。
但也有其局限性。
一个小的光斑仍然可能比衍射极限的艾里斑大。比艾里斑还小的光斑可能影响不大,因为此时衍射占主导地位。接近衍射极限的点列图可能不如波前、PSF 或 MTF 信息量大。
因此,对于每个视场,将光斑尺寸与艾里斑尺度进行比较。
一个简单的计算:
def airy_radius_um(wavelength_m, f_number):
return 1.22 * wavelength_m * f_number * 1e6
green = 587.6e-9
f_number = 4.0 # 示例;使用实际系统的近轴结果
print("艾里斑第一暗环半径 [um]:", airy_radius_um(green, f_number))
如果光斑的 RMS 半径远大于艾里斑尺度,几何像差占主导。
如果光斑与艾里斑尺度相当或更小,衍射分析就变得尤为重要。
此时正好可以回顾本书反复强调的一点:
点列图是计算得出的结果,并非最终的真理。
它能很好地回答一个问题,但不能回答所有的问题。
12. 光扇图:光斑内部的结构
点列图显示光线落在何处。光扇图显示光线误差如何随光瞳坐标变化。
已验证的封装调用:
# 适用于 optiland==0.6.0 的已验证封装工作流。
ray_fan = run_ray_fan(
optic,
fields=fields,
wavelengths=wavelengths,
pupil_coordinate="normalized",
)
ray_fan.plot()
光扇图之所以有用,是因为它们揭示了结构。
点列图可能显示一团模糊光斑。光扇图可以表明这种模糊来自:
- 类球差行为;
- 类彗差非对称性;
- 像散差异;
- 色差分离;
- 离焦;
- 视场相关的不平衡。
这与第9章衔接。
光扇图问的是:
横向光线误差如何在光瞳上变化?
对于三片式镜头,比较不同视场下的光扇图。
在轴上,光扇图可能主要显示对称的像差图案。
在轴外,光扇图可能变得不对称,或具有方向依赖性。
这就是库克三片式镜头成为良好教学实例的原因之一。它虽小,但并不单调。
13. 波前与光程差:从光线跨越到相位
现在我们从光线拦截点转向波前误差。
光程差图回答的是:
在光瞳的每一点上,实际波前相对于参考波前是超前还是滞后了多少?
已验证的封装调用:
# 适用于 optiland==0.6.0 的已验证封装工作流。
wavefront = run_wavefront(
optic,
field=fields[1],
wavelength=wavelengths[1],
pupil_sampling=128,
)
opd = wavefront.opd
mask = wavefront.pupil_mask
wavefront.plot_opd(units="waves")
wavefront.report()
光程差是计算链特性发生变化的地方。
点列图和光扇图是基于几何光学的视图。光程差则为衍射计算做好准备。
这是从第10章架起的桥梁:
实际光线追迹
→ 光程长度
→ 与参考波前比较
→ 光程差图
对于三片式镜头,按视场检查光程差。
优质的轴上波前可能基本对称。轴外波前可能显示出彗差状或像散状的结构。由于折射率的变化,不同波长可能表现出不同的光程差行为。
这不仅仅是另一幅图,它还是光瞳函数的输入。
14. 光瞳函数:将波前编码为复振幅场
由光程差我们构建光瞳函数:
这是第11章的内容在一个真实镜头工作流中的回归。
如果 Optiland 的波前工具为我们提供了光程差和光瞳掩膜,教学转换过程如下:
import numpy as np
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
然后:
pupil, phase = pupil_function_from_opd(
opd=opd,
mask=mask,
wavelength=wavelengths[1]["wavelength_m"],
)
此时,库克三片式镜头不再只是一张表面表格或光线图。
它已变成一个光瞳上的复振幅场。
这是表征上的重大转变:
表面处方
→ 光线几何
→ 波前误差
→ 复光瞳函数
这恰恰是通常隐藏在分析软件内部的那一步转换。
现在我们能亲眼看到了。
15. PSF:三片式镜头的点像
PSF 是由光瞳函数计算得到的:
一个最小实现如下:
def pad_array_centered(array, pad_factor=4):
if pad_factor == 1:
return array
n, m = array.shape
new_n = n * pad_factor
new_m = m * pad_factor
padded = np.zeros((new_n, new_m), dtype=array.dtype)
start_n = (new_n - n) // 2
start_m = (new_m - m) // 2
padded[start_n:start_n + n, start_m:start_m + m] = array
return padded
def compute_psf_from_pupil(pupil, pad_factor=4):
padded = pad_array_centered(pupil, pad_factor=pad_factor)
field = np.fft.fftshift(
np.fft.fft2(
np.fft.ifftshift(padded)
)
)
psf = np.abs(field) ** 2
psf = psf / np.sum(psf)
return psf, field
然后:
psf, image_field = compute_psf_from_pupil(
pupil,
pad_factor=4
)
Optiland 也可能直接提供 PSF 分析:
# 适用于 optiland==0.6.0 的已验证封装工作流。
psf_result = run_psf(
optic,
field=fields[1],
wavelength=wavelengths[1],
sampling=128,
padding=4,
)
psf_result.plot(scale="linear")
psf_result.plot(scale="log")
双轨对比具有教育意义。
Optiland 的 PSF 是实际分析结果。NumPy PSF 展示了计算骨架。
如果它们不一致,不要立刻慌张。检查:
- 视场是否相同;
- 波长是否相同;
- 焦面是否相同;
- 光瞳采样是否相同;
- 孔径掩膜是否相同;
- 光程差符号约定是否相同;
- 补零方式是否相同;
- 归一化方式是否相同;
- 坐标缩放是否相同。
这份检查清单正是第14章在发挥作用。
16. 解读三片式镜头的 PSF
对于轴上视场,校正良好的三片式镜头可能有一个紧凑的 PSF,但不一定是完美的艾里斑图案。
对于轴外视场,PSF 可能变得不对称。能量可能从中心核区向外漂移。可能出现拖尾。对数尺度视图可能会显示出线性尺度下不明显的微弱结构。
分层解读 PSF:
中心核区
问:
主要能量有多集中?
这关系到锐度和类斯特列尔行为。
周围结构
问:
能量流向了哪里?
衍射环、拖尾和定向扩散可能在视觉上影响很大。
视场依赖性
问:
PSF 从中心到边缘如何变化?
这至关重要。一个镜头可能在轴上表现良好,而在边缘很弱。
波长依赖性
问:
不同波长是否产生了不同的 PSF 或焦面位置?
这指向了色差行为。
PSF 是镜头开始看起来像一个成像系统,而不仅是一个光线追迹模型的地方。
17. OTF 与 MTF:三片式镜头作为频率响应
现在从 PSF 计算 OTF 和 MTF:
def compute_otf_mtf(psf):
otf = np.fft.fftshift(
np.fft.fft2(
np.fft.ifftshift(psf)
)
)
center = (otf.shape[0] // 2, otf.shape[1] // 2)
if np.abs(otf[center]) > 0:
otf = otf / otf[center]
mtf = np.abs(otf)
return otf, mtf
然后:
otf, mtf = compute_otf_mtf(psf)
Optiland 风格的分析:
# 适用于 optiland==0.6.0 的已验证封装工作流。
mtf_result = run_mtf(
optic,
fields=fields,
wavelengths=wavelengths,
frequencies=[10, 20, 40, 80],
directions=["sagittal", "tangential"],
)
mtf_result.plot()
mtf_result.report()
现在,MTF 不再神秘。
它是:
PSF
→ 傅里叶变换
→ OTF
→ 幅度
→ MTF
对于三片式镜头,MTF 曲线应当随视场和方向而变化。
一个典型的模式可能是:
- 中心视场性能优于边缘视场;
- 轴外时,弧矢和子午曲线分离;
- 较高频率下降更快;
- 多色 MTF 可能低于单色 MTF。
确切的结果取决于实际的处方、焦面、孔径和优化状态。
解读时,应始终将其与配置联系在一起。
不能说:
这是库克三片式镜头的 MTF。
而应说:
这是此三片式处方,
在这些视场、波长、孔径、焦面设定和频率方向下的 MTF。
这样才能避免对曲线过度解读。
18. 一个紧凑的分析运行器
对于一个真实的配套仓库,将全部分析封装到一个函数里很有用。
这个封装器不应隐藏链条,而应组织它。
# 已验证的适配器结构。
def analyze_triplet(optic, fields, wavelengths):
"""
运行库克三片式镜头的主要分析。
这是一个工作流大纲。实际实现应调用
固定光学设计库的精确 API。
"""
results = {}
results["layout"] = plot_layout(
optic,
fields=fields,
wavelengths=wavelengths
)
results["paraxial"] = optic.paraxial_analysis()
results["spot"] = run_spot_diagram(
optic,
fields=fields,
wavelengths=wavelengths
)
results["ray_fan"] = run_ray_fan(
optic,
fields=fields,
wavelengths=wavelengths
)
results["wavefront"] = run_wavefront(
optic,
field=fields[1],
wavelength=wavelengths[1]
)
results["psf"] = run_psf(
optic,
field=fields[1],
wavelength=wavelengths[1]
)
results["mtf"] = run_mtf(
optic,
fields=fields,
wavelengths=wavelengths
)
return results
然后:
before = analyze_triplet(
optic,
fields=fields,
wavelengths=wavelengths
)
返回的字典不仅方便,它迫使我们记住,“镜头性能”是复数。
不存在一幅单一的图表能讲述全部故事。
一个严谨的优化前后对比应当包含若干视图。
19. 优化前:写下诊断
在优化前,做一份简短的诊断。
例如:
优化前:
- 有效焦距接近目标,但不精确。
- 轴上光斑可以接受。
- 边缘视场光斑被拉伸。
- 光扇图显示轴外不对称性。
- 中等视场波前显示出像散结构。
- PSF 向边缘拓宽。
- 在较高频率处,子午 MTF 比弧矢 MTF 下降更快。
这份诊断很重要,因为它告诉我们试图解决什么问题。
如果我们跳过诊断,直接进行优化,我们可能在未理解设计的情况下提升某个数字。
这是工程版本的简单医学法则:
在诊断之前不要开处方。
用光学设计的话说:
在决定哪种性能缺陷最重要之前,不要开始优化。
对于一个教学用三片式镜头,一个合理的目标可能是:
在保持焦距、合理厚度和实用曲率的同时,改善轴外像质。
现在我们可以设计评价函数了。
20. 选择变量
三片式镜头有许多可能的变量。
对于首次优化,不要把所有变量都释放。
从一个可控的集合开始,例如:
选定的表面曲率半径
选定的空气间隙
像面位置
首先固定玻璃不变。固定半孔径。保持整体结构可识别。
已验证的封装调用:
# 适用于 optiland==0.6.0 的已验证封装工作流。
variables = [
optic.surface(1).radius,
optic.surface(2).radius,
optic.surface(3).radius,
optic.surface(4).radius,
optic.surface(5).radius,
optic.surface(6).radius,
optic.gap(2).thickness,
optic.gap(4).thickness,
optic.image_plane.position,
]
for variable in variables:
variable.set_variable(True)
同样,具体的 API 名称取决于库版本。
这个概念是稳定的:
优化器只能改变你释放的变量。
如果你不释放像面位置,它就不能重新对焦。
如果你不释放曲率,它就不能改变镜片的光焦度分布。
如果你过早释放太多变量,设计可能变得不稳定或不切实际。
变量选择是一种设计判断。
21. 选择操作数
现在定义我们想要改进的内容。
一个教学用评价函数可能包括:
有效焦距目标
各视场 RMS 光斑尺寸
色差焦点控制
畸变控制
最小厚度约束
曲率惩罚
选定频率下的 MTF 目标
一个简化的评价函数结构:
如果您正在学习,不要一次性加入所有项。
分阶段优化更容易理解:
第一阶段
将镜头保持在期望的一阶布局附近:
有效焦距
后焦距
合理的厚度
第二阶段
改善几何成像:
RMS 光斑
光线像差
视场平衡
第三阶段
改善波动/像质指标:
波前 RMS
PSF 集中度
选定频率的 MTF
这种分阶段的方法不是胆怯,而是清晰。
优化器遵循评价函数。一个分阶段的评价函数有助于您理解它在遵循什么。
22. 验证用评价函数
这是一个伪代码形式的简化评价函数。
它不是为了不经过 API 调用调整就直接复制到 Optiland 中。它旨在展示设计逻辑。
def triplet_merit(optic, fields, wavelengths):
"""
库克三片式镜头示例的验证用评价函数。
越小越好。
"""
merit = 0.0
# 1. 一阶目标
paraxial = optic.paraxial_analysis()
target_efl = 50.0 # mm
efl_error = (paraxial.efl - target_efl) / target_efl
merit += 100.0 * efl_error**2
# 2. 几何像质
for field in fields:
spot = run_spot_diagram(
optic,
fields=[field],
wavelengths=wavelengths,
return_data=True
)
rms_spot_um = spot.rms_radius_um
allowed_spot_um = 20.0
spot_error = rms_spot_um / allowed_spot_um
merit += 1.0 * spot_error**2
# 3. 畸变控制
for field in fields[1:]:
distortion_percent = optic.distortion(field=field)
allowed_distortion = 2.0
distortion_error = distortion_percent / allowed_distortion
merit += 0.5 * distortion_error**2
# 4. 几何惩罚
geometry_penalty = optic.geometry_penalty(
min_thickness=1.0,
min_air_gap=0.5,
max_abs_curvature=0.2,
)
merit += 10.0 * geometry_penalty
return merit
这是一个验证用评价函数,而不是一个通用的三片式镜头设计目标。
这是一个翻译示例:
光学目标
→ 计算所得量
→ 归一化误差
→ 权重
→ 标量评价值
一个真实的库可能提供内置的评价操作数和优化器,因此您可能并不会完全按这种方式编写评价函数。但即便如此,其逻辑是相同的。
23. 运行优化
已验证的 Optiland 适配器工作流:
# 适用于 optiland==0.6.0 的已验证封装工作流。
optimizer_settings = {
"method": "damped_least_squares",
"max_iterations": 100,
"tolerance": 1e-6,
}
optimized_optic, optimization_log = optimize_triplet(
optic,
variables=variables,
merit_function=triplet_merit,
fields=fields,
wavelengths=wavelengths,
settings=optimizer_settings,
)
如果使用更通用的 Python 优化器,其结构如下:
# 已验证的适配器结构。
from scipy.optimize import minimize
def pack_variables(optic):
# 从光学系统中提取变量值。
...
def unpack_variables(optic, x):
# 将变量值写回光学系统。
...
def merit_from_vector(x, optic, fields, wavelengths):
unpack_variables(optic, x)
return triplet_merit(optic, fields, wavelengths)
x0 = pack_variables(optic)
result = minimize(
merit_from_vector,
x0,
args=(optic, fields, wavelengths),
method="Nelder-Mead",
)
unpack_variables(optic, result.x)
optimized_optic = optic
对于真实的光学设计,专用优化器和库原生评价函数可能更好。这里的重点在于概念:
优化器改变变量。
每个新设计被分析。
评价函数对其评分。
优化器搜索更低的分数。
这个循环就是引擎。
24. 优化后:再次运行相同的分析
不要使用不同的分析设置来对比优化前后。
使用相同的视场、波长、孔径、焦面约定、采样和绘图尺度。
after = analyze_triplet(
optimized_optic,
fields=fields,
wavelengths=wavelengths
)
现在进行比较。
compare_layout(before["layout"], after["layout"])
compare_spot_diagrams(before["spot"], after["spot"])
compare_ray_fans(before["ray_fan"], after["ray_fan"])
compare_wavefronts(before["wavefront"], after["wavefront"])
compare_psfs(before["psf"], after["psf"])
compare_mtfs(before["mtf"], after["mtf"])
这些封装函数名在配套验证包中针对 optiland==0.6.0 实现。
比较应当严格:
相同的视场
相同的波长
相同的频率轴
相同的归一化
相同的显示尺度
否则,您可能是在比较分析设置,而不是镜头性能。
25. 改善可能的样子
优化之后,您可能看到:
处方
曲率改变了,但保持在边界内。
空气间隙发生了移动。
像面轻微移动。
三片式镜头的形状仍然可识别。
点列图
轴上光斑保持紧凑。
中等视场和边缘视场光斑缩小。
波长分离可能减小。
某些视场可能比其他视场改善更多。
光扇图
曲线变得平坦或更平衡。
较大的非对称误差减少。
残余结构仍然存在,因为设计仍然是一个紧凑的三片式镜头。
波前
所选视场的光程差 RMS 降低。
波前图变得更平滑或不再那么极端。
某些像差项与其他项之间发生权衡。
PSF
中心能量变得更加集中。
轴外 PSF 不那么拉伸。
对数尺度的拖尾可能减小。
MTF
选定频率得到改善。
弧矢和子午曲线可能变得更接近。
边缘视场 MTF 可能上升,尤其是在中等频率处。
但并非一切事物都同等改善。
这是正常的。
优化是权衡管理,不是创造奇迹。
一份成熟的报告不仅要说:
什么改善了
还要说:
什么没有改善,或者什么变差了
这种坦诚是技术可靠性的一部分。
26. 优化前后报告表
固定的配套代码当前出此优化摘要:
| 指标 | 优化前 | 优化后 | 备注 |
|---|---|---|---|
| 光斑 RMS 评价值 [mm^2] | 1.6625847615e-4 | 1.5389416611e-4 | 降低了 7.44% |
| 优化的变量 | R1, R3, R4, R6, BFD | R1, R3, R4, R6, BFD | 有界 L-BFGS-B 方法 |
| 迭代次数 | 0 | 20 | 历史记录已保存于配套输出中 |
这些数值来自固定的 optiland==0.6.0 配套运行结果。评价值是 Optiland 的 SpotDiagram 中主波长 RMS 光斑半径的均方值。
报告格式很重要,因为最终的评价值本身并不足够。
它也让我们得以追问:
- 我们所针对的指标改善了吗?
- 重要的未指定指标是否降级了?
- 几何形状是否仍然实用?
- 视场平衡是否改善了?
- 设计是否变得更敏感了?
这就是优化如何变成设计审查的。
27. MTF 为何改变
当 MTF 改善时,不要只停留在曲线上。
要问为什么。
一个可能的链条可能是:
曲率改变了
→ 轴外光线像差减小
→ 波前 OPD 减小
→ PSF 能量变得更集中
→ OTF 在选定频率处的幅度增大
→ MTF 曲线上升
这就是全链路的解释。
这比以下说法好得多:
优化器改善了 MTF。
那句话是正确的,但很浅薄。
更深入的解释将最终指标联系回物理和计算的原因。
例如:
- 如果光扇图变得更平坦,横向像差就改善了。
- 如果 OPD RMS 降低了,相位误差就减小了。
- 如果 PSF 中心核区获得了能量,点像集中度就改善了。
- 如果选定频率的 OTF 幅度增大了,对比度传递就改善了。
现在,MTF 曲线不再是孤立的。
它是你可检验的链条中的最后一环。
28. 当优化使某些东西变得更差
有时,优化后的设计评价值更低,却带来了意外的糟糕情况。
例如:
边缘厚度变得太小
畸变增加
一个视场改善,而另一个视场变差
色差行为恶化
表面曲率变得极端
MTF 在某一频率处改善,但在另一频率处下降
这不一定意味着软件故障。
这可能意味着评价函数没有包含正确的惩罚项、权重、视场、波长或约束。
回到评价函数。
问:
糟糕的行为被测量了吗?
它被赋予了足够的权重吗?
它受到约束了吗?
变量空间过度自由了吗?
初始设计是否很差?
然后进行修改。
例如:
- 增加最小厚度惩罚;
- 包含更多的视场点;
- 添加波长权重;
- 添加畸变操作数;
- 约束曲率;
- 改变优化阶段;
- 如果三片式结构无法达到目标,改变镜头架构。
这才是真正的循环:
分析
→ 诊断
→ 修订评价函数
→ 优化
→ 再次分析
优化器不能替代这个循环,它只是其中一部分。
29. 库克三片式镜头作为全链路图谱
现在让我们将整个三片式工作流写成一幅地图。
1. 处方
表面、厚度、材料、孔径
2. 配置
视场、波长、孔径、焦面、采样
3. 近轴分析
有效焦距、后焦距、F数、光瞳量
4. 实际光线追迹
表面交点、折射、像面拦截点
5. 点列图
按视场和波长的光线落点分布
6. 光扇图
作为光瞳坐标函数的横向误差
7. 波前 / 光程差
光瞳上的光程差
8. 光瞳函数
振幅掩膜和来自光程差的相位
9. PSF
复光瞳的傅里叶变换,幅度平方
10. OTF
PSF 的傅里叶变换
11. MTF
OTF 的幅度,按方向和频率截取
12. 评价函数
来自所选操作数的加权误差
13. 优化
降低评价值的变量更新循环
14. 优化前后审查
比较所有相关图形和约束
这就是本书的列表概览。
每一项之前都已出现过。三片式镜头只不过迫使它们协同工作。
这种协同才是真正的成就。
30. 完整验证脚本大纲
验证包包含以下高层脚本结构。封装函数针对 optiland==0.6.0 实现,库版本若变化应重新验证。
# cooke_triplet_full_chain.py
#
# 验证脚本大纲。
# 封装函数针对 optiland==0.6.0 实现。
from optics_examples import (
make_cooke_triplet,
set_fields,
set_wavelengths,
plot_layout,
run_paraxial_analysis,
run_spot_diagram,
run_ray_fan,
run_wavefront,
run_psf,
run_mtf,
set_triplet_variables,
build_triplet_merit_function,
run_optimization,
compare_reports,
)
def main():
# 1. 构建系统
optic = make_cooke_triplet()
# 2. 定义配置
fields = [
{"name": "axis", "angle_deg": 0.0},
{"name": "mid", "angle_deg": 7.0},
{"name": "edge", "angle_deg": 14.0},
]
wavelengths = [
{"name": "blue", "wavelength_m": 486.1e-9, "weight": 1.0},
{"name": "green", "wavelength_m": 587.6e-9, "weight": 1.0},
{"name": "red", "wavelength_m": 656.3e-9, "weight": 1.0},
]
set_fields(optic, fields)
set_wavelengths(optic, wavelengths)
# 3. 优化前分析
before = {}
before["layout"] = plot_layout(optic)
before["paraxial"] = run_paraxial_analysis(optic)
before["spot"] = run_spot_diagram(optic, fields, wavelengths)
before["ray_fan"] = run_ray_fan(optic, fields, wavelengths)
before["wavefront"] = run_wavefront(optic, fields, wavelengths)
before["psf"] = run_psf(optic, fields, wavelengths)
before["mtf"] = run_mtf(optic, fields, wavelengths)
# 4. 定义变量与评价函数
variables = set_triplet_variables(
optic,
vary_radii=True,
vary_air_spaces=True,
vary_image_plane=True,
keep_glass_fixed=True,
)
merit_function = build_triplet_merit_function(
optic,
fields=fields,
wavelengths=wavelengths,
target_efl_mm=50.0,
max_distortion_percent=2.0,
min_thickness_mm=1.0,
mtf_targets=[
{"field": "axis", "frequency_cyc_per_mm": 40, "target": 0.60},
{"field": "edge", "frequency_cyc_per_mm": 40, "target": 0.30},
],
)
# 5. 优化
optimized_optic, log = run_optimization(
optic,
variables=variables,
merit_function=merit_function,
max_iterations=100,
)
# 6. 优化后分析
after = {}
after["layout"] = plot_layout(optimized_optic)
after["paraxial"] = run_paraxial_analysis(optimized_optic)
after["spot"] = run_spot_diagram(optimized_optic, fields, wavelengths)
after["ray_fan"] = run_ray_fan(optimized_optic, fields, wavelengths)
after["wavefront"] = run_wavefront(optimized_optic, fields, wavelengths)
after["psf"] = run_psf(optimized_optic, fields, wavelengths)
after["mtf"] = run_mtf(optimized_optic, fields, wavelengths)
# 7. 对比
compare_reports(before, after, optimization_log=log)
if __name__ == "__main__":
main()
这个脚本并非故弄玄虚,而是刻意保持可读性。
它的结构就是全链路方法:
构建
→ 配置
→ 分析
→ 优化
→ 再次分析
→ 对比
这正是本书一直在瞄准的实用工作流。
31. 图形集中应包含什么
对于本章最终版本或配套笔记本,应包含一组固定的图形。
图 1:布局图
目的:
展示三片式镜头的几何形状和光线路径。
图 2:优化前的点列图
目的:
按视场和波长展示几何像质。
图 3:优化前的光扇图
目的:
揭示光瞳上的像差结构。
图 4:光程差图
目的:
展示将变为光瞳相位的波前误差。
图 5:优化前后的 PSF
目的:
展示点像能量分布如何变化。
图 6:优化前后的 MTF
目的:
按视场和方向展示对比度传递的改善。
图 7:处方变化表
目的:
展示优化器实际改变了什么。
图 8:操作数报告
目的:
展示哪些目标改善了,哪些约束仍然是限制性的。
这组图形可防止本章内容变成一堆互不相干的图表。
每一幅图都有其作用。
32. 最重要的比较:相同设置
在比较优化前后时,保持分析设置完全相同。
这一点非常重要,值得重复。
使用相同的:
视场
波长
孔径
焦面约定
光瞳采样
PSF 补零
MTF 频率轴
归一化
图形界限
尽可能的色彩比例
如果优化前后使用不同的设置,对比就变得很弱。
例如,如果优化前的 PSF 以对数尺度显示,而优化后的 PSF 以线性尺度显示,视觉对比就变得具有误导性。
如果优化前的 MTF 用的是单色绿光,而优化后的 MTF 用的是多色加权,这些曲线回答的就不是同一个问题。
如果优化前的边缘视场是 14 度,而优化后的边缘视场是 10 度,这种改善可能是虚假的。
一个好的对比是乏味地严格的。
正是这种乏味的严格,使得结果值得信赖。
33. 本章证明了什么
本章并不证明库克三片式镜头是最好的镜头。
它不证明我们的教学用评价函数可以投入生产。
它不证明每一个光学问题都应该用相同的工作流来解决。
它证明的是对本书更核心的一点:
光学设计软件的输出可以连接到一个可见的计算链上。
MTF 曲线不是孤立的。
PSF 不是孤立的。
波前图不是孤立的。
点列图不是孤立的。
每一个都是由更早的数据和更早的计算产生的。
一旦你了解了这个链条,就能更聪明地审视结果。
你可以问:
- 处方正确吗?
- 视场和波长合适吗?
- 光线追迹正确吗?
- 波前参考明确吗?
- 光瞳函数是用正确的相位和掩膜构建的吗?
- PSF 采样正确吗?
- MTF 归一化和标注正确吗?
- 评价函数衡量的是我们所关心的吗?
- 优化是改进了设计,还是仅仅提高了分数?
这些问题才是实用技能。
34. 从 MTF 回溯到处方的最终演练
让我们练习一下反向解读。
假设优化后的三片式镜头在 40 线对/毫米处,边缘视场的子午 MTF 得到改善。
不要就此止步。
向后走:
MTF 改善
→ 该频率和方向的 OTF 幅度改善
→ 对于该视场,PSF 变得更好
→ 光瞳相位误差发生了变化
→ OPD 图改善
→ 光线像差发生了变化
→ 表面曲率和间隔发生了变化
→ 优化器根据评价函数改变了变量
这种反向解读能力是很强大的。
它将一幅图转化为一个关于计算和设计的故事。
它还能帮助你挑战这个结果。
如果 MTF 改善了,但 OPD 没有改善,这是为什么?如果光斑改善了,但 MTF 没有改善,这是为什么?如果评价值下降,但边缘 PSF 变差了,评价函数忽略了什么?
问出这些问题的能力,就是使用光学软件与理解光学计算之间的区别。
35. 我们此刻所处的位置
现在,我们已经完成了本书开篇所承诺的经典计算路径。
这条路径始于一个问题:
一条 MTF 曲线从哪里来?
现在,我们能够详细回答。
它来自 OTF。
OTF 来自 PSF。
PSF 来自复光瞳函数。
光瞳函数来自振幅和相位。
相位来自光程差。
光程差来自通过镜头计算的光程差。
那些光程来自追迹光线。
那些光线来自表面、材料、波长、视场和孔径。
而优化改变了处方,使得选定的被计算量朝着选定的目标移动。
这就是那条链。
它并不短,但不再隐藏。
下一章将向前展望。现代计算光学越来越希望这整条链是可微分的、可编程的,并连接到更大的成像系统。这便是自动微分和可微光线追迹进入的领域。
但在到达那里之前,我们需要这一章。
一个可微分的光学系统仍然是一个光学系统。
如果你不理解这条经典链路,自动微分只会给你一条更快的途径来产生困惑。
现在,我们已将此链握在手中。
本章可复现性检查清单
配套代码应通过四项检查:
1. 根据本章表格重建教学处方。
2. 从同一系统生成布局图、点列图、光扇图、光程差图、PSF 和 MTF 输出。
3. 使用相同的视场、波长、孔径、焦面、采样、归一化和绘图设置进行优化前后分析。
4. 用固定代码生成的数值替换任何示意性的报告数字。
这是刻意保持严格的。本章是本书的全链路测试。它应让人感觉像一次审查,而不是一个装饰性的例子。
本章小结
库克三片式镜头是一个有用的全链条实例,因为它足够小,便于检视,但又足够丰富,能展示真实的光学设计行为。
完整的工作流是:
处方
→ 配置
→ 近轴分析
→ 实际光线追迹
→ 点列图
→ 光扇图
→ 光程差 / 波前
→ 光瞳函数
→ PSF
→ OTF
→ MTF
→ 评价函数
→ 优化
→ 优化前后对比
处方是起始数据。视场、波长、孔径和焦面设定定义了分析配置。实际光线追迹产生点列图和光扇图。光程差产生光程差图。光程差变成光瞳相位。光瞳函数产生 PSF。PSF 产生 OTF 和 MTF。
优化改变选定变量以降低评价函数。它不会自动知道“更好”是什么意思。评价函数定义了目标。
一项可信赖的优化前后对比必须使用相同的视场、波长、采样、归一化和绘图设置。更低的评价值并不足够;设计必须通过布局图、处方、点列图、光扇图、波前、PSF、MTF 和几何约束来审查。
主要经验简单而重要:
最终的 MTF 曲线是一条漫长计算链中最后可见的一环。
本章使用一个完整的镜头走过了这一链条。
生成的验证图形


按迭代记录的评价历史被记录在 ../figures/chapter_16/cooke_optimization_history.csv 文件中。
来源与验证说明
本章是手稿的全链路验证锚点。所报告的优化前后评价值来自固定的配套运行结果,使用的是 optiland==0.6.0;不应手动编辑。如果处方、边界、变量、视场、波长、采样或 Optiland 版本发生变化,请重新运行验证,并替换本章图形以及记录的评价历史。