1. 问题背景与需求拆解
上周我在处理PFC5.0离散元模拟数据时,遇到了一个看似简单却颇为棘手的问题——如何将颗粒间的接触力按照接触角度进行统计分类。这个需求源于我们需要分析颗粒体系在不同方向上的力链分布特征,而原始数据只提供了每个接触点的力和坐标信息。
在岩土工程和颗粒材料研究中,接触力的角度分布能直观反映体系内部的各向异性特征。比如在边坡稳定分析中,特定方向的力链断裂往往预示着滑移面的形成;在粉体流动研究中,力链的方向性又与物料流动性直接相关。
PFC5.0虽然提供了强大的计算引擎和基础的Fish脚本支持,但接触力的角度统计功能并未内置。官方文档中只有简单的contact.list命令可以输出接触力数据,这迫使我不得不自己搭建整个分析流程。经过反复试错和多方查阅,最终形成了一套可靠的方法论,下面就把这个"从原始数据到极坐标等高线图"的完整解决方案分享给大家。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据提取与预处理
2.1 接触力数据获取
首先需要通过Fish脚本从PFC模型中提取原始接触力数据。关键命令是contact.list,但直接使用它会输出所有接触信息,包括我们不需要的冗余数据。更高效的做法是指定输出字段:
fish复制contact_list = contact.list(contact.prop('force'))
这个命令会返回一个包含所有接触力向量的列表。但要注意,PFC5.0中的接触力是成对出现的(每个接触会被记录两次),所以后续处理时需要去重。我建议在提取数据时就加入去重逻辑:
fish复制unique_contacts = map
while_cycling(contact_list,
if(not map.contains(contact.id),
map.put(contact.id, contact.force)
)
)
2.2 接触角度计算
获取接触力向量后,下一步是计算每个接触面的角度。这里有个关键细节容易被忽略:PFC中的接触力向量是法向力和切向力的合成,而我们需要的是接触面的空间方位角。
正确的计算流程应该是:
- 通过contact.pos获取接触点坐标
- 计算接触点与两个颗粒中心的连线夹角
- 取两个夹角的平分线作为接触面法向
- 将法向向量投影到XY平面(假设是二维情况)
- 用atan2函数计算投影向量的方位角
对应的Fish代码实现:
fish复制angle_calc =
local pos = contact.pos(contact.id)
local ball1 = contact.end1(contact.id)
local ball2 = contact.end2(contact.id)
local vec1 = ball1.pos - pos
local vec2 = ball2.pos - pos
local normal = vec1.normalized + vec2.normalized
local proj = vector(normal.x, normal.y, 0)
return math.atan2(proj.y, proj.x) * 180 / math.pi
注意:这里角度计算结果单位为度,范围在-180到180之间。后续统计时建议转换为0-360度范围。
3. 角度区间统计实现
3.1 分箱策略选择
将连续的角度值离散化为若干个区间是统计分析的关键步骤。这里有几个技术细节需要考虑:
-
分箱数量:通常选择36个区间(每10度一个区间)或72个区间(每5度一个区间)。我经过测试发现,对于大多数颗粒体系,36个区间已经足够反映力链的方向分布特征。
-
边界处理:角度是环形数据,0度和360度实际上是同一点。需要特别注意359-1度这样的跨边界情况。
-
权重分配:简单的计数统计会丢失力的大小信息,更好的做法是按接触力大小进行加权统计。
实现代码示例:
fish复制bins = 36
angle_counts = array.create(bins, 0)
angle_forces = array.create(bins, 0)
loop foreach local c unique_contacts
local angle = (angle_calc(c) + 360) % 360
local bin = math.floor(angle * bins / 360)
local force_mag = c.force.mag
angle_counts[bin] = angle_counts[bin] + 1
angle_forces[bin] = angle_forces[bin] + force_mag
end_loop
3.2 统计结果归一化
原始统计结果会受到接触总数和力值大小的影响,不利于不同模型间的比较。我推荐两种归一化方法:
- 频率归一化:将每个角度区间的计数值除以总接触数
- 力密度归一化:将力值和除以对应角度区间的弧长
第二种方法在物理意义上更明确,实现代码如下:
fish复制total_force = array.sum(angle_forces)
for i = 0 to bins-1
angle_forces[i] = angle_forces[i] / (2 * math.pi * i / bins)
end_for
4. 可视化呈现技巧
4.1 极坐标等高线图绘制
将统计结果可视化的最佳方式是极坐标等高线图。虽然PFC本身没有内置绘图功能,但可以通过Fish输出数据,再用Python/matplotlib等工具绘制。这里分享一个我优化过的Python绘图代码:
python复制import numpy as np
import matplotlib.pyplot as plt
theta = np.linspace(0, 2*np.pi, bins, endpoint=False)
radii = angle_forces # 使用前面计算得到的力密度值
fig = plt.figure(figsize=(8,8))
ax = fig.add_subplot(111, polar=True)
ax.plot(theta, radii, 'r-', linewidth=2)
ax.fill(theta, radii, 'r', alpha=0.3)
ax.set_theta_zero_location('N') # 0度指向北方
ax.set_theta_direction(-1) # 顺时针方向增加角度
plt.title('Contact Force Angular Distribution', pad=20)
plt.show()
4.2 常见绘图问题排查
在实际操作中,可能会遇到以下典型问题:
-
图形出现锯齿状不平滑:这是因为分箱数量不足,可以尝试增加分箱数或使用滑动平均平滑数据
-
极坐标图标签重叠:调整标签位置和字体大小
python复制ax.set_thetagrids(np.arange(0,360,30), frac=1.1)
- 力值差异过大导致图形变形:考虑使用对数刻度
python复制ax.set_rscale('log')
5. 实战中的经验教训
5.1 颗粒形状的影响
最初我假设所有接触面角度都能准确反映力链方向,但在处理非球形颗粒时遇到了问题。对于clump或pebble类颗粒,接触面法向与实际力传递方向可能存在偏差。解决方案是:
- 对于clump颗粒,改用颗粒间中心连线方向作为力链方向
- 对于复杂形状颗粒,建议先进行接触力方向验证
5.2 三维情况的扩展
虽然本文以二维情况为例,但方法可以扩展到三维。此时需要计算两个角度(方位角和仰角),并使用球面坐标系分箱统计。关键修改点:
- 角度计算改用球坐标公式
- 分箱采用立体角划分(如使用HEALPix网格)
- 可视化改用三维球面热图
5.3 性能优化技巧
当接触数量巨大时(>1e6),脚本运行速度可能变慢。我总结的优化方法包括:
- 使用contact.find过滤掉弱接触(力值小于阈值)
- 采用并行计算:将角度范围分区,用多个Fish线程分别处理
- 预分配数组内存,避免动态扩容
fish复制// 示例:接触力阈值过滤
strong_contacts = contact.find(contact.prop('force.mag') > 1e-3)
这套方法在我最近参与的颗粒破碎研究中发挥了重要作用,成功识别出了加载方向上的优势力链演变规律。整个过程虽然踩了不少坑,但最终构建的分析流程不仅解决了当前问题,还形成了可复用的分析框架。对于更复杂的分析需求,比如时变力链演化或考虑摩擦效应的方向分析,这个基础框架也能方便地进行扩展。
