☰
DDSCAT多核壳结构建模:用Matlab批量生成shape文件
2026/10/5 6:08:57 网站建设 项目流程

平时用DDSCAT做纳米粒子散射计算的朋友应该都有同感:程序本身算起来很省心,真正费时间的是怎么把脑海里的三维结构变成它认得的shape文件。前四篇我们聊过安装、单球建模、材料参数配置和一些基础报错,这一篇直接把难度往上提一个台阶,聊多核壳结构。多核壳(multi-core-shell)就是多个核心颗粒被同一个外壳包裹,在等离激元传感、SERS增强基底、纳米催化反应器这类体系的光学响应计算里非常常见,DDSCAT里要建这种模型,靠手写坐标根本不可能,必须用Matlab批量生成偶极子坐标。这篇就把我实际调试过的建模思路和完整代码放出来,覆盖多核壳球和多核壳圆柱两种结构,顺便把DDSCAT读shape文件的格式衔接讲清楚,能帮你省下好几个晚上的调试时间。

1. 为什么多核壳结构的模型文件必须自己写脚本生成

先想清楚这一篇要解决的问题到底是什么。DDSCAT本身是一个纯计算内核,它不对“多核壳”这个概念做任何语义理解,只接收一个朴素的偶极子列表——每个偶极子的位置和材料编号。你给它一个形状文件,它就按里面的坐标摆阵子、算极化率、迭代求解。所以建模的全部工作,本质上就是回答一个问题:在离散格点上,哪些点属于壳,哪些点属于核,哪些点是空白。

单球或单圆柱很好办,一个球形判断条件r <= R就全部解决了,用Excel甚至文本编辑器都能生成。但多核壳结构的难点在于,一个点可能同时落在多个几何区域的判断范围内。比如某个点既在壳层内部,又靠近某个核心球,这时候到底算壳材料还是核材料?这类问题必须通过程序判断优先级,逐点扫描包围盒里的每一个格点。一旦核的数量上到三四个,手工写坐标文件就是灾难。

另外,多核壳结构在真实实验里并不是一个标准化形状。核的个数、核心位置、核半径、壳厚度、外壳整体形状,每个课题组都有自己的参数组合。DDSCAT内置的规则形状生成器里,只有实心球、实心圆柱、椭球这些基础选项,没有“多个球核嵌在一个壳里”的预设。所以无论你用什么语言写生成器,本质都是在做一个参数化的几何建模工具,Matlab只是我比较顺手的选择。

还有一个容易被忽略的原因:科研实验方案经常要扫参数。核半径从2 nm扫到10 nm,核间距从5 nm扫到30 nm,如果每次改参数都手动改坐标文件,不仅慢而且容易出错。写成Matlab脚本之后,改几个数字重新跑一遍,几分钟就拿到新shape文件,这才是能支撑论文里那批参数扫描图的效率。所以这一篇不只是给一组代码,更是给一套“如何把结构参数翻译成DDSCAT输入”的方法。

2. 动手写代码前先定好的三件事:几何参数、材料编号、格点分辨率

写代码之前别急着开Matlab,先把三个基本问题定下来:几何参数怎么定义、材料编号怎么分配、格点分辨率选多大。这三件事看似简单,但后面所有代码逻辑都建立在它们之上,我调试时返工最多的也正是这几处。

2.1 几何参数化:半径、核心坐标和位置约束

多核壳球的结构可以拆成三层看。最外层是一个完整的球壳,外半径记作Rshell,壳的内边界半径记作Rcore_outer,壳层厚度就是Rshell - Rcore_outer。壳内部有若干个核心球,每个核心球由两个参数描述:核半径coreR(k)和核心坐标coreC{k}。判断一个格点属于哪里时,先计算它到外壳球心的距离,再计算它到每个核心球心的距离,按优先级赋予材料编号。

这里有一个必须提前想清楚的几何约束:每个核心球必须完全位于外壳内部。也就是说,对任意核心,必须满足norm(coreC{k}) + coreR(k) <= Rshell。如果不满足,生成的模型会出现“核戳出壳外”的情况,这在物理上可能就不是你想要的结构了。代码里最好加上这个检查,跑出的模型要能自己报警。

多核壳圆柱的参数化类似,只是外层从球壳换成圆柱壳。圆柱的几何定义有四个量:圆柱外半径Rcyl、壳内半径RcylCore(或者直接定义壳厚度shellT = Rcyl - RcylCore)、圆柱总长度Lcyl,以及圆柱轴向方向。我默认轴向为z,这样柱坐标判断最直接。至于圆柱壳内的多个核,可以继续用球形核,也可以定义成小圆柱核,取决于你的物理模型。为了代码统一,我推荐核一律用球心坐标加半径来描述,因为球的判断在任意参考系下都是一行代码,圆柱核还要额外处理轴向变换。

2.2 材料编号:icomp不是你想填几就填几

DDSCAT的shape文件里,每个偶极子后面带的那个整数是材料组份标记(通常写成icomp),它不直接存折射率或介电常数,只存一个“第几号材料”的索引。真正对应什么材料,是在ddscat.par里通过介电常数输入区定义的。比如你希望核是金、壳是二氧化硅,那么在par文件里第二种材料写金的介电常数,第三种材料写二氧化硅的介电常数,shape文件里相应位置就填2和3。

因此写Matlab脚本时,最好在文件头部用两个变量shellMat和coreMat单独管理材料编号,而不是把魔法数字直接散落在判断逻辑里。我习惯用2表示壳、3表示核,因为1通常留给真空或环境介质,这样和DDSCAT里“第一种组分默认是背景”的习惯对齐。如果你在别的例子里见过0也代表某种材料,那属于不同版本或自定义映射,不用纠结,只要保证shape文件里的编号和par文件里的介电常数顺序严格对应就不会出问题。

2.3 格点分辨率:dpl选多大直接决定计算量和精度

DDSCAT把目标离散成一个个偶极子,偶极子之间的间距通常记作d,程序里也叫dpl。分辨率怎么选?不能太任性。常规准则是:每个波长内至少要有10个偶极子,更稳妥是15到20个。这里要考虑介质内的波长,也就是lambda / n,其中n是材料折射率。比如入射波长600 nm,壳材料折射率1.5,那么介质内波长是400 nm,偶极子间距最好取400/15≈26 nm以内。

从形状建模的角度,分辨率还决定了一个核能不能被“画出来”。如果核半径只有2个格点,那么核的形状会明显离散化,散射结果里可能出现伪影。一般来说,核半径至少要覆盖6到8个格点,形状才算完整。下表给一个粗略的选参参考:

入射波长介质折射率建议偶极子间距d核半径最小格点数
400 nm1.020 nm3
600 nm1.520 nm4
800 nm1.3330 nm4
1064 nm1.050 nm5

需要特别提醒的是,多核壳结构的包围盒往往比单球大很多,因为核要分散排布。如果你按照这个间距去算总格点数,可能会发现偶极子数量轻松超过几十万甚至上百万。DDSCAT计算内存和时间会随之急剧上升。比较好的做法是先按你要计算的波长反推一个合理的d,再估计包围盒需要的格点数,如果数量级太大,要么扩大d(但要保证分辨率),要么缩小模型尺寸,要么换更高性能的机器。

3. Matlab实现多核壳球:核心判断逻辑与完整脚本

其实整个建模代码的核心就是“逐点判断”。理解了这个逻辑,你完全可以自己改成任意结构。

3.1 思路拆解:从包围盒到核壳归属

先在包围盒内生成一个三维整数格点网格。比如Nx、Ny、Nz分别代表三个方向的格点个数,那么所有格点的整数坐标就可以用ndgrid生成。DDSCAT读shape文件时关注的是整数坐标的相对位置,所以这里坐标直接用整数即可。为了把粒子放在包围盒中心,我习惯把所有坐标平移到以盒子中心为原点。

接着逐点判断:

判断区域条件材料编号
核内到任一核心距离 ≤ 核半径coreMat
壳层在壳内且不在任何核内shellMat
外部真空其余格点不输出

注意核的判断要优先于壳的判断。也就是说,即使某个点既落在壳层区域又落进核内,它也要被赋予核材料。这就是常见的“核优先”规则,写代码时先标壳,再用核的标记覆盖壳的标记,逻辑最清晰。

3.2 完整代码:多核壳球生成脚本

下面这段代码直接在Matlab里运行即可,输出文件是shape_sphere_mcs.dat。文件名无所谓,关键是内容和后续DDSCAT设置保持一致。

% multi_core_shell_sphere.m % 生成 DDSCAT 可读取的多核壳球 shape 数据 % 说明:输出文件每行为 [ix iy iz icomp] clear; clc; %% 1. 参数定义 Nx = 51; Ny = 51; Nz = 51; % 包围盒格点数,奇数方便居中 Rshell = 20.0; % 外壳外半径(格点单位) RcoreOuter = 14.0; % 壳层内边界半径,壳厚 = 6 nuc = 3; % 核的数量 coreR = [4.0, 4.0, 4.0]; % 每个核的半径 coreC = {[-8, 0, 0], [8, 0, 0], [0, 0, 8]}; % 每个核的球心坐标 shellMat = 2; % 壳材料编号 coreMat = 3; % 核材料编号 %% 2. 生成包围盒内所有整数格点 [X, Y, Z] = ndgrid(0:Nx-1, 0:Ny-1, 0:Nz-1); X = X(:); Y = Y(:); Z = Z(:); % 平移到盒子中心,方便几何判断 xc = (Nx-1)/2; yc = (Ny-1)/2; zc = (Nz-1)/2; xp = X - xc; yp = Y - yc; zp = Z - zc; %% 3. 核必须在外壳内的预检查 for k = 1:nuc dist = norm(coreC{k}); if dist + coreR(k) > Rshell warning('第 %d 个核超出了外壳范围,请检查参数!', k); end end %% 4. 球壳归属判断 r = sqrt(xp.^2 + yp.^2 + zp.^2); % 先用0初始化,0代表“不输出” icomp = zeros(size(r)); % 标定壳层区域 inShell = (r <= Rshell) & (r > RcoreOuter); icomp(inShell) = shellMat; % 逐个核判断,核内点覆盖为核材料 for k = 1:nuc c = coreC{k}; rk = sqrt((xp - c(1)).^2 + (yp - c(2)).^2 + (zp - c(3)).^2); inCore = (rk <= coreR(k)); icomp(inCore) = coreMat; end %% 5. 提取有效偶极子并输出 idx = find(icomp > 0); N = length(idx); % 输出坐标恢复成从1开始的正整数,方便和DDSCAT的网格约定对齐 out = [X(idx)+1, Y(idx)+1, Z(idx)+1, icomp(idx)]; fid = fopen('shape_sphere_mcs.dat', 'w'); % 第一行写一个注释行,第二行写偶极子总数 fprintf(fid, 'multi-core-shell sphere, Nx=%d Ny=%d Nz=%d\n', Nx, Ny, Nz); fprintf(fid, '%d\n', N); for i = 1:N fprintf(fid, '%d %d %d %d\n', out(i,1), out(i,2), out(i,3), out(i,4)); end fclose(fid); fprintf('完成:共生成 %d 个偶极子\n', N);

说一下几个关键点:

第一,RcoreOuter的存在是为了让壳层有厚度。如果你的模型是“多个核直接包在一层薄壳里”,那么壳层的内边界其实就是核的外边界再往内一点,这个值可以按物理需要调整,甚至可以把壳内边界设成比核的最大外延小,让核嵌入壳中。不过为了代码清晰,我建议壳内边界和核的位置解耦,通过几何检查保证核不超出外壳。

第二,坐标从0:Nx-1生成,输出时+1变成1:Nx,这和DDSCAT很多示例里坐标从1开始的习惯一致。实际DDSCAT对坐标的正负没有硬性要求,关键是相对距离,但统一成正整数可以少踩一些“坐标从0还是1开始”的坑。

第三,inShell = (r <= Rshell) & (r > RcoreOuter)里用的是严格大于RcoreOuter。边界上的点判给哪一侧影响不大,只要不重复就行。

3.3 三核壳球的运行结果验证

用上面参数跑一遍,程序会输出类似这样的统计信息:

完成:共生成 65412 个偶极子

这时候别急着拿去算,先在Matlab里用scatter3可视化检查一下结构是否合理:

figure; hold on; % 壳层偶极子 shellIdx = out(:,4) == shellMat; scatter3(out(shellIdx,1), out(shellIdx,2), out(shellIdx,3), 1, 'b', '.'); % 核偶极子 coreIdx = out(:,4) == coreMat; scatter3(out(coreIdx,1), out(coreIdx,2), out(coreIdx,3), 3, 'r', 'filled'); axis equal;

从图上应该能看到一个蓝色球壳里嵌着三个红色核心球。如果红点跑到蓝色区域外面,说明核位置或者半径参数设置出了问题,改参数重新生成。这一步可视化检查非常重要,花费两分钟能避免后续在DDSCAT算完才发现模型错了。

4. 圆柱变体:从球坐标系切换到柱坐标系的注意点

多核壳圆柱写起来和球形差不多,核心区别就是把“到球心的距离”换成“到圆柱轴线的距离”。我默认圆柱轴向为z,那么柱坐标半径就是rho = sqrt(xp.^2 + yp.^2),轴向范围用abs(zp) <= Lcyl/2控制。

4.1 圆柱壳判断条件与核分布方式

外壳是圆柱壳时,判断条件有两个:一是径向距离要在RcylCore < rho <= Rcyl,二是轴向z要在[-Lcyl/2, Lcyl/2]内。落在圆柱内部空腔里的点,继续去判断它是否属于某个核心球。

核怎么分布?这里有两种常见做法。第一种是核仍然用球体,多个球核沿轴向排布在圆柱内,适合模拟“柱状容器里装了多个催化剂颗粒”的结构。第二种是核本身也用圆柱体,多个同轴或错位的圆柱核嵌在外壳里,适合模拟多层柱状波导。我的代码里默认是第一种,因为球的判断逻辑通用、参数直观;如果你需要圆柱核,把核判断部分改成柱坐标条件即可。

4.2 完整代码:多核壳圆柱生成脚本

% multi_core_shell_cylinder.m % 生成 DDSCAT 可读取的多核壳圆柱 shape 数据 % 默认圆柱轴向为 z 轴 clear; clc; %% 1. 参数定义 Nx = 61; Ny = 61; Nz = 101; % 包围盒格点数 Rcyl = 15.0; % 圆柱外半径 RcylInner = 10.0; % 圆柱壳内半径,壳厚 = 5 Lcyl = 60.0; % 圆柱总长度(轴向) nuc = 2; % 核的数量(球核) coreR = [4.0, 4.0]; coreC = {[0, 0, -15], [0, 0, 15]}; shellMat = 2; coreMat = 3; %% 2. 生成包围盒格点 [X, Y, Z] = ndgrid(0:Nx-1, 0:Ny-1, 0:Nz-1); X = X(:); Y = Y(:); Z = Z(:); xc = (Nx-1)/2; yc = (Ny-1)/2; zc = (Nz-1)/2; xp = X - xc; yp = Y - yc; zp = Z - zc; %% 3. 核位置预检查(同样要求核不超出圆柱外壳) for k = 1:nuc c = coreC{k}; axialDist = abs(c(3)); radialPos = sqrt(c(1)^2 + c(2)^2); if radialPos + coreR(k) > Rcyl || axialDist + coreR(k) > Lcyl/2 warning('第 %d 个核超出了圆柱外壳范围,请检查参数!', k); end end %% 4. 圆柱壳归属判断 rho = sqrt(xp.^2 + yp.^2); % 初始化 icomp = zeros(size(rho)); % 圆柱壳区域:径向在壳层内,轴向在圆柱范围内 inShellCyl = (rho <= Rcyl) & (rho > RcylInner) & (abs(zp) <= Lcyl/2); icomp(inShellCyl) = shellMat; % 核区域:球核 for k = 1:nuc c = coreC{k}; rk = sqrt((xp - c(1)).^2 + (yp - c(2)).^2 + (zp - c(3)).^2); icomp(rk <= coreR(k)) = coreMat; end %% 5. 输出 idx = find(icomp > 0); N = length(idx); out = [X(idx)+1, Y(idx)+1, Z(idx)+1, icomp(idx)]; fid = fopen('shape_cyl_mcs.dat', 'w'); fprintf(fid, 'multi-core-shell cylinder\n'); fprintf(fid, '%d\n', N); for i = 1:N fprintf(fid, '%d %d %d %d\n', out(i,1), out(i,2), out(i,3), out(i,4)); end fclose(fid); fprintf('完成:共生成 %d 个偶极子\n', N);

这版代码的包围盒长度取得比较长,因为圆柱轴向跨度大,偶极子数量可能会显著增多。我建议先跑一遍看N的数量级,如果太大,优先缩小Nz方向的范围,把包围盒贴合模型尺寸,避免把大量空白格点也纳入统计。当然,DDSCAT计算时空白点本身不占内存,但生成阶段遍历所有格点的耗时还是会随包围盒体积增加。

4.3 圆柱壳判断中容易忽略的轴向边界

圆柱壳和球壳最大的不同在于,球壳只有一个径向自由度,而圆柱壳有两个独立方向:径向和轴向。判断时不能只写rho <= Rcyl,必须同时加上轴向范围限制,否则你会得到一个无限长的圆柱壳。反过来,轴向范围也不能单独判断,否则会把圆柱上下两个圆面之外的点也算进去。两个条件缺一不可。

另外,圆柱上下两个端面默认是平的。如果你需要半球封头或者圆顶,可以在轴向边界处再嵌一个半球壳判断,类似球壳代码里的r <= Rshell,相当于在两端各接半个球壳。这个变体在模拟柱状纳米反应器时很常用,可以自己扩展。

5. shape.dat格式衔接:把Matlab数组变成DDSCAT认得的文件

模型生成只是第一步,接下来要确保DDSCAT能正确读取。很多新手在这里卡住,报错后以为是程序问题,其实只是文件格式和设置没对齐。

5.1 DDSCAT读取外部shape文件的基本格式

DDSCAT支持从外部文件读入目标形状,通常文件名是shape.dat,但具体名称可以在运行参数里指定。文件结构可以按下面这种兼容性好的写法组织:

multi-core-shell sphere generated by Matlab <- 第一行注释,可任意写 65412 <- 第二行:偶极子总数N 1 1 1 3 <- 之后每行:ix iy iz icomp 1 1 2 3 ......

第一行是注释行这一做法在不同版本里略有差异,有的版本会忽略,有的版本要求必须有。我的建议是保留注释行,如果你的DDSCAT版本读取时报错“读文件错误”或“N异常”,优先删掉第一行再试一次。

每行的四个整数依次是:

  • ix:偶极子在x方向的格点序号
  • iy:y方向格点序号
  • iz:z方向格点序号
  • icomp:材料组份编号

只要这四个整数正确,DDSCAT就能在目标空间中构造出完整的偶极子阵列。需要注意的是,偶极子间距d和初始坐标原点的物理位置并不在这个文件里设置,它们是在ddscat.par里配置的。Matlab这边只需要关心整数格点坐标的相对关系。

5.2 ddscat.par里如何把shape文件和材料对应起来

在ddscat.par里,通常有一个指定目标形状的字段,你需要把它设置成“从外部文件读取”模式,并告诉程序shape文件路径。接下来是介电常数输入区,这一块要按你shape文件里的材料编号顺序逐一填写。

举一个最简单的例子:如果你的shape文件里有2和3两种材料编号,2代表壳、3代表核,那么介电常数部分就要保证第二种材料是壳材料的介电常数,第三种材料是核材料的介电常数。第一种材料通常是背景介质(比如空气或水)。如果顺序填反了,最直观的结果就是散射谱峰位置完全对不上,甚至出现“核和壳互换”的结构,这种错误在可视化阶段很难发现,因为几何形状一样,只是材料反了。

所以我的习惯是:在Matlab脚本头部就把shellMat和coreMat写清楚,然后生成完shape文件后,立刻在ddscat.par里按同样编号填写材料。两边用同一套编号体系,不要想着“反正壳是主要材料就默认填2”,一定要回头核对。

5.3 有效半径aeff和偶极子间距d的换算

DDSCAT计算截面时经常要用到目标有效半径aeff,物理上定义为一个等体积球的半径。对于离散偶极子模型,可以用下面这个关系换算:

[ a_{\rm eff} = \left( \frac{3 N}{4 \pi} \right)^{1/3} \cdot d ]

其中N是shape文件里的偶极子总数,d是偶极子间距。也就是说,当你在Matlab里生成了N个偶极子,又在ddscat.par里设置了d,aeff其实就已经确定了。反过来,如果你想按某个固定aeff建模,可以反推需要的N值。

我在实际使用中一般分两步走:先根据物理尺寸确定d,再运行Matlab脚本得到N,最后在par文件里填d。举个例子,入射波长800 nm,壳材料折射率1.5,我取d=30 nm,生成的核壳球N=65412,那么aeff约等于:

[ a_{\rm eff} = \left( \frac{3 \times 65412}{4\pi} \right)^{1/3} \times 30 \text{ nm} \approx 207 \text{ nm} ]

这样算出来的aeff可以直接用于归一化散射截面。如果你在文档或公式里看到aeff相关的参数,指的就是这个。

6. 导入前的仿真级自检:可视化、数量统计和那些我踩过的坑

模型文件生成、格式看起来也对,是不是就可以直接提交计算了?我建议再花几分钟做几个自检步骤。这些都是我被实际报错逼出来的习惯。

6.1 可视化检查与偶极子数量合理性

第一个自检是可视化。用Matlab的scatter3把shape文件里的点全部画出来,分别用不同颜色标记核和壳,然后旋转视角看几个方向。重点检查三件事:

  • 核是否完全在壳内部,有没有“穿模”
  • 外壳形状是否完整,有没有因为包围盒太小导致边缘被截断
  • 核的数量和相对位置是否符合预期

第二个自检是偶极子数量。如果你的N值比同尺寸单球模型的N大出很多倍,多半是包围盒开太大了。比如一个半径20格点的球,体积大约是33510个格点,如果生成的N有十几万,那可能是把大量空白区域也算进去了。偶极子数量直接影响运行内存和时间,尽量让包围盒贴合模型表面,不要留太多空白边距。

6.2 常见错误对照表

我在调试多核壳结构时整理了一张问题对照表,基本覆盖了最容易踩的坑:

现象可能原因解决办法
生成的核位置偏移明显核心坐标和包围盒中心没有对齐检查coreC里坐标是否以粒子中心为参考
核戳出外壳表面核半径或核心距离设置不合理增加预检查,打印警告
shape文件读入DDSCAT后报错文件头格式不兼容当前版本尝试删掉第一行注释
计算完成后散射谱明显不对材料编号和par文件介电常数顺序不一致核对icomp和par文件里材料顺序
偶极子数量比预期多得多包围盒体积过大缩小Nx/Ny/Nz范围
圆柱壳上下端面缺失轴向判断条件写错或Lcyl设置过小检查abs(zp) <= Lcyl/2和Lcyl值

这里面最坑的是“材料顺序反了”。因为几何图形看起来完全正常,你甚至会在可视化阶段觉得模型没问题,最后散射结果却离谱。所以我建议生成完shape文件后,直接打开文件看前几行:如果壳材料编号是2、核材料编号是3,那么par文件里第二种材料就必须是壳材料、第三种材料必须是核材料。这个习惯养成后,至少能避开一半的无效计算。

6.3 两个实用扩展:多层同心壳和随机核位置

如果你研究的不是“多核单壳”,而是“同心多层壳”,比如二氧化硅包金核再包一层二氧化硅,那代码只需要小改一下。把单个外壳判断改成循环遍历多个壳层半径,每一层赋予不同的材料编号即可。本质上还是一个径向距离判断,只不过从if-else变成了for循环:

shellRadii = [15, 10, 6]; % 从外到内各层边界 shellMatList = [4, 2, 4]; % 每层材料编号 for j = 1:length(shellRadii)-1 r = sqrt(xp.^2 + yp.^2 + zp.^2); layerMask = (r <= shellRadii(j)) & (r > shellRadii(j+1)); icomp(layerMask) = shellMatList(j); end

如果你需要模拟随机分布的多核结构,比如核位置在一定范围内随机抖动,可以在Matlab里用rand或randn生成核心坐标,然后加一个“最小核间距”约束,避免核之间重叠或太近导致离散后无法分辨。这个变体在生物医学光热计算里很常用,因为实验上核的位置往往不完全规则。

最后再分享一个小技巧:跑DDSCAT之前,可以在Matlab里统计一下每个材料编号的偶极子数量,估算一下核体积占比。核壳结构的光学响应强烈依赖核与壳的体积比,这个比值如果跟实验TEM估计的对不上,说明几何建模可能出现了系统性偏差。回头检查参数,比全部算完再返工要划算得多。

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

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

立即咨询