
干这行的都知道混凝土、颗粒复合材料这类细观仿真里最让人头大的往往不是本构模型选什么而是几何建模阶段那些随机分布的骨料怎么放进去。二维平面里用圆形模拟骨料还算好搞Python几行循环就能塞满一个方形区域可一进入三维在 Abaqus 里手动摆球体根本不现实一个人能忍受在界面里点几百次坐标吗反正我受不了。这次我专门把“三维随机圆形骨料”在 Abaqus 里的 Python 生成流程完整做了一遍从随机位置算法、无重叠判定、part 构建到最后布尔运算和网格划分前的几何处理全程实测踩坑把能跑通的那条路记在这里供做混凝土细观、颗粒增强复合材料、多相多孔介质这类仿真的朋友直接参考。1. 需求分析与整体实现思路1.1 三维随机骨料到底在解决什么问题在混凝土细观力学里骨料是体积占比最高、力学影响最大的组分。把骨料、砂浆、界面过渡区ITZ三者分别建模才能比较真实地模拟裂纹从界面萌生、穿过砂浆、被骨料偏转的整个过程。二维圆骨料模型能解决一部分问题但平面应力/应变假设忽略了骨料在厚度方向的形态、堆叠和约束效果很多宏观响应的定量结果误差不小。一旦升到三维骨料的几何表示就自然从“圆形”变成了“球体”。注意三维里对应的随机圆形其实就是随机球体这个说法很多人绕不过弯——其实只要看切片三维球体在任意截面上都表现为圆所以标题里说“随机生成圆形骨料”在三维语境下就是指随机位置、随机半径分布的球体集合。三维随机骨料建模要解决三个核心问题几何填充在一定体积的基体空间里放置 N 个球体满足体积分数比如 30%~45%要求同时不能相互穿插。边界处理球体不能越过基体的外边界可能需要留出保护层厚度。建模可操作性生成的几何必须能被 Abaqus 正确读取后续能划分六面体或四面体网格。1.2 为什么选 Python 而不是 GUI 操作Abaqus 的二次开发接口基于 Python这意味着你在界面里点的每一步操作后台都会生成对应的 Python 命令。反过来你写一段 Python 脚本提交给 Abaqus它就能自动完成建模。这个方法最大的优势是可重复性和参数化。我见过不少朋友做随机骨料建模坚持用 CAD 软件画好再导入 Abaqus。不是不行但每次调骨料半径范围、体积分数都得重画一遍效率低得可怜。相比之下Python 脚本从随机数生成、坐标计算到模型构建一把梭一个脚本能连续生成几十个不同骨料分布的模型做参数化分析或者随机样本统计分析都非常方便。再说一个细节Abaqus 自带的 Python 环境是它可以识别的完整解释器但不是所有 Python 第三方库都能用。random、math、numpy部分版本可以这类基础库一般没问题但能不能装 pandas、scipy 这类重库得看版本和许可模块。所以我这次的核心随机生成逻辑只用了 random 和 math完全避开对第三方库的强依赖保证脚本拿到任何装了 Abaqus 的机器上都能跑。1.3 技术路线总览整个建模流程可以分成三步在 Python 里生成随机球体的球心坐标和半径同时做重叠判定输出一个列表。在 Abaqus 里创建基体立方体和每个球体的 part。组装、定位球体到随机坐标再通过布尔运算把球体从基体中切掉形成空腔最后把球体实体合并成骨料 part方便后续赋予材料和划分网格。这套路线本质上就是“先算好位置再驱动 CAD 内核建模”不涉及任何复杂的三维布尔交互逻辑清晰每一步出问题都很容易定位。2. 随机骨料核心算法位置生成与重叠判定2.1 随机数生成的基本设定生成骨料的第一步是确定目标区域的尺寸和骨料的尺寸范围。比如一个边长 100 mm 的立方体基体骨料半径范围设为 3~8 mm目标体积分数为 30%。半径的生成需要遵循一定的粒度分布。现实中混凝土骨料的级配很复杂Fuller 级配、一粒石、二粒石等但在几何建模阶段常用均匀分布、对数正态分布或基于筛分曲线的级配函数。我这次先用最简单的均匀分布作为例子因为代码清晰逻辑好懂。实际工程中可以替换成服从任意分布的随机函数算法框架不变。import random import math # 基体立方体尺寸 Lx, Ly, Lz 100.0, 100.0, 100.0 # 骨料半径范围mm r_min, r_max 3.0, 8.0 # 目标骨料数量 num_agg 30 # 骨料间最小净距 gap_min 0.5骨料与骨料之间要留间隙不是单纯为了避免几何干涉——后续如果要做界面过渡区ITZ球体外表面还要偏置一层比基体尺寸小得多的“界面单元”没有间隙就根本没法生成界面层。2.2 随机放置与无重叠判定随机放置的核心逻辑是在立方体空间里随机抽样一个球心坐标和半径判断该球是否与已有球重叠也判断是否超出了基体边界。不满足条件就重新抽样直到放满目标数量或达到最大尝试次数。判断两个球是否重叠很简单两球心距离大于等于两球半径之和再加一个最小间隙就说明不重叠。def sphere_overlap(pos, radii, new_pos, new_r, gap): for px, py, pz in pos: pr radii[pos.index((px, py, pz))] # 避免用index效率太低实际用zip dx new_pos[0] - px dy new_pos[1] - py dz new_pos[2] - pz dist math.sqrt(dx*dx dy*dy dz*dz) if dist (new_r pr gap): return True return False实际写的时候不要用这种低效的 index 查找直接并行遍历positions [] # 存放已经放置的球心坐标 radii [] # 存放已放置球的半径 max_attempts 20000 attempts 0 while len(positions) num_agg and attempts max_attempts: attempts 1 r random.uniform(r_min, r_max) x random.uniform(r, Lx - r) y random.uniform(r, Ly - r) z random.uniform(r, Lz - r) overlap False for (px, py, pz), pr in zip(positions, radii): dist math.sqrt((x-px)**2 (y-py)**2 (z-pz)**2) if dist (r pr gap_min): overlap True break if not overlap: positions.append((x, y, z)) radii.append(r)边界条件就是在抽 x、y、z 时直接限制在 r 到 L-r 的范围内保证球体完全在立方体内部。看起来简单但很多刚开始写随机骨料脚本的人就是在这里出 bug球体摆到了边界外后面布尔运算直接报错。2.3 体积分数怎么控制随机抽样本质上有运气成分。目标体积分数设 30%跑一次可能只填到 27%换个随机种子又可能到 31%。最直接的办法是固定一个随机种子跑完检查体积分数不满意就换种子重跑。工程上常见做法是先用脚本试算几次挑一个能稳定落在目标区间的种子序号。更高级的办法是用“增长法”从一个较小的初始半径开始放置完成后逐步增大每个球的半径直到总体积分数达到目标值。这个逻辑在二维圆骨料生成里很普遍三维球体也适用。但注意球增大时可能产生新重叠需要重新判定实现起来要多加一层循环。# 实际体积分数计算 cube_vol Lx * Ly * Lz agg_vol sum([4.0/3.0 * math.pi * r**3 for r in radii]) volume_fraction agg_vol / cube_vol print(当前骨料体积分数为: %.2f%% % (volume_fraction * 100.0))这个体积分数计算建议在建模之前就完成因为生成的位置和半径列表在后续建模中不会变如果你想调整体积分数直接改目标数量和半径范围重新跑随机段即可不用碰 Abaqus 内核那部分。2.4 为什么需要最大尝试次数有人可能会问为什么设一个 max_attempts不让循环一直跑因为当目标体积分数偏高比如接近 45%~50%时立方体里能按给定间隙要求放下的球数是有限的到了后期剩下的大半径球很难找到合适位置循环会陷入死循环。设一个最大尝试次数到数量没凑够时提前跳出提醒你降低体积分数、减小半径范围或减少最小净距这样比卡死不动的体验好得多。我实测过在一个 100³ 的立方体里半径 3~8 mm、净距 0.5 mm 条件下想填到 35% 体积分数是很困难的通常是 25%~30% 就差不多到极限了。要进一步提高填充率就得引入不同尺寸级别的粒径配合级配或者允许骨料略微超出边界再做切割这在后面的扩展思路里会提。3. Abaqus 中 Python 建模的关键实现3.1 Abaqus Python 对象层次回顾要顺利写建模脚本得先搞清 Abaqus Python 接口的层次结构最外层是 mdbmdb 下面有 ModelModel 下面有 Part、Material、Assembly、Step 等对象。我们要操作的主要是 Model 下的 Part 和 Assembly。Abaqus 中 part 是几何的载体assembly 是实例的载体。每个 part 可以实例化多次并放入 assembly 中进行装配、布尔运算。生成随机骨料时我们的思路是建一个大立方体 part 当作基体。为每个球体单独建一个 part球心默认在原点。在 assembly 中把每个球体实例平移到随机坐标。用 assembly 级布尔运算把基体减成带空腔的实体。把球体实例用合并操作合并成一个整体的骨料 part。3.2 创建基体 part 和球体 part创建基体立方体用草图拉伸即可注意 sheetSize 如果比模型尺寸小容易出显示问题可以设大一点。from abaqus import * from abaqusConstants import * model_name RandomAggModel if model_name in mdb.models.keys(): del mdb.models[model_name] my_model mdb.Model(namemodel_name) # 创建基体 part base_sketch my_model.ConstrainedSketch(name__base_sketch__, sheetSize200.0) base_sketch.rectangle(point1(0.0, 0.0), point2(Lx, Ly)) matrix_part my_model.Part(nameMatrix, dimensionalityTHREE_D, typeDEFORMABLE_BODY) matrix_part.BaseSolidExtrude(sketchbase_sketch, depthLz)创建球体 part 和创建立方体类似只是需要一个圆草图然后用 BaseSolidRevolve 绕轴旋转 360 度得到球体。def create_sphere_part(model, part_name, radius): sk model.ConstrainedSketch(name__sphere_sketch__, sheetSize200.0) sk.CircleByCenterPerimeter(center(0.0, 0.0), point1(radius, 0.0)) p model.Part(namepart_name, dimensionalityTHREE_D, typeDEFORMABLE_BODY) p.BaseSolidRevolve(sketchsk, angle360.0) return p注意一个坑球体 part 的命名不能和已有的 part 重名所以每个球体 part 都要用唯一编号比如Agg_0、Agg_1。创建完 part 之后球心默认位于原点 (0,0,0)这个默认位置是后面所有平移操作的基准。3.3 装配、平移和布尔运算在 assembly 中实例化所有 part然后逐个平移球体实例到目标位置。assembly my_model.rootAssembly assembly.DatumCsysByDefault(CARTESIAN) # 创建基体实例 matrix_instance assembly.Instance(nameMatrix-1, partmatrix_part, dependentON) # 创建球体实例并平移到目标位置 instance_names [] for i, (r, (x, y, z)) in enumerate(zip(radii, positions)): part_name Agg_%d % i agg_part create_sphere_part(my_model, part_name, r) inst_name %s-1 % part_name inst assembly.Instance(nameinst_name, partagg_part, dependentON) assembly.translate(instanceList(inst_name,), vector(x, y, z)) instance_names.append(inst_name)这里有个需要特别注意的地方assembly.translate的 vector 参数必须是浮点数元组如果坐标里有整型Abaqus 有时会报类型错误。建议所有随机生成的坐标都用 float。接下来是核心的布尔运算。先把所有球体从基体中剪掉得到带空腔的基体再用 InstanceFromBooleanMerge 把球体模型整合成一个独立的骨料 part。# 从基体中减去所有球体实例 assembly.BooleanCut( mainInstanceList(Matrix-1,), toolInstanceListtuple(instance_names) ) # 球体实例在布尔切割后已被删除需要重建球体 part # 先创建新的球体实例 new_instance_names [] for i, (r, (x, y, z)) in enumerate(zip(radii, positions)): part_name Agg_%d % i inst_name AggInst_%d-1 % i inst assembly.Instance(nameinst_name, partmy_model.parts[part_name], dependentON) assembly.translate(instanceList(inst_name,), vector(x, y, z)) new_instance_names.append(inst_name)等等这里有一个我踩过的坑BooleanCut 执行后工具实例会被自动删除但 part 本身还留在 model 里。所以如果要重新实例化这些球体 part完全没问题part 定义依然存在。但如果你之前把 part 的名字在循环里动态生成得保证在重新实例化时能找到对应的 part。另一种更省事的方式把骨料位置生成和建模分成两步——先生成随机数据再在同一个 session 里直接用这些数据创建 part 和实例等到要执行布尔时从 assembly 的 instances 字典里直接取名字。再补充一点dependentON 的参数会让实例引用 part 的几何布尔运算后 part 的几何会被更新如果没有特别需求建议保持默认 or 使用 ON。对于需要独立编辑的 part可以用 dependentOFF但占用的内存会大很多。3.4 合并骨料 part 的细节球体实例都被切出之后如果需要把 N 个独立的球体合并成一个“骨料” part这样便于整体赋予材料、划分网格、统计数量就用 InstanceFromBooleanMerge。agg_result assembly.InstanceFromBooleanMerge( nameAggregates, instancestuple(new_instance_names), originalInstancesDELETE, domainGEOMETRY )这个操作会生成一个名为 Aggregates 的新 part同时原来的球体实例被删除。注意得到的 Aggregates part 是一个包含多个实体的 part在 Abaqus 的 part 树里打开能看到它下面有多个 cell。这完全正常后续可以整体划分网格也可以按 cell 设置不同的网格种子。但合并之后的 part 有个新问题球体和球体之间曾经是独立的几何体合并成一个 part 后Abaqus 默认会把它们作为同一个实体的多个 cell 分开管理在网格划分阶段是支持“每个 cell 独立划分但在接触面共享节点”的这正好符合我们想要骨料之间不共网格、但几何上互不穿透的需求。4. 实操过程与踩坑记录4.1 运行环境和执行方式Abaqus Python 脚本可以通过两种方式运行在 Abaqus/CAE 界面里点击 File - Run Script选择和运行脚本交互式看到每一步建模结果。在命令行里用abaqus cae noGUIscript.py批处理方式运行适合批量生成模型。我建议先交互式跑一遍确认每一步没有报错再用 noGUI 模式批量跑。因为 GUI 模式下 Abaqus 会打开可视化界面能直观看到球体分布情况对排查边界溢出和异常重叠非常有帮助。4.2 常见报错许可证问题有朋友反馈在跑 Abaqus 脚本时遇到许可证报错如 “Abaqus License Manager could not be located” 或 “Invalid license key”。这类问题看起来像是脚本问题其实是环境配置问题。我处理这类问题的一般步骤检查环境变量LM_LICENSE_FILE是否指向正确的许可证服务器和 27011 端口。确认 Abaqus 服务ABAQUSLM在服务列表中是启动状态。如果刚装完 Abaqus 2020 这类版本需要重启电脑或手动启动服务。脚本本身尽量在本机 Abaqus 版本对应的 Python 环境里调试不要用外部 Python 导入 Abaqus 模块两者不互通。这些问题都不难解决但一张“许可证错误”的弹窗往往把新手的注意力从真正的建模代码转移开挺误导人的。4.3 点击 Run Script 后没有反应脚本运行了但 Abaqus 界面啥也没出现这种问题多半是脚本里有语法错误但 Abaqus 静默吃掉了异常。建议在脚本开头加一行输出print(脚本开始执行)然后在关键步骤后面加 print 输出比如“基体 part 创建完成”、“球体 part 循环执行中”、“布尔运算完成”。这样在 Abaqus 的消息区Message Area可以实时看到执行进度快速定位卡在哪一步。我个人的习惯是把执行日志写到文件里import sys log_file open(rD:\temp\agg_log.txt, w) sys.stdout log_file print(Start...)这样即使 Abaqus 崩溃或者消息区被刷爆也能从文件里排查完整日志。4.4 布尔运算失败怎么办布尔运算失败是最常见的 Abaqus 几何内核报错症状是Boolean cut failed或Features cannot be deleted。原因主要有三个第一球体与基体边界相切。因为随机生成的坐标如果刚好让球体边缘与基体表面重合Abaqus 的内核在布尔运算时会出现退化边导致运算失败。解决方法是前面生成坐标时不要等于 r 或 L-r留出一点点余量margin 0.05 # 最小边界余量 x random.uniform(r margin, Lx - r - margin)第二两个球体之间的间隙太小小于建模容差通常约为模型尺寸的 1e-6 量级。解决方法是把 gap_min 适当调大到至少 0.1 mm 或更大。第三球体个数太多一次性对基体做多对象布尔切割容易失败。可以把球体分组每次剪 5~10 个batch_size 5 for idx in range(0, len(instance_names), batch_size): batch instance_names[idx: idxbatch_size] assembly.BooleanCut(mainInstanceList(Matrix-1,), toolInstanceListtuple(batch))注意每次切割后主实例 Matrix-1 会更新几何但实例名保持不变所以可以连续切。切割完成后再通过 InstanceFromBooleanMerge 生成骨料 part。4.5 一个容易忽略的坐标问题球心坐标用随机数生成时如果 A、B 两个球恰好被分配了几乎一样的半径和位置造成两球只有一个极小的接触点后续网格划分会出现一坨极小的单元或尖角几何虽然不是致命错误但划分出来的网格质量非常差容易出现扭曲单元。解决办法可以在重叠判定里增加一个“相对间隙系数”比如要求两球间距不小于球径平均值的 2%min_dist_required (r pr) * 1.02这样虽然会降低可填充率但换来的是网格质量大幅提升值得。5. 模型验证、网格与扩展方向5.1 几何验证模型建好后先别急着划分网格建议先做几何验证。在 Abaqus 里用 Query 工具查看球体个数和总体积也可以写 Python 脚本计算各 part 的体积或者把骨料 part 的体积分数重新算出来和随机阶段的理论值放在一起对比。如果两者偏差超过 2%说明布尔运算中可能有实体丢失需要回头检查。另外可以用 Abaqus 的 Visualization 模块查看剖面图确认球体确实在基体内部没有穿透边界。剖面图的创建用 View Cut 功能随便切一个平面就能看到空腔和骨料的分布情况。5.2 网格划分建议带空腔的基体和合并后的球体是两个独立 part先分别划分网格再用 Tie 约束把它们绑定在一起。这是混凝土细观建模最常用的界面处理方法。基体用 C3D4 四面体单元在空腔周围加密骨料用 C3D10 修正四面体单元或者 C3D8R 六面体单元都行。划分网格时注意基体空腔曲面是应力集中的关键区域网格应设置较小的种子尺寸。如果后续要做 ITZ 界面层可以在骨料表面偏置一层壳单元或者使用 Cohesive 单元模拟界面开裂。球体表面用“Sweep”或“Tet”方法都能处理但尽量避免用结构网格因为球体表面曲率较大结构化网格容易生成严重扭曲单元。5.3 扩展到真实骨料形状写完全部代码后我相信你已经有能力扩展到任意随机形状了。常见做法是用随机凸多面体替代球体先在球面上随机取点再生成凸包就能得到类长方体或不规则棱形骨料。用椭球替代球体对球体 part 做三轴方向缩放即可生成椭球骨料。用 Voronoi 剖分生成凸多边形颗粒这种方法适合模拟晶粒和多边形集料。核心的随机位置算法不变变的只是“单个颗粒怎么画”以及“颗粒间距判定用外接球还是精确多面体距离”。球体其实是最简单的计算情形因为球与球之间的距离判定只有一条公式换成多面体后计算复杂度会明显上升建议用外接球先做粗判再对可能重叠的少数几对做精确相交测试。5.4 周期性边界与级配的后续处理如果要做 RVE 尺度下的周期边界条件随机骨料需要在立方体边界处“穿过对面”也就是把超出边界的部分循环复制到对面。这个在几何层面可以用切分和复制实现但实现起来比普通随机骨料多一道手续。常见的简化做法是让骨料完全在内部不跨边界然后在边界处施加统计均匀的位移约束避免几何上的周期复制。级配方面如果希望粒径分布符合 Fuller 级配可以把均匀分布换成级配函数抽样def fuller_radius(cdf_value, d_max, cdf_max1.0): # 简化 Fuller 反函数 return d_max * math.sqrt(cdf_value / cdf_max)在实际代码里就是先抽一个 (0,1) 的随机数再代入级配反函数得到半径然后继续走无重叠判定和建模流程。整个框架完全兼容。6. 实操总结与经验心得重新看一遍这套流程最核心的其实就是两件事随机数算法和 Abaqus 几何操作。随机数算法本身不复杂但要注意边界、间距、最大尝试次数这些细节Abaqus 几何操作也不复杂关键是把 BooleanCut 和 InstanceFromBooleanMerge 的前后顺序理清楚避免工具实例被删除后找不到对象。代码写完后我习惯把整个随机生成逻辑封装成一个函数输入基体尺寸、半径范围、目标体积分数和随机种子输出装配好的模型。这样不但可以快速切换参数还能在同一脚本里连续生成多个样本。对做 Monte Carlo 随机样本分析的人来说这一步会让效率提升一个数量级。最后提醒一句Abaqus 的 Python 版本和外部 Python 不是一个环境脚本里尽量不要用需要安装的外部库最好只依赖 random 和 math。这样即使换了机器、换了 Abaqus 版本脚本也能无障碍运行。三维随机骨料这条路走通了以后后面再要加 ITZ、嵌入裂纹、真实级配都是在现有骨料框架上叠加功能难度会小很多。