Description
When recomputing energies from saved simulation coordinates using the specialized model checkpoint, a systematic energy shift is observed in some energy components. Interestingly, with SchNet models, shifts in different components can partially compensate each other, making the total energy shift smaller than individual component shifts.
The problem becomes more evident with RANGESchNet models where the shifts do not compensate.
To reproduce
- Run simulation saving energy components, total energy and coordinates
- Load specialized model checkpoint and recompute energies on saved coordinates
- Compare energy components between simulation and rerun
Observed behavior
Mean energy distribution shift $\Delta E = E_{simulation}-E_{rerun}$
| Component |
Shift (kcal/mol) |
| Model (SchNet) |
~-0.1 |
| Model (SchNet/RANGESchNet) |
~2.5 |
| non_bonded |
~0.15 |
| bonds |
~0 |
| angles |
~0 |
| dihedrals |
~0 |
| Forces |
~0 |
Notably there is no energy shift between different rerun $E_{rerun1}-E_{rerun2} = 0$
Key observations:
- Shifts in model energy and
non_bonded partially compensate each other in schnet
- Forces match perfectly (centered at 0)
- Rerun vs rerun gives 0 shift (model is deterministic)
- Shift is present from frame 0 and does not accumulate over time
- Dummy model returning sum of positions shows no shift
Environment
- mlcg version: 0.1.3
- Device:
cuda
- Number of parallel simulations: 5
- Models: Transferable
SchNet and RANGESchNet
Images reporting bug
Code for rerun
Adapted from @kbno.
import torch
import numpy as np
import pandas as pd
from tqdm import tqdm
import os
import mdtraj as md
from copy import deepcopy
from glob import glob
import argparse
from torch_geometric.data.collate import collate
from mlcg.datasets.utils import chunker
import sys
from pathlib import Path
def parse_cli():
dir_path = os.path.dirname(os.path.realpath(__file__))
parser = argparse.ArgumentParser(
description="""
Script for computing energies of bound and unbound frames on mutations for ddG computation.
"""
)
parser.add_argument(
"-n",
"--sim_name",
type=str,
help=(
"name of the simulation"
),
)
parser.add_argument(
"-d",
"--sim_dir",
type=str,
help=(
"directory of the simulation"
),
)
parser.add_argument(
"-m",
"--model_name",
type=str,
default="SchNet",
help=(
"model name: `SchNet` or `RANGESchNet"
),
)
return parser.parse_args()
if __name__ == "__main__":
args = parse_cli()
device = "cuda"
model_path = os.path.join(args.sim_dir, f"{args.sim_name}_specialized_model_and_config.pt")
model, config = torch.load(model_path, weights_only=False)
data, _, _ = collate(
config[0].__class__,
data_list=config,
increment=True,
add_batch=True,
)
model = model.eval().to(device=torch.device(device), dtype=torch.float32)
for param in model.parameters():
param.requires_grad_(False)
data = data.to(torch.device(device))
coords_paths = sorted(glob(os.path.join(args.sim_dir, f"{args.sim_name}_coords_*.npy")))
for path in tqdm(coords_paths, desc=f"Computing energies for {args.sim_name}"):
coords = np.load(path)
# Initialize energy arrays
forces = np.zeros_like(coords)
energies = np.zeros((coords.shape[0], coords.shape[1]), dtype=coords.dtype)
model_energies = np.zeros((coords.shape[0], coords.shape[1]))
bonds_energies = np.zeros((coords.shape[0], coords.shape[1]))
angles_energies = np.zeros((coords.shape[0], coords.shape[1]))
dihedrals_energies = np.zeros((coords.shape[0], coords.shape[1]))
repulsion_energies = np.zeros((coords.shape[0], coords.shape[1]))
# Compute energies
for fr_idx in range(coords.shape[1]):
fr = coords[:, fr_idx]
assert data.pos.shape[0] == fr.shape[0]*fr.shape[1]
data.pos = torch.as_tensor(np.concatenate(fr), device=torch.device(device))
data.out = {}
data = model(data)
forces[:, fr_idx, :, :] = data.out["forces"].detach().cpu().numpy().reshape(coords.shape[0], coords.shape[2], 3)
energies[:, fr_idx] = data.out["energy"].detach().cpu().numpy()
model_energies[:, fr_idx] = data.out[args.model_name]["energy"].detach().cpu().numpy()
bonds_energies[:, fr_idx] = data.out["bonds"]["energy"].detach().cpu().numpy()
angles_energies[:, fr_idx] = data.out["angles"]["energy"].detach().cpu().numpy()
dihedrals_energies[:, fr_idx] = data.out["dihedrals"]["energy"].detach().cpu().numpy()
repulsion_energies[:, fr_idx] = data.out["non_bonded"]["energy"].detach().cpu().numpy()
# Save energies
p = Path(path)
name_forces = p.stem.replace("_coords_", "_forces_rerun_")
name = p.stem.replace("_coords_", "_potential_rerun_")
name_components = p.stem.replace("_coords_", f"_energy_components_rerun_")
np.save(os.path.join(p.parent, name_forces), forces)
np.save(os.path.join(p.parent, name), energies)
np.savez(
os.path.join(p.parent, name_components),
model=model_energies,
bonds=bonds_energies,
angles=angles_energies,
dihedrals=dihedrals_energies,
non_bonded=repulsion_energies
)
Description
When recomputing energies from saved simulation coordinates using the specialized model checkpoint, a systematic energy shift is observed in some energy components. Interestingly, with
SchNetmodels, shifts in different components can partially compensate each other, making the total energy shift smaller than individual component shifts.The problem becomes more evident with
RANGESchNetmodels where the shifts do not compensate.To reproduce
Observed behavior
Mean energy distribution shift$\Delta E = E_{simulation}-E_{rerun}$
Notably there is no energy shift between different rerun$E_{rerun1}-E_{rerun2} = 0$
Key observations:
non_bondedpartially compensate each other in schnetEnvironment
cudaSchNetandRANGESchNetImages reporting bug
Code for rerun
Adapted from @kbno.