4. 面形误差的Zernike多项式拟合

任务说明

本案例给定一个镜面面形误差数据文件(离散采样点上的矢高偏差,单位nm),要求使用Zernike圆域多项式对该面形进行最小二乘拟合,提取前15项(对应noll序号1~15,含平移项)系数,并利用拟合系数重构面形,输出重构残差以评估拟合质量。

数据约定

输入数据为文本文件 surface_error.txt,每行三个浮点数:归一化坐标 x、y(圆域内,x²+y²≤1)以及该点面形误差值 z(单位nm)。数据点数量不少于200,均匀分布于单位圆内。本案例附带生成该数据文件的Python脚本,确保可复现。

实现代码

import numpy as np
from numpy.polynomial.legendre import leggauss

# ---------- Zernike 圆域多项式(Noll 序号 1~15) ----------
def zernike_noll(n, m, x, y):
    """返回第 noll 序号对应的 Zernike 多项式值(向量化)"""
    r = np.sqrt(x**2 + y**2)
    theta = np.arctan2(y, x)
    if n == 0 and m == 0:      # 1: piston
        return np.ones_like(r)
    if n == 1 and m == -1:     # 2: tilt y
        return 2*r*np.sin(theta)
    if n == 1 and m == 1:      # 3: tilt x
        return 2*r*np.cos(theta)
    if n == 2 and m == 0:      # 4: defocus
        return np.sqrt(3)*(2*r**2 - 1)
    if n == 2 and m == -2:     # 5: astig 45°
        return np.sqrt(6)*r**2*np.sin(2*theta)
    if n == 2 and m == 2:      # 6: astig 0°
        return np.sqrt(6)*r**2*np.cos(2*theta)
    if n == 3 and m == -1:     # 7: coma y
        return np.sqrt(8)*(3*r**3 - 2*r)*np.sin(theta)
    if n == 3 and m == 1:      # 8: coma x
        return np.sqrt(8)*(3*r**3 - 2*r)*np.cos(theta)
    if n == 3 and m == -3:     # 9: trefoil y
        return np.sqrt(8)*r**3*np.sin(3*theta)
    if n == 3 and m == 3:      # 10: trefoil x
        return np.sqrt(8)*r**3*np.cos(3*theta)
    if n == 4 and m == 0:      # 11: primary spherical
        return np.sqrt(5)*(6*r**4 - 6*r**2 + 1)
    if n == 4 and m == -2:     # 12: secondary astig y
        return np.sqrt(10)*(4*r**4 - 3*r**2)*np.sin(2*theta)
    if n == 4 and m == 2:      # 13: secondary astig x
        return np.sqrt(10)*(4*r**4 - 3*r**2)*np.cos(2*theta)
    if n == 4 and m == -4:     # 14: tetrafoil y
        return np.sqrt(10)*r**4*np.sin(4*theta)
    if n == 4 and m == 4:      # 15: tetrafoil x
        return np.sqrt(10)*r**4*np.cos(4*theta)
    raise ValueError(f"Unsupported Noll index: n={n}, m={m}")

# Noll 序号对应的 (n, m) 列表,前15项
NOLL_LIST = [(0,0),(1,-1),(1,1),(2,0),(2,-2),(2,2),
             (3,-1),(3,1),(3,-3),(3,3),(4,0),
             (4,-2),(4,2),(4,-4),(4,4)]

def fit_zernike(x, y, z, terms=15):
    """最小二乘拟合,返回系数数组(顺序对应 NOLL_LIST 前 terms 项)"""
    A = np.column_stack([zernike_noll(n, m, x, y) for (n, m) in NOLL_LIST[:terms]])
    coeffs, _, _, _ = np.linalg.lstsq(A, z, rcond=None)
    return coeffs

def reconstruct(x, y, coeffs):
    """利用系数重构面形"""
    terms = len(coeffs)
    A = np.column_stack([zernike_noll(n, m, x, y) for (n, m) in NOLL_LIST[:terms]])
    return A @ coeffs

# ---------- 生成模拟面形数据(用于演示,实际使用时替换为读文件) ----------
def generate_demo_data(n_points=500, seed=42):
    """在单位圆内生成随机点,并叠加已知Zernike面形与噪声"""
    rng = np.random.default_rng(seed)
    # 均匀圆域采样(拒绝采样)
    pts = []
    while len(pts) < n_points:
        x = rng.uniform(-1, 1, n_points*2)
        y = rng.uniform(-1, 1, n_points*2)
        mask = x**2 + y**2 <= 1
        pts.extend(zip(x[mask], y[mask]))
    pts = np.array(pts[:n_points])
    x, y = pts[:,0], pts[:,1]
    # 真实系数(前15项,单位nm)
    true_coeffs = np.array([5.0, -3.0, 2.0, 8.0, -1.5, 4.0,
                            2.5, -6.0, 1.0, -2.0, 7.5,
                            0.5, -1.0, 0.8, -0.3])
    z_true = reconstruct(x, y, true_coeffs)
    noise = rng.normal(0, 0.5, n_points)  # 0.5nm 噪声
    z = z_true + noise
    return x, y, z, true_coeffs

# ---------- 主流程 ----------
if __name__ == "__main__":
    # 方式1:读取外部数据文件(取消注释使用)
    # data = np.loadtxt("surface_error.txt")
    # x, y, z = data[:,0], data[:,1], data[:,2]

    # 方式2:生成演示数据(本案例自带,保证可复现)
    x, y, z, true_coeffs = generate_demo_data()

    # 拟合前15项
    coeffs_fit = fit_zernike(x, y, z, terms=15)

    # 重构面形与残差
    z_recon = reconstruct(x, y, coeffs_fit)
    residual = z - z_recon
    rms_residual = np.sqrt(np.mean(residual**2))
    rms_original = np.sqrt(np.mean(z**2))

    # 输出结果
    print("拟合系数(Noll 1~15,单位nm):")
    for i, c in enumerate(coeffs_fit, 1):
        print(f"  Z{i:2d}: {c:8.3f}")
    print(f"\n原始面形 RMS: {rms_original:.3f} nm")
    print(f"重构残差 RMS: {rms_residual:.3f} nm")
    print(f"残差/原始比: {rms_residual/rms_original*100:.2f}%")

    # 保存系数到文件
    np.savetxt("zernike_coeffs.txt", coeffs_fit, fmt="%.6f", header="Noll 1-15 coefficients (nm)")

落地与运行方式

  1. 将上述代码保存为 zernike_fit.py;
  2. 若使用外部数据,将 surface_error.txt 置于同目录,并取消主流程中“方式1”注释、注释掉“方式2”;
  3. 运行 python zernike_fit.py,依赖仅需 numpy。

验证方法与判定标准

  • 程序输出15个拟合系数、原始RMS、残差RMS及百分比;
  • 使用自带演示数据时,拟合系数与真实系数(true_coeffs)偏差应小于0.1nm(因噪声0.5nm,15项拟合可良好恢复);
  • 残差RMS应明显小于原始RMS(通常低于10%),表明前15项已捕获主要面形成分;
  • 若使用外部数据,观察残差RMS是否达到预期拟合精度,并检查 zernike_coeffs.txt 是否成功生成。
Zernike 面形拟合流程与数据流 输入数据 surface_error.txt (x, y, z) 单位圆内 数据预处理 坐标归一化 圆域内点筛选 Zernike 拟合 构造设计矩阵 A 最小二乘 lstsq 提取前15项系数 输出:zernike_coeffs.txt 15个系数(Noll 1~15,单位nm) 面形重构 z_recon = A·coeffs 残差 = z - z_recon RMS 评估 验证判定 残差RMS / 原始RMS < 10% 离散采样点 单位圆内有效 最小二乘 矩阵乘法 Noll 序号 1~15:piston, tilt, defocus, astigmatism, coma, trefoil, spherical 等