@@ -1241,5 +1241,125 @@ def test_spawn_ts_jobs_unknown_family_admission_predicate(self):
12411241 self .assertEqual (admit_unknown_family , expected_admission )
12421242
12431243
1244+ class TestSchedulerAdaptiveReactionLevels (unittest .TestCase ):
1245+ """
1246+ Contains unit tests for the reaction-wide adaptive levels of theory logic
1247+ (Scheduler._apply_adaptive_reaction_levels and the spawn_job override).
1248+ """
1249+ @classmethod
1250+ def setUpClass (cls ):
1251+ """A method that is run before all unit tests in this class."""
1252+ cls .ess_settings = {'gaussian' : ['server1' ], 'molpro' : ['server2' , 'server1' ], 'qchem' : ['server1' ]}
1253+ # A grain at exactly 1 heavy atom, and another for everything larger, so single-heavy-atom wells
1254+ # (e.g. CH4, OH) land on a different grain than a 2-heavy-atom reaction.
1255+ cls .adaptive_levels = {(1 , 1 ): {('opt' , 'freq' ): Level (repr = 'wb97xd/def2tzvp' ),
1256+ ('sp' ,): Level (repr = 'ccsd(t)-f12/cc-pvtz-f12' )},
1257+ (2 , 'inf' ): {('opt' , 'freq' ): Level (repr = 'b3lyp/6-31g(d,p)' ),
1258+ ('sp' ,): Level (repr = 'b3lyp/6-311+g(d,p)' )}}
1259+
1260+ def build_scheduler (self , rxn , species_list , name ):
1261+ """
1262+ Build a testing Scheduler for a single reaction under the class adaptive levels.
1263+
1264+ Args:
1265+ rxn (ARCReaction): The reaction.
1266+ species_list (list): The species list.
1267+ name (str): A unique project name.
1268+
1269+ Returns:
1270+ Scheduler: The constructed (testing) scheduler.
1271+ """
1272+ project_directory = os .path .join (ARC_PATH , 'Projects' , f'{ name } _delete' )
1273+ self .addCleanup (shutil .rmtree , project_directory , ignore_errors = True )
1274+ return Scheduler (project = name ,
1275+ ess_settings = self .ess_settings ,
1276+ species_list = species_list ,
1277+ rxn_list = [rxn ],
1278+ opt_level = Level (repr = 'b3lyp/6-31g(d,p)' ),
1279+ sp_level = Level (repr = 'b3lyp/6-311+g(d,p)' ),
1280+ freq_level = Level (repr = 'b3lyp/6-31g(d,p)' ),
1281+ adaptive_levels = self .adaptive_levels ,
1282+ project_directory = project_directory ,
1283+ job_types = initialize_job_types (),
1284+ testing = True )
1285+
1286+ def test_bimolecular_creates_copies (self ):
1287+ """Test that a reaction with wells on a different grain than the reaction gets relabeled copies"""
1288+ r = [ARCSpecies (label = 'CH4' , smiles = 'C' ), ARCSpecies (label = 'OH' , smiles = '[OH]' )]
1289+ p = [ARCSpecies (label = 'CH3' , smiles = '[CH3]' ), ARCSpecies (label = 'H2O' , smiles = 'O' )]
1290+ rxn = ARCReaction (label = 'CH4 + OH <=> CH3 + H2O' , r_species = r , p_species = p )
1291+ sched = self .build_scheduler (rxn , r + p , 'adaptive_bimol' )
1292+
1293+ # The reaction is now defined with the copies, and stays self-consistent.
1294+ self .assertEqual (rxn .reactants , ['CH4_TS0' , 'OH_TS0' ])
1295+ self .assertEqual (rxn .products , ['CH3_TS0' , 'H2O_TS0' ])
1296+ self .assertEqual ([s .label for s in rxn .r_species ], ['CH4_TS0' , 'OH_TS0' ])
1297+ self .assertEqual (rxn .label , 'CH4_TS0 + OH_TS0 <=> CH3_TS0 + H2O_TS0' )
1298+ rxn .check_attributes () # Must not raise.
1299+
1300+ # The copies are autonomous, evaluated at the reaction-wide level, and not part of the thermo library.
1301+ for copy_label in ['CH4_TS0' , 'OH_TS0' , 'CH3_TS0' , 'H2O_TS0' ]:
1302+ self .assertIn (copy_label , sched .species_dict )
1303+ self .assertIn (copy_label , sched .output )
1304+ copy_spc = sched .species_dict [copy_label ]
1305+ self .assertEqual (copy_spc .adaptive_lot_n_heavy , 2 )
1306+ self .assertFalse (copy_spc .compute_thermo )
1307+ self .assertFalse (copy_spc .include_in_thermo_lib )
1308+
1309+ # The originals are untouched - they keep their own granular level and their own thermo.
1310+ for original_label in ['CH4' , 'OH' , 'CH3' , 'H2O' ]:
1311+ original = sched .species_dict [original_label ]
1312+ self .assertIsNone (original .adaptive_lot_n_heavy )
1313+ self .assertTrue (original .compute_thermo )
1314+
1315+ def test_thermo_at_own_level_false_no_copy (self ):
1316+ """Test that with thermo_at_own_level=False the species itself takes the reaction-wide level, with no copy"""
1317+ r = [ARCSpecies (label = 'CH4' , smiles = 'C' , thermo_at_own_level = False ),
1318+ ARCSpecies (label = 'OH' , smiles = '[OH]' , thermo_at_own_level = False )]
1319+ p = [ARCSpecies (label = 'CH3' , smiles = '[CH3]' , thermo_at_own_level = False ),
1320+ ARCSpecies (label = 'H2O' , smiles = 'O' , thermo_at_own_level = False )]
1321+ rxn = ARCReaction (label = 'CH4 + OH <=> CH3 + H2O' , r_species = r , p_species = p )
1322+ sched = self .build_scheduler (rxn , r + p , 'adaptive_noflag' )
1323+
1324+ self .assertEqual (rxn .label , 'CH4 + OH <=> CH3 + H2O' )
1325+ self .assertFalse (any ('_TS' in label for label in sched .species_dict ))
1326+ self .assertEqual (sched .species_dict ['CH4' ].adaptive_lot_n_heavy , 2 )
1327+
1328+ def test_unimolecular_no_copy (self ):
1329+ """Test that a reaction whose well shares the reaction's grain gets no copies"""
1330+ r = [ARCSpecies (label = 'nC3H7' , smiles = '[CH2]CC' )]
1331+ p = [ARCSpecies (label = 'iC3H7' , smiles = 'C[CH]C' )]
1332+ rxn = ARCReaction (label = 'nC3H7 <=> iC3H7' , r_species = r , p_species = p )
1333+ sched = self .build_scheduler (rxn , r + p , 'adaptive_unimol' )
1334+
1335+ self .assertEqual (rxn .label , 'nC3H7 <=> iC3H7' )
1336+ self .assertFalse (any ('_TS' in label for label in sched .species_dict ))
1337+ self .assertIsNone (sched .species_dict ['nC3H7' ].adaptive_lot_n_heavy )
1338+
1339+ def test_spawn_job_uses_override (self ):
1340+ """Test that determine_adaptive_level is driven by adaptive_lot_n_heavy when set"""
1341+ spc = ARCSpecies (label = 'CH4' , smiles = 'C' )
1342+ sched = Scheduler (project = 'adaptive_override' ,
1343+ ess_settings = self .ess_settings ,
1344+ species_list = [spc ],
1345+ opt_level = Level (repr = 'b3lyp/6-31g(d,p)' ),
1346+ sp_level = Level (repr = 'b3lyp/6-311+g(d,p)' ),
1347+ freq_level = Level (repr = 'b3lyp/6-31g(d,p)' ),
1348+ adaptive_levels = self .adaptive_levels ,
1349+ project_directory = os .path .join (ARC_PATH , 'Projects' , 'adaptive_override_delete' ),
1350+ job_types = initialize_job_types (),
1351+ testing = True )
1352+ self .addCleanup (shutil .rmtree , os .path .join (ARC_PATH , 'Projects' , 'adaptive_override_delete' ),
1353+ ignore_errors = True )
1354+ original = Level (method = 'CBS-QB3' )
1355+ # 1 heavy atom (own count) -> the (1, 1) grain.
1356+ self .assertEqual (sched .determine_adaptive_level (original , 'sp' , spc .number_of_heavy_atoms ).simple (),
1357+ 'ccsd(t)-f12/cc-pvtz-f12' )
1358+ # With the override set to a 2-heavy-atom reaction, the (2, 'inf') grain is used instead.
1359+ spc .adaptive_lot_n_heavy = 2
1360+ heavy = spc .adaptive_lot_n_heavy if spc .adaptive_lot_n_heavy is not None else spc .number_of_heavy_atoms
1361+ self .assertEqual (sched .determine_adaptive_level (original , 'sp' , heavy ).simple (), 'b3lyp/6-311+g(d,p)' )
1362+
1363+
12441364if __name__ == '__main__' :
12451365 unittest .main (testRunner = unittest .TextTestRunner (verbosity = 2 ))
0 commit comments