Skip to content

Commit dc333c1

Browse files
Add lattice constants benchmark (#157)
1 parent 5497f56 commit dc333c1

File tree

6 files changed

+594
-0
lines changed

6 files changed

+594
-0
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
============
2+
Bulk Crystal
3+
============
4+
5+
Lattice constants
6+
=================
7+
8+
Summary
9+
-------
10+
11+
Performance in evaluating lattice constants for 23 solids, including pure elements,
12+
binary compounds, and semiconductors.
13+
14+
15+
Metrics
16+
-------
17+
18+
1. MAE (Experimental)
19+
20+
Mean lattice constant error compared to experimental data
21+
22+
For each formula, a bulk crystal is built using the experimental lattice constants and
23+
lattice type for the initial structure. This structure is optimised for each model
24+
using the LBFGS optimiser, with the FrechetCellFilter applied to allow optimisation of
25+
the cell, until the largest absolute Cartesian component of any interatomic force is
26+
less than 0.03 eV/Å. The lattice constants of this optimised structure are then
27+
compared to experimental values.
28+
29+
30+
2. MAE (PBE)
31+
32+
Mean lattice constant error compared to PBE data
33+
34+
Same as (1), but optimised lattice constants are compared to reference PBE data.
35+
36+
37+
Computational cost
38+
------------------
39+
40+
Low: tests are likely to less than a minute to run on CPU.
41+
42+
Data availability
43+
-----------------
44+
45+
Input structures:
46+
47+
* Built from experimental lattice constants from various sources
48+
49+
Reference data:
50+
51+
* Experimental data same as input data
52+
53+
* DFT data
54+
55+
* Batatia, I., Benner, P., Chiang, Y., Elena, A.M., Kovács, D.P., Riebesell, J.,
56+
Advincula, X.R., Asta, M., Avaylon, M., Baldwin, W.J. and Berger, F., 2025. A
57+
foundation model for atomistic materials chemistry. The Journal of Chemical
58+
Physics, 163(18).
59+
* PBE-D3(BJ)

docs/source/user_guide/benchmarks/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ Benchmarks
1111
physicality
1212
molecular_crystal
1313
molecular
14+
bulk_crystal
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
"""Analyse lattice constants benchmark."""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
7+
from ase.io import read, write
8+
import numpy as np
9+
import pytest
10+
11+
from ml_peg.analysis.utils.decorators import build_table, plot_parity
12+
from ml_peg.analysis.utils.utils import load_metrics_config, mae
13+
from ml_peg.app import APP_ROOT
14+
from ml_peg.calcs import CALCS_ROOT
15+
from ml_peg.models.get_models import get_model_names
16+
from ml_peg.models.models import current_models
17+
18+
MODELS = get_model_names(current_models)
19+
CALC_PATH = CALCS_ROOT / "bulk_crystal" / "lattice_constants" / "outputs"
20+
OUT_PATH = APP_ROOT / "data" / "bulk_crystal" / "lattice_constants"
21+
22+
METRICS_CONFIG_PATH = Path(__file__).with_name("metrics.yml")
23+
DEFAULT_THRESHOLDS, DEFAULT_TOOLTIPS, DEFAULT_WEIGHTS = load_metrics_config(
24+
METRICS_CONFIG_PATH
25+
)
26+
27+
28+
def get_crystal_formulae() -> list[str]:
29+
"""
30+
Get list of crystal formulae.
31+
32+
Returns
33+
-------
34+
list[str]
35+
List of crystal formulae from structure files.
36+
"""
37+
formulae = []
38+
for model_name in MODELS:
39+
model_dir = CALC_PATH / model_name
40+
if not model_dir.exists():
41+
continue
42+
struct_files = sorted(model_dir.glob("*-traj.extxyz"))
43+
for struct_file in struct_files:
44+
atoms = read(struct_file)
45+
name = atoms.info["name"]
46+
if name == "SiC":
47+
formulae.extend(("SiC(a)", "SiC(c)"))
48+
else:
49+
formulae.append(name)
50+
break
51+
52+
return formulae
53+
54+
55+
FORMULAE = get_crystal_formulae()
56+
57+
58+
@pytest.fixture
59+
@plot_parity(
60+
filename=OUT_PATH / "figure_lattice_consts_exp.json",
61+
title="Lattice constants",
62+
x_label="Predicted lattice constant / Å",
63+
y_label="Experimental lattice constant / Å",
64+
hoverdata={
65+
"Formula": FORMULAE,
66+
},
67+
)
68+
def lattice_constants_exp() -> dict[str, list]:
69+
"""
70+
Get experimental and predicted lattice constant for all crystals.
71+
72+
Returns
73+
-------
74+
dict[str, list]
75+
Dictionary of experimental and predicted lattice energies.
76+
"""
77+
results = {"ref": []} | {mlip: [] for mlip in MODELS}
78+
ref_stored = False
79+
80+
for model_name in MODELS:
81+
model_dir = CALC_PATH / model_name
82+
83+
if not model_dir.exists():
84+
continue
85+
86+
struct_files = sorted(model_dir.glob("*-traj.extxyz"))
87+
if not struct_files:
88+
continue
89+
90+
for struct_file in struct_files:
91+
structs = read(struct_file, index=":")
92+
93+
formula = structs[-1].info["name"]
94+
lattice_type = structs[-1].info["lattice_type"]
95+
96+
a_exp = structs[-1].info["a_exp"]
97+
a_pred = structs[-1].cell.lengths()[0]
98+
if formula == "SiC":
99+
c_exp = structs[-1].info["c_exp"]
100+
c_pred = structs[-1].cell.lengths()[2]
101+
else:
102+
c_exp = None
103+
c_pred = None
104+
105+
if lattice_type in ("fcc", "diamond", "rocksalt", "zincblende"):
106+
a_pred = a_pred * np.sqrt(2)
107+
elif lattice_type == "bcc":
108+
a_pred = a_pred * 2 / np.sqrt(3)
109+
110+
results[model_name].append(a_pred)
111+
if c_pred:
112+
results[model_name].append(c_pred)
113+
114+
# Store reference energies (only once)
115+
if not ref_stored:
116+
results["ref"].append(a_exp)
117+
if c_exp:
118+
results["ref"].append(c_exp)
119+
120+
# Copy individual structure files to app data directory
121+
structs_dir = OUT_PATH / model_name
122+
structs_dir.mkdir(parents=True, exist_ok=True)
123+
write(structs_dir / f"{structs[-1].info['name']}.xyz", structs)
124+
125+
ref_stored = True
126+
127+
return results
128+
129+
130+
@pytest.fixture
131+
@plot_parity(
132+
filename=OUT_PATH / "figure_lattice_consts_dft.json",
133+
title="Lattice constants",
134+
x_label="Predicted lattice constant / Å",
135+
y_label="DFT lattice constant / Å",
136+
hoverdata={
137+
"Formula": FORMULAE,
138+
},
139+
)
140+
def lattice_constants_dft() -> dict[str, list]:
141+
"""
142+
Get DFT and predicted lattice constant for all crystals.
143+
144+
Returns
145+
-------
146+
dict[str, list]
147+
Dictionary of DFT and predicted lattice constants.
148+
"""
149+
results = {"ref": []} | {mlip: [] for mlip in MODELS}
150+
ref_stored = False
151+
152+
for model_name in MODELS:
153+
model_dir = CALC_PATH / model_name
154+
155+
if not model_dir.exists():
156+
continue
157+
158+
struct_files = sorted(model_dir.glob("*-traj.extxyz"))
159+
if not struct_files:
160+
continue
161+
162+
for struct_file in struct_files:
163+
structs = read(struct_file, index=":")
164+
165+
formula = structs[-1].info["name"]
166+
lattice_type = structs[-1].info["lattice_type"]
167+
168+
a_dft = structs[-1].info["a_dft"]
169+
a_pred = structs[-1].cell.lengths()[0]
170+
if formula == "SiC":
171+
c_dft = structs[-1].info["c_dft"]
172+
c_pred = structs[-1].cell.lengths()[2]
173+
else:
174+
c_dft = None
175+
c_pred = None
176+
177+
if lattice_type in ("fcc", "diamond", "rocksalt", "zincblende"):
178+
a_pred = a_pred * np.sqrt(2)
179+
elif lattice_type == "bcc":
180+
a_pred = a_pred * 2 / np.sqrt(3)
181+
182+
results[model_name].append(a_pred)
183+
if c_pred:
184+
results[model_name].append(c_pred)
185+
186+
# Store reference lattice constants (only once)
187+
if not ref_stored:
188+
results["ref"].append(a_dft)
189+
if c_dft:
190+
results["ref"].append(c_dft)
191+
192+
ref_stored = True
193+
194+
return results
195+
196+
197+
@pytest.fixture
198+
def lattice_constant_exp_errors(lattice_constants_exp) -> dict[str, float]:
199+
"""
200+
Get mean absolute error for lattice constants compared to experimental reference.
201+
202+
Parameters
203+
----------
204+
lattice_constants_exp
205+
Dictionary of experimental and predicted lattice constants.
206+
207+
Returns
208+
-------
209+
dict[str, float]
210+
Dictionary of predicted lattice constant errors for all models.
211+
"""
212+
results = {}
213+
for model_name in MODELS:
214+
results[model_name] = mae(
215+
lattice_constants_exp["ref"], lattice_constants_exp[model_name]
216+
)
217+
return results
218+
219+
220+
@pytest.fixture
221+
def lattice_constant_dft_errors(lattice_constants_dft) -> dict[str, float]:
222+
"""
223+
Get mean absolute error for lattice constants compared to DFT reference.
224+
225+
Parameters
226+
----------
227+
lattice_constants_dft
228+
Dictionary of DFT and predicted lattice constants.
229+
230+
Returns
231+
-------
232+
dict[str, float]
233+
Dictionary of predicted lattice constant errors for all models.
234+
"""
235+
results = {}
236+
for model_name in MODELS:
237+
results[model_name] = mae(
238+
lattice_constants_dft["ref"], lattice_constants_dft[model_name]
239+
)
240+
return results
241+
242+
243+
@pytest.fixture
244+
@build_table(
245+
filename=OUT_PATH / "lattice_constants_metrics_table.json",
246+
metric_tooltips=DEFAULT_TOOLTIPS,
247+
thresholds=DEFAULT_THRESHOLDS,
248+
)
249+
def metrics(
250+
lattice_constant_exp_errors: dict[str, float],
251+
lattice_constant_dft_errors: dict[str, float],
252+
) -> dict[str, dict]:
253+
"""
254+
Get all lattice constant metrics.
255+
256+
Parameters
257+
----------
258+
lattice_constant_exp_errors
259+
Mean absolute errors.
260+
lattice_constant_dft_errors
261+
Mean absolute errors.
262+
263+
Returns
264+
-------
265+
dict[str, dict]
266+
Metric names and values for all models.
267+
"""
268+
return {
269+
"MAE (Experimental)": lattice_constant_exp_errors,
270+
"MAE (PBE)": lattice_constant_dft_errors,
271+
}
272+
273+
274+
def test_lattice_constants(metrics: dict[str, dict]) -> None:
275+
"""
276+
Run lattice constant test.
277+
278+
Parameters
279+
----------
280+
metrics
281+
All lattice constant metrics.
282+
"""
283+
return
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
metrics:
2+
MAE (Experimental):
3+
good: 0.0
4+
bad: 0.2
5+
unit: Å
6+
tooltip: "Mean Absolute Error of lattice constants for all crystals compared to experiments"
7+
level_of_theory: Experimental
8+
MAE (PBE):
9+
good: 0.0
10+
bad: 0.2
11+
unit: Å
12+
tooltip: "Mean Absolute Error of lattice constants for all crystals compared to PBE"
13+
level_of_theory: PBE

0 commit comments

Comments
 (0)