Skip to content

Adding cleavage benchmarks#404

Open
vue1999 wants to merge 1 commit intoddmms:mainfrom
vue1999:cleavage_benchmark
Open

Adding cleavage benchmarks#404
vue1999 wants to merge 1 commit intoddmms:mainfrom
vue1999:cleavage_benchmark

Conversation

@vue1999
Copy link

@vue1999 vue1999 commented Mar 4, 2026

Pre-review checklist for PR author

PR author must check the checkboxes below when creating the PR.

Summary

Adding a benchmark to evaluate the accuracy of predicting cleavage energies of crystalline surfaces.

Linked issue

Resolves #403

Progress

  • Calculations
  • Analysis
  • Application
  • Documentation

Testing

MACE omat, ORB v3

New decorators/callbacks

no

@ElliottKasoar ElliottKasoar added the new benchmark Proposals and suggestions for new benchmarks label Mar 4, 2026
Computational cost
------------------

Medium: benchmark involves only single-point calculations, but for 36,718 slab-bulk pairs.
Copy link
Collaborator

Choose a reason for hiding this comment

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

it would be good to give a rough indication of time e.g. hours on gpu and minutes on gpu?

from pathlib import Path
from typing import Any

from ase.io import read
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
from ase.io import read
from ase.io import read, write
from tqdm import tqdm

/ "cleavage_energy"
)

results = {}
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
results = {}
write_dir = OUT_PATH / model_name
write_dir.mkdir(parents=True, exist_ok=True)


results = {}

for mpid_dir in sorted(d for d in data_dir.iterdir() if d.is_dir()):
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
for mpid_dir in sorted(d for d in data_dir.iterdir() if d.is_dir()):
idx = 0
for mpid_dir in tqdm(sorted(d for d in data_dir.iterdir() if d.is_dir())):

Comment on lines +61 to +76
unique_id = slab.info["unique_id"]
results[unique_id] = {
"slab_energy": slab_energy,
"bulk_energy": bulk_energy,
"area_slab": float(slab.info["area_slab"]),
"thickness_ratio": float(slab.info["thickness_ratio"]),
"ref_cleavage_energy": float(slab.info["ref_cleavage_energy"]),
"mpid": slab.info["mpid"],
"miller": slab.info["miller"],
"term": int(slab.info["term"]),
}

OUT_PATH.mkdir(parents=True, exist_ok=True)
output_file = OUT_PATH / f"{model_name}.json"
with open(output_file, "w", encoding="utf-8") as f:
json.dump(results, f)
Copy link
Collaborator

@joehart2001 joehart2001 Mar 22, 2026

Choose a reason for hiding this comment

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

Suggested change
unique_id = slab.info["unique_id"]
results[unique_id] = {
"slab_energy": slab_energy,
"bulk_energy": bulk_energy,
"area_slab": float(slab.info["area_slab"]),
"thickness_ratio": float(slab.info["thickness_ratio"]),
"ref_cleavage_energy": float(slab.info["ref_cleavage_energy"]),
"mpid": slab.info["mpid"],
"miller": slab.info["miller"],
"term": int(slab.info["term"]),
}
OUT_PATH.mkdir(parents=True, exist_ok=True)
output_file = OUT_PATH / f"{model_name}.json"
with open(output_file, "w", encoding="utf-8") as f:
json.dump(results, f)
slab.info.update(
{
"slab_energy": slab_energy,
"bulk_energy": bulk_energy,
"area_slab": float(slab.info["area_slab"]),
"thickness_ratio": float(slab.info["thickness_ratio"]),
"ref_cleavage_energy": float(slab.info["ref_cleavage_energy"]),
"mpid": slab.info["mpid"],
"miller": slab.info["miller"],
"term": int(slab.info["term"]),
}
)
write(write_dir / f"{idx}.xyz", slab, format="extxyz")
idx += 1

@joehart2001
Copy link
Collaborator

joehart2001 commented Mar 22, 2026

Hey @vue1999, thanks for the PR and its looking super good. A few things:

  • ive made some code suggestions to the calc script so we save files in the generalised format. I know there are 30,000 files... but what do you think @ElliottKasoar?
  • ive made some knock on suggestions to the analysis based on these calc changes
  • ive also made suggestions which implement the density scatter plot + structure visualisation, as theres a lot of structures
  • We are also going to in the future add the ability to swithc betwen types of errors e.g. mae and rmse, so ive suggested we just keep mae for now unless you're against this?

Let me know if you've got any ideas or are unsure about any of my suggestions, thanks!

Comment on lines +8 to +13
RMSE:
good: 0.0
bad: 10.0
unit: meV/A^2
tooltip: Root Mean Squared Error of cleavage energies
level_of_theory: PBE
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
RMSE:
good: 0.0
bad: 10.0
unit: meV/A^2
tooltip: Root Mean Squared Error of cleavage energies
level_of_theory: PBE

Comment on lines +11 to +12
from ml_peg.analysis.utils.decorators import build_table, plot_parity
from ml_peg.analysis.utils.utils import load_metrics_config, mae
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
from ml_peg.analysis.utils.decorators import build_table, plot_parity
from ml_peg.analysis.utils.utils import load_metrics_config, mae
from ml_peg.analysis.utils.decorators import build_table, plot_density_scatter
from ml_peg.analysis.utils.utils import (
load_metrics_config,
mae,
write_density_trajectories,
)

Comment on lines +29 to +65

def _load_model_results(model_name: str) -> dict | None:
"""
Load the JSON energy results for a model, or None if absent.

Parameters
----------
model_name
Name of the model whose results file to load.

Returns
-------
dict | None
Parsed JSON results dictionary, or None if the file does not exist.
"""
result_file = CALC_PATH / f"{model_name}.json"
if not result_file.exists():
return None
with open(result_file, encoding="utf-8") as f:
return json.load(f)


def system_names() -> list[str]:
"""
Get list of system identifiers from calc outputs.

Returns
-------
list[str]
Sorted list of unique_id values from the first model with results.
"""
for model_name in MODELS:
data = _load_model_results(model_name)
if data is not None:
return sorted(data.keys())
return []

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
def _load_model_results(model_name: str) -> dict | None:
"""
Load the JSON energy results for a model, or None if absent.
Parameters
----------
model_name
Name of the model whose results file to load.
Returns
-------
dict | None
Parsed JSON results dictionary, or None if the file does not exist.
"""
result_file = CALC_PATH / f"{model_name}.json"
if not result_file.exists():
return None
with open(result_file, encoding="utf-8") as f:
return json.load(f)
def system_names() -> list[str]:
"""
Get list of system identifiers from calc outputs.
Returns
-------
list[str]
Sorted list of unique_id values from the first model with results.
"""
for model_name in MODELS:
data = _load_model_results(model_name)
if data is not None:
return sorted(data.keys())
return []

Comment on lines +96 to +105
@plot_parity(
filename=OUT_PATH / "figure_cleavage_energies.json",
title="Cleavage Energies",
x_label="Predicted cleavage energy / meV/\u00c5\u00b2",
y_label="Reference cleavage energy / meV/\u00c5\u00b2",
hoverdata={
"System": system_names(),
},
)
def cleavage_energies() -> dict[str, list]:
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
@plot_parity(
filename=OUT_PATH / "figure_cleavage_energies.json",
title="Cleavage Energies",
x_label="Predicted cleavage energy / meV/\u00c5\u00b2",
y_label="Reference cleavage energy / meV/\u00c5\u00b2",
hoverdata={
"System": system_names(),
},
)
def cleavage_energies() -> dict[str, list]:
def cleavage_energies() -> dict[str, dict[str, list]]:

Comment on lines +113 to +114
dict[str, list]
Dictionary of reference and predicted cleavage energies in meV/A^2.
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
dict[str, list]
Dictionary of reference and predicted cleavage energies in meV/A^2.
dict[str, dict[str, list]]
Dictionary of model names to ``{"ref": [...], "pred": [...]}`` in meV/A^2.

Comment on lines +116 to +117
results = {"ref": []} | {mlip: [] for mlip in MODELS}
canonical_ids = None
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
results = {"ref": []} | {mlip: [] for mlip in MODELS}
canonical_ids = None
results = {mlip: {"ref": [], "pred": []} for mlip in MODELS}
ref_stored = False
stored_ref = []

Comment on lines +120 to +121
data = _load_model_results(model_name)
if data is None:
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
data = _load_model_results(model_name)
if data is None:
model_dir = CALC_PATH / model_name
if not model_dir.exists():


from ml_peg.app import APP_ROOT
from ml_peg.app.base_app import BaseApp
from ml_peg.app.utils.load import read_plot
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
from ml_peg.app.utils.load import read_plot
from ml_peg.app.utils.build_callbacks import plot_from_table_cell, struct_from_scatter
from ml_peg.app.utils.load import collect_traj_assets, read_density_plot_for_model

"""Cleavage energy benchmark app layout and callbacks."""

def register_callbacks(self) -> None:
"""No interactive callbacks needed."""
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
"""No interactive callbacks needed."""
"""Register callbacks to app."""
density_plots: dict[str, dict] = {}
for model in MODELS:
density_graph = read_density_plot_for_model(
filename=DATA_PATH / "figure_cleavage_energies.json",
model=model,
id=f"{BENCHMARK_NAME}-{model}-density",
)
if density_graph is not None:
density_plots[model] = {"MAE": density_graph}
plot_from_table_cell(
table_id=self.table_id,
plot_id=f"{BENCHMARK_NAME}-figure-placeholder",
cell_to_plot=density_plots,
)
struct_trajs = collect_traj_assets(
data_path=DATA_PATH,
assets_prefix="assets/surfaces/cleavage_energy",
models=MODELS,
traj_dirname="density_traj",
suffix=".extxyz",
)
for model in struct_trajs:
struct_from_scatter(
scatter_id=f"{BENCHMARK_NAME}-{model}-density",
struct_id=f"{BENCHMARK_NAME}-struct-placeholder",
structs=struct_trajs[model],
mode="traj",
)

Comment on lines +38 to +41
scatter = read_plot(
DATA_PATH / "figure_cleavage_energies.json",
id=f"{BENCHMARK_NAME}-figure",
)
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
scatter = read_plot(
DATA_PATH / "figure_cleavage_energies.json",
id=f"{BENCHMARK_NAME}-figure",
)

docs_url=DOCS_URL,
table_path=DATA_PATH / "cleavage_energy_metrics_table.json",
extra_components=[
Div(scatter, style={"marginTop": "20px"}),
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
Div(scatter, style={"marginTop": "20px"}),
Div(id=f"{BENCHMARK_NAME}-figure-placeholder"),
Div(id=f"{BENCHMARK_NAME}-struct-placeholder"),

full_app = Dash(
__name__,
assets_folder=DATA_PATH.parent.parent,
suppress_callback_exceptions=True,
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
suppress_callback_exceptions=True,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new benchmark Proposals and suggestions for new benchmarks

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cleavage energy benchmark

3 participants