2. 计算化学任务的对象建模

在计算化学里,一个任务通常由若干彼此耦合的输入决定:分子结构、基组、泛函、溶剂模型、计算类型、资源需求。如果把这些信息直接拼成命令行,例如 g16 -m "B3LYP/6-31G(d) opt freq",短期看很方便,长期却会带来三个问题:无法复用、无法校验、无法批量调度。对象建模的目标,就是把这些输入抽象成结构化的、可序列化的对象,让任务描述成为可存储、可传递、可校验、可调度的统一格式。

一、任务描述的最小对象模型

一个计算化学任务可以抽象为 Task 对象,它至少包含五个核心字段:molecule(分子结构)、method(理论方法)、job(计算类型)、environment(环境/溶剂)、resources(资源需求)。每个字段本身又是一个子对象,而不是一个字符串。例如 method 不应写成 "B3LYP/6-31G(d)",而应拆成 functional、basis、dispersion 等独立字段,这样程序才能对“泛函是否支持色散校正”“基组是否包含极化函数”做规则校验。

分子结构对象 Molecule 需要保存原子序数、坐标、电荷、自旋多重度、坐标单位(Å 或 Bohr)以及来源(SMILES、XYZ、PDB、内坐标)。坐标单位必须显式声明,因为 Gaussian 默认输入是 Å,而某些内部工具使用 Bohr;如果单位不明确,几何优化会得到完全错误的结果。电荷与自旋多重度也必须成对出现,例如 charge=0, multiplicity=1 表示闭壳层单重态,charge=1, multiplicity=2 表示阳离子自由基。

二、可序列化格式的设计

“可序列化”意味着对象能无损地转换为 JSON、YAML 或 MessagePack,并能从这些格式还原。JSON 是最常用的选择,因为它与语言无关、易于阅读、便于版本控制。下面是一个任务描述示例,采用 JSON 表达:

{
  "task_id": "opt-freq-0001",
  "molecule": {
    "atoms": ["C", "H", "H", "H", "O", "H"],
    "coords": [[0.0,0.0,0.0],[1.09,0.0,0.0],[-0.36,1.03,0.0],[-0.36,-0.51,0.89],[0.0,0.0,1.43],[0.89,0.0,1.43]],
    "unit": "angstrom",
    "charge": 0,
    "multiplicity": 1,
    "source": "xyz"
  },
  "method": {
    "functional": "B3LYP",
    "basis": "6-31G(d)",
    "dispersion": "GD3BJ"
  },
  "job": {
    "type": "opt+freq",
    "solvent_model": "PCM",
    "solvent": "water"
  },
  "resources": {
    "cores": 8,
    "memory_gb": 16,
    "walltime_h": 24,
    "queue": "normal"
  }
}

这个格式的关键在于:每个字段都有明确的类型和取值域。unit 只能是 angstrom 或 bohr;multiplicity 必须是正整数;cores 必须是 2 的幂或调度器允许的值。字段一旦固定,就可以写校验函数,在任务提交前拦截错误。

三、对象之间的关系与约束

对象建模不只是“把字段列出来”,还要表达字段之间的约束。例如:job.type 为 opt+freq 时,method 必须包含 basis;solvent_model 为 PCM 时,solvent 不能为空;multiplicity 与 charge 的奇偶性必须一致(电子数为偶数时 multiplicity 为奇数)。这些约束可以用 JSON Schema 描述,也可以在代码中用 pydantic 或 dataclass 实现。

下图展示了任务对象的核心结构、字段关系以及从对象到序列化格式的映射路径。图中左侧是对象模型,右侧是序列化后的 JSON 片段,中间用箭头表示字段映射与约束检查。

Task 对象模型 Molecule atoms / coords / unit charge / multiplicity Method functional / basis dispersion Job type / solvent_model solvent Resources cores / memory_gb walltime_h / queue 约束检查 charge 与 multiplicity 奇偶性一致 opt+freq 必须有 basis PCM 必须有 solvent cores 为 2 的幂 序列化 JSON { "task_id" : "opt-freq-0001", "molecule" : { ... }, "method" : { ... }, "job" : { ... }, "resources" : { ... } } // 校验通过后写入队列 // 可版本控制、可 diff // 可被调度器直接读取 通过 关键原则:字段类型明确、单位显式声明、约束可校验、序列化无损。任务描述一旦固定,即可被调度器、数据库和版本控制系统统一处理。 示例参数仅用于说明格式,不代表实际计算结果。

四、从对象到调度:统一任务描述格式的落地

有了对象模型和序列化格式,下一步是让它真正进入计算流程。一个典型的落地方式是:用户提交 JSON 任务描述 → 校验器检查字段与约束 → 调度器读取 resources 字段分配节点 → 生成器把对象转换为 Gaussian/ORCA/Psi4 输入文件 → 计算完成后把输出解析回对象,附加 energy、frequencies、status 等结果字段。这样,任务描述不仅是输入,也是结果记录的载体。

在实际工程中,建议把任务描述分为“输入部分”和“结果部分”。输入部分由用户填写,结果部分由程序回填。两者用同一个 task_id 关联,便于追溯。例如,一个优化+频率任务完成后,结果对象可以包含 final_energy_hartree、imaginary_frequencies、converged 等字段。如果 imaginary_frequencies 不为空,说明结构不是极小点,需要调整初始构型或方法。

实践建议:在项目初期就固定任务描述的字段命名和单位约定,并写入 JSON Schema。后续新增计算类型时,只扩展 job.type 的枚举值,而不是随意增加顶层字段。这样可以让任务描述保持稳定,避免“每换一个计算类型就换一套格式”的混乱。

五、常见建模错误与排查

对象建模中最常见的错误有三类。第一类是单位缺失或混用:坐标写成了 Bohr 但标注为 Å,导致优化后键长异常。排查方法是检查 unit 字段,并在生成输入文件时统一转换为目标程序要求的单位。第二类是电荷与自旋多重度不匹配:例如 charge=0, multiplicity=2 对应奇数电子,但闭壳层分子应为偶数电子,程序会报错或给出无意义结果。排查方法是计算电子数 = 各原子序数之和 − charge,再判断奇偶性。第三类是资源需求与任务规模不匹配:cores=8 但 memory_gb=2,导致大基组计算频繁读写磁盘。排查方法是根据基函数数量和原子数估算内存,通常每核至少 2–4 GB。

统一任务描述格式的价值,不在于“看起来整齐”,而在于它让计算化学任务从“一次性命令”变成“可管理的数据”。当任务可以被序列化、校验、排队、追溯和复现时,自动化才真正有了基础。下一阶段的工作,就是在这个对象模型之上,构建任务生成、提交、监控和结果解析的完整流水线。