编程指南
PSuM 编程指南¶
如果你还不熟悉 C++ 模板、lambda 和 SYCL 的基本写法,可以先读 PSuM 中的 C++。本文只讲“如何使用 PSuM 写程序”,内部实现细节见 框架设计。
本文面向已经能编译 PSuM 示例并准备写自己算例的用户。如果环境还没配置好,可优先考虑 Docker 部署(如果你的环境可控如个人电脑,这比手动配置简单得多)。 如果 Docker 不适用,阅读 依赖安装 和 构建系统说明 手动配置。
环境确认¶
在写代码前,通常需要先加载项目生成的环境变量:
env_load.sh 通常由 env_scan.sh 生成,用来设置 AdaptiveCpp、Eigen、CUDA、UMFPACK 等依赖的路径。若要关闭 CUDA 或调整架构,应在 config.mk.local 中覆盖 USE_CUDA、CUDA_ARCH 等选项,而不是修改 config.mk。
确认环境是否基本可用,可以先运行:
该测试通常需要几分钟时间,请耐心等待。
或进入一个示例目录执行 make。programming.md 中的代码默认已经处在上述环境下。
代码入口与命名空间¶
大多数用户程序只需要包含主入口:
psum::prelude 汇总了常用模块的命名空间,包括 tag、序列化、粒子容器、场量、边界、场求解器、碰撞、随机数和若干 SYCL 工具。
使用 prelude 可以让代码更精炼。
如果你希望显式控制依赖,也可以包含模块入口:
并相应地使用模块命名空间:
using namespace psum::field;
using namespace psum::particle_container;
using namespace psum::field_solver;
应当优先使用 include/psum/ 下的公共入口。
src/ 下的头文件是项目内部组织方式,当前许多实现以 header-only 形式存在,但它们不一定都是稳定的用户入口。
Hello PSuM!¶
下面是一个最小程序:创建计算设备、在设备端数组中写入数据,再拷贝回主机端。
#include <psum/psum.hpp>
#include <iostream>
using namespace psum::prelude;
int main() {
sycl::queue q{sycl::default_selector_v};
std::cout << q.get_device().get_info<sycl::info::device::name>() << std::endl;
device_vector<double> values(q, 16);
values.for_each([&](sycl::handler& h) {
return [=](double& x) {
x = 3.14;
};
});
auto host_values = values.to_host();
std::cout << host_values[0] << std::endl;
}
for_each 使用 PSuM 中常见的“双层 lambda”写法:外层在主机端准备资源,内层在设备端并行执行。内层通常应使用 [=] 值捕获。更详细的解释见 PSuM 中的 C++。
定义粒子类型¶
PSuM 使用 tag 系统描述粒子属性。使用 tagged_struct:
using Particle = tagged_struct<
tag_bind<property::position, Eigen::RowVector3d>,
tag_bind<property::velocity, Eigen::RowVector3d>,
tag_bind<property::weight, double>,
tag_bind<property::random_seed, uint32_t>
>;
访问属性时使用 get<Tag>(particle):
Particle p;
get<property::position>(p) = Eigen::RowVector3d(0.0, 0.0, 0.0);
get<property::velocity>(p) = Eigen::RowVector3d::Zero();
get<property::weight>(p) = 1.0;
get<property::random_seed>(p) = 1;
常用内置 tag 包括:
property::positionproperty::velocityproperty::accelerationproperty::massproperty::chargeproperty::weightproperty::random_seedproperty::species_idproperty::internal_energy
自定义 tag 只需要继承 abstract_tag 并提供 tag_name:
struct init_position : tag::foundation::abstract_tag {
inline const static std::string tag_name = "init_position";
};
如果某个 tag 需要限制绑定类型,可以实现 check<T>()。tag 系统的编译期查找和类型验证细节属于内部机制,见 框架设计。
一个常见坑是局部变量名和 tag 名冲突。例如局部变量 mass 会遮蔽 property::mass。实际代码中建议保留 property:: 前缀。
使用 device_vector¶
device_vector<T> 是设备端动态数组,类似 std::vector<T>,但数据存放在 SYCL 设备内存中。
常见构造方式:
std::vector<Particle> host_particles(100);
device_vector<Particle> from_host(q, host_particles);
device_vector<Particle> buffer(q, 1000); // 预留长度:1000
主机和设备之间的数据移动:
设备端遍历:
from_host.for_each([&](sycl::handler& h) {
return [=](Particle& p) {
get<property::velocity>(p) = Eigen::RowVector3d::Zero();
};
});
设备端填充新元素需要先在外层获取访问器:
device_vector<Particle> selected(q, 1000);
from_host.for_each([&](sycl::handler& h) {
auto out = selected.get_access(h);
return [=](Particle& p) {
if (get<property::position>(p).x() > 0.0) {
out.push_back(p);
}
};
});
push_back 是线程安全的,但容量不会自动增长。如果写入数量超过预留容量,容器会进入 overflow 状态,后续主机端操作会尽早暴露这个错误。
使用 particle_group¶
particle_group 是 PSuM 中主要的粒子容器。它建立在 device_vector 之上,额外支持惰性删除和空间复用。
如果:
- 数据有频繁的删除需求
- 不需要设备端填充
则建议使用 particle_group。
定义粒子组时需要指定粒子类型和 validator:
using ParticleGroup = particle_group<Particle, pos_x_nan_is_invalid>;
ParticleGroup particles(q);
particles.insert(host_particles);
pos_x_nan_is_invalid 使用 position.x() 是否为 NaN 判断粒子是否有效。遍历 particle_group 时,无效粒子会被自动跳过:
particles.for_each([&](sycl::handler& h) {
return [=](Particle& p) {
if (get<property::position>(p).x() > 1.0) {
ParticleGroup::validator::make_invalid(p);
}
};
});
删除只是标记无效,并不会立刻移动内存。后续可以使用:
size() 表示有效粒子数,capacity() 表示已分配容量。particle_group 的双数组结构和压缩算法见 框架设计。
使用网格和场量¶
场量系统负责在规则网格上存储电荷密度、电势、电场等物理量。
创建网格:
常见场量有两类:
device_field:设备端场量,适合在核函数中读写。host_field:主机端场量,在 host 端直接操作会更方便。
对于粒子-场交互的场景,通常都应使用device_field 。
示意:
使用时关注三件事:维度、位置类型(节点或单元中心)、数值类型。
插值与沉积¶
PIC 中粒子和场之间有两个基本操作:
- interp:从网格场插值到粒子位置。
- add_back:把粒子权重沉积回网格。
PSuM 提供的核心函数包括:
auto value = interp(pos, field_acc);
auto grad = interp_diff(pos, field_acc);
add_back(pos, weight, field_acc);
这些函数通常在粒子遍历内使用:
particles.for_each([&](sycl::handler& h) {
auto rho_acc = rho.get_access(h);
return [=](Particle& p) {
auto pos = get<property::position>(p);
auto w = get<property::weight>(p);
add_back(pos, w, rho_acc); // 使用 rho_acc 而非 rho!
};
});
不同场量支持的插值/沉积方式有差异。
如果只需要最近邻版本,可以使用 interp_nearest 和 add_back_nearest。插值核、张量积展开和原子沉积的实现见 框架设计。
求解 Poisson 方程¶
典型静电 PIC 程序可以概括为:
for each time step:
rho.setZero()
particles.for_each(deposit charge to rho)
solver.solve(phi, rho)
particles.for_each(push particles by -interp_diff(phi); apply boundary conditions)
execute collisions
粒子边界有时可以相当简单并直接写在推动函数中; 粒子碰撞有时也可忽略, 但场求解器是必须的. Poisson 求解器按维度分为:
Poisson_solver_1dPoisson_solver_2dPoisson_solver_3d
使用流程通常是:
1. 创建 grid
2. 创建固定边界或混合边界列表
3. solver.init(grid, boundaries, ...)
4. 每个时间步调用 solver.solve(phi, source)
固定边界(Dirichlet)用于直接指定电势值;混合边界(Robin)用于包含函数值和法向导数的边界条件。
求解器可以使用默认 native 后端,也可以在初始化时选择其他后端。
矩阵装配、ghost 节点消元和 backend 插件机制属于内部实现,见 框架设计。
处理粒子边界¶
粒子边界系统处理粒子轨迹和几何表面的相交。使用思路是:
核函数中的典型调用形式是:
particles.for_each([&](sycl::handler& h) {
auto router_acc = router.get_access(h);
return [=](Particle& p) {
auto old_pos = get<property::position>(p);
// update position here
auto new_pos = get<property::position>(p);
router_acc.deal(
old_pos.x(), old_pos.y(), old_pos.z(),
new_pos.x(), new_pos.y(), new_pos.z(),
p
);
};
});
内置材料行为包括吸收、镜面反射、漫反射和 Maxwell 反射。许多反射类行为还需要粒子具有 random_seed 属性。
处理碰撞¶
PSuM 的碰撞模块主要包括:
- MCC:粒子与背景物种的 Monte Carlo Collision。
- DPMCC:需要显式配对的延迟配对碰撞。
用户侧需要理解三个概念:
- 碰撞模型:描述有哪些入射物种和碰撞通道。
- 物种上下文:绑定粒子组、背景密度场和新粒子 buffer。
- 执行函数:在时间步中调用碰撞模型,并在结束后清理 buffer。
简化流程:
prepare species contexts
execute_mcc_model<Model>(ctx, dt)
execute_all_clean_buffers<SpeciesTuple>(ctx)
自定义碰撞截面、通道选择和配对算法通常较长,不适合放在主教程里。完整写法应通过 examples.md 或具体应用文档说明。
保存、恢复与配置¶
基础保存/读取使用 mas_file:
mas_file fp("state.mas", mas_file::replaceMode);
save(fp, "particles", particles);
load(fp, "particles", particles);
需要管理多个对象时,可以使用 object_manager。它适合断点续跑:使用的对象从 object_manager 对象中注册,下一次运行从已有 MAS 文件中恢复。
配置文件可以用 json_loader:
json_loader loader;
loader.load_json("config.json");
double dt = loader["solver"].obj<double>("dt");
device_vector 和 particle_group 的序列化会涉及设备信息,不同硬件之间不能默认认为可以无条件交换。
下一步读什么¶
- 想看完整示例:读 示例。
- 想理解双层 lambda、模板和 concept:读 PSuM 中的 C++。
- 想理解内部实现:读 框架设计。
- 想贡献代码:读 代码风格规范。