saddlemill 0.1.0__py3-none-any.whl
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.
- saddlemill/__main__.py +193 -0
- saddlemill/analyze_neb.py +1362 -0
- saddlemill/catsunami/autoframe.py +1688 -0
- saddlemill/catsunami/ocpneb.py +375 -0
- saddlemill/catsunami/reaction.py +99 -0
- saddlemill/config.py +1009 -0
- saddlemill/dimeropt.py +324 -0
- saddlemill/dimertools/kappa_dimer.py +201 -0
- saddlemill/dimertools/structure_edit.py +1067 -0
- saddlemill/geomopt.py +616 -0
- saddlemill/init_function.py +67 -0
- saddlemill/nebopt.py +647 -0
- saddlemill/nebtools/create_endpoints_for_MP_batteries.py +135 -0
- saddlemill/sellaopt.py +473 -0
- saddlemill/status.py +328 -0
- saddlemill/tools.py +1060 -0
- saddlemill/vasp_io.py +552 -0
- saddlemill-0.1.0.dist-info/METADATA +452 -0
- saddlemill-0.1.0.dist-info/RECORD +22 -0
- saddlemill-0.1.0.dist-info/WHEEL +5 -0
- saddlemill-0.1.0.dist-info/licenses/LICENSE +21 -0
- saddlemill-0.1.0.dist-info/top_level.txt +1 -0
saddlemill/__main__.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import concurrent.futures
|
|
3
|
+
from ase.io import Trajectory
|
|
4
|
+
from itertools import groupby
|
|
5
|
+
from contextlib import nullcontext
|
|
6
|
+
from saddlemill.init_function import init_function
|
|
7
|
+
from saddlemill.tools import (save_ordered_traj_names, read_ordered_traj_names,
|
|
8
|
+
clean_up_files, load_and_sanitize, passes_input_filter,
|
|
9
|
+
extract_previous_results, bank_singlepoint_vasp_restarts,
|
|
10
|
+
bank_doublemin_partials)
|
|
11
|
+
from saddlemill.config import (load_config, load_method, get_trajes_and_indices,
|
|
12
|
+
create_results_directories, get_remaining_trajes,
|
|
13
|
+
get_flux_resources, archive_and_clean_csvs,
|
|
14
|
+
archive_and_clean_outputs, build_redo_info)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def check_and_print_status(futures, total):
|
|
18
|
+
done, futures = concurrent.futures.wait(futures, timeout=0.1)
|
|
19
|
+
if len(done)!=0:
|
|
20
|
+
print(f"{len(futures)} REMAINING --- {total-len(futures)} FINISHED --- {total} TOTAL")
|
|
21
|
+
return futures
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main():
|
|
25
|
+
|
|
26
|
+
config_dict = load_config("config.ini")
|
|
27
|
+
print(config_dict,"\n")
|
|
28
|
+
|
|
29
|
+
method = load_method(config_dict)
|
|
30
|
+
trajes_and_idxs = get_trajes_and_indices(config_dict)
|
|
31
|
+
can_resume = os.path.exists('traj_files_ordered.json')
|
|
32
|
+
previous_results = {}
|
|
33
|
+
redo_info = {}
|
|
34
|
+
if can_resume:
|
|
35
|
+
trajes_and_idxs_old = read_ordered_traj_names()
|
|
36
|
+
if trajes_and_idxs != trajes_and_idxs_old:
|
|
37
|
+
raise ValueError("Provided dirpath creates a different trajes_and_idxs. I can't resume.")
|
|
38
|
+
job_IDs, trajes_and_idxs = get_remaining_trajes(trajes_and_idxs, config_dict)
|
|
39
|
+
|
|
40
|
+
# Build per-job redo info (which subunits to redo)
|
|
41
|
+
redo_info = build_redo_info(job_IDs, config_dict)
|
|
42
|
+
|
|
43
|
+
# Extract previous output BEFORE archiving (output files still intact).
|
|
44
|
+
# Always extract when redoing subunits: NEB needs sub-band endpoints,
|
|
45
|
+
# DoubleMinimization needs the kept side for reaction check,
|
|
46
|
+
# Dimer needs previous structures only when continue_from_result=True.
|
|
47
|
+
if redo_info:
|
|
48
|
+
print(f"Extracting previous results for {len(redo_info)} jobs...", flush=True)
|
|
49
|
+
previous_results = extract_previous_results(list(redo_info.keys()), config_dict, redo_info)
|
|
50
|
+
print(f" Extracted {len(previous_results)} of {len(redo_info)} results.", flush=True)
|
|
51
|
+
|
|
52
|
+
# SinglePoint+VASP resume: bank each wall-killed job's mid-run VTST-dimer
|
|
53
|
+
# state (CENTCAR/NEWMODECAR) BEFORE clean_up_files wipes the VASP_* dirs,
|
|
54
|
+
# then plumb it through the same continuation_data channel the other
|
|
55
|
+
# methods use. Gated by continue_from_result (default True).
|
|
56
|
+
if (config_dict["Main"]["method"] == "SinglePoint"
|
|
57
|
+
and config_dict["Main"]["Calculator"] in ("Vasp", "VaspInteractive")
|
|
58
|
+
and config_dict["Main"]["continue_from_result"]):
|
|
59
|
+
sp_resume = bank_singlepoint_vasp_restarts(job_IDs, config_dict)
|
|
60
|
+
if sp_resume:
|
|
61
|
+
print(f"Banked SP resume state for {len(sp_resume)} wall-killed job(s).",
|
|
62
|
+
flush=True)
|
|
63
|
+
previous_results = {**previous_results, **sp_resume}
|
|
64
|
+
|
|
65
|
+
from saddlemill.config import _normalize_run_jobs
|
|
66
|
+
categories_to_clean = _normalize_run_jobs(config_dict["Main"]["run_jobs"])
|
|
67
|
+
cleaned = archive_and_clean_csvs(config_dict, job_IDs, categories_to_clean)
|
|
68
|
+
archive_and_clean_outputs(config_dict, cleaned)
|
|
69
|
+
# DoubleMinimization partial resume: bank each re-run side's live
|
|
70
|
+
# trajectory BEFORE clean_up_files deletes optimization_*.traj (same
|
|
71
|
+
# ordering constraint as the SinglePoint banking above). Without this
|
|
72
|
+
# the resume in geomopt finds nothing and every wall-killed side
|
|
73
|
+
# restarts from scratch.
|
|
74
|
+
if (config_dict["Main"]["method"] == "DoubleMinimization"
|
|
75
|
+
and config_dict["ourDoubleMinimization"].get("resume_partial")):
|
|
76
|
+
n_banked = bank_doublemin_partials(job_IDs, config_dict)
|
|
77
|
+
if n_banked:
|
|
78
|
+
print(f"Banked partial trajectories for {n_banked} side(s).", flush=True)
|
|
79
|
+
|
|
80
|
+
clean_up_files(config_dict)
|
|
81
|
+
else:
|
|
82
|
+
job_IDs = list(range(len(trajes_and_idxs)))
|
|
83
|
+
save_ordered_traj_names(trajes_and_idxs)
|
|
84
|
+
create_results_directories(config_dict)
|
|
85
|
+
|
|
86
|
+
if config_dict["Main"]["executorlib"]:
|
|
87
|
+
from executorlib import FluxJobExecutor
|
|
88
|
+
|
|
89
|
+
max_workers, cores, gpus_per_core, threads_per_core = get_flux_resources(config_dict)
|
|
90
|
+
|
|
91
|
+
executor = FluxJobExecutor(
|
|
92
|
+
flux_log_files = True,
|
|
93
|
+
max_workers = max_workers,
|
|
94
|
+
block_allocation = True,
|
|
95
|
+
init_function = init_function,
|
|
96
|
+
restart_limit = config_dict["Main"]["restart_limit"],
|
|
97
|
+
resource_dict = {
|
|
98
|
+
"cores": cores,
|
|
99
|
+
"gpus_per_core": gpus_per_core,
|
|
100
|
+
"threads_per_core": threads_per_core,
|
|
101
|
+
"num_nodes": 1,
|
|
102
|
+
"error_log_file": "error",
|
|
103
|
+
"cwd": os.getcwd(),
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
# 'exe' will be the executor instance
|
|
107
|
+
get_submitter = lambda exe: exe.submit
|
|
108
|
+
else:
|
|
109
|
+
# Serial Mode: Use empty context and a dummy submitter that runs immediately
|
|
110
|
+
init_data = init_function()
|
|
111
|
+
executor = nullcontext()
|
|
112
|
+
get_submitter = lambda _: lambda fn, *args, **kwargs: fn(*args, **init_data, **kwargs)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# 2. Unified Execution Loop
|
|
116
|
+
input_format = config_dict["Main"].get("input_format", "traj")
|
|
117
|
+
if input_format == "lmdb":
|
|
118
|
+
# SinglePoint + LMDB: read rows via ase.db (fairchem.core.datasets
|
|
119
|
+
# registers the aselmdb backend).
|
|
120
|
+
import fairchem.core.datasets # noqa: F401
|
|
121
|
+
from ase.db import connect as _lmdb_connect
|
|
122
|
+
|
|
123
|
+
def open_src(path):
|
|
124
|
+
return _lmdb_connect(path, type='aselmdb', readonly=True)
|
|
125
|
+
|
|
126
|
+
def load_item(src, i, j):
|
|
127
|
+
rows = [src.get(rid) for rid in range(i, j)]
|
|
128
|
+
atoms_list = []
|
|
129
|
+
extras = []
|
|
130
|
+
for r in rows:
|
|
131
|
+
atoms = r.toatoms()
|
|
132
|
+
# row.toatoms() doesn't populate atoms.info from row.data,
|
|
133
|
+
# so we lift it across explicitly. This keeps existing
|
|
134
|
+
# orig_info / status conventions readable by passes_input_filter.
|
|
135
|
+
atoms.info = dict(r.data.get("info", {}))
|
|
136
|
+
atoms_list.append(atoms)
|
|
137
|
+
extras.append({"kvp": dict(r.key_value_pairs),
|
|
138
|
+
"row_data": dict(r.data)})
|
|
139
|
+
images = atoms_list if len(atoms_list) > 1 else atoms_list[0]
|
|
140
|
+
return images, {"extras": extras}
|
|
141
|
+
|
|
142
|
+
def close_src(_):
|
|
143
|
+
pass
|
|
144
|
+
else:
|
|
145
|
+
def open_src(path):
|
|
146
|
+
return Trajectory(path, 'r')
|
|
147
|
+
|
|
148
|
+
def load_item(src, i, j):
|
|
149
|
+
return load_and_sanitize(src, i, j), {}
|
|
150
|
+
|
|
151
|
+
def close_src(src):
|
|
152
|
+
src.close()
|
|
153
|
+
|
|
154
|
+
with executor as exe:
|
|
155
|
+
submitter = get_submitter(exe)
|
|
156
|
+
futures = []
|
|
157
|
+
idx = 0
|
|
158
|
+
submitted = 0
|
|
159
|
+
|
|
160
|
+
for src_path, group in groupby(trajes_and_idxs, key=lambda x: x[0]):
|
|
161
|
+
src = open_src(src_path)
|
|
162
|
+
try:
|
|
163
|
+
for _, i, j in group:
|
|
164
|
+
job_id = job_IDs[idx]
|
|
165
|
+
images, extra = load_item(src, i, j)
|
|
166
|
+
if not passes_input_filter(images, config_dict):
|
|
167
|
+
idx += 1
|
|
168
|
+
continue
|
|
169
|
+
try:
|
|
170
|
+
f = submitter(method, job_id, config_dict, images,
|
|
171
|
+
continuation_data=previous_results.get(job_id),
|
|
172
|
+
entries_to_run=redo_info.get(job_id),
|
|
173
|
+
**extra)
|
|
174
|
+
if config_dict["Main"]["executorlib"]: futures.append(f)
|
|
175
|
+
submitted += 1
|
|
176
|
+
except Exception as e:
|
|
177
|
+
print(f"CRITICAL ERROR on job {idx} ({src_path}): {e}")
|
|
178
|
+
# In serial mode, we catch it and move on.
|
|
179
|
+
# In parallel mode, 'submitter' usually doesn't raise immediately, so this is safe.
|
|
180
|
+
idx += 1
|
|
181
|
+
finally:
|
|
182
|
+
close_src(src)
|
|
183
|
+
|
|
184
|
+
print(f"Scanned {idx} input frame(s); submitted {submitted} "
|
|
185
|
+
f"(skipped {idx - submitted} by input_statuses).", flush=True)
|
|
186
|
+
|
|
187
|
+
if config_dict["Main"]["executorlib"]:
|
|
188
|
+
while len(futures):
|
|
189
|
+
futures = check_and_print_status(futures, submitted)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
if __name__ == "__main__":
|
|
193
|
+
main()
|