Skip to content
1 change: 1 addition & 0 deletions docs/source/user_guide/benchmarks/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ Benchmarks
non_covalent_interactions
tm_complexes
conformers
molecular_dynamics
40 changes: 40 additions & 0 deletions docs/source/user_guide/benchmarks/molecular_dynamics.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
==================
Molecular dynamics
==================

Liquid densities
================

Summary
-------

Performance in predicting densities for 61 organic liquids, each system consisting of
about 1000 atoms. The dataset covers aliphatic, aromatic molecules, as well as different
functional groups and halogenated molecules.

Metrics
-------

1. Density error

For each system, the density is calculated by taking the average density of an NPT molecular
dynamics run. The initial part of the simulation, here 500 ps, is omitted from the density
calculation. This is compared to the reference density, obtained from experiment.

Computational cost
------------------

Low: tests are likely to take several days to run on GPU.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Low: tests are likely to take several days to run on GPU.
Very high: tests are likely to take several days to run on GPU.

?


Data availability
-----------------

Input structures:

* Weber et al., Efficient Long-Range Machine Learning Force Fields for
Liquid and Materials Properties.
arXiv:2505.06462 [physics.chem-ph]

Reference data:

* Experiment
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
"""Analyse the liquid densities benchmark."""

from __future__ import annotations

from pathlib import Path

from ase.io import Trajectory, write
import numpy as np
import pytest

from ml_peg.analysis.utils.decorators import build_table, plot_parity
from ml_peg.analysis.utils.utils import (
build_dispersion_name_map,
load_metrics_config,
mae,
)
from ml_peg.app import APP_ROOT
from ml_peg.calcs import CALCS_ROOT
from ml_peg.models.get_models import load_models
from ml_peg.models.models import current_models

MODELS = load_models(current_models)
D3_MODEL_NAMES = build_dispersion_name_map(MODELS)


LOG_INTERVAL_PS = 0.1
EQUILIB_TIME_PS = 500

CALC_PATH = CALCS_ROOT / "molecular_dynamics" / "liquid_densities" / "outputs"
OUT_PATH = APP_ROOT / "data" / "molecular_dynamics" / "liquid_densities"


METRICS_CONFIG_PATH = Path(__file__).with_name("metrics.yml")
DEFAULT_THRESHOLDS, DEFAULT_TOOLTIPS, DEFAULT_WEIGHTS = load_metrics_config(
METRICS_CONFIG_PATH
)


def labels() -> list:
"""
Get list of system names.

Returns
-------
list
List of all system names.
"""
for model_name in MODELS:
labels = [path.stem for path in (CALC_PATH / model_name).glob("*.log")]
return labels


def compute_density(fname, density_col=13):
"""
Compute average density from NPT log file.

Parameters
----------
fname
Path to the log file.
density_col
Which column the density numbers are in.

Returns
-------
float
Average density in g/cm3.
"""
density_series = []
with open(fname) as lines:
for line in lines:
items = line.strip().split()
if len(items) != 15:
continue
density_series.append(float(items[13]))
skip_frames = int(EQUILIB_TIME_PS / LOG_INTERVAL_PS)
return np.mean(density_series[skip_frames:])


@pytest.fixture
@plot_parity(
filename=OUT_PATH / "figure_liquid_densities.json",
title="Densities",
x_label="Predicted density / kcal/mol",
y_label="Reference density / kcal/mol",
hoverdata={
"Labels": labels(),
},
)
def liquid_densities() -> dict[str, list]:
"""
Get liquid densities for all systems.

Returns
-------
dict[str, list]
Dictionary of all reference and predicted densities.
"""
results = {"ref": []} | {mlip: [] for mlip in MODELS}
ref_stored = False

for model_name in MODELS:
for label in labels():
atoms = Trajectory(CALC_PATH / model_name / f"{label}.traj")[-1]

results[model_name].append(
compute_density(CALC_PATH / model_name / f"{label}.log")
)
if not ref_stored:
results["ref"].append(atoms.info["exp_density"])

# Write structures for app
structs_dir = OUT_PATH / model_name
structs_dir.mkdir(parents=True, exist_ok=True)
write(structs_dir / f"{label}.xyz", atoms)
ref_stored = True
return results


@pytest.fixture
def get_mae(liquid_densities) -> dict[str, float]:
"""
Get mean absolute error for liquid densities.

Parameters
----------
liquid_densities
Dictionary of reference and predicted liquid densities.

Returns
-------
dict[str, float]
Dictionary of predicted liquid densities errors for all models.
"""
results = {}
for model_name in MODELS:
results[model_name] = mae(liquid_densities["ref"], liquid_densities[model_name])
return results


@pytest.fixture
@build_table(
filename=OUT_PATH / "liquid_densities_metrics_table.json",
metric_tooltips=DEFAULT_TOOLTIPS,
thresholds=DEFAULT_THRESHOLDS,
mlip_name_map=D3_MODEL_NAMES,
)
def metrics(get_mae: dict[str, float]) -> dict[str, dict]:
"""
Get all metrics.

Parameters
----------
get_mae
Mean absolute errors for all models.

Returns
-------
dict[str, dict]
Metric names and values for all models.
"""
return {
"MAE": get_mae,
}


def test_liquid_densities(metrics: dict[str, dict]) -> None:
"""
Run liquid densities test.

Parameters
----------
metrics
All new benchmark metric names and dictionary of values for each model.
"""
return
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
metrics:
MAE:
good: 0.0
bad: 0.4
unit: g/cm^3
tooltip: Mean Absolute Error for all systems
level_of_theory: Experimental density
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
level_of_theory: Experimental density
level_of_theory: Experimental

Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Run liquid densities app."""

from __future__ import annotations

from dash import Dash
from dash.html import Div

from ml_peg.app import APP_ROOT
from ml_peg.app.base_app import BaseApp
from ml_peg.app.utils.build_callbacks import (
plot_from_table_column,
struct_from_scatter,
)
from ml_peg.app.utils.load import read_plot
from ml_peg.models.get_models import get_model_names
from ml_peg.models.models import current_models

MODELS = get_model_names(current_models)
BENCHMARK_NAME = "DipCONFS"
DOCS_URL = "https://ddmms.github.io/ml-peg/user_guide/benchmarks/molecular_dynamics.html#liquid_densities"
DATA_PATH = APP_ROOT / "data" / "molecular_dynamics" / "liquid_densities"


class LiquidDensitiesApp(BaseApp):
"""Liquid densities benchmark app layout and callbacks."""

def register_callbacks(self) -> None:
"""Register callbacks to app."""
scatter = read_plot(
DATA_PATH / "figure_liquid_densities.json",
id=f"{BENCHMARK_NAME}-figure",
)

model_dir = DATA_PATH / MODELS[0]
if model_dir.exists():
labels = sorted([f.stem for f in model_dir.glob("*.xyz")])
structs = [
f"assets/molecular_dynamics/liquid_densities/{MODELS[0]}/{label}.xyz"
for label in labels
]
else:
structs = []

plot_from_table_column(
table_id=self.table_id,
plot_id=f"{BENCHMARK_NAME}-figure-placeholder",
column_to_plot={"MAE": scatter},
)

struct_from_scatter(
scatter_id=f"{BENCHMARK_NAME}-figure",
struct_id=f"{BENCHMARK_NAME}-struct-placeholder",
structs=structs,
mode="struct",
)


def get_app() -> LiquidDensitiesApp:
"""
Get liquid densities benchmark app layout and callback registration.

Returns
-------
LiquidDensitiesApp
Benchmark layout and callback registration.
"""
return LiquidDensitiesApp(
name=BENCHMARK_NAME,
description=(
"Performance in predicting organic liquid densities."
"Reference data is experimental densities."
),
docs_url=DOCS_URL,
table_path=DATA_PATH / "liquid_densities_metrics_table.json",
extra_components=[
Div(id=f"{BENCHMARK_NAME}-figure-placeholder"),
Div(id=f"{BENCHMARK_NAME}-struct-placeholder"),
],
)


if __name__ == "__main__":
full_app = Dash(__name__, assets_folder=DATA_PATH.parent.parent)
benchmark_app = get_app()
full_app.layout = benchmark_app.layout
benchmark_app.register_callbacks()
full_app.run(port=8063, debug=True)
Loading
Loading