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 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()