44Separated from heuristics so CREST can be conditionally imported and reused.
55"""
66
7+ import datetime
78import os
89import re
910import time
10- from typing import List
11+ from typing import TYPE_CHECKING , List , Optional , Union
1112
1213import numpy as np
1314import pandas as pd
1415
15- from arc .common import get_logger
16+ from arc .common import almost_equal_coords , get_logger
1617from arc .imports import settings , submit_scripts
18+ from arc .job .adapter import JobAdapter
19+ from arc .job .adapters .common import _initialize_adapter , ts_adapters_by_rmg_family
20+ from arc .job .factory import register_job_adapter
1721from arc .job .local import check_job_status , submit_job
22+ from arc .plotter import save_geo
1823from arc .species .converter import reorder_xyz_string , str_to_xyz , xyz_to_dmat , xyz_to_str
24+ from arc .species .species import ARCSpecies , TSGuess
25+ from arc .job .adapters .ts .heuristics import h_abstraction
26+
27+ if TYPE_CHECKING :
28+ from arc .level import Level
29+ from arc .reaction import ARCReaction
1930
2031logger = get_logger ()
2132
@@ -38,6 +49,229 @@ def crest_available() -> bool:
3849 return bool (SERVERS .get ("local" )) and bool (CREST_PATH or CREST_ENV_PATH )
3950
4051
52+ class CrestAdapter (JobAdapter ):
53+ """
54+ A class for executing CREST TS conformer searches based on heuristics-generated guesses.
55+ """
56+
57+ def __init__ (self ,
58+ project : str ,
59+ project_directory : str ,
60+ job_type : Union [List [str ], str ],
61+ args : Optional [dict ] = None ,
62+ bath_gas : Optional [str ] = None ,
63+ checkfile : Optional [str ] = None ,
64+ conformer : Optional [int ] = None ,
65+ constraints : Optional [List ] = None ,
66+ cpu_cores : Optional [str ] = None ,
67+ dihedral_increment : Optional [float ] = None ,
68+ dihedrals : Optional [List [float ]] = None ,
69+ directed_scan_type : Optional [str ] = None ,
70+ ess_settings : Optional [dict ] = None ,
71+ ess_trsh_methods : Optional [List [str ]] = None ,
72+ execution_type : Optional [str ] = None ,
73+ fine : bool = False ,
74+ initial_time : Optional [Union ['datetime.datetime' , str ]] = None ,
75+ irc_direction : Optional [str ] = None ,
76+ job_id : Optional [int ] = None ,
77+ job_memory_gb : float = 14.0 ,
78+ job_name : Optional [str ] = None ,
79+ job_num : Optional [int ] = None ,
80+ job_server_name : Optional [str ] = None ,
81+ job_status : Optional [List [Union [dict , str ]]] = None ,
82+ level : Optional ['Level' ] = None ,
83+ max_job_time : Optional [float ] = None ,
84+ run_multi_species : bool = False ,
85+ reactions : Optional [List ['ARCReaction' ]] = None ,
86+ rotor_index : Optional [int ] = None ,
87+ server : Optional [str ] = None ,
88+ server_nodes : Optional [list ] = None ,
89+ queue : Optional [str ] = None ,
90+ attempted_queues : Optional [List [str ]] = None ,
91+ species : Optional [List [ARCSpecies ]] = None ,
92+ testing : bool = False ,
93+ times_rerun : int = 0 ,
94+ torsions : Optional [List [List [int ]]] = None ,
95+ tsg : Optional [int ] = None ,
96+ xyz : Optional [dict ] = None ,
97+ ):
98+
99+ self .incore_capacity = 50
100+ self .job_adapter = 'crest'
101+ self .command = None
102+ self .execution_type = execution_type or 'incore'
103+
104+ if reactions is None :
105+ raise ValueError ('Cannot execute TS CREST without ARCReaction object(s).' )
106+
107+ dihedral_increment = dihedral_increment or 30
108+
109+ _initialize_adapter (obj = self ,
110+ is_ts = True ,
111+ project = project ,
112+ project_directory = project_directory ,
113+ job_type = job_type ,
114+ args = args ,
115+ bath_gas = bath_gas ,
116+ checkfile = checkfile ,
117+ conformer = conformer ,
118+ constraints = constraints ,
119+ cpu_cores = cpu_cores ,
120+ dihedral_increment = dihedral_increment ,
121+ dihedrals = dihedrals ,
122+ directed_scan_type = directed_scan_type ,
123+ ess_settings = ess_settings ,
124+ ess_trsh_methods = ess_trsh_methods ,
125+ fine = fine ,
126+ initial_time = initial_time ,
127+ irc_direction = irc_direction ,
128+ job_id = job_id ,
129+ job_memory_gb = job_memory_gb ,
130+ job_name = job_name ,
131+ job_num = job_num ,
132+ job_server_name = job_server_name ,
133+ job_status = job_status ,
134+ level = level ,
135+ max_job_time = max_job_time ,
136+ run_multi_species = run_multi_species ,
137+ reactions = reactions ,
138+ rotor_index = rotor_index ,
139+ server = server ,
140+ server_nodes = server_nodes ,
141+ queue = queue ,
142+ attempted_queues = attempted_queues ,
143+ species = species ,
144+ testing = testing ,
145+ times_rerun = times_rerun ,
146+ torsions = torsions ,
147+ tsg = tsg ,
148+ xyz = xyz ,
149+ )
150+
151+ def write_input_file (self ) -> None :
152+ pass
153+
154+ def set_files (self ) -> None :
155+ pass
156+
157+ def set_additional_file_paths (self ) -> None :
158+ pass
159+
160+ def set_input_file_memory (self ) -> None :
161+ pass
162+
163+ def execute_incore (self ):
164+ self ._log_job_execution ()
165+ self .initial_time = self .initial_time if self .initial_time else datetime .datetime .now ()
166+
167+ supported_families = [key for key , val in ts_adapters_by_rmg_family .items () if 'crest' in val ]
168+
169+ self .reactions = [self .reactions ] if not isinstance (self .reactions , list ) else self .reactions
170+ for rxn in self .reactions :
171+ if rxn .family not in supported_families :
172+ logger .warning (f'The CREST TS search adapter does not support the { rxn .family } reaction family.' )
173+ continue
174+ if any (spc .get_xyz () is None for spc in rxn .r_species + rxn .p_species ):
175+ logger .warning (f'The CREST TS search adapter cannot process a reaction if 3D coordinates of '
176+ f'some/all of its reactants/products are missing.\n Not processing { rxn } .' )
177+ continue
178+ if not crest_available ():
179+ logger .warning ('CREST is not available. Skipping CREST TS search.' )
180+ continue
181+
182+ if rxn .ts_species is None :
183+ rxn .ts_species = ARCSpecies (label = 'TS' ,
184+ is_ts = True ,
185+ charge = rxn .charge ,
186+ multiplicity = rxn .multiplicity ,
187+ )
188+
189+ tsg = TSGuess (method = 'CREST' )
190+ tsg .tic ()
191+
192+ crest_job_dirs = []
193+ xyz_guesses = h_abstraction (reaction = rxn ,
194+ dihedral_increment = self .dihedral_increment ,
195+ path = self .local_path )
196+ if not xyz_guesses :
197+ logger .warning (f'CREST TS search failed to generate any seed guesses for { rxn .label } .' )
198+ tsg .tok ()
199+ continue
200+
201+ for iteration , xyz_entry in enumerate (xyz_guesses ):
202+ if isinstance (xyz_entry , dict ):
203+ xyz_guess = xyz_entry .get ("xyz" )
204+ else :
205+ xyz_guess = xyz_entry
206+ if xyz_guess is None :
207+ continue
208+
209+ df_dmat = convert_xyz_to_df (xyz_guess )
210+ try :
211+ h_abs_atoms_dict = get_h_abs_atoms (df_dmat )
212+ crest_job_dir = crest_ts_conformer_search (
213+ xyz_guess ,
214+ h_abs_atoms_dict ["A" ],
215+ h_abs_atoms_dict ["H" ],
216+ h_abs_atoms_dict ["B" ],
217+ path = self .local_path ,
218+ xyz_crest_int = iteration ,
219+ )
220+ crest_job_dirs .append (crest_job_dir )
221+ except (ValueError , KeyError ) as e :
222+ logger .error (f"Could not determine the H abstraction atoms, got:\n { e } " )
223+
224+ if not crest_job_dirs :
225+ logger .warning (f'CREST TS search failed to prepare any jobs for { rxn .label } .' )
226+ tsg .tok ()
227+ continue
228+
229+ crest_jobs = submit_crest_jobs (crest_job_dirs )
230+ monitor_crest_jobs (crest_jobs )
231+ xyz_guesses_crest = process_completed_jobs (crest_jobs )
232+ tsg .tok ()
233+
234+ for method_index , xyz in enumerate (xyz_guesses_crest ):
235+ if xyz is None :
236+ continue
237+ unique = True
238+ for other_tsg in rxn .ts_species .ts_guesses :
239+ if almost_equal_coords (xyz , other_tsg .initial_xyz ):
240+ if 'crest' not in other_tsg .method .lower ():
241+ other_tsg .method += ' and CREST'
242+ unique = False
243+ break
244+ if unique :
245+ ts_guess = TSGuess (method = 'CREST' ,
246+ index = len (rxn .ts_species .ts_guesses ),
247+ method_index = method_index ,
248+ t0 = tsg .t0 ,
249+ execution_time = tsg .execution_time ,
250+ success = True ,
251+ family = rxn .family ,
252+ xyz = xyz ,
253+ )
254+ rxn .ts_species .ts_guesses .append (ts_guess )
255+ save_geo (xyz = xyz ,
256+ path = self .local_path ,
257+ filename = f'CREST_{ method_index } ' ,
258+ format_ = 'xyz' ,
259+ comment = f'CREST { method_index } , family: { rxn .family } ' ,
260+ )
261+
262+ if len (self .reactions ) < 5 :
263+ successes = [tsg for tsg in rxn .ts_species .ts_guesses if tsg .success and 'crest' in tsg .method .lower ()]
264+ if successes :
265+ logger .info (f'CREST successfully found { len (successes )} TS guesses for { rxn .label } .' )
266+ else :
267+ logger .info (f'CREST did not find any successful TS guesses for { rxn .label } .' )
268+
269+ self .final_time = datetime .datetime .now ()
270+
271+ def execute_queue (self ):
272+ self .execute_incore ()
273+
274+
41275def crest_ts_conformer_search (
42276 xyz_guess : dict ,
43277 a_atom : int ,
@@ -376,3 +610,6 @@ def get_h_abs_atoms(dataframe: pd.DataFrame) -> dict:
376610 }
377611 else :
378612 raise ValueError ("No valid hydrogen atom found." )
613+
614+
615+ register_job_adapter ('crest' , CrestAdapter )
0 commit comments