| """Copyright (c) Meta Platforms, Inc. and affiliates.""" |
|
|
| from __future__ import annotations |
|
|
| import os |
| import pickle |
| from argparse import ArgumentParser, Namespace |
| from pathlib import Path |
|
|
| import pandas as pd |
| from ase.io.trajectory import Trajectory |
| from pymatgen.analysis.phase_diagram import PatchedPhaseDiagram, PDEntry, PhaseDiagram |
| from pymatgen.core import Structure |
| from pymatgen.entries.compatibility import MaterialsProject2020Compatibility |
| from pymatgen.entries.computed_entries import ComputedStructureEntry |
| from pymatgen.io.ase import AseAtomsAdaptor |
| from pymatgen.io.vasp.inputs import Incar, Poscar |
| from tqdm import tqdm |
|
|
| from flowmm.pandas_ import filter_prerelaxed, maybe_get_missing_columns |
| from flowmm.pymatgen_ import COLUMNS_COMPUTATIONS, to_structure |
|
|
| EntriessType = list[list[ComputedStructureEntry]] |
| EntryDictssType = list[list[dict]] |
| PATH_PPD_MP = Path(__file__).parents[1] / "mp_02072023/2023-02-07-ppd-mp.pkl" |
|
|
|
|
| NAME_FN_FOR_PATH = { |
| "original_index": lambda path: int(path.stem.split("_")[-1]), |
| "method": lambda path: path.stem.rsplit("_", maxsplit=1)[0], |
| } |
|
|
| NAME_FN_FOR_TRAJ = { |
| "energy_initial": lambda traj: traj[0].get_potential_energy(), |
| "energy": lambda traj: traj[-1].get_potential_energy(), |
| "forces": lambda traj: traj[-1].get_forces(), |
| |
| |
| "num_sites": lambda traj: traj[-1].get_global_number_of_atoms(), |
| } |
|
|
|
|
| def get_compat_params(dft_path): |
| incar_file = os.path.join(dft_path, "INCAR") |
| poscar_file = os.path.join(dft_path, "POSCAR") |
| incar = Incar.from_file(incar_file) |
| poscar = Poscar.from_file(poscar_file) |
| param = {"hubbards": {}} |
| if "LDAUU" in incar: |
| param["hubbards"] = dict(zip(poscar.site_symbols, incar["LDAUU"])) |
| param["is_hubbard"] = ( |
| incar.get("LDAU", True) and sum(param["hubbards"].values()) > 0 |
| ) |
| if param["is_hubbard"]: |
| param["run_type"] = "GGA+U" |
| return param |
|
|
|
|
| def get_energy_correction(traj, dft_path): |
| ase_atoms = traj[-1] |
| params = get_compat_params(dft_path) |
| struct = AseAtomsAdaptor.get_structure(ase_atoms) |
| cse_d = { |
| "structure": struct, |
| "energy": ase_atoms.get_potential_energy(), |
| "correction": 0.0, |
| "parameters": params, |
| } |
| cse = ComputedStructureEntry.from_dict(cse_d) |
| out = MaterialsProject2020Compatibility(check_potcar=False).process_entries( |
| cse, |
| |
| clean=True, |
| ) |
| pde = PDEntry(composition=cse.composition, energy=cse.energy) |
| return (cse, cse.energy, pde) |
|
|
|
|
| def apply_name_fn_dict( |
| obj: any, |
| name_fn: dict[str, callable], |
| ) -> dict[str, any]: |
| return {prop: fn(obj) for prop, fn in name_fn.items()} |
|
|
|
|
| def get_record( |
| file: Path, |
| dft_path: Path, |
| ) -> dict[str, any]: |
| record = {} |
| record.update(apply_name_fn_dict(file, NAME_FN_FOR_PATH)) |
| traj = Trajectory(file) |
| record.update(apply_name_fn_dict(traj, NAME_FN_FOR_TRAJ)) |
| _, file_id = os.path.split(file) |
| file_id = file_id.split(".")[0] |
| dft_files = os.path.join(dft_path, file_id) |
| cse, corrected_energy, pde = get_energy_correction(traj, dft_files) |
| record["corrected_energy"] = corrected_energy |
| record["computed_structure_entry"] = cse.as_dict() |
| record["phase_diagram_entry"] = pde.as_dict() |
|
|
| return record |
|
|
|
|
| def get_dft_results( |
| root: Path, |
| |
| ) -> pd.DataFrame: |
| dft_path = os.path.join(root, "dft") |
| output_path = Path(os.path.join(root, "clean_outputs")) |
| files: list[Path] = list(output_path.glob("*.traj")) |
| |
| records = [get_record(file, dft_path) for file in tqdm(files)] |
| df = pd.DataFrame.from_records(records) |
| df["method"] = df["method"].map( |
| { |
| "cdvae": "cdvae", |
| "diffcsp_mp20": "diffcsp_mp20", |
| "diffscp_mp20": "diffcsp_mp20", |
| } |
| ) |
| df["e_per_atom_dft"] = df["energy"] / df["num_sites"] |
| df["e_per_atom_dft_initial"] = df["energy_initial"] / df["num_sites"] |
| return df |
|
|
|
|
| def get_patched_phase_diagram_mp(path: Path) -> PatchedPhaseDiagram: |
| with open(path, "rb") as f: |
| ppd_mp = pickle.load(f) |
| return ppd_mp |
|
|
|
|
| def get_e_hull_from_phase_diagram( |
| phase_diagram: PhaseDiagram | PatchedPhaseDiagram, |
| structure: Structure | dict, |
| ) -> float: |
| """returns e_hull_per_atom""" |
| structure = to_structure(structure) |
| try: |
| return phase_diagram.get_hull_energy_per_atom(structure.composition) |
| except (ValueError, AttributeError, ZeroDivisionError): |
| return float("nan") |
|
|
|
|
| def get_e_hull_per_atom_from_pymatgen( |
| phase_diagram: PhaseDiagram | PatchedPhaseDiagram, |
| pde: PDEntry | dict, |
| ) -> tuple[dict, float]: |
| """returns e_hull_per_atom""" |
| pde = PDEntry.from_dict(pde) |
| try: |
| out = phase_diagram.get_decomp_and_e_above_hull(pde, allow_negative=True) |
| except (ValueError, AttributeError, ZeroDivisionError): |
| out = ({}, float("nan")) |
| return out |
|
|
|
|
| def main(args: Namespace) -> None: |
| |
| print("readying json_in") |
| df = pd.read_json(args.json_in) |
| print("potentially getting missing columns") |
| df = maybe_get_missing_columns(df, COLUMNS_COMPUTATIONS) |
| print("filtering to those which are prerelaxed") |
| if args.maximum_nary is not None: |
| print(f"and maximum nary={args.maximum_nary}") |
| df = filter_prerelaxed( |
| df, |
| args.num_structures, |
| maximum_nary=args.maximum_nary, |
| ) |
|
|
| print(f"loading the saved mp phase diagram at {PATH_PPD_MP=}") |
| ppd_mp = get_patched_phase_diagram_mp(PATH_PPD_MP) |
| e_hulls = [get_e_hull_from_phase_diagram(ppd_mp, s) for s in df["structure"]] |
| out = pd.DataFrame(data={"e_hull_per_atom": e_hulls}) |
| out.index = df.index |
| out["e_above_hull_per_atom_chgnet_gen"] = (df["e_gen"] / df["num_sites"]) - out[ |
| "e_hull_per_atom" |
| ] |
| out["e_above_hull_per_atom_chgnet"] = (df["e_relax"] / df["num_sites"]) - out[ |
| "e_hull_per_atom" |
| ] |
|
|
| df_dft = get_dft_results(args.root_dft_clean_outputs) |
| decomp_and_e = [ |
| get_e_hull_per_atom_from_pymatgen(ppd_mp, p) |
| for p in df_dft["phase_diagram_entry"] |
| ] |
|
|
| decomposition, e_above_hull_dft_per_atom_corrected = zip(*decomp_and_e) |
| df_dft["e_above_hull_per_atom_dft_corrected"] = list( |
| e_above_hull_dft_per_atom_corrected |
| ) |
| df_dft["decomposition"] = list(decomposition) |
|
|
| if args.method is not None: |
| df_dft["method"] = args.method |
| df_dft = df_dft[df_dft["original_index"].isin(df.index)] |
| df_dft = df_dft.set_index("original_index") |
| out["e_above_hull_per_atom_dft"] = df_dft["e_per_atom_dft"] - out["e_hull_per_atom"] |
| out["e_above_hull_per_atom_dft_initial"] = ( |
| df_dft["e_per_atom_dft_initial"] - out["e_hull_per_atom"] |
| ) |
|
|
| out["e_above_hull_per_atom_dft_corrected"] = df_dft[ |
| "e_above_hull_per_atom_dft_corrected" |
| ] |
| out["decomposition"] = df_dft["decomposition"] |
|
|
| |
| out.to_json(Path(args.json_out)) |
| print(f"wrote file to: ") |
| print(f"{args.json_out}") |
|
|
|
|
| if __name__ == "__main__": |
| parser = ArgumentParser() |
| parser.add_argument("json_in", type=Path, help="prerelaxed dataframe") |
| parser.add_argument("json_out", type=Path, help="new dataframe") |
| parser.add_argument("-n", "--num_structures", type=int, default=None) |
| parser.add_argument( |
| "--root_dft_clean_outputs", |
| type=Path, |
| default=None, |
| help="root dir which contains a folder called `dft` and `clean_outputs`.", |
| required=True, |
| ) |
| parser.add_argument( |
| "--maximum_nary", |
| type=int, |
| default=None, |
| help="Any queries to structures with higher nary are avoided.", |
| ) |
| parser.add_argument("--method", type=str, default=None) |
|
|
| args = parser.parse_args() |
|
|
| main(args) |
|
|