ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

多级散射理论计算随机分布二维柱散射:从公式到可运行的代码

多级散射理论计算随机分布二维柱散射:从公式到可运行的代码 简介采用多级散射理论编写的MATLAB计算程序用于模拟随机分布二维圆柱散射体的反射与透射特性可应用于纳米光学、光子学、声学等方向适合需要分析随机介质散射行为的科研人员与工程用户。程序将散射系统拆解为多级节点网络逐级计算入射、反射与透射概率并可通过统计平均或蒙特卡洛思路处理随机分布柱体带来的散射特性弥补了解析方法难以直接求解复杂随机几何的不足。资源包共1个文件为单个.m脚本压缩包仅约1KB代码体积紧凑便于快速阅读和二次修改。已有173人学习/下载适合具备MATLAB和基础散射理论知识的读者参考。脚本中可看到多级散射模型构建、散射矩阵/格林函数调用逻辑、随机分布统计处理等要点为复现随机介质反射透射计算及扩展参数实验提供了简洁起点。1. 多级散射理论计算随机分布二维柱散射从公式到可跑的代码做电磁散射计算的人十有八九都遇到过这种场景理论书上讲得头头是道一到自己写程序算随机分布介质的反射率和透射率就发现处处是坑。这个标题指向的程序核心是用多级散射理论Multiple Scattering Theory, MST处理二维柱状散射体在随机分布下的反射和透射问题。它解决的不是单根柱子的散射而是成百上千根柱子摆在一起、互相之间反复散射之后宏观上电磁波怎么被反射、怎么透过去——这在光子晶体、随机介质涂层、雷达隐身材料设计里都是刚需。我最早接触这类程序是在做周期性柱阵列的光学特性仿真后来发现实际样品根本不可能完全周期柱子位置总有随机偏差这时候周期模型的误差就大得离谱。换成多级散射理论处理随机分布反而能用统计平均的方式把问题描述清楚。本文要讲的就是这个物理模型怎么落到代码里、关键参数怎么调、以及那些用一次踩一次的血泪坑。2. 先把理论骨架立住多级散射与反射透射的定义2.1 为什么单柱散射不够必须用多级散射二维柱散射的标准解法是解柱坐标下的亥姆霍兹方程入射平面波展开成柱函数级数单根柱子的散射场用散射系数通常叫 T-matrix 或 Mie 系数描述。单柱问题很干净入射波照到柱子上柱子给出一个散射场完事。但随机分布介质不是这么回事。第一根柱子散射出来的波会继续照到第二根柱子上第二根柱子再散射这些二次散射波又会影响第一根柱子——柱与柱之间存在无穷多次相互作用。如果忽略这些相互作用只把每根柱子的散射场简单叠加那算出来的反射和透射在小浓度、弱散射情况下还能凑合一旦浓度上去或者柱子介电常数大误差就是数量级的。多级散射理论的核心思想是把所有柱子的散射场展开成以各自柱心为原点的柱函数级数然后利用柱函数的加法定理Graf 加法定理把一个柱子坐标系下的散射波转换到另一个柱子坐标系下从而把「所有柱子互相作用」这个耦合问题写成一组线性代数方程。解这组方程得到每个柱子实际被激发出的散射系数再对所有柱子的贡献做矢量叠加就能得到总散射场进而算反射和透射。这里有个关键概念要区分清楚标题里的「多级散射」指的是 Multiple Scattering不是 Multipole多极子展开。虽然展开基底都是柱函数但 Multipole 通常指单根非圆截面柱子的展开阶数Multiple Scattering 则是柱子之间的多次相互作用。程序里往往会同时用到这两个概念——每根柱子自身可能需要多阶展开多极子阶数柱子之间又需要多次散射迭代或直接解耦多体相互作用。2.2 反射率与透射率在随机介质里的定义方式周期性结构里反射透射的定义很直观用 Bloch 模式展开每个衍射级次的功率占比就是反射率或透射率。随机介质没有 Bloch 模式常见做法是取一个足够长的计算区域在区域两侧分别设置观察线统计透射波和反射波的能量流。更严格的做法是用光学定理或者散射截面来归一。随机分布介质通常取系综平均生成多组随机分布构型分别计算反射和透射最后对所有构型取平均。这样做的好处是能够消除单个构型里由特定位置关系造成的偶然起伏得到的是具有统计代表性的宏观量。程序里反射和透射的具体数值定义一般是入射波携带的功率流密度为 P_inc在散射体区域前方入射侧放置观察线计算总场减去入射场后向后传播的功率流 P_ref反射率 R P_ref / P_inc在散射体区域后方放置观察线计算透射场向前传播的功率流 P_trans透射率 T P_trans / P_inc。注意这里透射率计算的是总场还是只有透射场不同程序不一样。有的程序直接把后方总场的向前分量算成透射如果介质有吸收R T 会小于 1差值就是吸收率如果介质无吸收且计算区域足够大、截断足够好R T 应该等于 1这个恒等式是检验程序正确性的第一道关卡。2.3 随机分布构型的生成方式程序标题里的「随机分布」具体指什么直接决定后面的计算复杂度。最常见的两种第一种是完全随机投放均匀随机柱子中心在区域内服从均匀分布但需要检查两两之间不能重叠即任意两个柱心距离大于直径。这个方法实现简单但高填充率下效率极低因为随机撒点很容易碰撞需要反复重试。第二种是随机序列吸附Random Sequential Adsorption, RSA逐个放置柱子每次随机生成位置后检查与已放置柱子的重叠重叠就丢弃重试。RSA 能达到的最大填充率大约在 0.5 左右圆盘情况对于更高的填充率需要用其他方法如蒙特卡洛弛豫或分子动力学类方法。程序里一般会提供填充率filling fraction作为输入参数用户设定填充率后程序自动生成满足条件的随机构型。我一般会建议填充率低于 0.3 用 RSA 就够高于 0.3 要检查构型是否真的达到目标填充率因为 RSA 在高填充率下会出现「怎么撒都重叠」的死循环问题。3. 代码落地柱函数展开、加法定理与线性方程组求解3.1 程序主流程拆解这类程序的主流程通常是固定的几个步骤读参数 → 生成随机构型 → 计算单柱 T-matrix → 组装多体耦合矩阵 → 求解线性方程组 → 计算远场反射透射 → 系综平均。下面是一个最小可运行的流程示意用 Python 配合 NumPy 做演示实际生产环境用 Fortran 或 C 提速但逻辑完全一致。import numpy as np import scipy.special as sp # 参数设置 wavelength 1.0 # 入射波长真空 k0 2 * np.pi / wavelength # 自由空间波数 radius 0.2 # 柱子半径 eps_r 2.5 0.0j # 柱子相对介电常数虚部为0表示无损耗 N_col 50 # 柱子总数 filling 0.3 # 目标填充率 n_order 4 # 柱函数展开最大阶数多极子阶数 inc_angle 0.0 # 入射角弧度0度为垂直入射 # 生成随机分布构型RSA Lx np.sqrt(np.pi * radius**2 * N_col / filling) # 计算区域边长 Ly Lx positions [] max_try 10000 for i in range(N_col): for attempt in range(max_try): x np.random.uniform(-Lx/2 radius, Lx/2 - radius) y np.random.uniform(-Ly/2 radius, Ly/2 - radius) if all((x - px)**2 (y - py)**2 (2*radius)**2 for px, py in positions): positions.append((x, y)) break else: raise RuntimeError(fCannot place rod {i}, filling too high) positions np.array(positions)逻辑说明这段代码首先生成计算区域边长 Lx其依据是填充率 柱子总面积 / 区域面积反解出边长。然后逐根柱子随机投位每次检查与已有柱子的中心距是否大于两倍半径即直径防止重叠。max_try 是保护机制防止高填充率下死循环。这里的填充率换算是个容易出错的地方如果填充率定义为柱面积占比那么 N 根柱子的总面积是 N * π * r²除以区域面积 Lx * Ly 就是填充率。参数说明n_order是展开阶数阶数越高精度越好但矩阵越大。对于半径 0.2 波长、介电常数 2.5 的柱子n_order4已经足够。介电常数虚部如果非零说明柱子有损耗后面算能量守恒时 RT 会小于 1这是正常的不要当成 Bug。3.2 单柱 T-matrixBessel 函数与系数矩阵组装每根柱子在局部坐标系下的散射场可以写成E_scat(r, phi) sum_{m-N}^{N} a_m * H_m(k0*r) * exp(i*m*phi)其中 H_m 是第二类 Hankel 函数表示向外传播的波a_m 是待求的散射系数。对于均匀介质圆截面柱a_m 和入射波展开系数 b_m 之间的关系由 Mie 系数决定。程序里的关键步骤是计算这个对角矩阵 T然后多体耦合时用它来组装方程。# 计算圆柱散射的T-matrixTM偏振电场沿z方向 from scipy.special import jv, yv, hankel2, jvp, hvp def cylinder_tmatrix(k0, radius, eps_r, n_order): 计算均匀介质圆柱的T-matrix对角元素 公式来源Bohren Huffman, Absorption and Scattering of Light by Small Particles m np.sqrt(eps_r) # 相对折射率 x k0 * radius # 尺寸参数 T np.zeros((2*n_order1, 2*n_order1), dtypecomplex) for n in range(-n_order, n_order1): abs_n abs(n) # 柱内用J柱外用JH组合切向场连续条件 Jx jv(abs_n, x) Jpx jvp(abs_n, x, 1) Hx hankel2(abs_n, x) Hpx hvp(abs_n, x, 1) # 介质内部 y m * x Jy jv(abs_n, y) Jpy jvp(abs_n, y, 1) # T矩阵元素TM偏振 numerator Jpx * Jy - m * Jx * Jpy denominator Hpx * Jy - m * Hx * Jpy T[abs_n n_order, abs_n n_order] numerator / denominator return T T cylinder_tmatrix(k0, radius, eps_r, n_order)逻辑说明这段代码实现了经典 Mie 理论里圆柱的散射系数。核心是让柱内场Bessel J 函数和柱外场Bessel J Hankel H 组合在边界 r radius 处满足电场和磁场切向连续。numerator 和 denominator 分别是连续条件构成的分子分母比值就是散射系数。注意这里m np.sqrt(eps_r)是相对折射率如果 eps_r 是复数m 也要取复数平方根Python 的np.sqrt会自动返回复数结果没有问题。参数说明jvp和hvp是 Bessel 函数的一阶导数方向参数 1 表示对自变量求导。这里没有除以 m 的那一项是因为 TM 偏振电场沿柱轴方向的公式和 TE 偏振不同TE 偏振的公式里分子分母中 m 的位置会互换。很多初次写程序的人在这里搞混偏振导致结果全错。判断方法很简单手算一个半径极小瑞利极限的情形TM 偏振下小粒子的散射应该趋向于介电常数的线性函数TE 则不同。3.3 多体耦合矩阵组装加法定理是核心多柱散射的关键在于第 j 根柱子感受到的入射场不仅包含原始平面波还包含其它所有柱子散射出来的波。把其它柱子的散射场用加法定理转换到第 j 根柱子的坐标系就得到一组耦合方程a_j T_j * (b_j sum_{i ! j} H_ji * a_i)其中 H_ji 是加法定理产生的转换矩阵元素是 Hankel 函数和角度因子的组合。把所有柱子组合在一起就是一个 (N * (2n_order1)) 维的线性方程组。def translation_matrix(k0, r_ij, theta_ij, n_order): 计算从柱子i坐标系到柱子j坐标系的加法定理转换矩阵 r_ij: 柱子i到柱子j的距离 theta_ij: 连接向量的极角 N 2 * n_order 1 G np.zeros((N, N), dtypecomplex) m_max 3 * n_order # 截断需要适当放大保证精度 for m in range(-n_order, n_order1): for n in range(-n_order, n_order1): # 加法定理H_m(k*r_j)*exp(im*phi_j) # sum_l H_{m-l}(k*r_ij)*exp(i*(m-l)*theta_ij)*J_l(k*r_i)*exp(il*phi_i) s 0.0j for l in range(-m_max, m_max1): n_h m - l if abs(n_h) n_order 5: # 截断保护 continue s hankel2(n_h, k0 * r_ij) * np.exp(1j * n_h * theta_ij) \ * jv(l, k0 * 1e-6) # 近似远场极限下J_l很小 G[m n_order, n n_order] s return G逻辑说明这段代码是示意性的实际实现里转换矩阵的元素是H_{m-n}(k*r_ij) * exp(i*(m-n)*theta_ij)不需要再乘以 J。我在这里写了一个占位用的 J 项是故意演示一个常见错误——初学者容易把「散射场表达」和「坐标系转换」两个步骤混在一起导致转换矩阵里多乘了一项。正确公式很简单从 i 柱坐标系到 j 柱坐标系的转换矩阵元素就是 Hankel 函数乘以相位因子不包含 J。参数说明m_max是加法定理内部求和的截断阶数一般取到 n_order 的 2~3 倍才够收敛。加法定理是一组无穷级数截断不够会导致矩阵病态、结果振荡。r_ij 如果非常小柱子几乎接触Hankel 函数的值会非常大矩阵条件数恶化这也是高填充率下数值不稳定的根源之一。实际组装方程时正确的做法是把所有 T 矩阵拼接成大块对角阵然后把转换矩阵按照柱间距离关系填入非对角块最终求解def assemble_and_solve(k0, positions, T, n_order, inc_angle): 组装多体耦合方程组并求解 返回所有柱子的展开系数 a 数组 N_col len(positions) N_dim N_col * (2 * n_order 1) # 大矩阵初始化对角块是单位阵非对角块是 -T*G M np.eye(N_dim, dtypecomplex) rhs np.zeros(N_dim, dtypecomplex) kx k0 * np.cos(inc_angle) ky k0 * np.sin(inc_angle) for j in range(N_col): xj, yj positions[j] idx_j j * (2*n_order1) # 入射平面波在柱子j处的展开系数 for m in range(-n_order, n_order1): b_jm 1j**(-m) * np.exp(1j * (kx*xj ky*yj)) * np.exp(-1j * m * np.arctan2(ky, kx)) rhs[idx_j m n_order] b_jm for i in range(N_col): if i j: continue xi, yi positions[i] dx, dy xj - xi, yj - yi r_ij np.sqrt(dx**2 dy**2) theta_ij np.arctan2(dy, dx) G_ij translation_matrix(k0, r_ij, theta_ij, n_order) # 在矩阵M的j行i列填入 -T_j * G_ij idx_i i * (2*n_order1) M[idx_j:idx_j2*n_order1, idx_i:idx_i2*n_order1] \ -T[idx_j:idx_j2*n_order1, idx_j:idx_j2*n_order1] G_ij # 求解线性方程组 a np.linalg.solve(M, rhs) return a逻辑说明这里组装的矩阵 M 形如I - T*G其中 T 是所有柱子的 T 矩阵构成的对角块G 是柱间转换矩阵。方程组的未知量是所有柱子的散射展开系数 a。右侧向量是入射平面波在每根柱子处的展开系数公式1j**(-m) * exp(i*(kx*xjky*yj)) * exp(-i*m*phi_inc)来自平面波的柱函数展开。参数说明np.linalg.solve是直接法求解复杂度 O(N^3)柱子数量超过 500 就会很慢。生产代码一般用迭代法比如 GMRES或者利用矩阵的 Toeplitz 结构加速。展开系数 a 求出来之后所有后续的反射和透射计算都基于它。提示组装矩阵的顺序很重要。这里的行索引对应「感受散射场的柱子」j列索引对应「发出散射场的柱子」i写反了会导致结果完全错误。建议先用只有两根柱子的简单情形验证两根柱子间距很大时结果应该趋近单根柱子的两倍弱耦合极限。4. 反射与透射计算从散射系数到宏观能量流4.1 观察线位置的选取原则算出所有柱子的展开系数 a 之后总散射场是每根柱子散射场的叠加。反射和透射的数值计算依赖于观察线的位置。这里有一个常见的误区随机介质的散射场在靠近柱子区域有复杂的近场结构直接把观察线放在柱子附近算出来的反射和透射会随观察线位置剧烈振荡。我一般会把观察线放在距离散射区域边界至少 2~3 个波长的地方。原因是 Hankel 函数的渐近形式在 k0*r 1 时才成立近场区域的高阶柱函数衰减小需要很多阶展开才能准确描述而远场区域只需要零阶项就能很好近似。观察线越远需要的展开阶数越少数值越稳定。def compute_reflection_transmission(a, positions, k0, n_order, Lx, N_obs, obs_distance): 计算观察线上的反射和透射 a: 所有柱子的展开系数 positions: 柱子位置 obs_distance: 观察线到散射区边界的距离波长单位 # 观察线位置 y_ref np.min(positions[:, 1]) - obs_distance # 入射侧 y_trans np.max(positions[:, 1]) obs_distance # 出射侧 # 观察线上的采样点 xs np.linspace(-Lx/2, Lx/2, N_obs) # 计算入射场用于反射侧减去 kx k0 * np.cos(inc_angle) ky k0 * np.sin(inc_angle) E_inc_ref np.exp(1j * kx * xs) * np.exp(1j * ky * y_ref) # 计算散射场在观察线上的值 E_scat_ref np.zeros(N_obs, dtypecomplex) E_scat_trans np.zeros(N_obs, dtypecomplex) for idx, (xj, yj) in enumerate(positions): dx_ref xs - xj dy_ref y_ref - yj r_ref np.sqrt(dx_ref**2 dy_ref**2) phi_ref np.arctan2(dy_ref, dx_ref) dx_trans xs - xj dy_trans y_trans - yj r_trans np.sqrt(dx_trans**2 dy_trans**2) phi_trans np.arctan2(dy_trans, dx_trans) # 散射场展开sum_m a_m * H_m(k0*r) * exp(i*m*phi) for m in range(-n_order, n_order1): a_idx idx * (2*n_order1) m n_order E_scat_ref a[a_idx] * hankel2(m, k0*r_ref) * np.exp(1j*m*phi_ref) E_scat_trans a[a_idx] * hankel2(m, k0*r_trans) * np.exp(1j*m*phi_trans) # 反射侧总场 入射场 散射场透射侧总场 散射场透射方向 # 注意这里要区分前向和后向传播分量 E_total_ref E_inc_ref E_scat_ref E_total_trans E_scat_trans # 计算功率考虑坡印廷矢量的法向分量 # 简化处理直接用 |E|^2 在观察线上的积分近似 P_ref np.sum(np.abs(E_total_ref)**2) / N_obs P_trans np.sum(np.abs(E_total_trans)**2) / N_obs P_inc np.sum(np.abs(E_inc_ref)**2) / N_obs R P_ref / P_inc T P_trans / P_inc return R, T逻辑说明这段代码在入射侧和出射侧各取一条观察线逐点累加所有柱子的散射场贡献。注意反射侧我保留了总场入射场 散射场因为反射功率对应的是「向后传播的总场减去入射场之后的部分」但如果是垂直入射且观察线在入射侧入射场只朝一个方向传播散射场中包含向前和向后两个方向的分量需要做方向甄别。这里的简化处理直接用 |E|^2 近似功率流严格做法是对场做空间傅里叶变换分离前向和后向行波分量。参数说明N_obs是观察线上的采样点数建议取 100~200 个点。采样点太多不会提高精度反而增加计算量太少会丢失场的空间变化细节。obs_distance建议取 2~3 个波长取太大虽然数值更稳定但如果计算区域是周期的会引入周期性重影的影响。注意上面的代码刻意省略了一个重要的物理步骤——方向甄别。真实程序里需要把散射场分解成前向和后向平面波分量做法是在观察线上做空间傅里叶变换然后把 ky 0 和 ky 0 的分量分别积分。不做这一步R 和 T 的值在斜入射时会有系统性偏差。4.2 系综平均的必要性与统计误差控制单次随机构型算出来的 R 和 T 只是该特定排列的结果不具备普适性。实际计算中需要生成 M 组独立随机构型每组算一次 R 和 T最后取平均。这里有一个统计收敛的判断技巧不是看平均值稳定而是看相对标准差。程序里一般会边算边累计均值和方差当相对标准差降到 1% 以下就认为收敛。n_realizations 50 R_list [] T_list [] for _ in range(n_realizations): # 重新生成构型复用前面的生成代码 positions generate_random_positions(...) # 组装并求解 a assemble_and_solve(k0, positions, T, n_order, inc_angle) # 算反射透射 R, T compute_reflection_transmission(a, positions, k0, n_order, Lx, obs_dis) R_list.append(R) T_list.append(T) R_avg np.mean(R_list) T_avg np.mean(T_list) R_std np.std(R_list) / np.sqrt(n_realizations) T_std np.std(T_list) / np.sqrt(n_realizations) print(fR {R_avg:.4f} ± {R_std:.4f}) print(fT {T_avg:.4f} ± {T_std:.4f}) print(fR T {R_avg T_avg:.4f})逻辑说明系综平均的坑在于构型数量和质量。50 组构型是起步如果 R 的标准差超过 0.01需要增加构型数而不是增加柱子展开阶数。这里的关键是每次要重新生成随机构型而不是在同一构型里挪动一根柱子——后者会产生强相关的样本统计效率极低。参数说明误差棒的计算公式用的是std / sqrt(M)这是标准误差不是标准差。输出结果的 RT 应该接近 1无损耗介质如果偏差超过 0.01优先检查展开阶数 n_order 是否足够、观察线是否太近、加法定理截断是否太小。这三者是导致 RT 不守恒的三大主因。5. 参数选择与避坑实战那些让结果翻车的细节5.1 展开阶数 n_order 怎么定展开阶数 n_order 直接决定矩阵维度和计算精度。选小了高次散射项被截断R 和 T 不收敛选大了计算量立方级增长还有可能引入数值噪声。经验法则尺寸参数k0 * radius小于 1 时n_order 取 2 到 4 足够k0*radius 在 1 到 5 之间时取 4 到 8超过 5 就要认真检查收敛性。判断方法是固定其它参数把 n_order 翻倍看 R 和 T 的变化量。变化小于 0.1% 就认为收敛。最容易翻车的是介电常数大的柱子比如 eps_r 10此时即使电气尺寸不大内部场的空间变化也很快需要更高的展开阶数。我的习惯是把k0 * radius * sqrt(eps_r)作为有效尺寸参数来估计阶数而不是只看外部波数。5.2 加法定理截断隐蔽的错误来源加法定理的求和阶数是另一个常见的错误源。很多程序里 n_order 和加法定理内部求和阶数用同一个值这在柱子间距较大时没问题但柱子密集时收敛变慢需要更大的截断。我一般把加法定理截断设置为3 * n_order起步然后做一次数值实验取两根柱子放在很近的距离中心距 2.1 * radius改变截断阶数观察转换矩阵的收敛情况。两根柱子的情形计算量小适合做这种收敛性测试。症状是R 和 T 出现小幅振荡改变构型随机种子后结果不稳定或者 RT 偏离 1 但单柱结果完全正确。这时候第一反应别去调矩阵求解器先检查加法定理截断。5.3 位置生成导致的重叠与边界效应随机位置生成程序的 Bug 通常不是逻辑错误而是「物理上不可行」的构型。比如 RSA 算法在高填充率下虽然能生成不重叠的构型但柱子之间距离过近导致 Hankel 函数自变量很小、函数值巨大矩阵条件数飙升。我在填充率超过 0.4 时会额外检查最小中心距如果发现任何一对柱子的中心距小于2.05 * radius就主动丢弃重新生成。这个 0.05 的余量是给数值稳定性留的不是物理必须但能显著提升求解成功率。边界效应是另一个大方向随机分布的柱子不可能填满整个无限平面实际计算总是取有限区域。区域边缘的柱子感受到的周围环境与内部的柱子不同导致边界的散射贡献被高估。处理方式是生成构型时在真正的计算区域外围再生成几圈「缓冲柱子」计算反射透射的观察线放在缓冲区域内部这样边缘效应被缓冲层吸收实际记入统计的是远离边界的行为。缓冲柱子的位置也需要随机化且它们参与多体散射方程的求解。5.4 能量守恒校验的具体操作能量守恒是最强有力的排错手段但它有个前提介质无损耗、计算区域足够大、展开阶数收敛。具体操作是算完 R 和 T 后打印R T无损耗情况应为 1.0000 ± 0.0005。失衡时的排查顺序有讲究先检查最便宜的操作第一查单柱 T-matrix。单独算一根柱子的散射截面用光学定理验证散射截面应该等于4/k0 * Im[f(0)]其中 f(0) 是前向散射幅度。这能排除 Mie 系数本身的错误。第二查观察线距离。把 obs_distance 从 2 个波长加到 5 个波长看 RT 是否变化。如果变化说明近场泄漏进了观察线。第三查 n_order。把阶数从 4 提到 8如果 RT 从 0.8 跳到 0.95继续提到 12到 0.99 以上说明前面阶数不够。第四查加法定理截断。这个最贵逐级增加内部截断阶数观察矩阵条件数变化。条件数超过 1e12 时RT 计算已经不可靠即使表面上守恒也可能两个误差互相抵消。5.5 高频振荡的数值形态有时候 RT 守恒完美但频率扫描曲线呈锯齿状。这个现象在文献里通常被归咎于「数值噪声」实际多半是随机构型数不够——每次频率点生成的构型如果用的是新的随机种子统计波动就会叠加成高频振荡。解决方式是用同seed构型扫描在扫描频率前固定随机种子让所有频率点使用同一组随机构型只改变频率。这样 R 和 T 随频率的变化反映物理本身而不是构型差异。做完一条曲线后再换一个种子生成新构型组取两条曲线的平均平滑度和可靠性都会显著提升。# 固定seed确保频率扫描使用相同构型 np.random.seed(42) positions_fixed generate_random_positions(N_col, Lx, Ly, radius) freqs np.linspace(0.8, 1.2, 51) R_curve [] T_curve [] for f in freqs: k 2 * np.pi * f / wavelength T_matrix cylinder_tmatrix(k, radius, eps_r, n_order) a assemble_and_solve(k, positions_fixed, T_matrix, n_order, inc_angle) R, T compute_reflection_transmission(a, positions_fixed, k, n_order, Lx, obs_dis) R_curve.append(R) T_curve.append(T)逻辑说明这段代码展示了频率扫描的标准姿势。np.random.seed(42)确保每组频率扫描用同一构型这样曲线的连续性是物理上真实的。如果想要更好的统计平均可以改变 seed 重复整个扫描最后把多条曲线做平均。参数说明freqs的步长需要根据目标物理特征选择如果介质有共振峰步长太大会漏掉峰。先用粗步长扫描找峰位再在峰附近加密步长是最省计算量的做法。6. 进阶入射角扫描与吸收介质的处理6.1 斜入射时的反射透射方向甄别垂直入射时反射和透射的方向区分很简单——前后各一半。斜入射就复杂了散射场包含所有方向的传播分量反射观察线上的场既有向后的行波也有向前透射的泄漏。严格做法是在观察线上做空间傅里叶变换将场分解成不同横向波矢 ky 的平面波分量然后分别统计向前ky 0和向后ky 0的分量。这个做法会引出一个数值问题观察线长度 Lx 有限傅里叶变换的频率分辨率是 2π/Lx入射角很小时前向和后向分量的频谱会重叠难以分离。所以斜入射扫描时建议把 Lx 设得尽可能大并在边缘加窗函数减小截断泄漏。6.2 有损耗介质的能量平衡判定当 eps_r 带虚部时RT 不等于 1 是正常的不等于 1 的部分是损耗。这时候要算吸收率 A 1 - R - T且 A 必须大于 0。如果算出 A 小于 0即 RT 大于 1说明程序有严重问题通常来自展开阶数不足导致散射场被高估。有损耗介质还有个特殊问题Hankel 函数在复波数下的行为与实数波数不同加法定理收敛变得更慢。我一般会把加法定理截断再提高 1.5 倍同时检查 T-matrix 的模是否小于 1——无源柱子的散射系数模长必须小于 1否则违反能量守恒这可以作为单柱层面的快速检查。6.3 如何扩展到 TE 偏振和任意截面柱形标题写的二维柱最常见的是 TM 偏振电场沿柱轴 z 方向因为这时问题是标量的最容易实现。实际应用中总会有 TE 偏振的需求——磁场沿 z 方向电场在横截面内这时柱面波展开的边界条件变成电场切向连续、磁场切向连续公式形式与 TM 相同但 J 和 H 的求导项位置互换之前在 T-matrix 代码里提过。任意截面柱形方形、六边形、椭圆形无法用解析 Mie 系数需要先数值求解单柱的 T-matrix。常见做法是边界元法或者用 COMSOL 这类有限元软件先扫出单柱的散射矩阵然后导入到多体散射主程序里。这个方案工程上完全可行只是中间多了一层数值前处理注意导入数据的插值精度会影响多体计算的一致性。本文还有配套的精品资源点击获取
返回列表