ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LAMMPS fix python/invoke:在分子动力学模拟过程中周期性调用 Python 回调函数

LAMMPS fix python/invoke:在分子动力学模拟过程中周期性调用 Python 回调函数
  • 科研
  • 科学计算
  • 高性能计算

【免费下载链接】lammps

Public development project of the LAMMPS MD software package

项目地址:https://gitcode.com/gh_mirrors/la/lammps
点击查看免费下载

fix python/invoke是 LAMMPS PYTHON 包提供的一个特殊 fix,它允许你在模拟运行过程中,每隔 N 个时间步自动调用一个预先定义好的 Python 函数。读透本文,你将掌握两种回调时机(post_force与end_of_step)的语义区别、回调函数的编写规范(含通过指针回连 LAMMPS 的方法)、该 fix 在源码层面的调用链(GIL 加锁、PyCapsule 指针传递、compute 步长注册),并能直接复用仓库自带的完整示例脚本完成自己的开发。

命令语法与参数

官方文档(doc/src/fix_python_invoke.rst)给出的完整语法为:

fix ID group-ID python/invoke N callback function_name

各参数含义如下:

参数说明
IDfix 的唯一标识符,此 fix 内部并不使用它
group-ID目标原子组,此 fix 内部并不使用它(可任意填写,惯例写all)
python/invoke本 fix 的样式名
N执行周期:每 N 个时间步调用一次回调,必须为正整数
callback回调时机,取值为post_force或end_of_step
function_namePython 回调函数的函数名,必须已存在于当前 Python 解释器的全局作用域中

其中两种回调时机的语义为:

  • post_force= 每 N 个时间步、在原子受力计算(force computation)完成之后回调;
  • end_of_step= 每 N 个时间步、在时间步末尾回调。

从源码可以确认参数校验逻辑:构造函数中要求参数总数恰好为 6(否则报Illegal fix python/invoke command),N解析后必须大于 0,且callback只接受post_force与end_of_step两个字符串,任何其它取值都会触发Unsupported callback name for fix python/invoke错误。这些检查位于 src/PYTHON/fix_python_invoke.cpp。

值得注意的是,ID和group-ID虽然出现在语法中,但实现里完全忽略它们:从 fix_python_invoke.h 的类定义看,FixPythonInvoke类只保存了lmpPtr、pFunc、selected_callback、nvalid等成员,没有任何与组或 fix ID 相关的状态。因此输入脚本中写fix pf all python/invoke ...里的all只是一个占位写法。

官方文档示例:定义回调并注册两个周期

文档给出的最小完整示例如下,它演示了如何在输入脚本中用python命令内联定义两个回调函数,并分别以两种时机、每 50 步注册一次:

python post_force_callback here """ from lammps import lammps def post_force_callback(lammps_ptr, vflag): lmp = lammps(ptr=lammps_ptr) # access LAMMPS state using Python interface """ python end_of_step_callback here """ def end_of_step_callback(lammps_ptr): lmp = lammps(ptr=lammps_ptr) # access LAMMPS state using Python interface """ fix pf all python/invoke 50 post_force post_force_callback fix eos all python/invoke 50 end_of_step end_of_step_callback

示例中有几个关键点:

  1. 回调函数的签名由回调时机决定。post_force回调接收两个参数:第一个是 LAMMPS 指针对象,第二个是vflag(整型,对应该 fix 被触发时的计算标志);end_of_step回调只接收 LAMMPS 指针一个参数。
  2. 指针对象用于回连 LAMMPS。回调的第一个参数是一个封装了LAMMPS *的指针对象,在 Python 侧通过from lammps import lammps导入模块后以lammps(ptr=lammps_ptr)构造接口实例,随后可用完整的 LAMMPS 库接口查询或操作模拟状态(例如extract_global、extract_compute、set_系列函数等)。
  3. 函数必须位于全局作用域。回调函数必须定义在嵌入式 Python 解释器的 "main"(__main__)模块全局作用域中。可以用python ... here内联定义(如上),也可以从其它 Python 模块导入;如果 LAMMPS 本身是由 Python 通过库接口驱动的,驱动端解释器中定义的函数同样可被找到。

从 C++ 实现看这一"全局作用域"要求:构造函数通过PyImport_AddModule("__main__")获取 main 模块,再用PyObject_GetAttrString(pyMain, fname)按名字查找函数,找不到即报Could not find Python function错误(见 fix_python_invoke.cpp)。这也意味着嵌套在其它函数内部的定义、或以import之外的方式注入的名字都不会被找到。

完整实战示例:examples/python/in.fix_python_invoke

仓库提供了可直接运行的完整输入脚本 examples/python/in.fix_python_invoke,它搭建了一个 4000 原子的 3D Lennard-Jones 熔化体系,并同时启用两种回调,每 50 步打印一次当前时间步:

# 3d Lennard-Jones melt units lj atom_style atomic lattice fcc 0.8442 region box block 0 10 0 10 0 10 create_box 1 box create_atoms 1 box mass 1 1.0 velocity all create 3.0 87287 pair_style lj/cut 2.5 pair_coeff 1 1 1.0 1.0 2.5 neighbor 0.3 bin neigh_modify every 20 delay 0 check no python end_of_step_callback here """ from __future__ import print_function from lammps import lammps def end_of_step_callback(lmp): L = lammps(ptr=lmp) t = L.extract_global("ntimestep") print("### END OF STEP ###", t) def post_force_callback(lmp, v): L = lammps(ptr=lmp) t = L.extract_global("ntimestep") print("### POST_FORCE ###", t) """ fix 1 all nve fix 2 all python/invoke 50 end_of_step end_of_step_callback fix 3 all python/invoke 50 post_force post_force_callback thermo 50 run 250

配套的运行日志(examples/python/log.27Nov18.fix_python_invoke.g++.1)记录了该脚本在单核下的实际输出,可用于验证回调行为是否符合预期。脚本中的细节值得注意:

  • neigh_modify every 20 delay 0 check no控制邻居列表更新策略,与回调本身无关,但体现了典型的熔化模拟配置;
  • 两个回调都通过extract_global("ntimestep")读取全局时间步并print。由于该脚本在单核下运行,每个打印只出现一次;多核运行时每个 MPI 进程都会执行本地副本的回调(详见后文"并行运行时注意事项")。

源码解析:回调是如何被调度的

理解 src/PYTHON/fix_python_invoke.cpp 的实现,能澄清这个 fix 的几个关键设计:

1. 回调时机的注册——setmask()机制

setmask()直接返回selected_callback(即POST_FORCE或END_OF_STEP位掩码),LAMMPS 的 modify 框架据此决定在时间步生命周期的哪个阶段调用该 fix。此外,setup(int vflag)中当选择了post_force时会转调post_force(vflag),这保证初始化阶段的受力计算也会触发回调逻辑。

2. 步长周期——nextvalid()与 compute 注册

构造函数与init()中都会计算nvalid = nextvalid()(公式为(ntimestep/nevery + 1)*nevery,即下一个能被nevery整除的步),并调用modify->addstep_compute_all(nvalid)把所有 compute 的调用步对齐到该值。源码注释解释了原因:Python 回调内部可能触发 compute 求值,而 LAMMPS 无法预先知道会触发哪些 compute,因此统一把它们都注册进来,避免步长不整除导致的错误。init()中的重新注册则针对minimize之后ntimestep被重置的场景——如果nvalid < update->ntimestep,说明之前发生过能量极小化,需要重新对齐。

3. 实际回调——GIL 与 PyObject_CallFunction

两个回调入口post_force(int vflag)和end_of_step()的骨架一致(见 fix_python_invoke.cpp):

void FixPythonInvoke::post_force(int vflag) { if (update->ntimestep % nevery != 0) return; // python code may invoke computes so wrap with clear/add modify->clearstep_compute(); PyUtils::GIL lock; char fmt[] = "Oi"; PyObject * result = PyObject_CallFunction((PyObject*)pFunc, fmt, (PyObject*)lmpPtr, vflag); if (!result) { PyUtils::Print_Errors(); error->all(FLERR,"Fix python/invoke post_force() method failed"); } ... }

要点:

  • 周期判断:update->ntimestep % nevery != 0时直接返回,post_force每个时间步都会被 modify 框架调到,但只有落在 N 的整数倍步才真正执行回调。
  • GIL 加锁:PyUtils::GIL是一个 RAII 包装类(定义于 src/PYTHON/python_utils.h),构造时PyGILState_Ensure()、析构时PyGILState_Release(),保证每次进入 CPython API 前正确持有全局解释器锁,这对嵌入式解释器在多线程/MPI 环境中的正确性至关重要。
  • 参数格式串:post_force用"Oi"格式传指针对象 + 整型 vflag;end_of_step用"O"只传指针对象。这与前面回调签名要求一一对应。
  • 指针封装:构造函数中lmpPtr = PyCapsule_New((void *)lmp, nullptr, nullptr),即 LAMMPS 指针是以 PyCapsule 形式传给 Python 的,Python 侧lammps(ptr=...)正是读取这个 Capsule 还原出 C++ 实例。
  • 错误处理:若 Python 函数抛出异常导致result为空,先打印 Python 侧异常(PyUtils::Print_Errors()调用PyErr_Print()后PyErr_Clear()),再以Fix python/invoke post_force() method failed或... end_of_step() method failed终止运行。

重要警告:回调中不要执行输入脚本命令

官方文档特别警告:虽然可以从回调中通过库函数访问 LAMMPS 状态,但尝试在其中执行输入脚本命令(即lmp.command(...)/lmp.file(...))最好情况是不生效,最坏情况会导致未定义行为。源码层面的佐证是,python命令文档(doc/src/python.rst)明确说明:当 Python 函数在时间步中间(例如被fix python/invoke每步调用)触发时,无法像在两次run之间那样安全地执行任意输入脚本命令。fix_python_invoke.cpp 中用modify->clearstep_compute()包裹回调的注释("python code may invoke computes so wrap with clear/add")也表明回调上下文是一个受限的、需要特殊保护的时间步中段环境。

因此,fix python/invoke回调的合理用途是查询状态(extract_global、extract_compute、extract_atom等)、写入外部文件,或调用不涉及完整命令流的库接口,而不是驱动run、pair_style这类命令——后者的正确用法是 python 命令的invoke模式(在输入脚本流程中显式调用),而不是嵌在时间步中间的回调里。

重启、输出与运行控制行为

官方文档明确了该 fix 的"静默"属性:

  • 重启文件:不向二进制重启文件写入任何该 fix 的信息,重启后需重新创建 fix 并重新定义/加载 Python 函数;
  • fix_modify:所有fix_modify选项对该 fix 均无意义;
  • 输出:该 fix 不存储任何全局或逐原子量,无法通过thermo/compute/variable等输出机制访问;
  • run start/stop:其任何参数都不能用作run命令的start/stop关键词;
  • 能量极小化:在minimize期间不会被调用(与源码中init()对 minimize 后步长重新对齐的逻辑相呼应)。

构建前提:PYTHON 包与 CMake 构建

该 fix 属于 PYTHON 包,只有编译时启用了该包的 LAMMPS 才可用。从仓库现状看,构建方式需要注意一个文档与代码的差异:

  • 原始文档提到"启用该功能的设置位于lib/python/Makefile.lammps,参见lib/python/README",但当前仓库中已不存在lib/python/目录;
  • src/PYTHON/Install.sh 明确提示:"The PYTHON package no longer supports the legacy build system. Please build LAMMPS with CMake instead."

也就是说,当前版本应使用 CMake 构建。CMake 侧的实际逻辑在 cmake/Modules/Packages/PYTHON.cmake:

find_package(Python COMPONENTS Interpreter) ... find_package(Python REQUIRED COMPONENTS Interpreter Development) target_link_libraries(lammps PRIVATE Python::Python) target_compile_definitions(lammps PRIVATE -DLMP_PYTHON)

即启用 PYTHON 包后,CMake 会查找系统中的 Python(需要解释器 + 开发头文件/库),把 LAMMPS 与Python::Python链接,并定义LMP_PYTHON宏来编入src/PYTHON/下的源码。典型配置命令形如:

cmake -BLAMMPS_build -BLAMMPS_build/pymodules -GLammps -DCMAKE_BUILD_TYPE=Release -DLAMMPS_KSPACE=pack -DLAMMPS_PACKAGE=python -DLAMMPS_BUILD_OMP=ON

(以-DLAMMPS_PACKAGE=python打开该包;若回调中要使用from lammps import lammps回连 LAMMPS,还需按 python/lammps 模块的构建说明生成 Python 绑定模块,且可执行文件与共享库需由同一份源码构建以保持一致。)

另一个来自 python 命令文档 的限制值得留意:如果回调代码要import lammps模块,那么嵌入式解释器和 LAMMPS 共享库必须链接到同一个 Python 运行时;若你的 Python 解释器是静态链接的(文档指出 Conda 环境有时如此),加载 LAMMPS 共享库会创建第二个隐藏的__main__模块,导致之前定义的全局函数和变量不可见。

并行运行时注意事项

与python命令的说明一致,使用fix python/invoke在 MPI 并行下运行时有三点:

  1. 每个 MPI 进程都会执行自己的回调副本。回调里的print会输出 P 份(P 为进程数),且可能交错出现。示例脚本中的打印在多核下会重复,这是正常现象;
  2. 回调内 import 的 Python 模块会由每个进程从磁盘加载。大规模并行时这可能成为瓶颈,某些超算甚至禁止 Python 从磁盘加载模块,需要预先构建好包含所需模块的 Python 库再链接 LAMMPS;
  3. 若回调调用需要全局通信的库函数(例如触发涉及 MPI_Allreduce 的操作),必须保证所有进程的回调都发起同样的调用,否则可能出现死锁。

与 python 命令的分工

fix python/invoke与 python 命令 同属 PYTHON 包,但职责不同,可配合使用:

python命令fix python/invoke
触发时机输入脚本处理流程中(invoke、变量求值)模拟运行中,每 N 步
典型用途复杂循环、条件逻辑、调用run等命令周期性采样状态、外部联动
可否执行输入脚本命令两次run之间可以不建议(最好情况不生效,最坏未定义行为)
参数传递input/format/return关键词定义固定为指针(+post_force时的 vflag)

一个常见的组合模式是:用python source/python <name> ... file xxx.py加载共享的回调函数文件,再用一条或多条fix ... python/invoke以不同周期、不同时机注册其中多个函数,从而在一个输入脚本中集中管理所有 Python 逻辑。

小结

  • fix ID group-ID python/invoke N callback function_name:ID 与 group-ID 被忽略,N 为回调周期,callback 取post_force(受力计算后回调,附带 vflag 参数)或end_of_step(步末回调);
  • 回调函数须定义在嵌入式解释器__main__全局作用域,第一个参数是指向 LAMMPS 的 PyCapsule 指针,通过lammps(ptr=...)还原为库接口实例;
  • 源码上,回调在 GIL 保护下经PyObject_CallFunction触发,并用clearstep_compute/addstep_compute保护可能受扰的 compute 步长;
  • 该 fix 不参与重启、输出与 fix_modify,不在minimize中触发;
  • 当前版本请使用 CMake(-DLAMMPS_PACKAGE=python)构建,旧版lib/python/Makefile.lammps流程已废弃。

可继续深入的文件:src/PYTHON/fix_python_invoke.cpp、examples/python/in.fix_python_invoke、cmake/Modules/Packages/PYTHON.cmake、doc/src/python.rst。

  • 科研
  • 科学计算
  • 高性能计算

【免费下载链接】lammps

Public development project of the LAMMPS MD software package

项目地址:https://gitcode.com/gh_mirrors/la/lammps
点击查看免费下载

相关推荐

上一篇:HS2-HF_Patch:Honey Select 2终极增强补丁完整安装与使用指南
下一篇:Visual Studio Code JavaScript 扩展入门指南:ESLint、代码片段与路径/npm IntelliSense 实战

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进