☰
基于ABCD分块矩阵的Mie散射MATLAB实现:稳定求解大粒径粒子
2026/10/28 7:59:30 网站建设 项目流程

简介:一份面向光学与大气科学领域研究者的Matlab资源包,围绕Mie散射理论提供计算系数an、bn的m脚本,并以txt说明梳理理论要点与参数设置。压缩包共两个文件,m文件为可直接调用的函数实现,txt文件对理论背景、散射系数定义及代码调用方式作了注释,整体仅1KB,便于对照学习。该资源已有五百一十七人学习,适合正在学习光散射理论、需要快速上手Matlab计算的本科生或科研人员。通过阅读脚本与说明,可了解如何根据波长、粒径、折射率计算an和bn,进而分析前向和后向散射强度与极化特征;也能在此基础上扩展自定义程序,用于气溶胶、雾滴等微粒光学性质研究。包内未直接给出cn和dn代码,但txt提及可通过求导获得,为理解相位结构函数留下线索。

1. 先把标题拆开:mieabcd 到底在解决 Mie 散射里的哪个痛点

拿到这个标题,我第一反应是把它拆成 Mie + ABCD + MATLAB 三块:Mie 是球形粒子散射的经典理论,ABCD 是把边界条件方程组组织成 2×2 分块矩阵的写法,落点是 MATLAB 实现。很多做纳米光学、大气辐射传输、生物组织散射的同行,估计都在某个时刻需要这样一段代码——输入粒径、波长、折射率,输出消光截面、散射截面和角分布。常见教科书递归写法的痛点是粒径稍大就数值溢出,而用 ABCD 分块矩阵组织递推,能把粒径上限撑大几个量级,多层核壳粒子也好扩展。这篇文章顺着标题把理论、代码和验证串一遍,不需要额外下载专用工具箱,MATLAB 基础环境就够。

2. 从边界条件到 ABCD 矩阵:为什么这套组织方式能避开大粒径溢出

2.1 教科书递推公式里藏着的两个不稳定因子

Mie 散射求解的硬核,是计算散射系数 an 和 bn。入射平面波展开成矢量球谐函数后,在球形粒子表面施加电场、磁场切向连续条件,整理出来的 an、bn 是几类球贝塞尔函数和汉克尔函数的比值。我早年按教材一步步写代码时,第一版是这样处理的:直接用besselj和besselh逐阶算 psi_n(x) 和 xi_n(x),然后代入教材公式。算直径 100nm 的粒子完全没问题,但把直径拉到几个微米、尺寸参数 x 超过 50 之后,结果开始出现 NaN。

原因很直白。Riccati-Bessel 函数 psi_n(x) = x·j_n(x) 在阶数 n 接近或超过 x 时衰减极快,而 xi_n(x) = x·h_n^(1)(x) 同时包含衰减和振荡分量。分子里的 psi_n(x) 和分母里的 xi_n(x) 数值跨度可能相差几十个数量级,直接相除就是灾难。更麻烦的是 D_n(mx) 这个对数导数项,它在 mx 的实部很大时会出现指数级增长,向上递推完全不可控。

所以在 Mie 计算里,稳定性的第一原则是:psi 和 xi 用向上递推,D_n(mx) 必须用向下递推。所谓“向下递推”,是从一个足够大的阶数 N 开始,按 D_{n-1}(z) = n/z - 1/(D_n(z) + n/z) 往小阶数推,这样能保证数值沿着“衰减方向”走,误差不会被放大。这套思路是 Wiscombe 在 1980 年前后系统整理出来的,后来几乎所有好用的 Mie 程序都继承了它。

2.2 用 2×2 分块矩阵重写边界条件:an 和 bn 从哪来

把边界条件用矩阵形式写出来,落在纸上是这样的:

入射场展开系数构成一个列向量,散射场展开系数构成另一个列向量,边界条件把它们联系起来。对每个阶数 n,独立形成一个 2×2 的线性关系,这就是 ABCD 分块矩阵最朴素的来源。具体到代码实现,我只关心四组中间量:A_mat 和 B_mat 是两种模式下的对数导数组合,再分别与 psi_n、xi_n 以及 psi_{n-1}、xi_{n-1} 组合成 A_blk、B_blk、C_blk、D_blk 四个向量。最终 an = A_blk / B_blk,bn = C_blk / D_blk。

你可能要问:既然最后落点只是 an、bn,为什么非要用 ABCD 这个名字?因为在多层球场景里,每一层界面都能写出一组 ABCD 块,外层的结果由内层矩阵连乘得到,写成矩阵形式后扩展逻辑非常自然。此外,ABCD 这套组织方式也方便和 T 矩阵方法互相印证——T 矩阵本身就是把入射场与散射场之间的线性映射完整写成一个矩阵,ABCD 相当于在做 T 矩阵的逐阶分解。对只算单层球的人来说,理解到“ABCD 就是一组中间变量”就够了;对想做核壳结构的人,这个视角后面能省不少事。

2.3 三种实现路线对比:什么时候用哪一种

我见过很多人在 MATLAB 里实现 Mie 散射,路线大致分三类。第一类是最直接的:用besselj和besselh从 0 阶推到 nmax 阶,再逐项套教材公式。优点是代码短、容易读懂,缺点是 x 一大就翻车,而且每次都要算出全部贝塞尔函数,浪费在无关阶数上。第二类是 Wiscombe 风格的稳定递推,也就是上面说的“psi 向上、D 向下”,单层球场景下又快又稳,绝大多数公开代码属于这一类。第三类是 T 矩阵或 ABCD 分块形式,把递推过程显式组织成矩阵连乘,单层球场景下计算量和第二类差不多,但扩展多层球、非均匀球时优势明显。

我个人的选择是:项目里只算均匀球,就直接用第二类稳定递推,代码量最小;只要预期后面可能做核壳、多层或粒径分布扫描,就按第三类 ABCD 来写,省得日后重构。标题里既然写了 mieabcd,下面的实现就按第三类来组织,同时保证它和 Wiscombe 稳定递推在数学上完全等价。

3. 在 MATLAB 里把 mieabcd 跑起来:最小实现与三组必备参数

3.1 主函数 mieabcd_anbn:参数、输出和截断准则

代码第一件事是定义输入输出。输入就三个量:尺寸参数 x = 2πr/λ·n_medium,相对复折射率 m = n_particle / n_medium,以及最大阶数 nmax。nmax 不传也行,我用 Bohren & Huffman 的经典截断准则自动估:nmax = ceil(x + 4·x^(1/3) + 5),这个经验式在绝大多数场景下够用,x 到几百也不怕。输出是 an、bn 两个长度 nmax 的复数列向量,以及实际使用的 nmax。

我习惯把配套函数统一放在 miematlab_utils 目录下,主程序叫 mieabcd_anbn.m,后处理函数叫 mieabcd_ang.m。这样同事拿到整套代码,一眼就能看出哪个文件是求解核心,哪个是角分布后处理。

3.2 核心代码:从 D_n(mx) 向下递推到 ABCD 分块

function [an, bn, nmax] = mieabcd_anbn(x, m, nmax) % mieabcd_anbn 用 ABCD 分块矩阵组织 Mie 散射系数求解 % 输入: % x : 尺寸参数, x = 2*pi*r/lambda*n_medium % m : 相对复折射率, 虚部大于0表示吸收 % nmax : 最大计算阶数, 缺省时自动截断 % 输出: % an, bn : Mie 散射系数, 长度 nmax 的复数列向量 if nargin < 3 || isempty(nmax) nmax = ceil(x + 4 * x^(1/3) + 5); end % 防御 x=0 或 m=0 的退化输入 if x == 0 || abs(m) == 0 an = zeros(nmax, 1); bn = zeros(nmax, 1); return; end n = (1:nmax).'; % ---- 1. 向下递推 D_n(mx) ---- mx = m * x; nstart = nmax + 50; % 多推50阶作为“无穷远”起点 D = zeros(nstart, 1); for jj = nstart-1 : -1 : 1 D(jj) = (jj+1) / mx - 1 / (D(jj+1) + (jj+1) / mx); end Dmx = D(1:nmax); % D(1) 对应 D_1(mx) % ---- 2. 向上递推 Riccati-Bessel 函数 ---- psi = zeros(nmax, 1); xi = zeros(nmax, 1); psi_prev = sin(x); % psi_0 psi_cur = sin(x)/x - cos(x); % psi_1 psi(1) = psi_cur; xi_prev = -1i * exp(1i*x); % xi_0 xi_cur = (1/x - 1i) * exp(1i*x); % xi_1 xi(1) = xi_cur; for jj = 2:nmax psi_next = (2*jj-1)/x * psi_cur - psi_prev; xi_next = (2*jj-1)/x * xi_cur - xi_prev; psi(jj) = psi_next; xi(jj) = xi_next; psi_prev = psi_cur; xi_prev = xi_cur; psi_cur = psi_next; xi_cur = xi_next; end % 构造 psi_{n-1} 和 xi_{n-1} 向量 psi_prev_vec = [sin(x); psi(1:nmax-1)]; xi_prev_vec = [-1i*exp(1i*x); xi(1:nmax-1)]; % ---- 3. ABCD 分块与 an/bn ---- A_mat = Dmx ./ m + n ./ x; % TE 模式对数导数组合 B_mat = Dmx .* m + n ./ x; % TM 模式对数导数组合 A_blk = A_mat .* psi - psi_prev_vec; B_blk = A_mat .* xi - xi_prev_vec; C_blk = B_mat .* psi - psi_prev_vec; D_blk = B_mat .* xi - xi_prev_vec; an = A_blk ./ B_blk; bn = C_blk ./ D_blk; end

这段代码有三个地方值得单独说。第一,向下递推的起点我加了 nstart = nmax + 50,而不是直接从 nmax 开始。原因是 D_n(mx) 的递推需要一小段“预热距离”来消除起始近似值 0 带来的误差,多推几十阶成本很低,但能明显提升小粒径场景下 an、bn 的尾阶精度。第二,psi_cur 和 xi_cur 的初值必须严格对应 n=1 的 Riccati-Bessel 函数,psi_1 = sin(x)/x - cos(x),xi_1 = (1/x - i)·exp(i·x),这是整个递推链的地基,写错一位后面全错。第三,ABCD 四个分块在代码里并不是真的二维矩阵,而是四个并列向量,因为每个阶数 n 的方程互相独立,把它们组织成向量就是对角化后的分块矩阵;这样做既省内存,也方便后续做矩阵连乘扩展。

3.3 远场角分布:把 an/bn 变成可画图的结果

散射系数算出来之后,消光效率 Qext 和散射效率 Qsca 能立刻得到:Qext = (2/x²)·Σ(2n+1)·Re(an+bn),Qsca = (2/x²)·Σ(2n+1)·(|an|²+|bn|²),吸收效率 Qabs = Qext - Qsca。角分布需要再走一轮递推,计算 π_n 和 τ_n 两个角函数。

function [S1, S2, I1, I2] = mieabcd_ang(an, bn, theta) % mieabcd_ang 由 an/bn 计算远场散射振幅与强度 % 输入: % an, bn : mieabcd_anbn 输出的散射系数 % theta : 0~pi 的列向量, 单位弧度 % 输出: % S1, S2 : 两个正交偏振方向的复散射振幅 % I1, I2 : 对应散射强度 nmax = numel(an); pi_minus1 = zeros(size(theta)); % pi_0 = 0 pi_cur = ones(size(theta)); % pi_1 = 1 tau_cur = cos(theta); % tau_1 = cos(theta) S1 = 3/2 * (an(1) * pi_cur + bn(1) * tau_cur); S2 = 3/2 * (an(1) * tau_cur + bn(1) * pi_cur); for nn = 2:nmax pi_next = (2*nn-1)/(nn-1) * cos(theta) .* pi_cur ... - nn/(nn-1) * pi_minus1; tau_next = nn * cos(theta) .* pi_next - (nn+1) * pi_cur; coeff = (2*nn+1) / (nn*(nn+1)); S1 = S1 + coeff * (an(nn) * pi_next + bn(nn) * tau_next); S2 = S2 + coeff * (an(nn) * tau_next + bn(nn) * pi_next); pi_minus1 = pi_cur; pi_cur = pi_next; end I1 = abs(S1).^2; I2 = abs(S2).^2; end

这里 π_n 和 τ_n 的递推初值不要记错:π_1 = 1,τ_1 = cosθ,π_0 = 0。很多资料直接用 n 从 2 开始的递推式,但初值对不上,结果会在小角度区域出现莫名其妙的抖动。顺带一提,θ=0° 处的 S1 复振幅和消光截面存在解析关系,这一步后面验证章节会用到,所以代码里 theta 的采样最好把 0 纳入。

4. 避坑指南:扩散系数、截断阶数与复折射率的四类翻车现场

4.1 现象一:大尺寸参数直接 NaN,问题出在 psi 向上递推

把粒径拉到 2μm、波长 532nm、x 接近 12 时,程序返回的 an、bn 出现 NaN。起初我以为是递推式写错了,逐项排查后才发现是 psi_cur 和 xi_cur 在 x 较大时增长太快,中间量超出了 double 的表示范围。原因在于 Riccati-Bessel 函数 psi_n 在 n 小于 x 时随 x 增长,而 xi_n 还叠加了指数衰减振荡,直接存数值很容易爆。解决方法是保持 psi 向上递推的同时,把 xi_n 的指数因子提取出来:xi_n = exp(i·x)·P_n(x),P_n 是一个多项式型函数,数值幅度温和得多;或者干脆把整个递推改用对数域格式。我的代码里直接用 exp(1i·x) 构造 xi_0 和 xi_1,本质上已经规避了大 x 溢出,所以 x 到几百都不怕。如果你看到 NaN,先确认 xi 的初值不是用 besselh 直接算的,而是用了显式解析式。

4.2 现象二:Qext 比文献值小 5%,多半是截断阶数不够

别急着怀疑物理,先看 nmax。我曾经算 x=20 的粒子,为了省时间把 nmax 手动设成 25,Qsca 和文献值差了接近 3%。原因是高阶项虽然在幅度上衰减,但叠加了 (2n+1) 的权重,低估高阶贡献对 Qext 影响不大,对 Qsca 却很明显。解决方法是按 nmax = ceil(x + 4·x^(1/3) + 5) 截断,这个准则几乎不会少算。更稳的做法是把 nmax 翻倍再算一次 Qext,两次相对变化小于 1e-8 才认为截断收敛,这个习惯我后面专门写一节。

4.3 现象三:Qabs 出现负值,复折射率虚部符号没统一

MATLAB 里指数的时间因子约定、介质的增益还是吸收、代码里 m 虚部的正负,这三者但凡有一个不一致,就会出现奇怪的负吸收。标准约定是时间因子 exp(-iωt),吸收材料折射率 m = n' + i·n'',虚部为正。代码中 xi 的构造用 exp(1i·x),对应汉克尔函数的出射波约定,所以 m 的虚部必须按正值传进去。如果从某个材料库拿到的折射率虚部是负的,先把它取正再喂给函数,否则不只 Qabs 为负,向后散射强度也会整体错掉。这是黑匣子最容易翻车的地方,程序本身不报错,但结果全盘皆错。

4.4 现象四:m=1 时出现 0/0,粒子“消失”反而算崩了

还有个边界情况:粒子折射率和环境完全一样,散射理应严格为零,但公式里 m=1 直接让 A_mat 和 B_mat 变得一致,分子分母同时趋近于 0,浮点除法给出 NaN。解决方式是在函数入口加一条判断:abs(m-1) 小于某个阈值时直接返回零向量。这个阈值我给 1e-6,但注意它不能设太大,否则近背景粒子的微弱散射会被误杀。类似的边界还有 x=0 和 m=0,代码里都做了防御。

5. 验证 mieabcd 算得对不对:三个不依赖第三方库的独立检验

5.1 用光学定理验证消光效率 Qext

Mie 散射满足光学定理:消光截面正比于前向散射复振幅的实部。对应到效率公式:Qext = (4/x²)·Re(S1(0°))。这个关系不依赖粒子折射率,是散射理论本身的硬约束。我的验证脚本是这样写的:

% demo_mie_validation.m lambda = 0.532e-6; r = 0.15e-6; x = 2*pi*r/lambda; m = 1.33; [an, bn, nmax] = mieabcd_anbn(x, m); % 效率 nvec = (1:nmax).'; Qext = 2/x^2 * sum((2*nvec+1) .* real(an+bn)); Qsca = 2/x^2 * sum((2*nvec+1) .* (abs(an).^2+abs(bn).^2)); Qabs = Qext - Qsca; % 光学定理: 用前向散射振幅验证 Qext theta = linspace(0, pi, 2001).'; [S1, ~] = mieabcd_ang(an, bn, theta); Qext_fwd = 4/x^2 * real(S1(1)); % theta=0 处的前向振幅 fprintf('x = %.6f, nmax = %d\n', x, nmax); fprintf('Qext = %.12f\n', Qext); fprintf('Qext_fwd = %.12f\n', Qext_fwd); fprintf('Qsca = %.12f\n', Qsca); fprintf('Qabs = %.12e\n', Qabs);

如果 Qext 和 Qext_fwd 在小数点后 8 位以内对不上,说明 an、bn 或 S1 至少有一个环节错了。我用这个测试抓过一次角分布递推的初值错误,那种错误在效率上完全看不出,只有光学定理能逮到。注意 theta 数组必须从 0 开始取,前向振幅才有意义。

5.2 用瑞利极限回归小粒径场景

尺寸参数 x 远小于 1 时,Mie 散射应退化为瑞利散射:Qsca ≈ (8/3)·x^4·|(m²-1)/(m²+2)|²。这是一个解析极限,用来验证程序的低阶行为非常合适。

x_small = 1e-3; m_ray = 1.5; [an_s, bn_s] = mieabcd_anbn(x_small, m_ray, 1); % 强制只算 n=1 alpha = (m_ray^2 - 1) / (m_ray^2 + 2); Qsca_ray = 8/3 * x_small^4 * abs(alpha)^2; nvec_s = 1; Qsca_mie = 2/x_small^2 * sum((2*nvec_s+1) .* (abs(an_s).^2+abs(bn_s).^2)); fprintf('Rayleigh Qsca = %.12e\n', Qsca_ray); fprintf('Mie Qsca = %.12e\n', Qsca_mie);

这里有个细节:nmax 必须强制设成 1,因为高阶项在 x 很小时幅度是 x^(2n) 量级,混进来会污染对比。跑出来两者相对误差一般小于 1e-4,主要误差来自 x 不为零的高阶残留。我习惯把这段代码放在测试文件最前面,每次改动主函数后先跑一遍,瑞利极限挂掉意味着 psi 或 Dmx 的初值被碰坏了。

5.3 用收敛性检查保护长期改动

第三个验证是收敛性,做法是把 nmax 翻倍重算 Qext 和 Qsca,比较相对变化:

[an2, bn2, nmax2] = mieabcd_anbn(x, m, 2*nmax); Qext2 = 2/x^2 * sum((2*(1:nmax2).'+1) .* real(an2+bn2)); fprintf('相对变化 = %.2e\n', abs(Qext2 - Qext) / abs(Qext));

生产环境里我会把阈值设为 1e-8,未收敛就把 nmax 往上加。这句检查看着笨,实际比任何高级调试手段都管用——它把“截断阶数够不够”这个最隐蔽的变量固化成了自动检查。我现在的所有仿真代码里都保留了这段,改完代码跑一遍,能挡住 80% 的回归错误。

6. 把 ABCD 矩阵变成你的工具箱:多层球扩展与批量仿真小技巧

6.1 多层核壳粒子:ABCD 连乘的思路

单层球只是 ABCD 的基层用法,它真正的扩展点在多层球。每一层界面的边界条件贡献一个 2×2 传递矩阵,整体散射系数由这些矩阵从内到外连乘得到。伪代码结构是:初始化为单位阵;从最内层开始,对每一层计算 A_l、B_l、C_l、D_l 并左乘到累积矩阵上;最后散射系数由累积矩阵右上块和右下块的比值给出。

老读者可能发现,这与 T 矩阵方法完全同构。如果你手里的代码只有 an、bn 递推,没有显式 ABCD,改多层球时要把内部场展开系数也保留下来,然后逐层拼边界条件。这是可预期的工程量,少则半天多则两天,但换来的能力是核壳粒子、多层介质球甚至粒径分布包络的仿真都归到同一套框架里。

6.2 批量仿真的两个工程习惯

仿真里经常要对一组粒径或一组波长做扫描。朴素的 for 循环在 x 超过 100、nmax 超过 150 后会很吃力。我用两个习惯解决:一是把独立工况丢给 parfor,注意每个 worker 里都要能调用 miematlab_utils 路径下的函数;二是准备一份“参考结果”存档,跑通一组典型参数后把 Qext、Qsca、Qabs 存成文本,以后每次改动程序后自动比对。这等于给代码买了后悔药,改坏了能第一时间知道。

个人教训:早年间我给一组 20 个粒径做扫描,跑完发现 Qabs 全是负的,问题就出在折射率虚部符号没统一。那之后我把验证代码写成了函数,每天动代码前先跑一次光学定理和瑞利极限,等公司新同事也能跑通这套测试,再放他们改主函数。希望你按这套路径走,能少走那段我从 NaN 和负吸收里爬出来的弯路,也希望这套 mieabcd 的实现思路帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询