2026年5月に公開された分子向け機械学習ポテンシャル OrbMol v2を動かしてみる。


OrbMol-v2とは

OrbMol-v2 は、 orb-v3をベースにOpen Molecules 2025 (OMol25) and OPoly26 を学習した、分子向け機械学習ポテンシャル (MLIP) である。

OrbMol-v2の特徴の1つは、潜在電荷の割り当てを含むことである。

汎用MLIPとして有名なUMAは荷電状態を扱うことができるが、系内に複数の分子が含まれる場合に、電荷がどこに分布しているかを判別することができず、正しく計算できないことが課題として知られている。

参考: 機械学習ポテンシャルの近似の限界を超えて【イオン系への挑戦】

潜在電荷によってこれが解決できるか検証してみる。

from array import array
import io

from ase.io import write
from IPython.display import HTML
import matplotlib.pyplot as plt
import pandas as pd
import torch
from ase import Atoms
from ase.optimize import FIRE
from orb_models.forcefield.inference.calculator import ORBCalculator
from orb_models.forcefield.pretrained import orbmol_v2
from tqdm.auto import tqdm
atoms_ammonium = Atoms(
    "NH4",
    positions=[
        (0.000, 0.000, 0.000),  # N
        (0.629, 0.629, 0.629),
        (-0.629, -0.629, 0.629),
        (-0.629, 0.629, -0.629),
        (0.629, -0.629, -0.629),
    ],
)
atoms_ammonium.info["charge"] = 1
atoms_ammonium.info["spin"] = 1
# io.StringIO() を用意
string_io = io.StringIO()

# Atoms オブジェクトのデータを 'html' 形式で StringIO に書き込む
write(string_io, atoms_ammonium, format="html")

# StringIO から文字列を取得し、HTMLとしてレンダリングして表示
html_data = string_io.getvalue()
display(HTML(html_data))
ASE atomic visualization </link>
if torch.cuda.is_available():
    device = "cuda"
else:
    device = "cpu"
print(f"{device=}")

model, atoms_adapter = orbmol_v2(
    "~/models/orbmolv2/orbmol-v2-teqabfhg-20260523.ckpt",
    device=device,
    compile=False,
)
device='cpu'


/Users/yu9824/opt/miniforge3/envs/orbmolv2_312/lib/python3.12/site-packages/orb_models/common/training/util.py:20: UserWarning: Setting global torch default dtype to torch.float32.
  warnings.warn(f"Setting global torch default dtype to {torch_dtype}.")
calc = ORBCalculator(model, atoms_adapter=atoms_adapter, device=device)
atoms_ammonium.calc = calc
atoms_ammonium.get_potential_energy()
-1547.5519552230835

以下の条件で計算

分子の配置は分子間の距離がそれぞれ10 nmとなるように直線状に配置した。構造最適化はASEライブラリのFIRE法を使い、力の収束条件を fmax=0.01 (eV/Å, 1 Å=0.1 nm) とした。

https://tech.preferred.jp/ja/blog/%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92%E3%83%9D%E3%83%86%E3%83%B3%E3%82%B7%E3%83%A3%E3%83%AB%E3%81%AE%E8%BF%91%E4%BC%BC%E3%81%AE%E9%99%90%E7%95%8C%E3%82%92%E8%B6%85%E3%81%88%E3%81%A6%E3%80%90%E3%82%A4/

spacing = 100.0  # 10 nm
force_threshold = 0.01

n_mol_list = range(1, 11)

energies_charge_state_orbmolv2 = array("f")
energies_spin_state_orbmolv2 = array("f")
n_molecules_array = array("I", n_mol_list)

for n_molecules in tqdm(n_mol_list):
    system = Atoms()

    # build linear chain
    for i in range(n_molecules):
        mol = atoms_ammonium.copy()
        mol.translate([i * spacing])
        system += mol

    assert len(system) == n_molecules * len(atoms_ammonium)

    # -------------------------
    # charged state calculation
    # -------------------------
    system_charge = system.copy()
    system_charge.info["charge"] = n_molecules
    system_charge.info["spin"] = 1
    system_charge.calc = calc

    opt = FIRE(system_charge, logfile=None)
    opt.run(fmax=force_threshold)

    energies_charge_state_orbmolv2.append(system_charge.get_potential_energy())

    # -------------------------
    # spin state calculation
    # -------------------------
    system_spin = system.copy()
    system_spin.info["charge"] = 0
    system_spin.info["spin"] = n_molecules + 1
    system_spin.calc = calc

    opt = FIRE(system_spin, logfile=None)
    opt.run(fmax=force_threshold)

    energies_spin_state_orbmolv2.append(system_spin.get_potential_energy())

energies_charge_per_mol_orbmolv2 = array(
    "f",
    [e / n for n, e in zip(n_mol_list, energies_charge_state_orbmolv2)],
)

energies_spin_per_mol_orbmolv2 = array(
    "f",
    [e / n for n, e in zip(n_mol_list, energies_spin_state_orbmolv2)],
)
  0%|          | 0/10 [00:00<?, ?it/s]
# reference: https://tech.preferred.jp/ja/blog/%E6%A9%9F%E6%A2%B0%E5%AD%A6%E7%BF%92%E3%83%9D%E3%83%86%E3%83%B3%E3%82%B7%E3%83%A3%E3%83%AB%E3%81%AE%E8%BF%91%E4%BC%BC%E3%81%AE%E9%99%90%E7%95%8C%E3%82%92%E8%B6%85%E3%81%88%E3%81%A6%E3%80%90%E3%82%A4/
energies_charge_per_mol_uma1p2 = array(
    "f",
    [
        -1547.90,
        -1547.20,
        -1546.82,
        -1546.58,
        -1545.63,
        -1544.58,
        -1543.73,
        -1542.76,
        -1541.87,
        -1541.02,
    ],
)
energies_spin_per_mol_uma1p2 = array(
    "f",
    [
        -1552.17,
        -1550.30,
        -1551.54,
        -1551.78,
        -1551.99,
        -1552.03,
        -1552.08,
        -1551.96,
        -1551.80,
        -1551.77,
    ],
)
fig, ax = plt.subplots(figsize=(6.4, 4.8), dpi=144)
ax.plot(
    n_molecules_array,
    energies_charge_per_mol_uma1p2,
    label="Multiple NH4+ (charge=N,spin=1, UMA 1.2)",
    marker="o",
    color="#6a3e97",
)
ax.plot(
    n_molecules_array,
    energies_charge_per_mol_orbmolv2,
    label="Multiple NH4+ (charge=N,spin=1, OrbMol v2)",
    marker="^",
)
ax.plot(
    n_molecules_array,
    energies_spin_per_mol_uma1p2,
    label="Multiple NH4 (charge=0, spin=N+1, UMA 1.2)",
    marker="s",
    color="#349d79",
)
ax.plot(
    n_molecules_array,
    energies_spin_per_mol_orbmolv2,
    label="Multiple NH4 (charge=0, spin=N+1, OrbMol v2)",
    marker="v",
)
ax.set_xlabel("Number of molecules")
ax.set_ylabel("Energy per molecule / eV")
ax.legend()
fig.tight_layout()

png

df = pd.DataFrame(
    {
        "n_molecules": n_molecules_array,
        "charge_uma1p2": energies_charge_per_mol_uma1p2,
        "charge_orbmolv2": energies_charge_per_mol_orbmolv2,
        "spin_uma1p2": energies_spin_per_mol_uma1p2,
        "spin_orbmolv2": energies_spin_per_mol_orbmolv2,
    }
).set_index("n_molecules")
df
charge_uma1p2 charge_orbmolv2 spin_uma1p2 spin_orbmolv2
n_molecules
1 -1547.900024 -1547.877686 -1552.170044 -1552.151367
2 -1547.199951 -1547.823364 -1550.300049 -1552.740601
3 -1546.819946 -1547.772461 -1551.540039 -1552.372437
4 -1546.579956 -1547.733521 -1551.780029 -1552.374878
5 -1545.630005 -1547.726807 -1551.989990 -1552.419189
6 -1544.579956 -1547.734009 -1552.030029 -1552.331055
7 -1543.729980 -1547.720581 -1552.079956 -1552.209106
8 -1542.760010 -1547.712646 -1551.959961 -1552.448608
9 -1541.869995 -1547.697876 -1551.800049 -1552.415405
10 -1541.020020 -1547.677979 -1551.770020 -1552.518555

UMAは、総電荷が増えるほど1分子あたりのエネルギーが増えていってしまったが、OrbMolv2の増加幅は大幅に小さい。

UMAは各分子に総電荷が与えられた状態で計算されてしまうため、エネルギーが増加してしまうと考えられる。他方、OrbMolv2は総電荷を各分子にある程度分配した状態で計算できていると考えられる。

https://github.com/yu9824/orbmolv2-tutorial