From 1e406a8906f4b3f0863008cfb3f5b2ca4f97f542 Mon Sep 17 00:00:00 2001 From: Petr Date: Thu, 29 Jan 2026 17:48:10 +0100 Subject: [PATCH 1/3] added first version of calc_eos script --- .../calc_equation_of_state.py | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 ml_peg/calcs/bulk_crystal/equation_of_state/calc_equation_of_state.py diff --git a/ml_peg/calcs/bulk_crystal/equation_of_state/calc_equation_of_state.py b/ml_peg/calcs/bulk_crystal/equation_of_state/calc_equation_of_state.py new file mode 100644 index 000000000..d8429e29f --- /dev/null +++ b/ml_peg/calcs/bulk_crystal/equation_of_state/calc_equation_of_state.py @@ -0,0 +1,105 @@ +"""Run calculations for EOS tests.""" + +from __future__ import annotations +from copy import copy +from pathlib import Path +from typing import Any +from datetime import datetime + +from ase import units +from ase.io import read, write + +import pandas as pd +import numpy as np +import pytest + +from ml_peg.calcs.utils.utils import download_s3_data +from ml_peg.models.get_models import load_models +from ml_peg.models.models import current_models + + +from ase.lattice.cubic import SimpleCubicFactory, \ + FaceCenteredCubic, BodyCenteredCubic + +MODELS = load_models(current_models) + +DATA_PATH = Path(__file__).parent / "data" +OUT_PATH = Path(__file__).parent / "outputs" + + + +class A15Factory(SimpleCubicFactory): + "A factory for creating A15 lattices." + xtal_name = "A15" + bravais_basis = [[0, 0, 0], + [0.5, 0.5, 0.5], + [0.5, 0.25, 0.0], + [0.5, 0.75, 0.0], + [0.0, 0.5, 0.25], + [0.0, 0.5, 0.75], + [0.25, 0.0, 0.5], + [0.75, 0.0, 0.5]] + + +A15 = A15Factory() + +lattices = {"BCC": BodyCenteredCubic, + "FCC": FaceCenteredCubic, + "A15": A15} + + +def equation_of_state(calc, lattice, symbol="W", size = (2, 2, 2), + volumes_per_atoms=np.linspace(12, 22, 10, endpoint=False)): + """Compute the equation of state for a given element and lattice. + """ + + # dummy call to have calc_num_atoms available + lattice(symbol="W", latticeconstant=3.16) + lattice_constants = (volumes_per_atoms * lattice.calc_num_atoms()) ** (1 / 3) + + structures = [lattice(latticeconstant=lc, size=size, symbol=symbol) for lc in lattice_constants] + for structure in structures: + structure.calc = calc + + energies = [structure.get_potential_energy() / len(structure) for structure in structures] + + return np.array(lattice_constants), np.array(energies) + + +@pytest.mark.parametrize("mlip", MODELS.items()) +def test_equation_of_state(mlip: tuple[str, Any]) -> None: + """Test equation of state calculation. + For the moment only for three BCC metals""" + + model_name, model = mlip + calc = model.get_calculator() + + volumes_per_atoms = np.linspace(12, 22, 100, endpoint=False) + results = {"V/atom": volumes_per_atoms} + + elements = ["W", "Mo", "Nb"] + + for element in elements: + for lattice_name, lattice in lattices.items(): + start_time = datetime.now() + print(f"Start time for {lattice_name} @ {model_name}: {start_time}") + lattice_constants, energies = equation_of_state( + calc, lattice, symbol=element, volumes_per_atoms=volumes_per_atoms + ) + end_time = datetime.now() + duration = end_time - start_time + hours, remainder = divmod(duration.seconds, 3600) + minutes, seconds = divmod(remainder, 60) + print(f"End time for {lattice_name} @ {model_name}: {end_time}") + print(f"Duration for {lattice_name} @ {model_name}: {hours} hours {minutes} minutes {seconds} seconds") + print(duration) + + + results[f"{element}_{lattice_name}_a"] = lattice_constants + results[f"{element}_{lattice_name}_E"] = energies + + write_dir = OUT_PATH / model_name + df = pd.DataFrame(results) + output_file = write_dir / "eos_results.csv" + write_dir.mkdir(parents=True, exist_ok=True) + df.to_csv(output_file, index=False) \ No newline at end of file From 3254cf495ec180b0a2cca20e14801e775201b080 Mon Sep 17 00:00:00 2001 From: Petr Date: Fri, 30 Jan 2026 11:22:45 +0100 Subject: [PATCH 2/3] split results to files per element --- .../equation_of_state/calc_equation_of_state.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/ml_peg/calcs/bulk_crystal/equation_of_state/calc_equation_of_state.py b/ml_peg/calcs/bulk_crystal/equation_of_state/calc_equation_of_state.py index d8429e29f..4daa9c66f 100644 --- a/ml_peg/calcs/bulk_crystal/equation_of_state/calc_equation_of_state.py +++ b/ml_peg/calcs/bulk_crystal/equation_of_state/calc_equation_of_state.py @@ -75,17 +75,19 @@ def test_equation_of_state(mlip: tuple[str, Any]) -> None: calc = model.get_calculator() volumes_per_atoms = np.linspace(12, 22, 100, endpoint=False) - results = {"V/atom": volumes_per_atoms} - + elements = ["W", "Mo", "Nb"] for element in elements: + results = {"V/atom": volumes_per_atoms} for lattice_name, lattice in lattices.items(): start_time = datetime.now() print(f"Start time for {lattice_name} @ {model_name}: {start_time}") + lattice_constants, energies = equation_of_state( calc, lattice, symbol=element, volumes_per_atoms=volumes_per_atoms ) + end_time = datetime.now() duration = end_time - start_time hours, remainder = divmod(duration.seconds, 3600) @@ -98,8 +100,8 @@ def test_equation_of_state(mlip: tuple[str, Any]) -> None: results[f"{element}_{lattice_name}_a"] = lattice_constants results[f"{element}_{lattice_name}_E"] = energies - write_dir = OUT_PATH / model_name - df = pd.DataFrame(results) - output_file = write_dir / "eos_results.csv" - write_dir.mkdir(parents=True, exist_ok=True) - df.to_csv(output_file, index=False) \ No newline at end of file + write_dir = OUT_PATH / model_name + df = pd.DataFrame(results) + output_file = write_dir / f"{element}_eos_results.csv" + write_dir.mkdir(parents=True, exist_ok=True) + df.to_csv(output_file, index=False) \ No newline at end of file From dd58ee3641773ddf9114e4e57e89121b49336267 Mon Sep 17 00:00:00 2001 From: Petr Date: Fri, 30 Jan 2026 13:38:00 +0100 Subject: [PATCH 3/3] added reference data --- .../calc_equation_of_state.py | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/ml_peg/calcs/bulk_crystal/equation_of_state/calc_equation_of_state.py b/ml_peg/calcs/bulk_crystal/equation_of_state/calc_equation_of_state.py index 4daa9c66f..6431482c5 100644 --- a/ml_peg/calcs/bulk_crystal/equation_of_state/calc_equation_of_state.py +++ b/ml_peg/calcs/bulk_crystal/equation_of_state/calc_equation_of_state.py @@ -5,6 +5,7 @@ from pathlib import Path from typing import Any from datetime import datetime +from glob import glob from ase import units from ase.io import read, write @@ -23,11 +24,10 @@ MODELS = load_models(current_models) -DATA_PATH = Path(__file__).parent / "data" +DATA_PATH = Path(__file__).parent / "../../../../inputs/bulk_crystal/equation_of_state/" OUT_PATH = Path(__file__).parent / "outputs" - class A15Factory(SimpleCubicFactory): "A factory for creating A15 lattices." xtal_name = "A15" @@ -74,15 +74,26 @@ def test_equation_of_state(mlip: tuple[str, Any]) -> None: model_name, model = mlip calc = model.get_calculator() - volumes_per_atoms = np.linspace(12, 22, 100, endpoint=False) - - elements = ["W", "Mo", "Nb"] + fns = list(DATA_PATH.glob("*DFT*")) + + + for fn in fns: + element = fn.name.split("_")[0] + print(f"Starting EOS calculations for {element} with model {model_name}") - for element in elements: + dft_data = pd.read_csv(fn, comment="#") + + volumes_per_atoms = np.linspace(np.round(dft_data[dft_data.columns[0]].min() * 0.95), + np.round(dft_data[dft_data.columns[0]].max() * 1.05), 50, endpoint=False) results = {"V/atom": volumes_per_atoms} - for lattice_name, lattice in lattices.items(): + + phases = [col.split("_")[1] for col in dft_data.columns if "Delta" in col] + + for phase in phases: + assert phase in lattices, f"Lattice {phase} not implemented for EOS test." + lattice = lattices[phase] start_time = datetime.now() - print(f"Start time for {lattice_name} @ {model_name}: {start_time}") + print(f"Start time for {phase} @ {model_name}: {start_time}") lattice_constants, energies = equation_of_state( calc, lattice, symbol=element, volumes_per_atoms=volumes_per_atoms @@ -92,13 +103,13 @@ def test_equation_of_state(mlip: tuple[str, Any]) -> None: duration = end_time - start_time hours, remainder = divmod(duration.seconds, 3600) minutes, seconds = divmod(remainder, 60) - print(f"End time for {lattice_name} @ {model_name}: {end_time}") - print(f"Duration for {lattice_name} @ {model_name}: {hours} hours {minutes} minutes {seconds} seconds") + print(f"End time for {phase} @ {model_name}: {end_time}") + print(f"Duration for {phase} @ {model_name}: {hours} hours {minutes} minutes {seconds} seconds") print(duration) - results[f"{element}_{lattice_name}_a"] = lattice_constants - results[f"{element}_{lattice_name}_E"] = energies + results[f"{phase}_a"] = lattice_constants + results[f"{phase}_E"] = energies write_dir = OUT_PATH / model_name df = pd.DataFrame(results)