当前位置:首页>python>Python光学仿真举例---单透镜

Python光学仿真举例---单透镜

  • 2026-10-05 14:01:38
Python光学仿真举例---单透镜

前言

在应用光学的学习与工程实践中,商用光学仿真软件(如 Zemax、CodeV)虽能快速输出仿真结果,但存在黑箱化问题,难以直观体现从光学公式到数值实现的核心逻辑;而课本中的公式推导又缺乏与工程化代码的直接映射。

本项目是一款面向光学专业人员的轻量级 Python 单透镜仿真系统,完全基于几何光学基本定律实现,无复杂封装,所有核心算法均与光学公式一一对应。项目实现了 3D 光线追迹、光路图 / 点列图 /光扇图可视化、成像质量量化分析(RMS 光斑)等功能,并采用模块化架构设计,可直接扩展至双胶合透镜、消色差系统、色散模拟等更复杂的光学场景。

本文将从光学专业视角,详细拆解项目的整体逻辑架构、核心模块的公式映射与代码实现、模块间调用关系,以及仿真结果的解读,为光学专业人员理解 “几何光学公式→数值计算→可视化呈现” 的完整流程提供参考。

一、项目定位与核心价值

1.1定位

本项目是几何光学框架下的单透镜数值仿真轻量化实现,聚焦几何光线追迹与成像质量基础分析,不涉及衍射、波前分析、复杂镜头优化等高阶内容,核心目标是让光学公式在代码中可落地、可追溯。

1.2设计原则

① 光学专业约定:所有参数定义、符号规则、公式实现均遵循光学行业通用标准(如曲率半径符号、光轴约定、Snell 定律矢量形式);

② 模块化解耦:按 “物理实体抽象→算法实现→质量分析→可视化→场景验证” 拆分模块,符合光学系统的分析逻辑;

③ 可扩展性:核心类 / 函数预留扩展接口(如材料折射率支持 Sellmeier 方程、透镜系统支持多表面扩展);

④ 量化分析:除可视化外,提供 RMS 光斑尺寸等定量评价指标,与光学工程中的成像质量评价标准一致。

1.3功能(光学专业视角)

功能模块

光学核心依据

工程价值

3D  矢量光线追迹

光线参数方程、Snell 定律

还原光线在光学系统中的真实传播

圆形光束均匀采样

入瞳光阑的光束能量分布

生成接近实际的光束模型

多视场追迹

视场光阑、离轴成像原理

分析场曲、像散等离轴像差

点列图可视化

光线像面聚焦分布原理

直观判断像差类型与大小

光扇图可视化

瞳坐标 - 像面偏差的对应关系

定量分析球差、彗差、像散

RMS  光斑计算

成像质量量化评价标准

客观对比不同光学系统的成像性能

二、项目整体逻辑架构与模块调用关系

项目采用分层模块化架构,按 “物理实体抽象→核心算法实现→成像质量分析→可视化渲染→工程场景验证” 的逻辑链路设计,所有模块均基于 Python 基础库(numpy、matplotlib)实现,无第三方光学仿真依赖,代码可直接调试、修改。

2.1整体架构图

singlet_optics/

    __init__.py

    core/                 # 核心光学数据结构

  │     __init__.py

  │     ray.py            # 光线 Ray

  │     surface.py        # 表面 Surface(曲率、厚度、材料、孔径)

  │     material.py       # 材料 Material(折射率)

  │     lens.py           # 单透镜 / 系统 Lens

    trace/                # 几何光线追迹算法

  │     __init__.py

  │     geometrical.py    # Snell 折射、直线传播

  │     tracer.py         # RayTracer:按顺序追迹系统中所有表面

    visual/               # 绘图与可视化

  │     __init__.py

  │     rayplot.py        # 光路图(系统剖面 + 光线,使用实际透镜曲率和孔径绘制透镜外形)

  │     spotplot.py       # 点列图(像面光斑)

  │     ray_fan_plot.py   # 光扇图(子午光扇、弧矢光扇、合并光扇)

    analysis/             # 简单分析工具

  │     __init__.py

  │     spot.py           # 计算光斑 RMS 等

  │     ray_fan.py        # 计算光扇图数据

    examples/             # 使用示例

  │     single_lens_demo.py      # 单透镜光线追迹 + 光路图 + 点列图 + 光扇图

  │     ray_fan_utils.py         # 光扇图光线生成工具

  │     field_utils.py           # 多视场光线生成工具

  │     multi_field_demo.py      # 多视场光线追迹 + 光路图 + 点列图 + 光扇图

    tests/                # 单元测试

  │     test_core.py

  │     test_trace.py

    README.md             # 项目说明文档

    .gitignore            # Git 忽略文件

    PROJECT_STRUCTURE.md  # 项目结构说明

2.2模块功能与输入输出关系

模块

核心功能

输入(光学实体 / 数据)

输出(仿真结果 / 数据)

依赖模块

core

光学物理实体的代码抽象

光学参数(焦距、曲率、折射率、厚度、半孔径)

Ray/Surface/Material/LensSystem  类实例

无

trace

几何光线追迹核心算法

core  层的实体类实例、光线初始参数(原点、方向、波长)

追迹后的光线(含传播路径 path)

core

analysis

成像质量量化分析

trace  层的追迹后光线数据、视场角

RMS  光斑尺寸、光扇图像差数据、主光线像点位置

core、trace

visual

光学结果可视化

trace/analysis  层的仿真数据

光路图、点列图、光扇图

core、trace、analysis

field_utils

多视场光线生成工具

视场角、入瞳参数(半径、采样方式)

带视场标记的 3D 平行光束

core

ray_fan_utils

光扇光线生成工具

光瞳半径、采样点数、视场角

子午/弧矢光扇光线

core

examples

工程场景演示

所有底层模块

完整仿真结果(图表 + 量化数据)

所有模块

tests

单元测试

core/trace  层核心函数

算法正确性验证结果

core、trace

2.3数据流转逻辑

光线参数是整个系统的核心流转数据,其生命周期为:

光线初始生成(field_utils/examples)→3D矢量定义(core/Ray)→几何追迹(trace)→路径记录(Ray.path)→质量分析(analysis,提取像面点)→可视化(visual,提取路径/像面点数据)

所有可视化图表和量化分析结果,均来自追迹后 Ray 类的 path 属性(光线传播的离散 3D 坐标点),保证了数据的统一性和可追溯性。

三、核心光学实体的代码抽象(core 模块)

光学系统的本质是光线在不同介质的光学表面上的传播与折射,core模块的核心作用是将光线、光学表面、材料、透镜系统这些物理实体,按几何光学规则抽象为可被代码计算的类,所有类的参数定义、符号规则均遵循光学行业通用标准,是后续算法实现的基础。

3.1 Ray 类:3D 矢量光线的抽象(core/ray.py)

3.1.1光学依据

在几何光学中,光线被视为一条有向直线,其在三维空间中的轨迹可用参数方程表示为:

其中:

:光线起点的位置矢量;

:光线的单位方向矢量(),决定光线在空间中的传播方向;

:标量参数,在 为单位向量的前提下,可直接理解为从起点沿方向 传播的距离;表示沿光线传播方向前进。

单透镜以及大多数成像系统通常为轴对称系统,因此本项目采用光学设计中通用的右手直角坐标系约定:

约定 z 轴为光轴,光线整体从左向右沿方向传播(物方 → 像方);

x-y 平面为垂直于光轴的横向平面,用于描述光线在光瞳/像面的横向分布;

在可视化时,常取 y–z 子午面投影,将光线在 y 方向的偏离随 z 的变化画出,便于直观展示单透镜的会聚过程。

在此约定下,一条 3D 光线可完全由「起点 + 单位方向 」唯一确定,后续所有几何追迹(与球面/平面的交点计算、表面法线计算、Snell 折射等)都建立在这一 3D 参数方程之上。因此,Ray 类在代码层面对上述物理量进行显式建模:

origin 对应 ,记录光线初始位置;

direction对应,在构造/更新时强制归一化,保证 (t) 与物理传播距离一致,有利于数值稳定;

path 用于离散记录光线在各光学表面及像面上的交点坐标 ((x,y,z)),为后续点列图、光扇图和 RMS 光斑计算提供统一数据源;

field_angle记录该光线所属的视场角(单位:度),在多视场仿真中用于区分轴上(0°)与离轴光线。

通过上述抽象,几何光学中「一条 3D 有向直线」的物理概念,被完整映射为可计算、可追踪、可视化的 Ray 对象,为后续 trace、analysis、visual 模块的所有算法提供了统一的光线基础描述。

3.1.2代码实现与光学映射

class   Ray:
"""
三维光线:x, y, z 坐标系,z 为光轴"""
origin: Vec3# (x, y, z)
direction: Vec3# 
单位方向向量 (dx, dy, dz)
wavelength: float = 587.6e-9

# 
追迹过程中累计的路径点:[(x0, y0,   z0), (x1, y1, z1), ...]
path: List[Vec3] =   field(default_factory=list)
field_angle: float = 0.0# 
这条光线对应的视场角(度)

def __post_init__(self):
if not self.path:
self.path.append(self.origin)

def add_point(self, point: Vec3):
self.path.append(point)

def normalize_direction(self):
d = np.array(self.direction,   dtype=float)
d /= np.linalg.norm(d)
self.direction = (float(d[0]),   float(d[1]), float(d[2]))

3.1.3属性的光学意义

origin/direction:严格遵循光线参数方程,direction 为单位矢量(保证光线追迹的数值计算精度);

wavelength:默认取 d 光(587.6nm),为目视光学系统的基准波长,预留波长接口用于后续色散模拟;

path:追迹过程中记录的光线与各光学表面的交点 + 像面点,是可视化和分析的核心数据来源,每个元素为 (x,y,z) 3D 坐标;

field_angle:标记光线的离轴视场角,为多视场追迹的视场区分依据,0° 表示轴上视场。

3.2 Surface 类:光学表面的抽象(core/surface.py)

3.2.1光学依据

单透镜可以看作由两个光学表面(球面或平面)组成,光线在各表面处发生折射。每个表面的核心光学参数包括:曲率半径、有效通光口径、入射侧与出射侧介质以及到下一个表面的中心厚度。通常约定光线沿+z方向(从左向右)传播,在此约定下,曲率半径 (R) 的符号规定为:

:表面凸向光线传播方向,即球心位于表面顶点的 一侧;

:表面凹向光线传播方向,即球心位于表面顶点的 (-z) 一侧;

:表示平面表面,此时曲率为0。

3.2.2代码实现与光学映射

class   Surface:
radius: float# 
曲率半径,>0 凸向右,<0 凹向右,=inf 为平面
thickness: float# 到下一个表面的距离(mm)
material_before: Material# 入射侧介质
material_after: Material# 出射侧介质
semi_diameter: float# 半孔径(mm)
is_stop: bool = False # 是否光阑面(本最小版可暂不使用)

@property
def curvature(self) -> float:
if abs(self.radius) < 1e-9   or self.radius == float('inf'):
return 0.0
return 1.0 / self.radius

3.2.3属性的光学意义

radius:曲率半径的符号规则是光线追迹正确性的关键,直接决定球面的球心位置;

thickness:轴向厚度为z 轴方向的距离,是光学系统结构尺寸的核心参数,薄透镜可设为极小值(如 1e-3mm);

material_before/material_after:为 Snell 定律提供两侧介质的折射率,是折射计算的核心参数;

semi_diameter:通光口径的半值,用于判断光线是否超出通光范围(可扩展挡光判断逻辑);

is_stop:光阑面标记,为后续入瞳 / 出瞳计算、光束限制提供依据,单透镜系统可默认设为 False。

3.3 Material 类:光学材料的抽象(core/material.py)

3.3.1光学依据

在几何光学中,介质的折射率 (n)是决定光线如何在不同材料界面发生折射的核心参数。Material 类的作用,就是在代码层面为每一种光学材料提供一个统一的、可计算的折射率模型。

从物理上看,可以分两种典型情形:

单色光(monochromatic)近似在很多基础几何光学仿真中,只关心某一固定波长(比如 d 线 587.6 nm)下的成像情况,此时可以将材料的折射率视为常数:

例如:BK7 在 d 光(587.6 nm)下的折射率约为 1.5168,在仅做单色轴上成像分析时,使用这个常数已经足够。

复色光与色散(polychromatic & dispersion)当考虑白光或多波长成像时,折射率会随波长变化,即色散。常用的经验公式是Sellmeier 方程,它给出了折射率随波长变化的高精度拟合:

其中:

:光在真空中的波长(工程上常用 µm 或 nm 表示,使用时需统一单位)

:材料的 Sellmeier系数(与色散强度相关)

:材料的 Sellmeier系数(与色散特征波长相关)

这组 系数由玻璃厂商提供,典型如Schott 给出的 BK7、SF11 等光学玻璃数据表。对给定波长,代入上式即可求得该波长处的折射率:

在本项目当前的单透镜几何光学仿真中:

默认采用单色光近似,用一个常数 描述材料折射率,例如 BK7取 (d 光);

Material 类的n(wavelength) 方法预留了波长参数,后续只需在该方法内部将常数形式替换为 Sellmeier 方程,即可无缝扩展到多波长追迹和色散分析(如模拟色差、设计消色差透镜等),而无需修改 trace、analysis、visual等其他模块的代码结构。

3.3.2代码实现与光学映射

class   Material:
name: str
n_const: float# 
简化:不考虑色散,常数折射率

def n(self, wavelength: float) ->   float:
"""
将来可扩展成 Sellmeier,这里先返回常数。"""
return self.n_const

# 
常用实例AIR =   Material("AIR", 1.0)

   def glass_bk7() -> Material:
return Material("BK7",   1.5168)

3.3.3设计的光学考量

n_const:为单色光仿真提供简化模型,取光学玻璃的基准折射率(如 BK7 的 d 光折射率 1.5168),符合单透镜基础仿真的需求;

n () 方法:预留波长入参和返回值,后续仅需修改该方法的实现即可支持色散模拟,无需改动其他模块的代码,保证了架构的可扩展性;

玻璃实例化函数:按光学玻璃牌号封装,符合工程中 “按牌号选择玻璃” 的习惯。

3.4 LensSystem 类:单透镜系统的抽象(core/lens.py)

3.4.1 光学依据

单透镜是最基础的成像光学系统,其光路可以抽象为:

物面 → 前表面 → 后表面 → 像面

在几何光学中通常约定光线沿着 +z 方向从左向右传播,因此在代码中也要求光学系统按这一顺序组织。LensSystem 类正是以「顺序系统」的思想,将所有光学表面按光线实际通过的先后次序依次存入列表,使后续的光线追迹可以简单地从左到右遍历这些表面,从而与物理传播过程一一对应。

在一阶成像理论中,单透镜的焦距可由经典的透镜制造者公式(Lensmaker’s Equation)给出。对折射率为 (n)、前后表面曲率半径分别为 、中心厚度为 (d) 的厚透镜,其有效焦距 (f) 满足:

其中符号约定为:

光线沿 +z 方向(从左到右)传播;

:第一表面的曲率半径,顶点在该表面上,若球心位于顶点右侧(朝 +z)则 ,表示表面凸向光线传播方向;

:第二表面的曲率半径,定义方式同上,通常对对称双凸单透镜有;

(d):两表面顶点之间的中心厚度;

当 时,对应平面表面,曲率为 0。

当厚度 时,上式自然退化为薄透镜公式:

这就是通常意义下「薄透镜的焦距公式」。在一阶近似中,给定材料折射率 (n) 以及所需焦距 (f),可以反过来设计 (例如对称取 ),从而完成单透镜的初始参数设计。

本项目的 LensSystem 设计遵循如下原则:

① 顺序存储光学表面将物方起始面(如物面或入瞳平面)、透镜前表面、透镜后表面、像面等,按光线从左到右的顺序依次存入 surfaces 列表。这样,几何追迹算法只需按列表顺序迭代,便可以完成整个系统的光线传播仿真,逻辑与物理过程高度一致。

② 用一阶公式指导结构,用数值追迹验证焦距

在构造单透镜(如 build_single_lens(f=50.0))时,可以利用上述薄透镜公式,在给定目标焦距 (f) 和材料折射率 (n) 的前提下,反推出合适的曲率半径组合 ,完成「按焦距设计透镜」的一阶结构初选;

但在实际仿真与结果验证时,本项目并不依赖公式直接计算像距或焦距,而是采用 数值光线追迹:对平行入射光束进行全程追迹,再根据光线在像面附近的会聚位置,反推出系统的实际焦点位置与有效焦距,并用点列图、光扇图、RMS 光斑尺寸等手段评价成像质量。

③ 贴近真实光学系统的工程思路采用「一阶公式做初设 + 数值追迹做验证与优化」的方式,有以下优势:

避免只停留在理想薄透镜模型,能够自然涵盖厚度、非对称曲率等实际结构带来的偏差;

后续扩展到更复杂的系统(如双胶合透镜、消色差透镜、多片镜头)时,依然可以沿用同样的LensSystem + RayTracer 框架,而不必为每一种结构重新推导封闭形式的焦距公式;

更贴近光学工程中「先做一阶近似设计,再用光线追迹和像差分析修正结构」的实际工作流程。

LensSystem 在光学上相当于把「单透镜的前后表面及相关空间面」按传播顺序组织成一个有序的光学系统,并以一阶透镜制造者公式为理论参照,通过数值追迹来验证和理解该系统的真实成像行为和有效焦距,从而在代码层面自然地连接了理论公式与工程仿真。

3.4.2代码实现与光学映射

class   LensSystem:
"""
顺序光学系统:按从左到右的顺序存放表面"""
surfaces: List[Surface] =   field(default_factory=list)
name: str = "SingleLens"

def add_surface(self, surface:   Surface):
self.surfaces.append(surface)

   def build_single_lens(f: float = 50.0) -> LensSystem:
"""
构造一个简单单透镜系统:
- 
左侧无限远物(用近似平行光射入表示)
- 
一片 BK7 薄透镜,前后曲率对称,厚度忽略或给一个小值
- 
右侧像面为 z   = image_plane_z
"""
lens =   LensSystem(name="SingleLensDemo")
glass = glass_bk7()

# 
修改参数:增大半孔径和厚度,使10度光线能在透镜口径范围内
# 
焦距约 50mm 的透镜,R1 = 100mm, R2 = -100mm, 中心厚 10mm
# 
介质:左侧空气   -> 玻璃 -> 空气
surface1 = Surface(radius=100.0,   thickness=10.0,
material_before=AIR, material_after=glass,
semi_diameter=30.0)# 
增大半孔径到30mm
surface2 = Surface(radius=-100.0,   thickness=50.0,
material_before=glass, material_after=AIR,
semi_diameter=30.0)# 
增大半孔径到30mm

lens.add_surface(surface1)
lens.add_surface(surface2)

return lens

3.4.3设计的光学考量

表面顺序:按 ** 光线传播方向(从左到右)** 存储,与光线追迹的顺序一致,是追迹算法的基础;

build_single_lens 函数:封装薄透镜的表面构造逻辑,根据目标焦距自动计算曲率半径,符合 “按焦距设计透镜” 的工程需求;

对称透镜:前后表面曲率半径取R1=−R2,是最常用的单透镜结构,可减少像差(如球差)。

四、几何光线追迹的核心算法实现(trace 模块)

光线追迹是光学仿真的核心算法,trace 模块基于几何光学基本定律,实现了光线与球面/平面的交点计算、表面法线计算、Snell 定律的 3D 矢量折射、批量光线追迹四大核心功能,所有算法均为数值精确实现,无近似(除薄透镜的厚度近似),是后续所有分析和可视化的基础。

本模块所有函数均与core层实体类深度联动,直接调用Ray/Surface/LensSystem的光学参数,追迹结果也直接写入Ray类的path属性,保证了项目数据流转的统一性;同时算法实现严格遵循光学行业的符号规则和矢量运算规范,与 Zemax、CodeV 等商用软件的追迹逻辑一致,确保仿真结果的专业性和可比性。

4.1 光线与球面 / 平面的交点计算(trace/geometrical.py - intersect_spherical_surface)

4.1.1 完整光学公式推导

(1)光线的 3D 参数方程

在本项目约定的右手坐标系下,z 轴为光轴,光线整体沿方向从左向右传播。一条三维光线可写为参数方程:

其中:

:光线起点位置矢量;

:光线单位方向矢量,满足 ;

(t) 为标量参数,表示沿光线传播方向前进,在 单位化的前提下可直接视为“传播距离”。

(2)球面方程与曲率半径符号约定

考虑光轴为 z 轴的旋转对称球面,其轴上顶点位于:

光学行业对曲率半径 (R) 采用统一符号约定(光线沿方向从左到右传播):

:表面凸向光线传播方向(凸向右),球心在顶点的 一侧;

:表面凹向光线传播方向(凹向右),球心在顶点的 (-z) 一侧;

:平面表面,曲率为 0。

在这一约定下,球心坐标统一写为:

无论 (R) 正负,都自动给出球心在顶点左/右的位置。

球面方程为:

即:球心到表面上任意点的距离恒为 。

(3)代入光线方程,推导二次方程系数

将 代入球面方程:

令 ,则:

展开:

整理为关于 (t) 的一元二次方程:

其中系数严格推导为:

又因为本项目在 Ray 类中强制 direction 为单位向量,即:

故有:

这保证了参数 (t) 的数值就是几何意义上的“传播距离”,既方便物理理解,又有利于数值稳定性。这也是 Ray.normalize_direction() 的核心意义所在:只有在 单位化的前提下,求得的 (t) 才有明确的物理尺度含义。

4.1.2解的物理筛选规则

形式上,二次方程的解为:

但在光学追迹中,仅保留有物理意义的有效交点。本项目采用如下筛选规则:

① 判别式条件:确保有实交点

若 :光线与该球面无任何实交点,直接判定为无交点;

若 :光线与球面相切,只有一个交点;

若 :光线与球面有两个数学交点,需要进一步物理筛选。

② 传播方向限制:只保留 的解

才表示交点位于光线传播方向上;

实际数值运算中,由于浮点误差,极小的正值或接近 0 的数值有可能被算成微小负数,因此本项目引入一个容差:

这样可以避免将本应视为“当前点附近、几乎不前进”的交点误判为“反方向上的负解”。

③ 最近交点优先:取最小的正解

当有两个正解 时:

选择:

对应光线从当前起点出发,在传播方向上首先遇到的表面交点;

这符合真实物理过程:光线总是先撞到离自己最近的那一段表面。

④ 通光口径校验:判断是否落在有效孔径内

对最终候选解 (t),计算交点坐标:

检查其相对于光轴的横向距离:

若满足:交点在表面有效通光口径内,视为有效交点;

若不满足:说明该光线打在了透镜外缘之外(被机械装调或光阑挡住),在几何仿真中应视为无有效交点(即该光线不继续参与后续传播与像质计算)。

综合以上 4 条规则,intersect_spherical_surface 在数学上保证“解存在且实数”,在物理上保证“交点在传播方向最近且落于有效孔径内”,从而实现了既正确又符合工程实际的交点判定逻辑。

4.1.3平面特例的算法优化

当表面为平面时,曲率半径 ,曲率 ,球面方程在轴向上退化为简单的平面方程:

此时无需再解一元二次方程,可以直接从光线的 z 分量出发,推导交点参数:

其中 (dz) 为光线方向矢量的 z 分量。

物理与数值筛选规则如下:

① 平行性判断

若 (例如 ):说明光线方向几乎完全平行于平面 ,则它与该平面无交点(或在数值上交点远在无穷远处),可直接返回“无交点”。

② 传播方向判断

若计算得 :交点在光线反向或刚好在起点之前,无物理意义;

仅当 时,才认为光线在传播方向上真正“走到”了该平面。

③ 通光口径校验

对有效 (t) 计算交点坐标:

同样检查:

不满足时,认为光线在该平面上的交点超出了有效通光范围,视作无有效交点。

④ 与球面情况的统一性

在代码实现层面,可以采用如下统一策略:

若 足够大(例如 )或显式标记为平面(如radius == float("inf")),则走“平面专用分支”,使用上述线性交点公式;

否则按一般球面逻辑,构造 (A,B,C) 并解一元二次方程。

这样既保留了理论上的严格性,又在工程实现上避免了在平面上解退化二次方程导致的数值不稳定;大幅简化了平面交点计算,提升运算效率;让“球面 / 平面”在同一 intersect_spherical_surface 接口下自然统一,便于扩展和维护。

4.1.4 代码实现与光学映射

函数输入直接调用core层Ray类的origin/direction和Surface类的radius/semi_diameter/顶点,输出为交点有效性标记、参数、3D交点坐标,同时嵌入通光口径校验逻辑,代码与公式一一对应:

def   intersect_spherical_surface(ray: Ray, surface: Surface, z_surface: float)   -> Tuple[float, float, float]:
"""
计算光线与球面/平面交点 (x, y, z)
3D
,光线方程 x =   x0 + t*dx, y = y0 + t*dy, z = z0 + t*dz
球面中心: (0,   0, z_c),半径 R(系统轴对称)
"""
x0, y0,   z0 = ray.path[-1]
dx, dy, dz = ray.direction

R = surface.radius
if surface.curvature == 0:
# 
平面:z = z_surface
t =   (z_surface - z0) / dz
return x0 + t * dx, y0 + t *   dy, z0 + t * dz

# 
球心坐标(系统轴对称,球心在光轴上)
zc =   z_surface + R# 约定:表面顶点在 z_surface,球心在其左右 R
xc, yc =   0.0, 0.0

# 
解 |P(t) - C|^2 = R^2
# (x0 + dx t - xc)^2 + (y0 + dy t -   yc)^2 + (z0 + dz t - zc)^2 = R^2
A = dx**2   + dy**2 + dz**2
B = 2 * (dx * (x0 - xc) + dy * (y0   - yc) + dz * (z0 - zc))
C = (x0 - xc)**2 + (y0 - yc)**2 +   (z0 - zc)**2 - R**2

disc = B**2 - 4 * A * C
if disc < 0:
raise RuntimeError("
光线与球面无实交点")

t1 = (-B - np.sqrt(disc)) / (2 * A)
t2 = (-B + np.sqrt(disc)) / (2 * A)
t = min(t1, t2) if t1 > 0 else   t2# 
取向前的交点

return   x0 + dx * t, y0 + dy * t, z0 + dz * t

4.2 表面法线计算(trace/(geometrical.py - calculate_surface_normal)

4.2.1 光学依据

在几何光学中,光线在光学表面上发生折射或反射时,入射角、折射角(或反射角)都是相对于表面法线来定义的。对于本项目采用的 3D 矢量 Snell 定律形式,表面法线必须以单位矢量形式参与计算,因此在每个光线与表面的交点处,精确、统一地定义表面法矢量是折射计算的前提。

在约定光线沿 方向(从左向右)传播、光轴为z 轴的坐标系中,表面法线在光学上的标准定义为:

① 球面法线

对于曲率半径为 (R) 的旋转对称球面,其球心位于

其中 为该表面顶点在 z 轴上的坐标(与交点计算中完全一致),(R) 的符号遵循行业约定:

:表面凸向光线传播方向,球心在顶点的 一侧;

:表面凹向光线传播方向,球心在顶点的 (-z) 一侧。

设光线与表面的 3D 交点为

则球面法线定义为过球心与交点的有向直线,从球心指向交点的方向。相应的法矢量为:

这种定义与球面几何完全一致:

为球心指向表面交点的半径矢量;

归一化后即为单位法矢量,垂直于该点处的球面切平面。

② 平面法线

当表面为平面时,曲率半径 ,曲率为 0,表面方程可写为:

对于这种与光轴垂直的平面,其几何法线沿 z 轴方向。根据本项目“光线从左向右沿 传播”的约定,将平面法线统一取为指向方向的单位矢量:

这对应于「平面为 时,法矢量为((0,0,1)),指向光线传播方向」。

③ 法矢量统一指向光线入射侧

为了在 3D 矢量 Snell 定律中获得一致、无歧义的符号规则,本项目对所有表面的法矢量采用统一约定:

在每一个光学表面上,表面法矢量 的方向始终定义为指向光线入射侧的单位矢量。

这样做的意义在于:

入射角 一律通过来计算,其中 为入射光线单位方向矢量,指向入射侧,无需在代码中额外区分“哪一面是物方/像方”以及“法线是否需要翻转”;

全反射判据、折射方向计算等公式均基于 的符号和大小,若法线方向不统一,就会导致同一公式在不同表面上出现符号反转甚至物理解读错误;

将法线统一指向入射侧,相当于为 Snell 定律建立了一个全局固定的“正方向”参考,使折射方向的求解在所有表面上都遵循同一套符号规则,有利于保证追迹结果的物理正确性和代码实现的一致性。

综上,表面法线的光学定义可以概括为:

球面:以球心为起点、指向交点的单位半径矢量;

平面:沿光轴方向的单位向量(在本项目中统一取为((0,0,1)),指向光线传播方向一侧);

所有表面的法矢量在使用前必须归一化为单位矢量,并且其方向统一视为「指向光线入射侧」,以为后续 Snell 定律的 3D 矢量折射计算提供稳定、一致的几何基础。

4.2.2 代码实现与光学映射

函数输入为光线与表面的交点、表面参数,输出为归一化的3D单位法矢量,直接为后续Snell定律折射提供输入,与光学定义完全一致:

def surface_normal(surface:   Surface, point: Tuple[float, float, float], z_surface: float) ->   np.ndarray:
"""
计算表面在交点处的法线(指向入射介质一侧或出射一侧,可约定)"""
x, y, z =   point
if surface.curvature == 0:
# 
平面,法线沿 +z 或 -z,这里取 +z
n =   np.array([0.0, 0.0, 1.0])
else:
R = surface.radius
zc = z_surface + R
xc, yc = 0.0, 0.0
n = np.array([x - xc, y - yc, z   - zc])# 
从球心指向表面
# 
归一化
return n   / np.linalg.norm(n)

4.3 Snell 定律的3D 矢量折射(trace/geometrical.py- refract_ray)

4.3.1 光学依据

几何光学中常见的 Snell 定律标量形式

只适用于子午面内的2D 情形,即光线、法线和光轴共面的情况。本项目采用的是3D 矢量光线追迹,光线和表面法线在空间中可指向任意方向,因此需要使用Snell 定律的 3D 矢量形式来计算折射方向。

① 3D 矢量 Snell 定律的基本形式

在入射介质折射率为 、出射介质折射率为 的界面上,设:

:入射光线的单位方向矢量;

:折射光线的单位方向矢量;

:表面交点处的单位法矢量,且方向统一定义为指向入射侧。

Snell 定律的 3D 矢量形式可以写成:

上式含义是:在界面处,入射光线和折射光线在切向方向上的分量(即与法线垂直的部分)满足 Snell 定律。

在此基础上,可以推导出折射光线方向矢量 的显式表达式:

其中:

:光线入射侧介质折射率(Surface.material_before.n());

:光线出射侧介质折射率(Surface.material_after.n());

:入射光线单位方向矢量(Ray.direction,在 Ray.normalize_direction() 中已归一化);

:折射光线单位方向矢量(refract_ray计算输出);

:表面交点处的单位法矢量(由calculate_surface_normal 计算,方向统一为指向入射侧)。

这正是 trace/geometrical.py - refract_ray 函数中折射方向计算公式的理论依据。

② 入射角的定义与计算

入射角 定义为入射光线方向 与表面法线 之间的夹角。在法线已经统一为指向入射侧的前提下,有:

由于 与 都是单位矢量,故有:

这与项目中使用的入射角计算完全一致,为后续的全反射判断提供了直接依据。

③ 全反射判断条件

当光线从光密介质射向光疏介质时(即 ),可能发生全反射。经典 Snell 定律给出全反射条件为:

结合上面对 的表达,有:

等价地,

一旦上述条件成立,将不再存在实数的折射方向解,物理上对应光线完全被反射。因此在代码实现中:

当判断出满足全反射条件时,refract_ray 应返回“无折射光”(例如 (False, None));

上层追迹逻辑据此判定:该光线在此表面不再产生透射部分,可视为追迹终止(或进入反射分支,若后续扩展反射模型)。

④ 与项目实现的一致性说明

结合上述光学依据与项目中已有实现,可以总结出refract_ray 的关键约定与行为:

输入量:

入射光线:Ray 实例,提供单位方向矢量 direction 和波长 wavelength;

光学表面:Surface 实例,提供入射/出射侧材料(material_before / material_after),从而得到;

表面法线:由calculate_surface_normal 计算的单位法矢量 ,方向统一为指向入射侧。

内部计算:

通过 计算入射角的余弦;

利用 与 判定是否发生全反射;

若无全反射,则按 3D 矢量 Snell 定律公式计算折射方向 ,并对结果再归一化,保证仍为单位矢量。

输出量:

has_refract:布尔量,表示是否存在折射光(即是否未发生全反射);

d2:折射光线的 3D 单位方向矢量,用于更新 Ray.direction 并继续后续表面的追迹。

通过以上设计,refract_ray 在数值上与 Snell 定律的 3D 矢量形式一一对应,同时配合统一的法线方向约定和全反射判定,使得整个追迹流程在任意 3D 几何配置下都具有明确、正确的物理含义。

4.3.2 代码实现与光学映射

函数输入直接调用core层Surface类的介质属性(获取)、入射光线方向矢量、表面法矢量,输出为折射有效性标记和3D折射单位方向矢量,嵌入全反射判断逻辑,算法与3D矢量Snell定律完全对应:

def   refract(direction: np.ndarray,
normal: np.ndarray,
n1: float, n2: float) ->   np.ndarray:
"""Snell 
折射,direction 为入射方向(指向传播方向)"""
# 3D 
版向量折射
d = direction /   np.linalg.norm(direction)
n = normal / np.linalg.norm(normal)

# 
使法线指向入射介质
if np.dot(d, n) > 0:
n = -n

cos_i = -np.dot(d, n)
sin2_i = max(0.0, 1.0 - cos_i**2)
n_ratio = n1 / n2
sin2_t = n_ratio**2 * sin2_i

if sin2_t > 1.0:
# 
全反射,这里可以先抛出异常或返回反射方向
raise RuntimeError("全反射发生")

cos_t = np.sqrt(1.0 - sin2_t)
# 
折射方向公式
t =   n_ratio * d + (n_ratio * cos_i - cos_t) * n
return t / np.linalg.norm(t)

4.4 批量光线追迹(trace/tracer.py - RayTracer)

前 3 个功能(光线与球面/平面交点、表面法线、Snell 折射)为原子算法,实现单步的几何运算;RayTracer 类则将这些原子算法工程化封装为完整的追迹流程,实现:

单条光线在整个透镜系统中的连续传播(实例方法 trace_ray);

多条光线的批量追迹(实例方法 trace_rays)。

它是 trace 模块对外提供的核心接口,直接对接后续的 analysis 和 visual 模块:所有的 RMS 光斑、点列图、光扇图等分析与可视化,最终都只依赖于 Ray.path 中记录的 3D 坐标序列。

整体追迹流程仍然严格遵循光学系统的光线传播顺序:从左到右遍历 LensSystem 中的所有光学表面,对每条光线依次执行「交点计算 → 法线计算 → 折射计算」,并将每一表面的交点写入 Ray.path,最终再传播到像面并记录像面交点坐标。

4.4.1 RayTracer 类与光学系统坐标

当前工程实现采用一个面向对象的追迹器类 RayTracer,而不是若干个独立的函数。其核心结构如下:

class RayTracer:
def __init__(self, lens:   LensSystem, z0: float = 0.0):
"""
z0: 
第一表面的顶点位置(例如 z=0)
系统中每个   surface.thickness 用来累加 z 位置
"""
self.lens   = lens
self.z0 = z0
self.surface_positions =   self._compute_surface_positions()

def   _compute_surface_positions(self) -> List[float]:
"""
计算每个 surface 顶点的绝对 z 坐标"""
positions   = []
z = self.z0
for s in self.lens.surfaces:
positions.append(z)
z += s.thickness
return positions

z0 作为第一表面顶点的 z 坐标,确定了整个光学系统在全局坐标中的位置。通常可取 z0 = 0.0。

_compute_surface_positions()通过累加 surface.thickness,得到每一个表面顶点在 z 轴上的绝对坐标 z_surf,后续所有的交点/法线计算都在这一全局坐标系中进行。

RayTracer持有一个 LensSystem 实例,后续对所有光线的追迹都共享同一套表面数据和空间布局。

4.4.2单条光线完整追迹(trace_ray 实例方法)

RayTracer.trace_ray对单条光线执行完整 3D 追迹。当前实现采用实例方法,并且像面位置采用简单的“固定距离”策略,而不是显式传入image_plane_z 参数。

def trace_ray(self, ray: Ray)   -> Ray:
"""
单条光线在当前 LensSystem 中的完整 3D 追迹,追迹结果写入 ray.path。

   :param ray: 
待追迹光线,core/Ray 实例
- ray.origin
:光线起点 3D 坐标
- ray.direction
:单位方向矢量
- ray.wavelength
:波长(用于查材料折射率)
   :return: 
更新后的 Ray 实例(与输入是同一个对象)追迹完成后:
   - ray.path 
中依次记录了光线在各表面以及像面上的 3D 交点坐标:
[
起点?, 表面1交点, 表面2交点, ..., 像面交点]
(是否包含起点取决于上层在调用前是否先 add_point)
   """
current_ray   = ray
# 
第一阶段:依次穿过所有光学表面
for   surface, z_surf in zip(self.lens.surfaces, self.surface_positions):
# 1) 
光线与当前表面的交点
p_int =   intersect_spherical_surface(current_ray, surface, z_surf)
current_ray.add_point(p_int)

# 2) 
计算交点处表面法线
n =   surface_normal(surface, p_int, z_surf)

# 3) 
根据 Snell 定律计算折射方向
n1 =   surface.material_before.n(current_ray.wavelength)
n2 =   surface.material_after.n(current_ray.wavelength)
d_new =   refract(np.array(current_ray.direction), n, n1, n2)
current_ray.direction =   (float(d_new[0]), float(d_new[1]), float(d_new[2]))

# 
第二阶段:通过最后一个面后,直线传播到像面
z_last =   self.surface_positions[-1] + self.lens.surfaces[-1].thickness
z_img = z_last   + 50.0# 
像面位置,可根据需要调整/优化x0, y0, z0 = current_ray.path[-1]
dx, dy, dz = current_ray.direction
t = (z_img - z0) / dz
p_img = (x0 + dx * t, y0 + dy * t,   z0 + dz * t)
current_ray.add_point(p_img)

return current_ray

4.4.3多光线批量追迹(trace_rays 实例方法)

批量追迹的当前实现同样是 RayTracer 的实例方法:

def   trace_rays(self, rays: List[Ray]) -> List[Ray]:
"""
多光线批量追迹:
:param rays: 
待追迹光线列表,可包含多视场、多孔径光线
:return: 
追迹完成后的光线列表(与输入列表中的对象相同)

对每条光线依次调用   trace_ray,并将追迹结果写入对应 Ray.path。
当前实现不筛选“失败光线”(无出瞳/全反射判断),所有输入光线都会被返回。
"""
return [self.trace_ray(r) for r in   rays]

4.4.4 trace 模块的联动机制

trace模块作为算法追迹层,是连接 core 实体层和 analysis/visual 应用层的核心桥梁,其与各层的联动严格遵循项目的数据流转逻辑,无任何冗余数据定义,保证了项目的模块化和解耦性。

与 core 层的联动:
trace 
模块直接读取 Ray、Surface、LensSystem 的光学参数(如光线起点与方向、曲率半径、折射率、厚度等),不对这些物理量做任何形式的“二次封装”或重定义。追迹结果仅通过统一的数据出口写入 Ray.path 属性,记录光线在各光学表面及像面上的 3D 交点坐标,从而保证 core 实体层的唯一性与一致性。

与 analysis 层的联动:
analysis 
模块只依赖追迹完成后的 Ray 对象,不直接调用 trace 模块的内部函数。它直接从 Ray.path 中提取像面交点坐标,用于计算 RMS 光斑尺寸、光扇图像差等像质指标。这样一来,analysis 层对 trace 的依赖完全通过“数据接口”完成,而不是通过算法实现细节,便于后续替换或扩展追迹实现。

与 visual 层的联动:
visual 
模块同样只依赖 Ray.path:

从 Ray.path 提取光线传播路径上的 3D 坐标,并在 y–z 子午面或其他投影平面上绘制 2D 光路图;

从 Ray.path 的像面交点提取 (x, y) 坐标,绘制点列图、光扇图等成像结果可视化。

整个可视化过程只关心“轨迹数据本身”,与具体的交点求解、法线计算、折射实现等追迹细节完全解耦。

五、成像质量量化分析(analysis 模块)

analysis 模块基于 trace 模块追迹后的 Ray.path 数据,实现基础的成像质量定量分析。当前版本聚焦两个最核心、也是工程中最常用的单透镜评价数据:

像面 RMS 光斑尺寸(analysis/spot.py)

子午/弧矢光扇数据(analysis/ray_fan.py)

模块本身不参与任何光线追迹,只读取已经由 RayTracer 写入的 Ray.path,对这些像面坐标做数值处理;计算结果又可直接被 visual 模块消费,用于点列图、光扇图等可视化,实现“定量分析 + 定性可视化”的闭环。

5.1分析指标的光学依据

本项目在单透镜场景下,围绕以下几个最基础的像质指标展开分析,这些指标都建立在“像面交点分布”的基础上:

RMS 光斑尺寸

所有光线在像面上的交点 相对于理想像点 有一定偏差。
RMS 
光斑尺寸(Root Mean Square Spot Size)定义为这些偏差距离的均方根,是衡量像斑“扩散程度”的标量指标,RMS 越小,光斑越集中,系统单色像差越小,成像越清晰。

子午/弧矢光扇数据

光扇图从“瞳坐标 → 像面坐标”的映射角度来刻画单色像差:

子午光扇(meridional fan):在子午面(通常是 y–z 面)内扫描不同入瞳高度的光线,记录它们在像面上的 y 坐标随“光瞳归一化坐标”的变化;

弧矢光扇(sagittal fan):在弧矢方向上(对应 x 方向)记录同一批光线的 x 坐标 随瞳坐标的变化。

这些“瞳坐标–像面坐标”数据是后续计算“像面偏差(= 实际−理想像点)”并绘制标准光扇图的基础,用于分析球差、彗差、像散等单色像差。

5.2 RMS 光斑尺寸计算(analysis/spot.py - rms_spot_size)

5.2.1光学公式

理论上,RMS 光斑尺寸的定义为:

其中:

N:追迹成功、到达像面的光线数量;

:第 (i) 条光线在像面上的横向坐标(mm),即 Ray.path[-1] 的((x,y)) 分量;

:理想像点的横向坐标: 

轴上视场时,理想像点位于光轴上,;

离轴视场时,理想像点由视场角和系统焦距结合高斯成像理论推导得到。

在当前实现中,为了简化接口,rms_spot_size 使用光斑的实际中心作为参考点,而不是显式传入。也就是说:先根据所有像面点的均值 计算光斑中心;然后将 视作参考点,计算每个点到该中心的距离的均方根。

从几何意义看,这相当于度量光斑自身的“扩散程度”,而不关心光斑整体是否偏离理想像点——这正是很多工程场景中最关心的“斑点大小”。

5.2.2代码实现

对应的实现位于 analysis/spot.py:

def spot_metrics(rays:   List[Ray],
ideal_x: float = 0.0,
ideal_y: float = 0.0)   -> Tuple[float, float, float, float]:
if not rays:
return 0.0, 0.0, 0.0, 0.0
points = np.array([r.path[-1] for r   in rays])
xs_mm = points[:, 0]
ys_mm = points[:, 1]
center_x_mm = float(np.mean(xs_mm))
center_y_mm = float(np.mean(ys_mm))
distances_sq_ideal = (xs_mm -   ideal_x)**2 + (ys_mm - ideal_y)**2
rms_mm =   float(np.sqrt(np.mean(distances_sq_ideal)))
rms_um = rms_mm * 1e3
distances_ideal =   np.sqrt(distances_sq_ideal)
geom_mm =   float(np.max(distances_ideal))
geom_um = geom_mm * 1e3
return rms_um, geom_um,   center_x_mm, center_y_mm

   def get_chief_ray_center(rays: List[Ray]) -> tuple[float, float]:
# 
根据 field_angle 和 origin≈(0,0,*) 筛选 chief ray
for r in   rays:
if abs(r.path[0][0]) < 1e-3   and abs(r.path[0][1]) < 1e-3:
x_chief, y_chief, _ =   r.path[-1]
return x_chief, y_chief
# fallback
pts = np.array([r.path[-1] for   r in rays])
return float(np.mean(pts[:,0])),   float(np.mean(pts[:,1]))

   def rms_spot_size(rays: List[Ray]) -> float:
points = np.array([r.path[-1] for r   in rays])
xs_mm = points[:, 0]
ys_mm = points[:, 1]
x_mean = np.mean(xs_mm)
y_mean = np.mean(ys_mm)
distances_sq = (xs_mm - x_mean)**2   + (ys_mm - y_mean)**2
rms_mm =   np.sqrt(np.mean(distances_sq))
rms_um = rms_mm * 1e3
return float(rms_um)

函数输入是追迹成功的光线列表 rays: List[Ray],每条 Ray 的 path[-1] 被认为是像面交点;

将所有像面点打包成二维数组 points,分别取出 x、y 坐标(单位 mm);

用 np.mean(xs_mm) 和np.mean(ys_mm) 求出光斑中心坐标 ;

计算每个点到中心的平方距离并平均,再开方得到 RMS 半径(mm);

最后将单位从 mm 转换为 μm 返回。

5.3光扇图数据计算(analysis/ray_fan.py)

根据使用场景,将光扇数据的生成拆分为两个方向:

子午光扇:compute_meridional_ray_fan

弧矢光扇:compute_sagittal_ray_fan

这两个函数都位于 analysis/ray_fan.py,它们本质上做两件事:

调用examples.ray_fan_utils 中的光线生成工具,构造一批在不同瞳高处通过系统的光线;

使用 RayTracer 追迹这些光线,并提取像面坐标作为光扇数据。

5.3.1子午光扇数据(compute_meridional_ray_fan)

def   compute_meridional_ray_fan(tracer: RayTracer,
pupil_radius: float,
n_rays:   int,
field_angle: float = 0.0) -> Tuple[List[float], List[float]]:
"""
计算子午光扇数据
- tracer: RayTracer
实例
- pupil_radius: 
入瞳半径(mm)
- n_rays: 
采样点数
- field_angle: 
视场角(度)
返回:   (PY_list, EY_mm)
PY_list: 
归一化光瞳坐标 [-1, 1]
EY_mm: 
子午方向像差(mm),相对于主光线像点
"""
PY_list,   rays = generate_meridional_fan_rays(pupil_radius, n_rays, field_angle)
traced_rays =   tracer.trace_rays(rays)

# 
找到主光线(光瞳中心,PY=0)的像点作为参考
y_chief =   0.0
for i, PY in enumerate(PY_list):
if abs(PY) < 1e-6:# 
主光线,PY=0
x_img,   y_chief, z_img = traced_rays[i].path[-1]
break

EY_mm = []
for ray in traced_rays:
x_img, y_img, z_img =   ray.path[-1]
EY_mm.append(y_img - y_chief)

return PY_list, EY_mm

generate_meridional_fan_rays 负责在给定入瞳半径、采样数和视场角下,生成一条沿子午面扫描的光扇:返回的 PY_list 是归一化的光瞳坐标(通常是 区间的均匀采样),rays 是对应的 Ray 列表;

RayTracer.trace_rays 对这组 rays 做完整几何追迹,ray.path[-1] 即为像面交点;

将每条光线的 y_img 组成EY_list 返回,单位为 mm。

这里返回的 EY_list 是像面绝对 y 坐标,不是已经减去理想像点的“偏差”。 

5.3.2弧矢光扇数据(compute_sagittal_ray_fan)

def   compute_sagittal_ray_fan(tracer: RayTracer,
pupil_radius: float,
n_rays:   int,
field_angle: float = 0.0) -> Tuple[List[float], List[float]]:
"""
计算弧矢光扇数据
- tracer: RayTracer
实例
- pupil_radius: 
入瞳半径(mm)
- n_rays: 
采样点数
- field_angle: 
视场角(度)
返回:   (PX_list, EX_mm)
PX_list: 
归一化光瞳坐标 [-1, 1]
EX_mm: 
弧矢方向像差(mm),相对于主光线像点
"""
PX_list,   rays = generate_sagittal_fan_rays(pupil_radius, n_rays, field_angle)
traced_rays =   tracer.trace_rays(rays)

# 
找到主光线(光瞳中心,PX=0)的像点作为参考
x_chief =   0.0
for i, PX in enumerate(PX_list):
if abs(PX) < 1e-6:# 
主光线,PX=0
x_chief,   y_img, z_img = traced_rays[i].path[-1]
break

EX_mm = []
for ray in traced_rays:
x_img, y_img, z_img =   ray.path[-1]
EX_mm.append(x_img - x_chief)

return PX_list, EX_mm

PX_list 同样是归一化的光瞳坐标;

EX_list 是每条光线在像面的 x 坐标(mm),代表弧矢方向的像面坐标;

这样,analysis/ray_fan.py 提供的是标准化的“瞳坐标–像面坐标”数据,visual 模块则在 ray_fan_plot.py 中将它们统一转换为 μm,并绘制出子午/弧矢光扇图。

六、光学结果可视化(visual 模块)

visual 模块基于 trace 模块追迹后的 Ray.path 数据,以及 analysis 模块输出的基础量化指标,实现光路图、点列图、光扇图三大核心光学图表的绘制。所有图表均采用光学工程中常见的可视化规范,与 Zemax 等商用软件的图表样式在视觉上保持一致,使几何追迹结果更直观、更易解读。

本模块完全基于 matplotlib 实现,所有绘图函数均为独立的纯可视化接口:

只读取 Ray.path 或analysis 输出的数值,不参与任何光线追迹和像差计算;

支持标题后缀、图形尺寸、颜色映射等自定义配置;

通过中文字体设置,支持中英文混排标注。

6.1光路图绘制(visual/rayplot.py)

6.1.1光学可视化规范

光路图直接展示光线在光学系统中传播的路径,是理解透镜成像行为的第一视角。本项目中,单透镜系统的光路图采用 y–z 子午面投影,遵循如下规范:

① 坐标系约定

z 轴为光轴方向(从左到右);

y 轴为子午方向高度;

在图中统一采用「横轴 z、纵轴 y」的 2D 投影方式。

② 透镜几何表示

根据每个表面的曲率半径radius 和半孔径 semi_diameter 精确绘制表面轮廓;

对球面,通过曲率符号和球心位置计算截面曲线;

对平面或近似平面(R→∞ 或极小半径),绘制垂直于光轴的直线段。

③ 光线路径表示

所有光线都来自Ray.path 中的 3D 轨迹点,取其中的 (z, y) 分量进行 2D 绘制;

可绘制多条光线,形成光束的整体传播形态;

在基础光路图中统一使用蓝色线条,在视场分色光路图中对不同视场角使用不同颜色。

④ 视场信息展示

对于具有field_angle 属性的光线,可以按视场角分配颜色,并在图例中显示,例如 “0°、5°、10°”;

图例自动去重,避免重复标签。

⑤ 图形设置

使用set_aspect('equal', 'box'),保证 z–y 比例真实;

打开网格线,便于读数和比较;

使用中文字体设置支持中文标题。

6.1.2代码实现

visual/rayplot.py 中包含三个核心函数:

(1)透镜表面轮廓绘制:draw_lens_surfaces

def draw_lens_surfaces(ax,   lens: LensSystem, surface_positions: List[float]):
"""
在 y–z 子午面上,根据曲率半径和孔径画出每个表面的轮廓。
只画表面线条(不填充整个透镜实体)。
"""
for s,   z_s in zip(lens.surfaces, surface_positions):
R = getattr(s,   "radius", None)
semi_dia = getattr(s,   "semi_diameter", None)
if semi_dia is None:
continue# 
没有孔径信息就不画

# 
平面或近似平面
if (R is   None) or (abs(R) < 1e-6) or (abs(R) > 1e6):
ys = np.linspace(-semi_dia,   semi_dia, 50)
zs = np.full_like(ys, z_s)
ax.plot(zs, ys,   color="k", linewidth=1.0)
continue

# 
球面:根据 R 计算球心,并在给定 y 范围内求 z(y)
z_c = z_s   + R# 球心 z
ys =   np.linspace(-semi_dia, semi_dia, 200)

inside = ys**2 <= R**2
ys_valid = ys[inside]
if ys_valid.size == 0:
continue

# 
选择靠近透镜内部的一侧(以 z_s 为参考)
if R >   0:
zs_valid = z_c -   np.sqrt(R**2 - ys_valid**2)# 
球心在右侧,取左侧弧
else:
zs_valid = z_c +   np.sqrt(R**2 - ys_valid**2)# 
球心在左侧,取右侧弧

ax.plot(zs_valid,   ys_valid, color="k", linewidth=1.0)

该函数只负责画出每一个表面的二维轮廓,不填充内部实体,既保留几何精度又保证绘图简洁。

(2)基础光路图:plot_ray_path

def plot_ray_path(rays:   List[Ray], lens: LensSystem, surface_positions: List[float], title_suffix:   str = ""):
"""
二维光路图,z 为横轴,y 为纵轴"""
fig, ax =   plt.subplots(figsize=(10, 4))

# 
先画透镜表面实际轮廓
draw_lens_surfaces(ax,   lens, surface_positions)

# 
再画光线
for r in   rays:
zs = [p[2] for p in r.path]
ys = [p[1] for p in r.path]
ax.plot(zs, ys, 'b-',   alpha=0.5)

ax.set_xlabel("z (mm)")
ax.set_ylabel("y (mm)")
ax.set_title(f"Ray Path -   {lens.name} {title_suffix}")
ax.grid(True)
ax.set_aspect('equal', 'box')
# 
显式设置 y 轴范围为 ±40mm
if   lens.surfaces:
semi_dia = getattr(lens.surfaces[0],   "semi_diameter", 40.0)
ax.set_ylim(-semi_dia * 1.1,   semi_dia * 1.1)
plt.tight_layout()
plt.show()

(3)按视场上色的光路图:plot_ray_path_by_field

def   plot_ray_path_by_field(rays: List[Ray], lens: LensSystem, surface_positions:   List[float], title_suffix: str = ""):
"""
按视场角自动上色的光路图,z 为横轴,y 为纵轴"""
fig, ax =   plt.subplots(figsize=(10, 4))

# 
先画透镜表面实际轮廓
draw_lens_surfaces(ax,   lens, surface_positions)

# 
不同 field_angle 用不同颜色
colors =   plt.cm.tab10.colors

# 
先收集所有的视场角
field_angles   = set()
for ray in rays:
field_angles.add(ray.field_angle)

# 
为每个视场角分配颜色
angle_list   = sorted(field_angles)
angle_color_map = {}
for i, angle in   enumerate(angle_list):
angle_color_map[angle] =   colors[i % len(colors)]

# 
绘制光线
for ray   in rays:
zs = [p[2] for p in ray.path]
ys = [p[1] for p in ray.path]
# 
获取该视场角对应的颜色
color =   angle_color_map[ray.field_angle]
ax.plot(zs, ys, color=color, alpha=0.7,   linewidth=1,
label=f"{ray.field_angle}°")

ax.set_xlabel("z (mm)")
ax.set_ylabel("y (mm)")
ax.set_title(f"
光路图{title_suffix}")
ax.set_aspect('equal', 'box')
ax.grid(True)
# 
显式设置 y 轴范围为 ±40mm
if   lens.surfaces:
semi_dia =   getattr(lens.surfaces[0], "semi_diameter", 40.0)
ax.set_ylim(-semi_dia * 1.1,   semi_dia * 1.1)

# 
图例去重
handles,   labels = ax.get_legend_handles_labels()
by_label = dict(zip(labels,   handles))
ax.legend(by_label.values(),   by_label.keys(), loc="upper right")

plt.tight_layout()
plt.show()

适用于:

多视场综合展示,如 0°、5°、10° 视场光线同时显示;

对比不同视场下光线在系统中的折射路径差异。

6.2点列图绘制(visual/spotplot.py)

6.2.1光学可视化规范

点列图通过展示像面上各光线的交点分布,直观体现成像质量和像差类型。当前实现遵循如下规范:

① 坐标含义

横轴:像面 x 坐标;

纵轴:像面 y 坐标;

单位统一换算为 µm,以便观察微小光斑结构。

② 参考中心

不显式画出“理论理想像点”,而以主光线(chief ray)的像点作为几何中心;

所有光线终点相对于主光线像点做平移,得到相对坐标,保证不同仿真条件下的点列图均以主光线为中心对齐。

③ 视觉风格

使用蓝色 + 号标记,与 Zemax 点列图默认风格一致;

坐标轴等比例,保证光斑形状不会被拉伸;

自动根据光斑外接范围设置坐标轴限,使光斑居中显示。

6.2.2代码实现

def plot_spot_diagram(rays:   List[Ray], title_suffix: str = ""):
"""
像面处点列图:取每条光线的最后一个点,画x-y平面,以主光线为中心"""
# 
获取所有光线的像平面位置
image_points   = np.array([r.path[-1] for r in rays])
xs_mm = image_points[:, 0]
ys_mm = image_points[:, 1]

# 
寻找主光线(光瞳中心出发的光线,即原点出发的光线)
chief_ray   = None
for ray in rays:
# 
检查光线原点是否接近光瞳中心 (0, 0, -100)
origin =   ray.path[0]
if abs(origin[0]) < 1e-3 and   abs(origin[1]) < 1e-3:
chief_ray = ray
break

# 
计算主光线在像平面上的位置作为参考点
if   chief_ray:
chief_x_mm, chief_y_mm,   chief_z_mm = chief_ray.path[-1]
# 
将所有光线的像平面位置减去主光线的位置,得到相对主光线的偏移
xs_mm -=   chief_x_mm
ys_mm -= chief_y_mm

# mm -> μm
xs_um = xs_mm * 1e3
ys_um = ys_mm * 1e3

fig, ax = plt.subplots(figsize=(6,   6))
# 
使用 '+' 标记,与 Zemax 风格一致,调整大小和颜色
ax.plot(xs_um,   ys_um, 'b+', markersize=5, alpha=0.7)

# 
设置坐标轴范围,使点列图居中
if   len(xs_um) > 0 and len(ys_um) > 0:
max_val = max(max(abs(xs_um)),   max(abs(ys_um))) * 1.2
ax.set_xlim(-max_val, max_val)
ax.set_ylim(-max_val, max_val)

# 
添加网格线,便于观察光斑分布
ax.grid(True,   linestyle='--', alpha=0.3)

# 
获取视场角信息,用于标题
field_angle   = None
if rays:
field_angle =   rays[0].field_angle

# 
构建标题
title =   "Spot Diagram"
if field_angle is not None:
title += f" (Field:   {field_angle}°)"
if title_suffix:
title += f"   {title_suffix}"

ax.set_xlabel("x (μm)")
ax.set_ylabel("y (μm)")
ax.set_title(title)
ax.set_aspect('equal', 'box')
plt.tight_layout()
plt.show()

该函数只依赖:

ray.path[0]:判断主光线(原点出发);

ray.path[-1]:像面交点坐标;

ray.field_angle:用于标题标注视场角。

上层若需要数值 RMS 光斑尺寸,可由analysis/spot.py 的 rms_spot_size 计算,再自行叠加到标题或图中文本中。本函数保持纯绘图职责。

6.3光扇图绘制(光扇图绘图库)

6.3.1光学可视化规范

光扇图用于分析单色像差在瞳面上的分布趋势,当前实现中:

① 输入数据形式

横坐标:归一化光瞳坐标(PX或 PY),范围 ;

纵坐标:像面坐标(EX 或 EY,单位 mm),代表光线在像面的x 或 y 坐标。

② 偏差基准

在子午/弧矢单图中,函数内部会以“光瞳中心(PX 或 PY 最接近 0 的点)对应的像面坐标”为参考,将所有数据平移,使该点作为 0 基准,形成交对称的像差曲线;

在合并光扇图中,直接以 µm为单位绘制 EX、EY 两组数据,纵坐标标签统一为“像差 (μm)”。

③ 绘图风格

使用连续曲线 + 散点叠加的形式,同时展示趋势和采样点;

使用蓝色表示子午光扇、红色表示弧矢光扇;

添加横纵零线,明确像差为 0的理想基线位置;

横坐标范围固定为 ,纵坐标自动调整。

6.3.2代码实现

(1)子午光扇图:plot_meridional_fan

def plot_meridional_fan(PY:   List[float],
EY_mm:   List[float],
title_suffix:   str = "",
y_lim: float =   None):
"""
绘制子午光扇图
- PY: 
归一化光瞳坐标 [-1, 1]
- EY_mm: 
子午方向像差(mm)
- title_suffix: 
标题后缀
- y_lim: 
纵坐标范围(μm),如果为None则自动计算
"""
EY_um =   [ey * 1e3 for ey in EY_mm]

fig, ax = plt.subplots(figsize=(8,   5))
ax.plot(PY, EY_um, 'b-',   linewidth=2, alpha=0.7)
ax.scatter(PY, EY_um, c='b', s=20,   alpha=0.7)

ax.axhline(y=0, color='k',   linestyle='--', linewidth=1, alpha=0.5)
ax.axvline(x=0, color='k',   linestyle='--', linewidth=1, alpha=0.5)

ax.set_xlabel('
归一化光瞳坐标 PY')
ax.set_ylabel('
像差 (μm)')
if title_suffix:
ax.set_title(f'
子午光扇图 {title_suffix}')
else:
ax.set_title('
子午光扇图')
ax.grid(True, alpha=0.3)
ax.set_xlim(-1.1, 1.1)
if y_lim is not None:
ax.set_ylim(-y_lim, y_lim)
else:
# 
自动 y 轴范围
y_min   = min(EY_um)
y_max = max(EY_um)
y_pad = (y_max - y_min) * 0.1   if y_max != y_min else 0.1
ax.set_ylim(y_min - y_pad,   y_max + y_pad)
plt.tight_layout()
plt.show()

(2)弧矢光扇图:plot_sagittal_fan

def plot_sagittal_fan(PX:   List[float],
EX_mm:   List[float],
title_suffix: str   = "",
y_lim: float =   None):
"""
绘制弧矢光扇图
- PX: 
归一化光瞳坐标 [-1, 1]
- EX_mm: 
弧矢方向像差(mm)
- title_suffix: 
标题后缀
- y_lim: 
纵坐标范围(μm),如果为None则自动计算
"""
EX_um =   [ex * 1e3 for ex in EX_mm]

fig, ax = plt.subplots(figsize=(8,   5))
ax.plot(PX, EX_um, 'r-',   linewidth=2, alpha=0.7)
ax.scatter(PX, EX_um, c='r', s=20,   alpha=0.7)

ax.axhline(y=0, color='k',   linestyle='--', linewidth=1, alpha=0.5)
ax.axvline(x=0, color='k',   linestyle='--', linewidth=1, alpha=0.5)

ax.set_xlabel('
归一化光瞳坐标 PX')
ax.set_ylabel('
像差 (μm)')
if title_suffix:
ax.set_title(f'
弧矢光扇图 {title_suffix}')
else:
ax.set_title('
弧矢光扇图')
ax.grid(True, alpha=0.3)
ax.set_xlim(-1.1, 1.1)
if y_lim is not None:
ax.set_ylim(-y_lim, y_lim)
else:
# 
自动 y 轴范围
y_min   = min(EX_um)
y_max = max(EX_um)
y_pad = (y_max - y_min) * 0.1   if y_max != y_min else 0.1
ax.set_ylim(y_min - y_pad,   y_max + y_pad)
plt.tight_layout()
plt.show()

(3)合并光扇图:plot_both_fans

def plot_both_fans(PX:   List[float],
EX_mm: List[float],
PY: List[float],
EY_mm: List[float],
title_suffix: str =   ""):
"""
绘制合并光扇图
- PX, EX_mm: 
弧矢光扇数据(mm)
- PY, EY_mm: 
子午光扇数据(mm)
- title_suffix: 
标题后缀
"""
EX_um =   [ex * 1e3 for ex in EX_mm]
EY_um = [ey * 1e3 for ey in EY_mm]

fig, ax = plt.subplots(figsize=(8,   5))
ax.plot(PY, EY_um, 'b-',   linewidth=2, alpha=0.7, label='
子午光扇')
ax.scatter(PY, EY_um, c='b', s=20,   alpha=0.7)
ax.plot(PX, EX_um, 'r-',   linewidth=2, alpha=0.7, label='
弧矢光扇')
ax.scatter(PX, EX_um, c='r', s=20,   alpha=0.7)

ax.axhline(y=0, color='k',   linestyle='--', linewidth=1, alpha=0.5)
ax.axvline(x=0, color='k',   linestyle='--', linewidth=1, alpha=0.5)

ax.set_xlabel('
归一化光瞳坐标')
ax.set_ylabel('
像差 (μm)')
if title_suffix:
ax.set_title(f'
合并光扇图 {title_suffix}')
else:
ax.set_title('
合并光扇图')
ax.grid(True, alpha=0.3)
ax.set_xlim(-1.1, 1.1)
ax.legend()

all_data = EX_um + EY_um
y_min = min(all_data)
y_max = max(all_data)
y_pad = (y_max - y_min) * 0.1 if   y_max != y_min else 0.1
ax.set_ylim(y_min - y_pad, y_max +   y_pad)

plt.tight_layout()
plt.show()

七、验证与对比

7.1 单透镜(单视场)

本项目提供examples模块中,可以实现100mm焦距、0.5876um单波长、单视场、BK7对称单透镜的仿真验证,涵盖从参数输入、光线生成、追迹到分析、可视化的全流程,仿真结果与Zemax交叉验证。

光路图对比:

点列图:

光扇图:

7.2单透镜(多视场)

本项目提供examples模块中,可以实现100mm焦距、0.5876um单波长、多视场、BK7对称单透镜的仿真验证,涵盖从参数输入、光线生成、追迹到分析、可视化的全流程,仿真结果与Zemax交叉验证。

光路图对比:

点列图:

光扇图:

八、项目扩展方向

本项目的模块化架构和预留接口让其具备极强的可扩展性,可基于现有模块快速扩展至更复杂的光学仿真场景,核心扩展方向包括:

色散模拟:修改Material类的n()方法,实现Sellmeier方程的折射率计算,支持复色光(红/绿/蓝)的追迹和色差分析;

厚透镜/双胶合透镜仿真:扩展LensSystem类,支持多表面、多玻璃的透镜系统,实现双胶合透镜的消色差仿真;

高级像差分析:扩展analysis模块,加入球差、彗差、像散、场曲、畸变的定量计算,实现单色像差的完整分析;

三维可视化:基于mpl_toolkits.mplot3d实现3D光路图和3D点列图,更直观地体现光线的3D传播规律;

交互化界面:基于Streamlit/PyQt实现可视化交互界面,支持用户实时调整透镜参数,实时查看仿真结果;

镜头优化:结合遗传算法/梯度下降算法,实现基于RMS光斑尺寸的透镜参数自动优化,满足工程化的设计需求。

最新文章

随机文章