@@ -98,20 +98,22 @@ class ARC(object):
9898 ess_settings (dict, optional): A dictionary of available ESS (keys) and a corresponding server list (values).
9999 bath_gas (str, optional): A bath gas. Currently used in OneDMin to calc L-J parameters.
100100 Allowed values are He, Ne, Ar, Kr, H2, N2, O2.
101- adaptive_levels (dict, optional): A dictionary of levels of theory for ranges of the number of heavy atoms in
102- the molecule. Keys are tuples of (min_num_atoms, max_num_atoms), values are dictionaries. Keys of the
103- sub-dictionaries are tuples of job types, values are levels of theory (str, dict or Level).
101+ adaptive_levels (list, optional): A list of levels of theory for ranges of the number of heavy atoms in the
102+ molecule. Each entry is a dictionary with an ``atom_range`` 2-list (min_num_atoms, max_num_atoms; the upper
103+ bound may be ``'inf'``) and a ``levels`` mapping from job type to level of theory (str, dict or Level).
104+ Job types sharing a level may be given as a whitespace- or comma-separated key (e.g. ``'opt freq'``).
104105 Job types not defined in adaptive levels will have non-adaptive (regular) levels.
105106 Example::
106107
107- adaptive_levels = {(1, 5): {('opt', 'freq'): 'wb97xd/6-311+g(2d,2p)',
108- 'sp': 'ccsd(t)-f12/aug-cc-pvtz-f12'},
109- (6, 15): {('opt', 'freq'): 'b3lyp/cbsb7',
110- 'sp': 'dlpno-ccsd(t)/def2-tzvp'},
111- (16, 30): {('opt', 'freq'): 'b3lyp/6-31g(d,p)',
112- 'sp': 'wb97xd/6-311+g(2d,2p)'},
113- (31, 'inf'): {('opt', 'freq'): 'b3lyp/6-31g(d,p)',
114- 'sp': 'b3lyp/6-311+g(d,p)'}}
108+ adaptive_levels = [{'atom_range': [1, 5],
109+ 'levels': {'opt freq': 'wb97xd/6-311+g(2d,2p)',
110+ 'sp': 'ccsd(t)-f12/aug-cc-pvtz-f12'}},
111+ {'atom_range': [6, 15],
112+ 'levels': {'opt freq': 'b3lyp/cbsb7',
113+ 'sp': 'dlpno-ccsd(t)/def2-tzvp'}},
114+ {'atom_range': [16, 'inf'],
115+ 'levels': {'opt freq': 'b3lyp/6-31g(d,p)',
116+ 'sp': 'wb97xd/6-311+g(2d,2p)'}}]
115117
116118 freq_scale_factor (float, optional): The harmonic frequencies scaling factor. Could be automatically determined
117119 if not available in Arkane and not provided by the user.
@@ -163,9 +165,8 @@ class ARC(object):
163165 ts_guess_level (Level): Level of theory for comparisons of TS guesses between different methods.
164166 irc_level (Level): The level of theory to use for IRC calculations.
165167 orbitals_level (Level): Level of theory for molecular orbitals calculations.
166- adaptive_levels (dict): A dictionary of levels of theory for ranges of the number of heavy atoms in
167- the molecule. Keys are tuples of (min_num_atoms, max_num_atoms), values are dictionaries. Keys of the
168- sub-dictionaries are tuples of job types, values are levels of theory (str, dict or Level).
168+ adaptive_levels (dict): The processed adaptive levels, keyed by (min_num_atoms, max_num_atoms) tuples, each
169+ mapping job-type tuples to ``Level`` objects (built from the user-facing ``adaptive_levels`` list).
169170 Job types not defined in adaptive levels will have non-adaptive (regular) levels.
170171 output (dict): Output dictionary with status and final QM file paths for all species. Only used for restarting,
171172 the actual object used is in the Scheduler class.
@@ -431,8 +432,10 @@ def as_dict(self) -> dict:
431432 """
432433 restart_dict = dict ()
433434 if self .adaptive_levels is not None :
434- restart_dict ['adaptive_levels' ] = {atom_range : {job_type : level .as_dict () for job_type , level in levels_dict }
435- for atom_range , levels_dict in self .adaptive_levels .items ()}
435+ restart_dict ['adaptive_levels' ] = [
436+ {'atom_range' : [atom_range [0 ], atom_range [1 ]],
437+ 'levels' : {' ' .join (job_types ): level .as_dict () for job_types , level in levels_dict .items ()}}
438+ for atom_range , levels_dict in self .adaptive_levels .items ()]
436439 if self .allow_nonisomorphic_2d :
437440 restart_dict ['allow_nonisomorphic_2d' ] = self .allow_nonisomorphic_2d
438441 if self .arkane_level_of_theory is not None :
@@ -1256,44 +1259,60 @@ def standardize_output_paths(self):
12561259 self .output = dict ()
12571260
12581261
1259- def process_adaptive_levels (adaptive_levels : dict | None ) -> dict | None :
1262+ def process_adaptive_levels (adaptive_levels : list | None ) -> dict | None :
12601263 """
12611264 Process the ``adaptive_levels`` argument.
12621265
1266+ The user-facing form is a YAML-friendly list of entries, each a dictionary with an
1267+ ``atom_range`` 2-list (the heavy-atom count range, the upper bound may be the string
1268+ ``'inf'`` or ``float('inf')``) and a ``levels`` mapping of job types to levels of theory.
1269+ Job types that share a level may be given as a single whitespace- or comma-separated key
1270+ (e.g. ``'opt freq'``). A level value may be a string or a ``Level`` dictionary. For example::
1271+
1272+ adaptive_levels = [{'atom_range': [1, 6],
1273+ 'levels': {'opt freq': 'wb97xd/def2tzvp',
1274+ 'sp': 'ccsd(t)-f12/cc-pvtz-f12'}},
1275+ {'atom_range': [7, 'inf'],
1276+ 'levels': {'opt freq': 'b3lyp/6-31g(d,p)',
1277+ 'sp': 'dlpno-ccsd(t)/def2-tzvp'}}]
1278+
12631279 Args:
1264- adaptive_levels (dict ): The adaptive levels dictionary .
1280+ adaptive_levels (list ): The adaptive levels specification (a list of entries) .
12651281
1266- Returns: dict
1267- The processed adaptive levels dictionary.
1282+ Returns: dict | None
1283+ The processed adaptive levels keyed by ``(min_heavy_atoms, max_heavy_atoms)`` tuples,
1284+ each mapping job-type tuples to ``Level`` objects, or ``None`` if the input is ``None``.
12681285 """
12691286 if adaptive_levels is None :
12701287 return None
1271- processed = dict ()
1272- if not isinstance (adaptive_levels , dict ):
1273- raise InputError (f'The adaptive levels argument must be a dictionary, '
1288+ if not isinstance (adaptive_levels , list ):
1289+ raise InputError (f'The adaptive levels argument must be a list of entries, '
12741290 f'got { adaptive_levels } which is a { type (adaptive_levels )} ' )
1275- for atom_range , adaptive_level in adaptive_levels .items ():
1276- if not isinstance (atom_range , tuple ) \
1277- or not all ([isinstance (a , int ) or a == 'inf' for a in atom_range ]) \
1278- or len (atom_range ) != 2 :
1279- raise InputError (f'Keys of the adaptive levels argument must be 2-length tuples of integers or an "inf" '
1280- f'indicator, got { atom_range } which is a { type (atom_range )} in:\n { adaptive_levels } ' )
1281- if not isinstance (adaptive_level , dict ):
1282- raise InputError (f'Each adaptive level in the adaptive levels argument must be a dictionary, '
1283- f'got { adaptive_level } which is a { type (adaptive_level )} in:\n { adaptive_levels } ' )
1291+ processed = dict ()
1292+ for entry in adaptive_levels :
1293+ if not isinstance (entry , dict ) or 'atom_range' not in entry or 'levels' not in entry :
1294+ raise InputError (f'Each adaptive levels entry must be a dictionary with "atom_range" and "levels" '
1295+ f'keys, got { entry } which is a { type (entry )} in:\n { adaptive_levels } ' )
1296+ atom_range = entry ['atom_range' ]
1297+ if not isinstance (atom_range , (list , tuple )) or len (atom_range ) != 2 \
1298+ or not isinstance (atom_range [0 ], int ) \
1299+ or not (isinstance (atom_range [1 ], int ) or atom_range [1 ] in ('inf' , float ('inf' ))):
1300+ raise InputError (f'The "atom_range" of each adaptive levels entry must be a 2-length list of an integer '
1301+ f'lower bound and an integer or "inf" upper bound, got { atom_range } '
1302+ f'in:\n { adaptive_levels } ' )
1303+ atom_range = (atom_range [0 ], 'inf' if atom_range [1 ] in ('inf' , float ('inf' )) else atom_range [1 ])
1304+ levels = entry ['levels' ]
1305+ if not isinstance (levels , dict ):
1306+ raise InputError (f'The "levels" of each adaptive levels entry must be a dictionary, '
1307+ f'got { levels } which is a { type (levels )} in:\n { adaptive_levels } ' )
12841308 processed [atom_range ] = dict ()
1285- for sub_key , level in adaptive_level .items ():
1286- new_sub_key = (sub_key ,) if isinstance (sub_key , str ) else sub_key
1287- if not isinstance (new_sub_key , tuple ):
1288- raise InputError (f'Job types specifications in adaptive levels must be tuples, got { sub_key } '
1289- f'which is a { type (sub_key )} in:\n { adaptive_levels } ' )
1290- new_level = Level (repr = level )
1291- processed [atom_range ][new_sub_key ] = new_level
1292- atom_ranges = sorted (list (adaptive_levels .keys ()), key = lambda x : x [0 ])
1309+ for job_types , level in levels .items ():
1310+ job_type_tuple = tuple (job_types .replace (',' , ' ' ).split ())
1311+ processed [atom_range ][job_type_tuple ] = Level (repr = level )
1312+ atom_ranges = sorted (processed .keys (), key = lambda x : x [0 ])
12931313 for i , atom_range in enumerate (atom_ranges ):
12941314 if i and atom_ranges [i - 1 ][1 ] + 1 != atom_ranges [i ][0 ]:
1295- raise InputError (f'Atom ranges of adaptive levels must be consecutive. '
1296- f'Got:\n { list (adaptive_levels .keys ())} ' )
1315+ raise InputError (f'Atom ranges of adaptive levels must be consecutive. Got:\n { atom_ranges } ' )
12971316 if atom_ranges [- 1 ][1 ] != 'inf' :
12981317 raise InputError (f'The last atom range must be "inf", got { atom_ranges [- 1 ][1 ]} in { atom_ranges } ' )
12991318 return processed
0 commit comments