Skip to content

力场

MolPy 把力场当作可查询、可验证的数据——在它被写入引擎文件之前,始终是透明的。

为什么把结构和参数分开?

经典分子动力学里,体系的物理行为完全由力场决定——也就是定义原子间相互作用的方程和参数集。参数设错是模拟流程中最隐蔽的错误来源。原子类型不对或二面角项缺失,程序不会报错,只会给出看似合理但实际错误的结果。

多数模拟工具把结构和参数捆在同一个文件里(LAMMPS data + coefficients、GROMACS topology)。这样一来,投入昂贵计算之前很难单独检查参数分配。MolPy 把它们分开存放,参数分配正确与否,在运行模拟之前就能验证。

先定义参数,再计算数值

很多库把力场定义和数值计算揉成一团。设一个参数,立刻得到一个数值对象,中间没有任何可以检查或验证的状态。MolPy 刻意把这两步拆开。

力场在被编译成可执行的数值对象之前,应该始终是可检查的数据结构。

参数化正是静默错误的高发区。原子类型搞错、键参数缺失或不一致——这些问题最好在模型还是透明数据结构的时候发现,而不是等它被打包成数组或特定引擎的文件之后。

三层结构:Style、Type、Potential

MolPy 的力场数据分为三层:

ForceField
├── AtomStyle "full"
│   ├── AtomType "CT"  (mass=12.011, charge=-0.18)
│   └── AtomType "HC"  (mass=1.008, charge=0.06)
├── BondStyle "harmonic"
│   ├── BondType "CT-HC"  (k=340.0, r0=1.09)
│   └── BondType "CT-CT"  (k=268.0, r0=1.529)
├── AngleStyle "harmonic"
│   └── AngleType "HC-CT-HC"  (k=33.0, theta0=107.8)
├── DihedralStyle "opls"
│   └── DihedralType "HC-CT-CT-HC"  (K1=0.0, K2=0.0, K3=0.3, K4=0.0)
└── PairStyle "lj126/cut"
    ├── PairType "CT"  (epsilon=0.066, sigma=3.50)
    └── PairType "HC"  (epsilon=0.030, sigma=2.50)

Style 定义一类相互作用及其参数契约——谐振键、OPLS 二面角、Lennard-Jones 对都是不同的 Style。Type 是某个 Style 下的一条具体参数记录。Potentials 求值器是数值实体,由完整的力场模型生成,在已赋类型的 Frame 上运行。数值内核位于 molrs Rust 扩展中。

整个流程始终是:定义 styles → 填入 types → 求值为 potentials。

构建最小力场

先创建 ForceField,定义原子类型。原子类型是基础——所有成键和非键相互作用都引用它们。

import molpy as mp

ff = mp.AtomisticForcefield(name="tutorial", units="real")

# "full" 对应 LAMMPS atom_style full(每原子电荷 + 分子 ID)
atom_style = ff.def_atomstyle("full")
ct = atom_style.def_type("CT", mass=12.011, charge=-0.18, element="C")
hc = atom_style.def_type("HC", mass=1.008,  charge=0.06,  element="H")
oh = atom_style.def_type("OH", mass=15.999, charge=-0.68, element="O")

键、角、二面角和对 styles 遵循同样的模式:创建 style,然后用显式参数名添加 types。

bond_style = ff.def_bondstyle("harmonic")
bond_style.def_type(ct, hc, k=340.0, r0=1.09)
bond_style.def_type(ct, ct, k=268.0, r0=1.529)
bond_style.def_type(ct, oh, k=320.0, r0=1.41)

angle_style = ff.def_anglestyle("harmonic")
angle_style.def_type(hc, ct, hc, k=33.0, theta0=107.8)

dihedral_style = ff.def_dihedralstyle("opls")
dihedral_style.def_type(hc, ct, ct, hc, K1=0.0, K2=0.0, K3=0.3, K4=0.0)

# "lj126/cut" = 带截断的 12-6 Lennard-Jones(LAMMPS:lj/cut)
pair_style = ff.def_pairstyle("lj126/cut")
pair_style.def_type(ct, epsilon=0.066, sigma=3.50)
pair_style.def_type(hc, epsilon=0.030, sigma=2.50)
pair_style.def_type(oh, epsilon=0.170, sigma=3.12)

到这里,力场还是一份完整的数据结构。没有创建任何数值内核。所有内容仍然可读可改。

检查模型

导出之前,先把力场当数据检查一遍。文件可能语法正确,但参数仍然是错的。

单个 type 通过字典接口暴露参数。

print(f"CT mass={ct['mass']}, charge={ct['charge']}")
print(f"CT element={ct.get('element')}")

bt = bond_style.get_type_by_name("CT-OH")
print(f"CT-OH: k={bt['k']}, r0={bt['r0']}")

遍历所有 styles 和 types,可以快速掌握模型的全局状态。

from molpy.core.forcefield import Style, Type

for style in ff.get_styles(Style):
    types = style.get_types(Type)
    print(f"style={style.name!r}  [{len(types)} types]")
    for t in types:
        params = {k: v for k, v in t.params.kwargs.items()}
        print(f"  {t.name}: {params}")

按名称查找可以直接定位到特定的 style 或 type。

bs = ff.get_style("bond", "harmonic")
ct_ct = bs.get_type_by_name("CT-CT")
print(f"CT-CT k={ct_ct['k']}")

求值为 Potentials

求值是模型的第一个严格完整性测试。ff.to_potentials() 返回一个惰性的 Potentials——它不关联任何 frame(len() == 0,不可迭代)。要计算数值,需要传入一个已赋类型的 Frame:一个包含坐标的 atoms block,加上携带 type 列的成键 block(bondsangles 等)。数值内核在 molrs Rust 扩展中运行。

import numpy as np

# 最小帧:两个原子相距 1.2 Å,由一个 CT-HC 键连接。
frame = mp.Frame()
atoms = mp.Block()
atoms.insert("x", np.array([0.0, 1.2]))
atoms.insert("y", np.array([0.0, 0.0]))
atoms.insert("z", np.array([0.0, 0.0]))
frame["atoms"] = atoms

bonds = mp.Block()
bonds.insert("atomi", np.array([0], dtype=np.uint32))
bonds.insert("atomj", np.array([1], dtype=np.uint32))
bonds.insert("type", np.array(["CT-HC"], dtype=str))
frame["bonds"] = bonds

pots = ff.to_potentials()
energy = pots.calc_energy(frame)
forces = pots.calc_forces(frame)
print(f"energy = {energy}")
print(f"forces =\n{forces}")

如果引用的 type 缺失或必需参数不存在,求值会在此处抛出异常,而不是默默给出错误的结果。

导出到模拟引擎

模型内部一致之后,序列化就只是接口问题,和建模无关。同一个力场可以渲染成不同引擎的格式,无需重新定义物理参数。

LAMMPS

import io
from molpy.io.forcefield import LAMMPSForceFieldWriter

buf = io.StringIO()
writer = LAMMPSForceFieldWriter(buf, precision=4)
writer.write(ff)
print(buf.getvalue())

GROMACS

from molpy.io.forcefield.top import GromacsForceFieldWriter

GromacsForceFieldWriter("system.itp", precision=4).write(ff)

XML

from molpy.io.forcefield import XMLForceFieldWriter

XMLForceFieldWriter("system.xml", precision=6).write(ff)

何时需要扩展内置 Styles

实际项目总归会遇到内置 styles 不够用的情况——Morse 键、Buckingham 对、自定义扭转势。新形式的数值内核在 molrs Rust 扩展中添加;Python 端只需暴露一个轻量的 Style,并为每个导出后端注册参数格式化器。

完整扩展方案见扩展力场

力场不在分子内部

还有一个值得明确指出的区别:结构和参数化是相关但独立的。分子可以在没有类型之前就存在。赋类型的系统也可以在力场导出之前就存在。MolPy 保留这些边界,目的是让模型验证和格式转换的逻辑更容易推理。

另请参阅:原子结构与拓扑Block 与 Frame