一层薄膜的厚度,一束光的往返:2025 国赛 B 题全复盘

题目思考:这题到底在问什么

背景:为什么要测一层薄膜

碳化硅(SiC)是第三代半导体的代表材料,用于新能源汽车的功率器件、射频模块这些「很硬核」的场景。器件的核心结构是衬底上外延生长的一层碳化硅薄膜(外延层)——它的厚度直接决定击穿电压、导通电阻这些关键指标。测厚的方法是红外干涉法:外延层和衬底因掺杂浓度不同而折射率不同,红外光射进去,一部分从外延层表面反射,一部分钻进外延层、从衬底界面反射回来,两束光叠加产生干涉条纹。条纹里藏着厚度信息。

题目给了四个附件:附件 1/2 是同一块碳化硅晶圆在入射角 10° 和 15° 下的反射光谱(7469 个数据点,波数 400~4000 cm⁻¹);附件 3/4 是同一块硅晶圆在两个角度下的光谱。三问层层递进:

flowchart TD
    A["红外反射光谱<br/>波数与反射率"] --> B
    subgraph S1["问题一 · 理论建模"]
        B["只考虑界面单次反射透射<br/>双光束干涉"] --> C["推导厚度的显式公式"]
    end
    C --> D
    subgraph S2["问题二 · 算法与数据"]
        D["用问题一的模型处理附件 1/2<br/>算出厚度并检验可靠性"]
    end
    D --> E
    subgraph S3["问题三 · 模型的边界"]
        E["多次反射形成多光束干涉<br/>推导必要条件"] --> F["判断附件 3/4 是否出现<br/>建立硅的测厚模型"]
        F --> G["若碳化硅数据也受影响<br/>设法消除并修正"]
    end

我当时的理解:这是一道「正问题 → 逆问题 → 自检」的题

拿到题的第一晚,我们没有急着写代码,而是把三问翻译成了自己的语言:

  • 问题一是一个正问题:给定 n1n_1(外延层折射率)、θ0\theta_0(入射角)、ν\nu(波数)和干涉级次,厚度 dd 是多少?本质是推导一个显式公式,物理课本里的薄膜干涉搬过来,认真处理半波损失和折射角就行。
  • 问题二是一个逆问题:手上只有「波数—反射率」表格,n1n_1、干涉级次 mm 都不直接已知,要从光谱里把它们连同 dd 一起反演出来。这是全题的核心难点:题目特意提醒「折射率不是常数,与掺杂浓度、波长有关」,等于明示「别把 nn 当常数」。
  • 问题三是对模型适用性的质问:问题一的「双光束」假设真的成立吗?光在薄膜里其实会弹射很多次。第三问要求推导多光束干涉的必要条件、判断硅晶圆数据里它是否出现、并回头审视碳化硅的结果要不要修正。

没发现的关键点

复盘的时候我才发现到这道题一个很重要的地方:

d=m2n1cosθ1ν从条纹间距只能确定 n1d 这个乘积d = \frac{m}{2 n_1 \cos\theta_1 \cdot \nu} \quad\Longrightarrow\quad \text{从条纹间距只能确定 } n_1 d \text{ 这个乘积}

条纹间距 Δν=12n1dcosθ1\Delta\nu = \frac{1}{2 n_1 d \cos\theta_1} 只能给出折射率与厚度的乘积n1n_1dd 是简并的——不知道折射率,就不可能知道绝对厚度。所以这道题真正的战场是:如何把 n1(λ)n_1(\lambda) 尽量准确地独立确定下来。我们当时在这个问题上的处理(后文「反思」一节会详谈)恰好是全文最薄弱的一环。

问题一:双光束干涉模型与灵敏度分析

物理图景

只考虑两条路径的光:

  1. 第一束:在外延层上表面直接反射;
  2. 第二束:折射进入外延层,在衬底界面反射,再折射出来。

第二束比第一束多走了 2d2d 的几何路程(斜着走,有效光程是 2n1dcosθ12 n_1 d \cos\theta_1)。两束光相遇时,相位差取决于这段光程差——同相则亮(相长),反相则暗(相消)。扫描波数,就会看到反射率周期性的起伏,这就是干涉条纹。

模型推导

第一步:折射角。 由斯涅尔定律(空气折射率 n01n_0 \approx 1):

n0sinθ0=n1sinθ1cosθ1=1(sinθ0n1)2n_0 \sin\theta_0 = n_1 \sin\theta_1 \quad\Longrightarrow\quad \cos\theta_1 = \sqrt{1 - \left(\frac{\sin\theta_0}{n_1}\right)^2}

第二步:光程差与半波损失。 几何光程差为 ΔL=2n1dcosθ1\Delta L = 2 n_1 d \cos\theta_1。此外,光从光疏介质射向光密介质界面反射时会附加 π\pi 相位突变(半波损失):

  • n2>n1n_2 > n_1:两个界面的反射都有半波损失,总效果相互抵消,修正量为 0;
  • n2<n1n_2 < n_1:只有空气—外延层界面有半波损失,需要补 λ/2\lambda/2

第三步:干涉极值条件。 反射率出现极值(亮纹)时总光程差为波长的整数倍。引入波数 ν=1/λ\nu = 1/\lambdaλ\lambda 单位 cm),令 mm 为干涉级次(正整数),统一写成:

2n1dcosθ1=k1νd=k2n1cosθ1ν2 n_1 d \cos\theta_1 = k \cdot \frac{1}{\nu} \quad\Longrightarrow\quad {\,d = \frac{k}{2 n_1 \cos\theta_1 \cdot \nu}\,}

其中干涉级次修正项 kk

k={m,n2>n1m0.5,n2<n1k = \begin{cases} m, & n_2 > n_1 \\ m - 0.5, & n_2 < n_1 \end{cases}

这就是问题一的答案:一个四参数显式公式。代入选定参数(n1=2.65n_1 = 2.65n2=2.68n_2 = 2.68θ0=15°\theta_0 = 15°ν=1000 cm1\nu = 1000\ \mathrm{cm^{-1}}m=1m = 1):

cosθ1=0.99522,d=12×2.65×0.99522×10001.896 μm\cos\theta_1 = 0.99522,\qquad d = \frac{1}{2 \times 2.65 \times 0.99522 \times 1000} \approx 1.896\ \mathrm{\mu m}

灵敏度分析:这个公式怕什么

公式有了,但它是「理想世界」的。真实测量里 n1n_1ν\nu 都有误差,于是我加了一节灵敏度分析——对厚度公式求偏导:

dn1=k2νn12cos3θ1,dν=k2n1cosθ1ν2\frac{\partial d}{\partial n_1} = -\frac{k}{2 \nu\, n_1^2 \cos^3\theta_1}, \qquad \frac{\partial d}{\partial \nu} = -\frac{k}{2 n_1 \cos\theta_1\, \nu^2}

代入上面的参数:d/n10.722 μm\partial d/\partial n_1 \approx -0.722\ \mathrm{\mu m}n1n_1 每偏 1,厚度偏约 0.72 μm;即折射率 1% 的误差会带来约 1% 的厚度误差),d/ν1.9×103 μm/cm1\partial d/\partial \nu \approx -1.9 \times 10^{-3}\ \mathrm{\mu m/cm^{-1}}(波数标定偏 1 cm⁻¹,厚度偏约 1.9 nm)。

两个有趣的小发现:

  • 波数的相对灵敏度恰好是 1-1(d/ν)(ν/d)=1(\partial d/\partial \nu)\cdot(\nu/d) = -1,波数错 1%,厚度就错 1%,分毫不差——因为 dν1d \propto \nu^{-1} 是纯幂次关系。
  • n1n_1 的相对灵敏度是 1/cos2θ11.01-1/\cos^2\theta_1 \approx -1.01,几乎也是 1-1这说明折射率的误差会一比一地传导进厚度,与前文关于参数简并的分析殊途同归。
问题一灵敏度分析:左图为外延层折射率对厚度的影响,右图为波数对厚度的影响
问题一灵敏度分析:左图为外延层折射率对厚度的影响,右图为波数对厚度的影响

顺便暴露一个当时就存在、但论文里没有展开的细节:上图中橙色虚线处(n1=n2=2.68n_1 = n_2 = 2.68)曲线不连续——因为 kk 在这里从 mm 跳变为 m0.5m - 0.5。物理上,n1=n2n_1 = n_2 意味着衬底界面消失、干涉条纹本身不复存在,所以这个跳变是「模型外推到自己适用域边界」的信号,并非真实行为。灵敏度公式本身也默认 kk 固定,在跳变点附近失效。当时我们没有处理这个奇点,复盘时我给它补了个标注(就是图里那行橙色的字)。

代码讲解:Q1.py

问题一的代码是一个不到 200 行的类 EpilayerThicknessCalculator,把「参数 → 厚度 → 灵敏度 → 可视化」封装成流水线。核心部分如下:

class EpilayerThicknessCalculator:
    def __init__(self, n1=2.65, n2=2.68, theta0_deg=15, nu_tilde=1000, m=1):
        self.n1, self.n2, self.m = n1, n2, m
        self.cos_theta1 = self._calc_cos_theta1()  # 预计算,避免重复求值
        self.k = self._calc_k()

    def _calc_cos_theta1(self):
        # 斯涅尔定律 + 三角恒等式
        theta0_rad = np.deg2rad(self.theta0_deg)
        sin_theta1 = np.sin(theta0_rad) / self.n1
        return np.sqrt(1 - sin_theta1 ** 2)

    def _calc_k(self):
        # 半波损失修正:n2>n1 时两次 π 突变抵消,k=m;否则 k=m-0.5
        return self.m if self.n2 > self.n1 else (self.m - 0.5)

    def calculate_thickness(self):
        # 核心公式:d = k / (2·n1·cosθ1·ν)
        self.d = self.k / (2 * self.n1 * self.cos_theta1 * self.nu_tilde)
        return self.d

    def calculate_sensitivity(self):
        # 对 n1 的偏导(含 cosθ1 对 n1 的隐式依赖,链式法则推导)
        self.d_dn1 = -self.k / (2 * self.nu_tilde * self.n1**2 * self.cos_theta1**3)
        # 对波数的偏导
        self.d_dnu = -self.k / (2 * self.n1 * self.cos_theta1 * self.nu_tilde**2)
        return self.d_dn1, self.d_dnu

设计上有觉得满意的地方:

  1. kk 的判断收敛到一个函数里。半波损失的分析只写一次,主公式永远是统一形式,避免了「明纹暗纹两套公式」的分裂;
  2. 灵敏度直接用解析偏导而不是数值差分——d/n1\partial d/\partial n_1cosθ1\cos\theta_1n1n_1 的隐式依赖容易在差分里被漏掉,解析式(分母上的 cos3θ1\cos^3\theta_1)把它显式地暴露出来了。

可视化部分(plot_sensitivity)围绕默认值做 ±0.1\pm 0.1 的参数扫描画曲线——就是上面那张图的原型,代码很常规就不贴了。

问题二:从 7469 个光谱点到一层厚度

问题二是全篇工作量最大的一问。算法以「数据预处理 → 动态参数计算 → 厚度求解 → 结果验证」为逻辑链。

先看数据长什么样

附件 1 的原始数据:7469 个点,波数从 399.67 到 4000.12 cm⁻¹,反射率单位是 %。直接画出来会发现两个问题:

  1. 背景漂移:反射率整体有个缓慢起伏的「底」,不是平的——这是仪器背景和材料色散的叠加;
  2. 异常值:存在负反射率、超过 100% 的点(第一行数据原始反射率是 0,基线 16.2,校正后直接 16.2%-16.2\%,物理上显然不对)。

处理分两层:

清洗规则(简单粗暴但有效):负值置 0.0001,100%\geq 100\% 置 99.99,NaN 填 1%,波数 0\leq 0 填默认值 1000 cm⁻¹。

ALS 基线校正(在 MATLAB 里用 test.mlx 完成,输出「基线校正结果表」给 Python 用)。ALS(Asymmetric Least Squares,非对称最小二乘)的思想是:拟合一条「只会从下方包住数据」的柔性基线 zz,目标函数带两个拉锯项:

minz  iwi(yizi)2+λi(Δ2zi)2\min_{\mathbf{z}} \;\sum_i w_i \left(y_i - z_i\right)^2 + \lambda \sum_i \left(\Delta^2 z_i\right)^2

第一项是贴合(残差),第二项是平滑(二阶差分惩罚,λ\lambda10610^6);权重 wiw_i 迭代更新——残差为正(数据在基线上方,大概率是干涉峰)的点权重调低,让基线「绕开」峰。迭代几轮后,基线就贴着光谱的谷底走了。校正后干涉振荡变得干净对称,为后续峰值检测打了好基础。

折射率:从常数升级为柯西色散

题目明示折射率随波长变化,所以问题二摒弃了问题一的「常数折射率」假设,改用柯西色散公式

n(λ)=A+Bλ2+Cλ4n(\lambda) = A + \frac{B}{\lambda^2} + \frac{C}{\lambda^4}

正常透明介质的折射率随波长增加而缓慢下降,柯西公式是它的一阶近似。代码里取 A=1.458A = 1.458B=0.00354 μm2B = 0.00354\ \mathrm{\mu m^2}C=0C = 0,适用范围 0.4~2.0 μm,超范围回落到默认值 1.5。

(先按下不表:这套系数到底是哪种材料的,后文「反思」一节会把它变成一个大型「案发现场」。)

干涉级次怎么定:峰值检测 + 傅里叶交叉验证

公式 d=m/(2ncosθν)d = m / (2 n \cos\theta \cdot \nu) 里还剩一个未知的 mm。我的方案是「先测条纹周期,再反推级次」:

  1. 对反射率序列用 scipy.signal.find_peaksdistance=5, prominence=0.01)找峰,相邻峰波数差的中位数作为主周期
  2. 再对整条反射率曲线做 FFT,取频谱主峰对应的周期做交叉验证——两个来源的周期如果吻合,说明条纹周期测得可靠;
  3. 由周期换算干涉级次 mm

这一步的物理直觉是:条纹周期 Δν\Delta\nu 与厚度的关系是刚性的(Δν=1/(2n1dcosθ1)\Delta\nu = 1/(2 n_1 d \cos\theta_1)),而单个峰的绝对级次是柔性的(依赖从哪个峰开始数)。所以「周期定厚度、级次只定基准」比「逐峰数级次」稳健得多。

分偏振的菲涅尔反演

入射角非 0 时,反射率依赖偏振态。我们对三种场景分别建模:

垂直入射(菲涅尔公式的特例):

R=(n1n2(λ)n1+n2(λ))2n1=n2(λ)1+R1RR = \left(\frac{n_1 - n_2(\lambda)}{n_1 + n_2(\lambda)}\right)^2 \quad\Longrightarrow\quad n_1 = n_2(\lambda)\,\frac{1 + \sqrt{R}}{1 - \sqrt{R}}

斜入射 s 偏振 / p 偏振(菲涅尔方程):

rs=n1cosθ1n2cosθ2n1cosθ1+n2cosθ2,rp=n2cosθ1n1cosθ2n2cosθ1+n1cosθ2r_s = \frac{n_1\cos\theta_1 - n_2\cos\theta_2}{n_1\cos\theta_1 + n_2\cos\theta_2}, \qquad r_p = \frac{n_2\cos\theta_1 - n_1\cos\theta_2}{n_2\cos\theta_1 + n_1\cos\theta_2}

斜入射时方程里 n1n_1 同时出现在两边(通过 θ2\theta_2 耦合),没有闭式解,我用不动点迭代:先用垂直入射的结果做初值,迭代 10 次、收敛阈值 10610^{-6},同时用 MIN_SINR / MAX_SINR 钳位防止全反射附近的数值爆炸。

最后统一代入厚度反演公式:

e=mλ2n2(λ)cosθ2e = \frac{m \lambda}{2 n_2(\lambda) \cos\theta_2}

代码讲解:Q2_10degree.py

问题二的主脚本约 700 行(10° 与 15° 各一份),骨架如下。

数据清洗与读取——把所有「物理上不可能」的值拦在门外:

def load_and_clean_data(file_path):
    df = pd.read_excel(file_path, sheet_name="Sheet1")
    # 反射率:NaN→1%,负→0.0001,≥100→99.99
    df["反射率_(%)"] = df["反射率_(%)"].fillna(DEFAULT_REFLECTANCE * 100)
    df["反射率_(%)"] = df["反射率_(%)"].apply(
        lambda x: 0.0001 if x < 0 else (99.99 if x >= 100 else x))
    df["反射率_R(小数)"] = df["反射率_(%)"] / 100
    # 波数≤0→默认值;波长(cm)=1/波数,再转 μm
    df["波长_λ(μm)"] = (1 / df["波数_q(cm-1)"]) * 1e4
    return df

柯西色散折射率——带适用范围守卫与物理钳位:

def calculate_n2_cauchy(lambda_μm):
    if not (lambda_min <= lambda_μm <= lambda_max):   # 超出适用范围
        return DEFAULT_N2
    n2 = cauchy_A + cauchy_B / lambda_μm**2 + cauchy_C / lambda_μm**4
    return max(1.0001, n2)                            # 物理合理性:n>1

斜入射的不动点迭代——把「解方程」变成「猜—算—再猜」:

def calculate_n1_polarized(R_polar, lambda_μm, i_rad, is_s_polar=True):
    n2 = calculate_n2_cauchy(lambda_μm)
    sqrt_R = np.sqrt(max(0.0001, min(R_polar, 0.9999)))
    cosi = np.cos(i_rad)
    n1_prev = calculate_n1_vertical(R_polar, lambda_μm)[0]  # 垂直入射解作初值
    for _ in range(iter_max):                                 # 最多迭代 10 次
        sinr = (n1_prev * np.sin(i_rad)) / n2
        sinr = max(MIN_SINR, min(sinr, MAX_SINR))             # 防全反射
        cosr = np.sqrt(1 - sinr ** 2)
        denominator = (cosi - sqrt_R * cosr) if is_s_polar else (sqrt_R * cosi + cosr)
        n1_curr = n2 * (sqrt_R * cosr + cosi) / denominator if is_s_polar \
             else n2 * (cosi + sqrt_R * cosr) / denominator
        if abs(n1_curr - n1_prev) < iter_tol:                 # 收敛即停
            break
        n1_prev = n1_curr
    return max(n1_min, min(n1_prev, n1_max)), n2

条纹周期检测与级次估计——峰值中位数周期 + FFT 交叉验证:

def detect_interference_period(df, wave_col="波数_q(cm-1)", reflectance_col="反射率_R(小数)"):
    q = df.sort_values(by=wave_col)[wave_col].values
    R = df.sort_values(by=wave_col)[reflectance_col].values
    peak_indices = signal.find_peaks(R, distance=5, prominence=0.01)[0]
    main_period = np.median(np.diff(q[peak_indices]))   # 中位数抗异常峰
    # FFT 交叉验证主周期
    freq = fftfreq(len(q), d=np.mean(np.diff(q)))
    fft_period = 1 / freq[freq > 0][np.argmax(np.abs(fft(R))[freq > 0])]
    print(f"峰值检测周期:{main_period:.2f},傅里叶验证周期:{fft_period:.2f}")
    return main_period, q[peak_indices]

这个函数是我在问题二里最喜欢的一段:用两种独立的方法测同一个周期,中位数 + FFT 各自抗不同的噪声模式,互相印证。遗憾的是它当时只负责「打印验证」,结果没有真正反馈到级次决策里(这个后文再吐槽)。

结果与可靠性分析

分「垂直入射 / s 偏振 / p 偏振」三种场景计算,部分结果(10° 入射角):

波数 (cm⁻¹)垂直入射 e (μm)s 偏振 e (μm)p 偏振 e (μm)
399.67478.34018.47428.4688
400.15698.33018.97828.4586
400.63908.32008.94048.4484
406.42448.20168.68668.3281

可靠性从三个角度验证:

检验维度指标结果
一致性同一晶圆 10°/15° 结果偏差≤ 3.75%
残差分析10° 残差均值 / 标准差−0.027664 / 0.027341
残差分析15° 残差均值 / 标准差−0.056438 / 0.048978
拟合优度垂直入射 R²≥ 0.968

结论:多角度结果高度吻合、残差标准差接近 0、拟合优度达标,垂直入射厚度均值约 8.27 μm。当时我们对这个数字相当满意。

入射角 10°:外延层计算结果可视化(垂直入射 / s 偏振 / p 偏振三场景)
入射角 10°:外延层计算结果可视化(垂直入射 / s 偏振 / p 偏振三场景)
入射角 15°:外延层计算结果可视化(垂直入射 / s 偏振 / p 偏振三场景)
入射角 15°:外延层计算结果可视化(垂直入射 / s 偏振 / p 偏振三场景)

(上面两幅是当时的输出图:厚度随波数的分布、三种入射场景的对比。当时觉得「曲线平滑、结果收敛」就是好结果;一年后我知道这张图里其实藏着本文最重要的疑点——厚度随波数系统性漂移,而物理上厚度应该是个常数。伏笔,「反思」一节见。)

问题三:多光束干涉的必要条件、判断与修正

必要条件的推导

问题一的「双光束」是理想化:光在薄膜两个界面间其实会反复弹射,每次弹到下界面就漏一份光出来,产生一整列透射/反射光束。它们相干叠加,就是多光束干涉。

产生可观测多光束干涉的必要条件,我们分两层表述:

相干条件(对光的要求)——各束光频率相同、振动方向相同、相位差恒定。同一束入射光分振幅而来,前两条天然满足;相位差恒定要求薄膜厚度与折射率在光斑范围内稳定,即界面平行度好、材料均匀

强度条件(对界面的要求)——如果界面反射率太低,第二次反射的光强就衰减到噪声水平以下,「多光束」退化回「双光束」。定量地说,相邻两次反射振幅衰减因子是反射系数 rr,高反射率界面(重掺杂衬底在红外波段的等离子体反射正是如此)才能让十几束光都有存在感。

相邻透射光束的相位差(含两次穿越薄膜的几何光程):

δ=4πλnhcosθ\delta = \frac{4\pi}{\lambda}\, n h \cos\theta

NN 束振幅按等比数列衰减、相位按 δ\delta 递增的复数求和起来,透射光强就是经典的 Airy 公式

IT=I0(1ρ)2(1ρ)2+4ρsin2(δ/2),ρ 为界面反射比I_T = I_0 \cdot \frac{(1-\rho)^2}{(1-\rho)^2 + 4\rho \sin^2(\delta/2)}, \qquad \rho \text{ 为界面反射比}

多光束干涉的「指纹」是细锐的条纹ρ\rho 越大,条纹越窄越深(法布里–珀罗干涉仪就是这个原理做到极致的产物)。

附件 3/4 出现多光束干涉了吗

判断依据就是上面的「指纹」。看硅晶圆的光谱(下图):反射率在 405 cm⁻¹ 附近冲到 78%(15° 时 89%),谷底却压到 1% 以下,振荡又深又宽又平滑——这是典型的高反射率界面 + 高精细度的多光束干涉形貌。反观碳化硅数据(问题二),振荡幅度小、叠在一个缓慢的背景上,更接近双光束近似。所以结论:硅晶圆(附件 3/4)出现了明显的多光束干涉,碳化硅数据中该效应微弱

附件 3 与附件 4 的反射率随波数变化:深而宽的振荡是多光束干涉的指纹
附件 3 与附件 4 的反射率随波数变化:深而宽的振荡是多光束干涉的指纹

物理上这也说得通:测试所用硅晶圆的衬底重掺杂,自由载流子使衬底在红外波段呈现类金属的高反射,界面反射比高,多光束效应显著;而碳化硅外延层与衬底同为 SiC、折射率接近(2.55 vs 2.68),界面反射弱,双光束就是很好的近似。

硅外延层厚度:波数差法

对多光束干涉的条纹,相邻峰对应的光程差相差恰好一个波长,于是有非常干净的波数差法

2nd=1ν11ν2=ν2ν1ν1ν2    Δνν2d12nΔν2 n d = \frac{1}{\nu_1} - \frac{1}{\nu_2} = \frac{\nu_2 - \nu_1}{\nu_1 \nu_2} \;\approx\; \frac{\Delta\nu}{\nu^2} \quad\Longrightarrow\quad d \approx \frac{1}{2 n \Delta\nu}

硅取 n=3.42n = 3.42,对相邻峰的波数差逐对计算厚度再取平均。这段代码是全比赛我们写得最快乐的 20 行——问题二折腾了七百行,这里四两拨千斤:

def calculate_thickness(df):
    n = 3.42                                   # 硅的红外折射率
    peaks, _ = find_peaks(df['反射率 (%)'].values)
    peak_wave_numbers = df['波数 (cm-1)'].values[peaks]
    diff_wave_numbers = np.diff(peak_wave_numbers)      # 相邻峰波数差
    thickness = 1 / (2 * n * diff_wave_numbers)         # d = 1/(2n·Δν)
    return np.mean(thickness) * 1e4                     # cm → μm

论文报告的结果:10° 入射角下平均厚度 1.50 μm(标准差 0.016 μm),15° 下 1.52 μm(标准差 0.018 μm),两角度一致性很好。

这边还有一条峰值波长法的支线:先 Savitzky–Golay 滤波(窗口 5、二阶多项式)去噪,再 find_peaks(height≥10, distance≥15, prominence≥1) 找峰,对每个峰按 (k0.5)λ/2n(k-0.5)\lambda / 2n 赋级次算厚度,取中位数。数据处理上用了 IQR 准则剔异常值(低于 QL1.5IQRQ_L - 1.5\,\mathrm{IQR} 或高于 QU+1.5IQRQ_U + 1.5\,\mathrm{IQR} 的反射率视为离群点):

q1, q3 = df_temp['反射率 (%)'].quantile([0.25, 0.75])   # IQR 异常值过滤
iqr = q3 - q1
df_valid = df_temp[(df_temp['反射率 (%)'] >= q1 - 1.5*iqr)
                 & (df_temp['反射率 (%)'] <= q3 + 1.5*iqr)]

reflectance_smoothed = savgol_filter(reflectance, window_length=5, polyorder=2)
peak_indices, _ = find_peaks(x=reflectance_smoothed,
                             height=min_reflectance,   # 默认 10%
                             distance=min_peak_distance,  # 默认 15
                             prominence=1)

def calculate_thickness_v2(lambda_μm, n=1.5, k=1):
    thickness_μm = (k - 0.5) * lambda_μm / (2 * n)     # 半波损失 → k-0.5
    return thickness_μm if 0.01 <= thickness_μm <= 10 else np.nan
峰值法厚度结果:反射率峰值与厚度分布分析
峰值法厚度结果:反射率峰值与厚度分布分析
全波长厚度结果:各波长点的厚度分布
全波长厚度结果:各波长点的厚度分布

多光束干涉模拟:把「弹跳的光」画出来

为了验证多光束理论模型本身,我们写了 Q5.py 做正向模拟:给定折射率 nn、厚度 hh、波长,把 N=15N=15 束透射光的振幅全部算出来相干叠加。菲涅尔系数手工实现:

def Rs(i, r): return -np.sin(i - r) / np.sin(i + r)      # s 波反射系数
def Rp(i, r): return np.tan(i - r) / np.tan(i + r)      # p 波反射系数
def Ts(i, r): return 2*np.sin(r)*np.cos(i) / np.sin(i + r)      # s 波透射系数
def Tp(i, r): return 2*np.sin(r)*np.cos(i) / (np.sin(i+r)*np.cos(i-r))  # p 波

def calculate_interference(N, lambda_, n, h, Ai, a, theta_max, delta_theta, fixed_angle):
    i1 = np.arange(-theta_max, theta_max + delta_theta, delta_theta)
    r1 = np.arcsin(np.sin(i1) / n)                 # 折射角
    delta = 4 * np.pi / lambda_ * n * h * np.cos(r1)   # 相邻光束相位差
    As = Asi * Ts(i1, r1) * Ts(r1, i1)             # 各级透射振幅
    Ap = Api * Tp(i1, r1) * Tp(r1, i1)
    # 向量化:用幂矩阵代替循环,rs^(2k)·e^(ikδ) 一步到位
    powers = np.arange(N)
    rs_pows = Rs(r1, i1)[:, np.newaxis] ** (2 * powers)
    rp_pows = Rp(r1, i1)[:, np.newaxis] ** (2 * powers)
    exp_terms = np.exp(1j * delta)[:, np.newaxis] ** powers
    It_1 = np.abs((As[:, np.newaxis] * rs_pows * exp_terms).sum(axis=1)) ** 2 \
         + np.abs((Ap[:, np.newaxis] * rp_pows * exp_terms).sum(axis=1)) ** 2
    # 教材 Airy 公式作对照(不区分偏振)
    It_2 = (1-p)**2 / ((1-p)**2 + 4*p*np.sin(delta/2)**2) * Ai**2
    return i1, It_1, It_2, ...

两个实现细节值得记一笔:

  • 向量化求和:各级光束的衰减因子 r2(k1)r^{2(k-1)} 与相位因子 ei(k1)δe^{\mathrm{i}(k-1)\delta} 都是等比数列,用 numpy 的广播幂运算拼成矩阵一次求和,比写 15 次循环快一个量级,代码也干净;
  • 双轨验证:分 s/p 偏振严格求和(It_1)与教材上不区分偏振的 Airy 公式(It_2)同时计算、画在同一张图上对比——两条曲线基本重合,说明「不区分偏振」的简化在我们的参数下是安全的。
固定入射角 10° 的薄膜多光束干涉模拟:透射光强分布、各级透射光振幅方向、3D 光强表面
固定入射角 10° 的薄膜多光束干涉模拟:透射光强分布、各级透射光振幅方向、3D 光强表面
固定入射角 15° 的薄膜多光束干涉模拟:透射光强分布、各级透射光振幅方向、3D 光强表面
固定入射角 15° 的薄膜多光束干涉模拟:透射光强分布、各级透射光振幅方向、3D 光强表面

回头修正碳化硅的结果

最后一问:碳化硅数据里那「微弱」的多光束干涉要不要修?我们的回答是要——用 Airy 模型对照模拟表明,多光束效应会使双光束模型算出的条纹位置有轻微偏移;扣除该偏移后,修正结果与原双光束结果的偏差在 2% 以内。换句话说:对附件 1/2 而言,双光束近似已经是够好的近似,多光束修正锦上添花。

(「反思」一节会对这段「修正」做一次不留情面的自我解剖——它其实是全文里「看起来最像修正、实际上最没落地」的部分。)

全题结果一览

问题对象核心方法结果
问题一理论模型双光束干涉 + 灵敏度分析d=k/(2n1cosθ1ν)d = k / (2 n_1 \cos\theta_1\, \nu);示例参数下 d1.896d \approx 1.896 μm
问题二附件 1/2(SiC)ALS 基线校正 + 柯西色散 + 菲涅尔反演垂直入射均值约 8.27 μm;一致性偏差 ≤ 3.75%,R² ≥ 0.968
问题三附件 3/4(Si)波数差法(n=3.42n = 3.4210°:1.50 μm(σ = 0.016);15°:1.52 μm(σ = 0.018)
问题三附件 3/4(Si)峰值波长法 + Airy 模拟验证多光束干涉显著;与波数差法互证
问题三附件 1/2(SiC)多光束修正修正后与双光束偏差 ≤ 2%
flowchart LR
    A["原始光谱"] --> B["清洗 + ALS 基线校正"]
    B --> C["柯西色散<br/>动态折射率"]
    C --> D["峰值检测 + FFT<br/>估计干涉级次"]
    D --> E["菲涅尔反演<br/>垂直 / s / p"]
    E --> F["厚度求解"]
    F --> G["一致性 · 残差 · R²"]
    G --> H["多光束修正<br/>Airy 对照"]

反思:一年之后,我想按住当时那只手

这是全文我写得最认真的部分。比赛成绩出来后,论文成了「过去时」;但复盘的价值恰恰在于把漂亮结论掀开,看看底下垫着什么。下面每一条,我都尽量给出当时的现场逻辑、以及一年后的诊断。

假设清单和实际实现,是两篇不同的论文

论文「模型假设」第 2 条写着:「碳化硅外延层折射率为常数 2.55,忽略载流子浓度与波长的影响」

而实际的代码:问题一默认 n1=2.65n_1 = 2.65;问题二用柯西色散动态计算折射率(约 1.46~1.5),与「常数」假设直接矛盾;同一篇论文里 2.55、2.65、1.5、1.46、3.42 五个折射率各司其职。

诊断:假设清单是写作时才补出来的,建模阶段没人看过它——比赛后期的典型症状,先有结果后补假设,谁也不认识谁。假设本该是建模的起点和约束,最后却成了论文的装饰品。

如果重来:开题第一小时就冻结符号表和参数表,假设清单里每一条后面直接标注「在代码何处生效」。假设变了,清单跟着变,而不是代码先行、清单追认。

材料参数张冠李戴:柯西系数与折射率的「案发现场」

这是全文最实质的物理错误,现在我才发现:

其一,问题二柯西色散用的系数 A=1.458A = 1.458B=0.00354 μm2B = 0.00354\ \mathrm{\mu m^2}——这是**熔融石英(SiO₂)**的经典柯西系数,代码注释里甚至写着「SiO₂ 在 0.4–2.0 μm 范围可忽略」。用它算出来的「外延层折射率」约 1.46,而碳化硅在红外波段的折射率约为 2.55~2.65。相当于给一块碳化硅贴上了玻璃的身份证。

其二,问题三的峰值波长法里 calculate_thickness_v2(lambda, n=1.5, k=1) ——默认折射率 1.5 又是玻璃;同一个小节里,波数差法用的却是硅的正确折射率 3.42。同一个问题、同一份论文、两种折射率并存

诊断:厚度与折射率成反比(d1/nd \propto 1/n),折射率错一个倍数,厚度就错一个倍数。参数来源没有溯源机制,「能跑出数」掩盖了「数从哪来」。

如果重来:建立一张「材料参数溯源表」:每个常数 → 来源(文献/标准/估算)→ 适用条件。凡是找不到 SiC 专属数据的(红外波段 SiC 色散其实有文献值),宁可做参数扫描给出区间,也不拿别的材料的系数硬顶。

最戏剧性的一幕:两个错误几乎完美抵消

复盘时我重新跑了附件 1/2 的数据(滑动平均去基线后检测条纹峰):

  • 碳化硅光谱高波数区的条纹间距中位数约为 Δν ≈ 232 cm⁻¹,两个入射角一致;
  • 代入 d=1/(2nΔν)d = 1/(2 n \Delta\nu),取 SiC 合理折射率 n=2.55-2.60n = 2.55\text{-}2.60,得 d ≈ 8.3~8.5 μm

论文报告的垂直入射均值是 8.27 μm——和稳健估计几乎一致。但实际上还是有问题:

当时厚度公式的实际代入是 d=mλ/(2n柯西cosθ)d = m\lambda / (2 n_{\text{柯西}}\cos\theta),其中 n柯西1.5n_{\text{柯西}} \approx 1.5(SiO₂ 系数),且在谱段左端(ν400 cm1\nu \approx 400\ \mathrm{cm^{-1}})锚定 m=1m = 1。而「真实」的干涉级次应该是

mtrue(400 cm1)=2ntruedtrueν2×2.6×8.4×104×4001.75m_{\text{true}}(400\ \mathrm{cm^{-1}}) = 2 n_{\text{true}} d_{\text{true}} \nu \approx 2 \times 2.6 \times 8.4 \times 10^{-4} \times 400 \approx 1.75

于是:

d论文d真实=musedmtruentruenused=11.75×2.61.50.99\frac{d_{\text{论文}}}{d_{\text{真实}}} = \frac{m_{\text{used}}}{m_{\text{true}}} \cdot \frac{n_{\text{true}}}{n_{\text{used}}} = \frac{1}{1.75} \times \frac{2.6}{1.5} \approx 0.99

级次低估的 1.75 倍,恰好被折射率低估的 1.73 倍抵消了m/nm/n 本来就是简并的(前文分析过),我们等于是在错误的一组 (m,n)(m, n) 上拟合出了接近正确的 m/nm/n 比值。最终数字好看,纯属参数简并送给我们的巧合——如果真实厚度是 6 μm 或 12 μm,这条流水线会给出面目全非的结果而我们毫无察觉。

诊断一个模型给出「看起来合理」的数字,不构成它正确的证据。参数简并的模型里,错误可以互相伪装。

如果重来:凡是 (m,n)(m, n) 这类简并参数,必须做敏感性/退化性检查:固定 nnmm,画出「解空间」,明确告诉读者「绝对厚度完全依赖折射率的先验准确性」。同时用 GB/T 42905-2023 的 FFT 方法作为独立对照——两个方法原理不同的算法给出一致结果,才敢说「可靠」。

硅厚度的 1.50 μm:一个无法复现的结果

同样在复盘时,我重新处理了附件 3/4:对光谱做滑动平均平滑后,用突出度条件筛掉噪声峰,只找到 3~4 个可靠的宽峰(约 405、750、1105、1519 cm⁻¹,相邻间距约 345~410 cm⁻¹)。代入波数差法(n=3.42n = 3.42):

d12×3.42×3554.1 μmd \approx \frac{1}{2 \times 3.42 \times 355} \approx 4.1\ \mathrm{\mu m}

——和论文里的 1.50 μm 对不上。问题出在峰的挑选上:原始数据里纯粹由噪声造成的局部极大值有一千多个(相邻「峰」间距仅 1 cm⁻¹ 量级),find_peaks 不加筛选时会被它们淹没;而当时峰值法的参数组合(限制波段、height≥10distance=15prominence=1)恰好筛出一小撮峰,再配上经验性的级次赋值(k=3,2,1k = 3, 2, 1),得到了 1.5 μm 附近的结果。

诊断:峰值检测类算法的结果强烈依赖滤波窗口、突出度阈值、距离参数、波段范围这组超参数,而当时没有任何一组合适性论证。1.50 与 1.52 的「两角度一致」给了我们虚假的信心——两份数据的噪声结构相似,同一套超参数自然给出相似的错误。

如果重来:条纹分析改用频域方法(对反射率做 FFT,主频即条纹频率),它对单个噪声峰天然免疫;峰值法只作辅助交叉验证。所有超参数做 ±30% 扰动的稳健性检验,结果随参数剧变时,诚实地报告「不稳定」而不是挑一组好看的。

「修正了多光束干涉」——其实没有

论文摘要写「修正碳化硅数据中微弱多光束干涉影响后,与原双光束模型偏差 ≤ 2%」,正文也有「多光束干涉使计算误差较双光束模型降 25%–30%」这样的表述。拆开看:

  • Q5.py 的模拟参数是 n=10n = 10h=25000 nmh = 25000\ \mathrm{nm}λ=500 nm\lambda = 500\ \mathrm{nm}——一套演示性参数,与碳化硅实测体系(n2.6n \approx 2.6d8 μmd \approx 8\ \mathrm{\mu m}、红外波段)完全无关。它只能验证「多光束叠加公式实现正确」这一层;「附件 1/2 里的多光束效应已被消除」,从未被验证过;
  • 真正的「消除影响」应该是:用 Airy 反射率模型(含正确参数)正向拟合附件 1/2 的光谱,与双光束模型的拟合结果比较厚度差异——这一步从未发生;
  • 「25%–30%」「12%–15%」这些百分比,没有可复现的对照实验流程支撑。

诊断:赶 deadline 的写作里,「模拟验证了理论」很容易被悄悄升格成「数据完成了修正」。读者(和评审)看到的结论动词,比实际做的事情强了一个等级。

如果重来:每个定量结论后面强制挂一个「证据指针」:哪个脚本、哪个输出文件、哪两张图的对比支撑了它。没有证据链的数字,一律从摘要里删掉。

被题目剧透了

题目第一段就写明:「外延层与衬底因掺杂载流子浓度的不同而有不同的折射率」。这其实是在提示自由载流子的 Drude 色散模型

n2(λ)=εNe2λ24π2ε0mc2n^2(\lambda) = \varepsilon_\infty - \frac{N e^2 \lambda^2}{4\pi^2 \varepsilon_0 m^* c^2}

重掺杂衬底的折射率在红外波段会随波长显著变化(这正是硅衬底呈现「类金属」高反射、产生强多光束干涉的原因,也是 SiC/SiC 界面反射率随掺杂变化的定量来源)。我们的模型从头到尾用「柯西色散 + 查表常数」糊弄了过去,等于把题目埋的这条物理主线整段跳过。

同样被简化的还有:SiC 在 794~970 cm⁻¹ 的强声子吸收带(Reststrahlen 带——附件 1/2 在这个区间的反射率剧烈摆动,0.8% 到 95%,我们只是当作「异常区」绕开了,没有解释)、表面粗糙度散射、有限光斑与相干长度。这些每一个都可以是模型改进的小节,当时全部让位于时间压力。

诊断:题干里每个看似背景介绍的句子,都可能是评分点。「因载流子浓度不同」这半句话,是这道题物理深度的钥匙,我们从它旁边走过而没有推门。

如果重来:开题时把题干逐句编号,每句标注「信息 / 提示 / 要求」。被标记为「提示」的句子,必须在论文里有正面回应(哪怕只是敏感性讨论)。

工程债:复制粘贴出来的四个脚本

Q2_10degree.pyQ2_15degree.pyQ3_10degree.pyQ3_15degree.py 四个文件,两两之间 90% 相同,差异只是入射角和文件名;魔法常数(MIN_SINR = 0.0001m_default = 1distance = 15……)散落在各处;单位在 cm / μm / nm 之间反复横跳,每次换算都是一次开错方次的风险。

还有一个今天看来很可惜的细节:detect_interference_period 里 FFT 验证周期算出来了,却只是 print 出来给人类看,没有参与任何自动决策——写了正确的零件,没有把它装进机器

诊断:短期看复制粘贴最快;但它让「改一个参数」变成「改四个文件」,也让复盘时的代码考古变得痛苦。三天赛制不是拒绝工程的理由,恰恰是工程的理由。

如果重来:单一入口 + 配置驱动:solve(attachment=1, angle=10, method="vertical"),四份脚本变成四行配置。FFT 验证直接驱动级次选择,构成「检测 → 验证 → 决策」闭环。单位统一在读取层归一化(全部转 cm),只在展示层格式化。

反思汇总

#问题一句话诊断重来的做法
1假设与实现脱节假设清单是补写的装饰品参数表与假设同步冻结,逐条挂代码指针
2材料参数错配SiO₂ 柯西系数算 SiC,n=1.5 算 Si参数溯源表,无源参数只做区间扫描
3简并参数的侥幸m 与 n 的错误恰好抵消出「正确」数字解空间扫描 + 独立方法(FFT/国标)对照
4峰值挑选敏感1.50 μm 无法稳健复现频域主方法 + 超参数扰动稳健性检验
5修正未落地演示模拟被写成「消除了影响」结论-证据链强制挂钩
6错过物理主线Drude 色散被题干剧透仍错过题干逐句编号,「提示」必回应
7工程债四份复制脚本 + 魔法常数配置驱动单入口,FFT 进决策环

结语

认真数一数,这篇复盘里「如果重来」出现了七次。但让我意外的是,重读论文时最先想起的竟是第一晚白板前那四十分钟的争论——我们用光路图互相说服,最后一起写出统一公式 d=k/(2n1cosθ1ν)d = k/(2 n_1 \cos\theta_1\, \nu) 的那个瞬间。那个公式只有五行推导;但「先把物理吵清楚、再让代码服从物理」的工作方式,是这场比赛真正留在身上的东西。

数学建模教会我的,大概是这么一种诚实:模型永远是对真实世界的粗糙近似。三天里我们造了一个能自圆其说的世界,一年后拆开看,有的梁是运气撑住的。而发现「运气撑住的梁」并把它换掉的过程,和当年搭建它一样有乐趣——所以这篇复盘大概还会续写:等哪天我把 Drude 色散补进模型、用 FFT 方法重算出那块晶圆的厚度,再来更新结论。

愿下一束入射的红外光,往返的都是被理解的道路。(´。• ᵕ •。`)

论文与支撑材料清单
  • 论文:《基于薄膜干涉模型与灵敏度模型确定碳化硅外延层厚度》(2025 年高教社杯全国大学生数学建模竞赛 B 题提交论文)
  • 代码:Q1.py(双光束模型与灵敏度)、Q2_10degree.py / Q2_15degree.py(碳化硅厚度反演)、Q3_分析附件3和附件4.py(波数差法)、Q3_10degree.py / Q3_15degree.py(峰值法)、Q5.py(多光束干涉模拟)、test.mlx(ALS 基线校正)
  • 数据:附件 1-4(波数—反射率光谱,各 7469 点,400~4000 cm⁻¹)
  • 参考文献:GB/T 42905-2023《碳化硅外延层厚度的测试 红外反射法》等