mpdd-alignn 1.2.0__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/LICENSE.md +1 -1
  2. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/PKG-INFO +3 -2
  3. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/pretrained.py +180 -60
  4. mpdd_alignn-1.3.0/alignn/tests/test_mpddalignn.py +184 -0
  5. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/mpdd_alignn.egg-info/PKG-INFO +3 -2
  6. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/mpdd_alignn.egg-info/SOURCES.txt +1 -0
  7. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/mpdd_alignn.egg-info/requires.txt +1 -0
  8. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/pyproject.toml +4 -2
  9. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/.github/workflows/publishToPyPI.yaml +0 -0
  10. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/.gitignore +0 -0
  11. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/MANIFEST.in +0 -0
  12. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/README.md +0 -0
  13. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/__init__.py +0 -0
  14. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/cli.py +0 -0
  15. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/config.py +0 -0
  16. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/config.yaml +0 -0
  17. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/data.py +0 -0
  18. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/examples/sample_data/POSCAR-JVASP-10.vasp +0 -0
  19. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/ff/__init__.py +0 -0
  20. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/ff/ff.py +0 -0
  21. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/graphs.py +0 -0
  22. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/models/__init__.py +0 -0
  23. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/models/alignn.py +0 -0
  24. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/models/alignn_atomwise.py +0 -0
  25. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/models/alignn_cgcnn.py +0 -0
  26. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/models/alignn_layernorm.py +0 -0
  27. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/models/dense_alignn.py +0 -0
  28. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/models/densegcn.py +0 -0
  29. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/models/gcn.py +0 -0
  30. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/models/icgcnn.py +0 -0
  31. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/models/modified_cgcnn.py +0 -0
  32. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/models/utils.py +0 -0
  33. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/profile.py +0 -0
  34. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/run_alignn_ff.py +0 -0
  35. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/__init__.py +0 -0
  36. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/alignn_evac.py +0 -0
  37. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/all_train_cgcnn.py +0 -0
  38. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/compare_cfid.py +0 -0
  39. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/cubic_mat_relax.py +0 -0
  40. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/data_1.json +0 -0
  41. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/dataset_props.json +0 -0
  42. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/defect.py +0 -0
  43. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/early_stopping_checker.py +0 -0
  44. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/ev_curve.py +0 -0
  45. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/ev_curve_comp.py +0 -0
  46. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/final_model.py +0 -0
  47. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/graph_viz.py +0 -0
  48. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/make_test_split_cross_pred.py +0 -0
  49. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/plot_ff_results.py +0 -0
  50. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/plot_phonons_ff.py +0 -0
  51. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/predict.py +0 -0
  52. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/predict_db.py +0 -0
  53. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/predict_db_all.py +0 -0
  54. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/train_all_hmof.py +0 -0
  55. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/train_all_hpov.py +0 -0
  56. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/train_all_jv.py +0 -0
  57. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/train_all_jv_class.py +0 -0
  58. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/train_all_jv_dal.py +0 -0
  59. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/train_all_mp.py +0 -0
  60. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/train_all_oqmd.py +0 -0
  61. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/train_all_pdbbind.py +0 -0
  62. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/train_all_qetb.py +0 -0
  63. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/train_all_qm9_jctc.py +0 -0
  64. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/train_all_qmof.py +0 -0
  65. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/train_cgcnn_repo.py +0 -0
  66. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/train_edos_pdos.py +0 -0
  67. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/scripts/train_megnet.py +0 -0
  68. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/tests/test_alignn_ff.py +0 -0
  69. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/tests/test_force_reduction.py +0 -0
  70. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/tests/test_prop.py +0 -0
  71. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/train.py +0 -0
  72. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/train_folder.py +0 -0
  73. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/train_folder_ff.py +0 -0
  74. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/train_props.py +0 -0
  75. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/alignn/utils.py +0 -0
  76. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/0-Cr8Fe18Ni4.POSCAR +0 -0
  77. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/1-Cr16Fe8Ni6.POSCAR +0 -0
  78. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/10-Ce4Ti4O12.POSCAR +0 -0
  79. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/11-Fe10Ni20.POSCAR +0 -0
  80. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/12-Gd4Cr4O12.POSCAR +0 -0
  81. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/13-Fe16Ni14.POSCAR +0 -0
  82. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/14-Fe24Ni6.POSCAR +0 -0
  83. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/15-Ta4Tl4O12.POSCAR +0 -0
  84. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/16-Fe18Ni12.POSCAR +0 -0
  85. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/17-Pr4Ga4O12.POSCAR +0 -0
  86. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/18-Fe28Ni2.POSCAR +0 -0
  87. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/19-Fe4Ni26.POSCAR +0 -0
  88. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/2-Fe8Ni22.POSCAR +0 -0
  89. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/20-Fe8Ni22.POSCAR +0 -0
  90. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/21-Fe10Ni20.POSCAR +0 -0
  91. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/22-Fe10Ni20.POSCAR +0 -0
  92. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/23-Fe12Ni18.POSCAR +0 -0
  93. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/24-Fe16Ni14.POSCAR +0 -0
  94. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/25-Fe12Ni18.POSCAR +0 -0
  95. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/26-Fe8Ni22.POSCAR +0 -0
  96. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/27-Cr28Fe2.POSCAR +0 -0
  97. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/28-Fe26Ni4.POSCAR +0 -0
  98. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/29-Fe12Ni18.POSCAR +0 -0
  99. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/3-Cr18Fe12.POSCAR +0 -0
  100. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/30-Cr26Fe4.POSCAR +0 -0
  101. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/4-Fe30.POSCAR +0 -0
  102. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/5-Cr22Fe8.POSCAR +0 -0
  103. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/6-Fe2Ni28.POSCAR +0 -0
  104. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/7-Cr18Fe12.POSCAR +0 -0
  105. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/8-Cr2Fe16Ni12.POSCAR +0 -0
  106. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/example.SigmaPhase/9-Pb8O12.POSCAR +0 -0
  107. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/mpdd_alignn.egg-info/dependency_links.txt +0 -0
  108. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/mpdd_alignn.egg-info/top_level.txt +0 -0
  109. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/quickstart.ipynb +0 -0
  110. {mpdd_alignn-1.2.0 → mpdd_alignn-1.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2024 Adam M. Krajewski
3
+ Copyright (c) 2024-2026 Adam M. Krajewski
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mpdd-alignn
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: Our version of the NIST-JARVIS ALIGNN optimized in terms of model performance and to some extent reliability, for large-scale deployments over the MPDD infrastructure by Phases Research Lab.
5
- Author-email: "Adam M. Krajewski" <adam@phaseslab.org>
5
+ Author-email: "Adam M. Krajewski" <adam@phaseslab.org>, Ricardo Amaral <rna5137@psu.edu>
6
6
  Project-URL: Homepage, https://github.com/PhasesResearchLab/mpdd-alignn
7
7
  Project-URL: Bug Tracker, https://github.com/PhasesResearchLab/mpdd-alignn/issues
8
8
  Project-URL: Research Page, https://phaseslab.com/mpdd
@@ -34,6 +34,7 @@ Requires-Dist: dgl>=1.1.3
34
34
  Requires-Dist: scikit-learn>=1.3.1
35
35
  Requires-Dist: matplotlib>=3.4.1
36
36
  Requires-Dist: tqdm>=4.60.0
37
+ Requires-Dist: natsort>=8.3.0
37
38
  Requires-Dist: pandas>=1.2.3
38
39
  Requires-Dist: pydantic_settings
39
40
  Requires-Dist: ruamel.yaml
@@ -5,7 +5,7 @@ import os
5
5
  import sys
6
6
  import json
7
7
  import time
8
- from typing import List, Dict, Union
8
+ from typing import List, Dict, Union, Optional
9
9
  from importlib import resources
10
10
 
11
11
  # Extra utility imports
@@ -18,6 +18,7 @@ import pandas as pd
18
18
  tqdm.pandas()
19
19
  from pysmartdl2 import SmartDL
20
20
  from dgl.data.utils import save_graphs, load_graphs
21
+ from natsort import natsorted
21
22
 
22
23
  # ML imports
23
24
  import torch
@@ -83,23 +84,46 @@ def unzip_default_models() -> None:
83
84
  with zipfile.ZipFile(model['model'], 'r') as zip_ref:
84
85
  zip_ref.extractall(model['model'].replace('.zip', ''))
85
86
 
86
- def run_models_from_directory(
87
- directory: str,
88
- mode: str = "serial",
89
- saveGraphs: bool = False):
90
- """Run all default models on all structures in a directory that are either in POSCAR or CIF format."""
91
- # Parse all structures into Atoms objects
92
- atoms_array = []
93
- outputs: List[Dict[str, Union[float, str]]] = []
94
- for file in os.listdir(directory):
95
- if file.endswith(("poscar", "POSCAR", "vasp", "VASP")):
96
- outputs.append({"name": file})
97
- atoms_array.append(Atoms.from_poscar(os.path.join(directory, file)))
98
- elif file.endswith(("cif", "CIF")):
99
- outputs.append({"name": file})
100
- atoms_array.append(Atoms.from_cif(os.path.join(directory, file)))
101
- else:
102
- print(f"Skipping file {file} as it is not a POSCAR or CIF file!", flush=True)
87
+ def _run_pretrained_models(
88
+ atoms_array: List[Atoms],
89
+ outputs: List[Dict[str, Union[float, str]]],
90
+ mode: str = "serial",
91
+ saveGraphs: bool = False,
92
+ models: Optional[List[str]] = None,
93
+ ):
94
+ """Internal helper to run default models on already parsed structures.
95
+
96
+ Parameters
97
+ ----------
98
+ atoms_array : List[Atoms]
99
+ Array of atomic structures to run predictions on.
100
+ outputs : List[Dict[str, Union[float, str]]]
101
+ List of output dictionaries (typically containing file names).
102
+ mode : str, optional
103
+ Either "serial" or "parallel" for execution mode. Default is "serial".
104
+ saveGraphs : bool, optional
105
+ Whether to save computed graphs to disk. Default is False.
106
+ models : List[str], optional
107
+ List of model names to run. If None, all default models are run.
108
+
109
+ Returns
110
+ -------
111
+ List[Dict[str, Union[float, str]]]
112
+ Output dictionaries with model predictions added as new keys.
113
+ """
114
+ # Validate and filter models
115
+ if models is None:
116
+ active_models = default_models
117
+ else:
118
+ model_names_set = {m["name"] for m in default_models}
119
+ invalid_models = [m for m in models if m not in model_names_set]
120
+ if invalid_models:
121
+ raise ValueError(
122
+ f"The following model names were not found in default models: {invalid_models}. "
123
+ f"Available models: {sorted(model_names_set)}"
124
+ )
125
+ active_models = [m for m in default_models if m["name"] in set(models)]
126
+
103
127
  # Convert all Atoms to Graphs
104
128
  print(f"Converting {len(atoms_array)} structures to graphs...", flush=True)
105
129
  if mode == "serial":
@@ -115,86 +139,182 @@ def run_models_from_directory(
115
139
  )
116
140
  else:
117
141
  raise ValueError(f"Mode {mode} not implemented!")
118
-
142
+
119
143
  if saveGraphs:
120
144
  print("Saving graphs to disk...", flush=True)
121
145
  input_files = [i["name"] for i in outputs]
122
146
  for g, name in zip(graph_array, input_files):
123
- save_graphs(f"graphs/{name}.graph.bin", list(g), formats='coo')
147
+ save_graphs(f"graphs/{name}.graph.bin", list(g), formats="coo")
124
148
  print("Graphs saved!", flush=True)
125
149
 
126
150
  modelArray = []
127
- for model in default_models:
128
- modelPath = str(resources.files('alignn').joinpath(model['model']))
129
- zp = zipfile.ZipFile(modelPath, 'r')
130
- # Get the full path of checkpoint_300.pt or best_model.pt in the zip. Pick the first one found.
131
- # This is a workaround for the fact that some models (new ones) have a different naming convention
132
- # for the ALIGNN checkpoint files.
151
+ for model in active_models:
152
+ modelPath = str(resources.files("alignn").joinpath(model["model"]))
153
+ zp = zipfile.ZipFile(modelPath, "r")
154
+
133
155
  modelCheckpoint = [
134
- i for i in zp.namelist()
156
+ i for i in zp.namelist()
135
157
  if ("checkpoint_" in i and "pt" in i) or "best_model.pt" in i
136
- ]
158
+ ]
137
159
  if len(modelCheckpoint) == 0:
138
- raise ValueError(f"No model identifier found in {modelPath} for model {model['name']}!", flush=True)
160
+ raise ValueError(
161
+ f"No model identifier found in {modelPath} for model {model['name']}!"
162
+ )
139
163
  if len(modelCheckpoint) > 1:
140
- print(f"Multiple model identifiers ({len(modelCheckpoint)}) found in {modelPath} for model {model['name']}: {modelCheckpoint}. Using the first one in the list.", flush=True)
141
- modelCheckpoint = modelCheckpoint[0]
142
- else:
143
- modelCheckpoint = modelCheckpoint[0]
144
- config = json.loads(zp.read([i for i in zp.namelist() if "config.json" in i][0]))
164
+ print(
165
+ f"Multiple model identifiers ({len(modelCheckpoint)}) found in "
166
+ f"{modelPath} for model {model['name']}: {modelCheckpoint}. "
167
+ "Using the first one in the list.",
168
+ flush=True,
169
+ )
170
+ modelCheckpoint = modelCheckpoint[0]
171
+
172
+ config = json.loads(
173
+ zp.read([i for i in zp.namelist() if "config.json" in i][0])
174
+ )
145
175
  data = zipfile.ZipFile(modelPath).read(modelCheckpoint)
146
-
147
- # Create model based on the model name in config
176
+
148
177
  model_type = config["model"]["name"]
149
178
  if model_type == "alignn":
150
179
  loadedModel = ALIGNN(ALIGNNConfig(**config["model"]))
151
180
  elif model_type == "alignn_atomwise":
152
181
  loadedModel = ALIGNNAtomWise(ALIGNNAtomWiseConfig(**config["model"]))
153
182
  else:
154
- raise ValueError(f"Unknown model type: {model_type} for model {model['name']}")
183
+ raise ValueError(
184
+ f"Unknown model type: {model_type} for model {model['name']}"
185
+ )
155
186
 
156
- _, filename = tempfile.mkstemp()
187
+ fd, filename = tempfile.mkstemp()
188
+ os.close(fd)
157
189
  with open(filename, "wb") as f:
158
190
  f.write(data)
159
-
160
- # Load checkpoint and handle different checkpoint formats
161
- checkpoint = torch.load(filename, map_location='cpu')
191
+
192
+ checkpoint = torch.load(filename, map_location="cpu")
162
193
  if "model" in checkpoint:
163
194
  loadedModel.load_state_dict(checkpoint["model"])
164
195
  else:
165
- # Some checkpoints store the model state dict directly
166
196
  loadedModel.load_state_dict(checkpoint)
167
-
168
- loadedModel.to('cpu')
197
+
198
+ loadedModel.to("cpu")
169
199
  loadedModel.eval()
170
200
  modelArray.append(loadedModel)
171
-
201
+
172
202
  print(f"Model {model['name']} loaded!", flush=True)
173
-
174
- print(f"Running {len(default_models)} models on {len(graph_array)} structures...", flush=True)
175
- # Run all models on all graphs
176
- for model, loaded_model in zip(default_models, modelArray):
203
+
204
+ print(
205
+ f"Running {len(active_models)} models on {len(graph_array)} structures...",
206
+ flush=True,
207
+ )
208
+ for model, loaded_model in zip(active_models, modelArray):
177
209
  for g, out in zip(graph_array, outputs):
178
210
  model_output = loaded_model([g[0], g[1]])
179
-
180
- # Handle different output formats
211
+
181
212
  if isinstance(model_output, dict):
182
- # For atomwise models that return dict with 'out' key
183
- if 'out' in model_output:
184
- out_data = model_output['out'].item()
213
+ if "out" in model_output:
214
+ out_data = model_output["out"].item()
185
215
  else:
186
- # Take the first value if dict has other keys
187
216
  out_data = list(model_output.values())[0].item()
188
217
  else:
189
- # For regular models that return tensor directly
190
218
  out_data = model_output.item()
191
-
192
- out[model['name']] = round(out_data, 6)
193
-
194
- print("All models runs complete!", flush=True)
195
219
 
220
+ out[model["name"]] = round(out_data, 6)
221
+
222
+ print("All models runs complete!", flush=True)
196
223
  return outputs
197
224
 
225
+
226
+ def run_models_from_directory(
227
+ directory: str,
228
+ mode: str = "serial",
229
+ saveGraphs: bool = False,
230
+ models: Optional[List[str]] = None,
231
+ ):
232
+ """Run default models on all structures in a directory that are either in POSCAR or CIF format.
233
+
234
+ Parameters
235
+ ----------
236
+ directory : str
237
+ Path to directory containing structure files.
238
+ mode : str, optional
239
+ Either "serial" or "parallel" for execution mode. Default is "serial".
240
+ saveGraphs : bool, optional
241
+ Whether to save computed graphs to disk. Default is False.
242
+ models : List[str], optional
243
+ List of model names to run. If None, all default models are run.
244
+
245
+ Returns
246
+ -------
247
+ List[Dict[str, Union[float, str]]]
248
+ Output dictionaries with model predictions added as new keys.
249
+ """
250
+ atoms_array = []
251
+ outputs: List[Dict[str, Union[float, str]]] = []
252
+
253
+ for file in natsorted(os.listdir(directory)):
254
+ if file.endswith(("poscar", "POSCAR", "vasp", "VASP")):
255
+ outputs.append({"name": file})
256
+ atoms_array.append(Atoms.from_poscar(os.path.join(directory, file)))
257
+ elif file.endswith(("cif", "CIF")):
258
+ outputs.append({"name": file})
259
+ atoms_array.append(Atoms.from_cif(os.path.join(directory, file)))
260
+ else:
261
+ print(f"Skipping file {file} as it is not a POSCAR or CIF file!", flush=True)
262
+
263
+ return _run_pretrained_models(
264
+ atoms_array=atoms_array,
265
+ outputs=outputs,
266
+ mode=mode,
267
+ saveGraphs=saveGraphs,
268
+ models=models,
269
+ )
270
+
271
+
272
+ def run_models_from_structure(
273
+ structure: str,
274
+ mode: str = "serial",
275
+ saveGraphs: bool = False,
276
+ models: Optional[List[str]] = None,
277
+ ):
278
+ """Run default models on a single structure file in POSCAR or CIF format.
279
+
280
+ Parameters
281
+ ----------
282
+ structure : str
283
+ Path to structure file.
284
+ mode : str, optional
285
+ Either "serial" or "parallel" for execution mode. Default is "serial".
286
+ saveGraphs : bool, optional
287
+ Whether to save computed graphs to disk. Default is False.
288
+ models : List[str], optional
289
+ List of model names to run. If None, all default models are run.
290
+
291
+ Returns
292
+ -------
293
+ List[Dict[str, Union[float, str]]]
294
+ List containing one output dictionary for the input structure, with
295
+ model predictions added as new keys.
296
+ """
297
+ atoms_array = []
298
+ outputs: List[Dict[str, Union[float, str]]] = []
299
+
300
+ file = os.path.basename(structure)
301
+ if file.endswith(("poscar", "POSCAR", "vasp", "VASP")):
302
+ outputs.append({"name": file})
303
+ atoms_array.append(Atoms.from_poscar(structure))
304
+ elif file.endswith(("cif", "CIF")):
305
+ outputs.append({"name": file})
306
+ atoms_array.append(Atoms.from_cif(structure))
307
+ else:
308
+ raise ValueError(f"File {structure} is not a POSCAR/VASP or CIF file!")
309
+
310
+ return _run_pretrained_models(
311
+ atoms_array=atoms_array,
312
+ outputs=outputs,
313
+ mode=mode,
314
+ saveGraphs=saveGraphs,
315
+ models=models,
316
+ )
317
+
198
318
  # ******* Old method to download models *******
199
319
  """
200
320
  Name of the model, figshare link, number of outputs,
@@ -0,0 +1,184 @@
1
+ """Standalone regression tests for pretrained MPDD ALIGNN wrappers."""
2
+
3
+ from pathlib import Path
4
+ from unittest import TestCase, mock
5
+
6
+ import pytest
7
+
8
+ from alignn import pretrained
9
+ from alignn.pretrained import _run_pretrained_models
10
+
11
+
12
+ def test_invalid_model_name_raises_error():
13
+ """Regression test: passing invalid model names should raise a ValueError
14
+ from `_run_pretrained_models` with the expected message.
15
+ """
16
+ invalid_models_list = ["fake_model_name_xyz", "another_bad_model"]
17
+
18
+ with pytest.raises(ValueError, match="The following model names were not found in default models"):
19
+ _run_pretrained_models(
20
+ atoms_array=[],
21
+ outputs=[],
22
+ models=invalid_models_list,
23
+ )
24
+
25
+
26
+ def _fake_run_pretrained_models(
27
+ atoms_array, outputs, mode="serial", saveGraphs=False, models=None
28
+ ):
29
+ """Return deterministic payloads so the wrapper behavior can be compared.
30
+
31
+ Includes models information in response for validation.
32
+ """
33
+ return [
34
+ {
35
+ **out,
36
+ "mock_pred": f"{out['name']}|{mode}|{saveGraphs}",
37
+ "models_used": models, # Include for verification
38
+ }
39
+ for out in outputs
40
+ ]
41
+
42
+
43
+ # Get model names from default models
44
+ _default_models = pretrained.get_default_models()
45
+ _model_names = [m["name"] for m in _default_models]
46
+ _first_model = _model_names[0] if _model_names else "model_1"
47
+ _second_model = _model_names[1] if len(_model_names) > 1 else "model_2"
48
+ _third_model = _model_names[2] if len(_model_names) > 2 else "model_3"
49
+
50
+
51
+ class TestMPDDAlignNPretrainedWrappers(TestCase):
52
+ def test_run_models_from_directory_and_structure_agree_for_sigma_phase(self):
53
+ sigma_phase_dir = Path(__file__).resolve().parents[2] / "example.SigmaPhase"
54
+ poscar_files = sorted(
55
+ path
56
+ for path in sigma_phase_dir.iterdir()
57
+ if path.is_file() and path.suffix.lower() == ".poscar"
58
+ )
59
+
60
+ self.assertGreater(
61
+ len(poscar_files),
62
+ 0,
63
+ "Expected at least one POSCAR file under example.SigmaPhase.",
64
+ )
65
+
66
+ with mock.patch.object(
67
+ pretrained,
68
+ "_run_pretrained_models",
69
+ side_effect=_fake_run_pretrained_models,
70
+ ):
71
+ directory_results = pretrained.run_models_from_directory(
72
+ str(sigma_phase_dir),
73
+ mode="serial",
74
+ saveGraphs=False,
75
+ )
76
+
77
+ directory_by_name = {result["name"]: result for result in directory_results}
78
+ expected_names = {path.name for path in poscar_files}
79
+
80
+ self.assertEqual(set(directory_by_name), expected_names)
81
+
82
+ for path in poscar_files:
83
+ with self.subTest(structure=path.name):
84
+ structure_result = pretrained.run_models_from_structure(
85
+ str(path),
86
+ mode="serial",
87
+ saveGraphs=False,
88
+ )
89
+
90
+ self.assertEqual(len(structure_result), 1)
91
+ self.assertEqual(structure_result[0], directory_by_name[path.name])
92
+
93
+
94
+ @pytest.mark.parametrize(
95
+ "models_selection,mode,saveGraphs",
96
+ [
97
+ (None, "serial", False), # Default: all models
98
+ ([_first_model, _second_model], "serial", False), # Two models
99
+ ([_first_model], "serial", True), # Single model with saveGraphs
100
+ ([], "serial", False), # Empty list (no models)
101
+ ([_third_model, _first_model], "parallel", False), # Multiple models, parallel mode
102
+ ],
103
+ )
104
+ class TestModelFiltering:
105
+ """Parametrized tests for model filtering functionality."""
106
+
107
+ def test_run_models_from_structure_with_model_filter(
108
+ self, models_selection, mode, saveGraphs
109
+ ):
110
+ """Test that run_models_from_structure correctly passes and validates model filtering."""
111
+ sigma_phase_dir = Path(__file__).resolve().parents[2] / "example.SigmaPhase"
112
+ poscar_files = sorted(
113
+ path
114
+ for path in sigma_phase_dir.iterdir()
115
+ if path.is_file() and path.suffix.lower() == ".poscar"
116
+ )
117
+
118
+ assert len(poscar_files) > 0, "Expected at least one POSCAR file in example.SigmaPhase"
119
+
120
+ test_path = str(poscar_files[0])
121
+
122
+ with mock.patch.object(
123
+ pretrained,
124
+ "_run_pretrained_models",
125
+ side_effect=_fake_run_pretrained_models,
126
+ ) as mock_run:
127
+ results = pretrained.run_models_from_structure(
128
+ test_path,
129
+ mode=mode,
130
+ saveGraphs=saveGraphs,
131
+ models=models_selection,
132
+ )
133
+
134
+ # Verify the mock was called with correct parameters
135
+ mock_run.assert_called_once()
136
+ call_kwargs = mock_run.call_args.kwargs
137
+
138
+ # Verify models parameter was passed correctly
139
+ assert call_kwargs["models"] == models_selection, (
140
+ f"Expected models={models_selection}, got {call_kwargs['models']}"
141
+ )
142
+
143
+ # Verify other parameters
144
+ assert call_kwargs["mode"] == mode
145
+ assert call_kwargs["saveGraphs"] == saveGraphs
146
+
147
+ # Verify the models information is in the returned results
148
+ assert len(results) == 1
149
+ assert results[0]["models_used"] == models_selection
150
+
151
+ def test_run_models_from_directory_with_model_filter(
152
+ self, models_selection, mode, saveGraphs
153
+ ):
154
+ """Test that run_models_from_directory correctly passes and validates model filtering."""
155
+ sigma_phase_dir = Path(__file__).resolve().parents[2] / "example.SigmaPhase"
156
+
157
+ with mock.patch.object(
158
+ pretrained,
159
+ "_run_pretrained_models",
160
+ side_effect=_fake_run_pretrained_models,
161
+ ) as mock_run:
162
+ results = pretrained.run_models_from_directory(
163
+ str(sigma_phase_dir),
164
+ mode=mode,
165
+ saveGraphs=saveGraphs,
166
+ models=models_selection,
167
+ )
168
+
169
+ # Verify the mock was called with correct parameters
170
+ mock_run.assert_called_once()
171
+ call_kwargs = mock_run.call_args.kwargs
172
+
173
+ # Verify models parameter was passed correctly
174
+ assert call_kwargs["models"] == models_selection, (
175
+ f"Expected models={models_selection}, got {call_kwargs['models']}"
176
+ )
177
+
178
+ # Verify other parameters
179
+ assert call_kwargs["mode"] == mode
180
+ assert call_kwargs["saveGraphs"] == saveGraphs
181
+
182
+ # Verify the models information is in all returned results
183
+ for result in results:
184
+ assert result["models_used"] == models_selection
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mpdd-alignn
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: Our version of the NIST-JARVIS ALIGNN optimized in terms of model performance and to some extent reliability, for large-scale deployments over the MPDD infrastructure by Phases Research Lab.
5
- Author-email: "Adam M. Krajewski" <adam@phaseslab.org>
5
+ Author-email: "Adam M. Krajewski" <adam@phaseslab.org>, Ricardo Amaral <rna5137@psu.edu>
6
6
  Project-URL: Homepage, https://github.com/PhasesResearchLab/mpdd-alignn
7
7
  Project-URL: Bug Tracker, https://github.com/PhasesResearchLab/mpdd-alignn/issues
8
8
  Project-URL: Research Page, https://phaseslab.com/mpdd
@@ -34,6 +34,7 @@ Requires-Dist: dgl>=1.1.3
34
34
  Requires-Dist: scikit-learn>=1.3.1
35
35
  Requires-Dist: matplotlib>=3.4.1
36
36
  Requires-Dist: tqdm>=4.60.0
37
+ Requires-Dist: natsort>=8.3.0
37
38
  Requires-Dist: pandas>=1.2.3
38
39
  Requires-Dist: pydantic_settings
39
40
  Requires-Dist: ruamel.yaml
@@ -68,6 +68,7 @@ alignn/scripts/train_edos_pdos.py
68
68
  alignn/scripts/train_megnet.py
69
69
  alignn/tests/test_alignn_ff.py
70
70
  alignn/tests/test_force_reduction.py
71
+ alignn/tests/test_mpddalignn.py
71
72
  alignn/tests/test_prop.py
72
73
  example.SigmaPhase/0-Cr8Fe18Ni4.POSCAR
73
74
  example.SigmaPhase/1-Cr16Fe8Ni6.POSCAR
@@ -8,6 +8,7 @@ dgl>=1.1.3
8
8
  scikit-learn>=1.3.1
9
9
  matplotlib>=3.4.1
10
10
  tqdm>=4.60.0
11
+ natsort>=8.3.0
11
12
  pandas>=1.2.3
12
13
  pydantic_settings
13
14
  ruamel.yaml
@@ -4,9 +4,10 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mpdd-alignn"
7
- version = "1.2.0"
7
+ version = "1.3.0"
8
8
  authors = [
9
- { name="Adam M. Krajewski", email="adam@phaseslab.org" }
9
+ { name="Adam M. Krajewski", email="adam@phaseslab.org" },
10
+ { name="Ricardo Amaral", email="rna5137@psu.edu" },
10
11
  ]
11
12
  classifiers = [
12
13
  "Programming Language :: Python :: 3",
@@ -37,6 +38,7 @@ dependencies = [
37
38
  "scikit-learn>=1.3.1",
38
39
  "matplotlib>=3.4.1",
39
40
  "tqdm>=4.60.0",
41
+ "natsort>=8.3.0",
40
42
  "pandas>=1.2.3",
41
43
  "pydantic_settings",
42
44
  "ruamel.yaml",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes