Skip to content

Commit 95b96b3

Browse files
committed
species: default thermo_at_own_level to False
Reaction wells take the reaction-wide level directly by default (no relabeled copies, no duplicated jobs); set True per species to opt into its own granular level for thermo.
1 parent 3dad5ed commit 95b96b3

4 files changed

Lines changed: 74 additions & 31 deletions

File tree

arc/scheduler_test.py

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,9 +1317,11 @@ def build_scheduler(self, rxn, species_list, name):
13171317
testing=True)
13181318

13191319
def test_bimolecular_creates_copies(self):
1320-
"""Test that a reaction with wells on a different grain than the reaction gets relabeled copies"""
1321-
r = [ARCSpecies(label='CH4', smiles='C'), ARCSpecies(label='OH', smiles='[OH]')]
1322-
p = [ARCSpecies(label='CH3', smiles='[CH3]'), ARCSpecies(label='H2O', smiles='O')]
1320+
"""Test that with thermo_at_own_level=True, wells on a different grain get relabeled copies"""
1321+
r = [ARCSpecies(label='CH4', smiles='C', thermo_at_own_level=True),
1322+
ARCSpecies(label='OH', smiles='[OH]', thermo_at_own_level=True)]
1323+
p = [ARCSpecies(label='CH3', smiles='[CH3]', thermo_at_own_level=True),
1324+
ARCSpecies(label='H2O', smiles='O', thermo_at_own_level=True)]
13231325
rxn = ARCReaction(label='CH4 + OH <=> CH3 + H2O', r_species=r, p_species=p)
13241326
sched = self.build_scheduler(rxn, r + p, 'adaptive_bimol')
13251327

@@ -1347,19 +1349,59 @@ def test_bimolecular_creates_copies(self):
13471349
self.assertIsNone(original.adaptive_lot_n_heavy)
13481350
self.assertTrue(original.compute_thermo)
13491351

1350-
def test_thermo_at_own_level_false_no_copy(self):
1351-
"""Test that with thermo_at_own_level=False the species itself takes the reaction-wide level, with no copy"""
1352-
r = [ARCSpecies(label='CH4', smiles='C', thermo_at_own_level=False),
1353-
ARCSpecies(label='OH', smiles='[OH]', thermo_at_own_level=False)]
1354-
p = [ARCSpecies(label='CH3', smiles='[CH3]', thermo_at_own_level=False),
1355-
ARCSpecies(label='H2O', smiles='O', thermo_at_own_level=False)]
1352+
def test_thermo_at_own_level_default_no_copy(self):
1353+
"""Test that by default (thermo_at_own_level=False) the species itself takes the reaction-wide level, no copy"""
1354+
r = [ARCSpecies(label='CH4', smiles='C'), ARCSpecies(label='OH', smiles='[OH]')]
1355+
p = [ARCSpecies(label='CH3', smiles='[CH3]'), ARCSpecies(label='H2O', smiles='O')]
13561356
rxn = ARCReaction(label='CH4 + OH <=> CH3 + H2O', r_species=r, p_species=p)
1357-
sched = self.build_scheduler(rxn, r + p, 'adaptive_noflag')
1357+
sched = self.build_scheduler(rxn, r + p, 'adaptive_default')
13581358

13591359
self.assertEqual(rxn.label, 'CH4 + OH <=> CH3 + H2O')
13601360
self.assertFalse(any('_TS' in label for label in sched.species_dict))
13611361
self.assertEqual(sched.species_dict['CH4'].adaptive_lot_n_heavy, 2)
13621362

1363+
def test_shared_species_across_grains_gets_copy(self):
1364+
"""Test that a no-copy species shared by reactions on different grains gets a copy for the second reaction"""
1365+
oh = ARCSpecies(label='OH', smiles='[OH]')
1366+
h2o = ARCSpecies(label='H2O', smiles='O')
1367+
rxn1 = ARCReaction(label='CH4 + OH <=> CH3 + H2O',
1368+
r_species=[ARCSpecies(label='CH4', smiles='C'), oh],
1369+
p_species=[ARCSpecies(label='CH3', smiles='[CH3]'), h2o])
1370+
rxn2 = ARCReaction(label='C3H8 + OH <=> nC3H7 + H2O',
1371+
r_species=[ARCSpecies(label='C3H8', smiles='CCC'), oh],
1372+
p_species=[ARCSpecies(label='nC3H7', smiles='[CH2]CC'), h2o])
1373+
project_directory = os.path.join(ARC_PATH, 'Projects', 'adaptive_shared_delete')
1374+
self.addCleanup(shutil.rmtree, project_directory, ignore_errors=True)
1375+
species_list = rxn1.r_species + rxn1.p_species + [rxn2.r_species[0], rxn2.p_species[0]]
1376+
sched = Scheduler(project='adaptive_shared',
1377+
ess_settings=self.ess_settings,
1378+
species_list=species_list,
1379+
rxn_list=[rxn1, rxn2],
1380+
opt_level=Level(repr='b3lyp/6-31g(d,p)'),
1381+
sp_level=Level(repr='b3lyp/6-311+g(d,p)'),
1382+
freq_level=Level(repr='b3lyp/6-31g(d,p)'),
1383+
adaptive_levels={(1, 1): {('sp',): Level(repr='ccsd(t)-f12/cc-pvtz-f12')},
1384+
(2, 3): {('sp',): Level(repr='dlpno-ccsd(t)/def2-tzvp')},
1385+
(4, 'inf'): {('sp',): Level(repr='b3lyp/6-311+g(d,p)')}},
1386+
project_directory=project_directory,
1387+
job_types=initialize_job_types(),
1388+
testing=True)
1389+
1390+
# rxn1 (2 heavy atoms) set the shared wells' overrides; rxn1 itself is unchanged.
1391+
self.assertEqual(rxn1.label, 'CH4 + OH <=> CH3 + H2O')
1392+
self.assertEqual(sched.species_dict['OH'].adaptive_lot_n_heavy, 2)
1393+
# rxn2 (4 heavy atoms) lands on a different grain, so the shared wells got dedicated copies.
1394+
self.assertEqual(set(rxn2.reactants), {'C3H8', 'OH_TS1'})
1395+
self.assertEqual(set(rxn2.products), {'nC3H7', 'H2O_TS1'})
1396+
self.assertEqual(rxn2.label,
1397+
rxn2.arrow.join([rxn2.plus.join(rxn2.reactants), rxn2.plus.join(rxn2.products)]))
1398+
for copy_label in ['OH_TS1', 'H2O_TS1']:
1399+
self.assertEqual(sched.species_dict[copy_label].adaptive_lot_n_heavy, 4)
1400+
self.assertFalse(sched.species_dict[copy_label].compute_thermo)
1401+
# Unshared rxn2 wells just took the rxn2 override, no copies.
1402+
self.assertEqual(sched.species_dict['C3H8'].adaptive_lot_n_heavy, 4)
1403+
self.assertEqual(sched.species_dict['nC3H7'].adaptive_lot_n_heavy, 4)
1404+
13631405
def test_unimolecular_no_copy(self):
13641406
"""Test that a reaction whose well shares the reaction's grain gets no copies"""
13651407
r = [ARCSpecies(label='nC3H7', smiles='[CH2]CC')]

arc/species/species.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,10 +109,10 @@ class ARCSpecies(object):
109109
bond_corrections (dict, optional): The bond additivity corrections (BAC) to be used. Determined from the
110110
structure if not directly given.
111111
compute_thermo (bool, optional): Whether to calculate thermodynamic properties for this species.
112-
thermo_at_own_level (bool, optional): Relevant only when ``adaptive_levels`` are used. If ``True`` (default),
113-
the species' thermochemistry is computed at its own size-appropriate (granular) adaptive level even when
114-
it participates in a reaction; the reaction's energetics then use a separate relabeled copy of the species
115-
evaluated at the reaction-consistent level. If ``False``, the species itself is evaluated at the
112+
thermo_at_own_level (bool, optional): Relevant only when ``adaptive_levels`` are used. If ``True``, the
113+
species' thermochemistry is computed at its own size-appropriate (granular) adaptive level even when it
114+
participates in a reaction; the reaction's energetics then use a separate relabeled copy of the species
115+
evaluated at the reaction-consistent level. If ``False`` (default), the species itself is evaluated at the
116116
reaction-consistent (coarser) level and no copy is made.
117117
include_in_thermo_lib (bool, optional): Whether to include in the output RMG library.
118118
e0_only (bool, optional): Whether to only run statmech (w/o thermo) to compute E0.
@@ -317,7 +317,7 @@ def __init__(self,
317317
charge: int | None = None,
318318
checkfile: str | None = None,
319319
compute_thermo: bool | None = None,
320-
thermo_at_own_level: bool = True,
320+
thermo_at_own_level: bool = False,
321321
adaptive_lot_n_heavy: int | None = None,
322322
include_in_thermo_lib: bool | None = True,
323323
consider_all_diastereomers: bool = True,
@@ -702,7 +702,7 @@ def as_dict(self,
702702
species_dict['charge'] = self.charge
703703
if not self.compute_thermo and not self.is_ts:
704704
species_dict['compute_thermo'] = self.compute_thermo
705-
if not self.thermo_at_own_level:
705+
if self.thermo_at_own_level:
706706
species_dict['thermo_at_own_level'] = self.thermo_at_own_level
707707
if self.adaptive_lot_n_heavy is not None:
708708
species_dict['adaptive_lot_n_heavy'] = self.adaptive_lot_n_heavy
@@ -902,7 +902,7 @@ def from_dict(self, species_dict):
902902
self.multi_species = species_dict['multi_species'] if 'multi_species' in species_dict else None
903903
self.charge = species_dict['charge'] if 'charge' in species_dict else 0
904904
self.compute_thermo = species_dict['compute_thermo'] if 'compute_thermo' in species_dict else not self.is_ts
905-
self.thermo_at_own_level = species_dict['thermo_at_own_level'] if 'thermo_at_own_level' in species_dict else True
905+
self.thermo_at_own_level = species_dict['thermo_at_own_level'] if 'thermo_at_own_level' in species_dict else False
906906
self.adaptive_lot_n_heavy = species_dict['adaptive_lot_n_heavy'] if 'adaptive_lot_n_heavy' in species_dict else None
907907
self.include_in_thermo_lib = species_dict['include_in_thermo_lib'] if 'include_in_thermo_lib' in species_dict else True
908908
self.e0_only = species_dict['e0_only'] if 'e0_only' in species_dict else False

arc/species/species_test.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -610,22 +610,22 @@ def test_as_dict(self):
610610

611611
def test_thermo_at_own_level_round_trip(self):
612612
"""Test that thermo_at_own_level and adaptive_lot_n_heavy round-trip through as_dict/from_dict"""
613-
# Defaults: not serialized, restored to True / None.
613+
# Defaults: not serialized, restored to False / None.
614614
default_spc = ARCSpecies(label='ethane', smiles='CC')
615615
default_dict = default_spc.as_dict()
616616
self.assertNotIn('thermo_at_own_level', default_dict)
617617
self.assertNotIn('adaptive_lot_n_heavy', default_dict)
618618
restored_default = ARCSpecies(species_dict=default_dict)
619-
self.assertTrue(restored_default.thermo_at_own_level)
619+
self.assertFalse(restored_default.thermo_at_own_level)
620620
self.assertIsNone(restored_default.adaptive_lot_n_heavy)
621621

622622
# Non-default values: serialized and restored.
623-
spc = ARCSpecies(label='ethane', smiles='CC', thermo_at_own_level=False, adaptive_lot_n_heavy=8)
623+
spc = ARCSpecies(label='ethane', smiles='CC', thermo_at_own_level=True, adaptive_lot_n_heavy=8)
624624
spc_dict = spc.as_dict()
625-
self.assertFalse(spc_dict['thermo_at_own_level'])
625+
self.assertTrue(spc_dict['thermo_at_own_level'])
626626
self.assertEqual(spc_dict['adaptive_lot_n_heavy'], 8)
627627
restored = ARCSpecies(species_dict=spc_dict)
628-
self.assertFalse(restored.thermo_at_own_level)
628+
self.assertTrue(restored.thermo_at_own_level)
629629
self.assertEqual(restored.adaptive_lot_n_heavy, 8)
630630

631631
def test_from_dict(self):

docs/source/advanced.rst

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -252,9 +252,10 @@ Adaptive levels of theory
252252
^^^^^^^^^^^^^^^^^^^^^^^^^
253253
ARC allows users to adapt the level of theory to the size of the molecule.
254254
To do so, pass the ``adaptive_levels`` attribute, which is a list of entries.
255-
Each entry is a dictionary with an ``atom_range`` 2-list giving the heavy
256-
(non-hydrogen) atom count range (the upper bound may be ``inf``), and a ``levels``
257-
mapping from job type to the respective level of theory (a string or a dictionary).
255+
Each entry is a dictionary with two keys: ``atom_range``, a two-element list ``[min, max]``
256+
giving the species' size as a range of heavy (non-hydrogen) atom counts (the upper bound of
257+
the last range must be ``inf``); and ``levels``, a mapping from job type(s) to the level of
258+
theory (a string or a dictionary) to use for that size range.
258259
Job types that share a level may be given as a single whitespace- or comma-separated
259260
key (e.g. ``opt freq``). Don't forget to bound the entire range between 1 and
260261
``inf``, and make sure there aren't any gaps in the heavy atom ranges. For example::
@@ -283,12 +284,12 @@ levels (defined separately, e.g., via ``opt_level``, or using ARC's defaults).
283284
When a species participates in a reaction, all of the reaction's species (the TS and the
284285
reactant/product wells) are energy-evaluated at a single, reaction-consistent level keyed
285286
by the largest participant (the TS), so the barrier is computed at one consistent level.
286-
By default (the per-species ``thermo_at_own_level`` flag, ``True``) each species'
287-
thermochemistry is still computed at its own size-appropriate (granular) adaptive level: an
288-
autonomous, relabeled copy of any well that lands on a coarser grain than its reaction is
289-
created and used by the reaction (at the reaction-wide level), while the original species
290-
keeps its own level for thermochemistry. Set ``thermo_at_own_level=False`` on a species to
291-
have it evaluated at the reaction-wide level directly (no copy, coarser thermochemistry).
287+
By default (the per-species ``thermo_at_own_level`` flag, ``False``) a well whose own size
288+
falls on a finer grain than its reaction's is evaluated directly at the reaction-wide
289+
(coarser) level, and its thermochemistry uses that same level. Set ``thermo_at_own_level=True`` on a species
290+
to instead compute its thermochemistry at its own size-appropriate (granular) adaptive level:
291+
an autonomous, relabeled copy of the species is then created and used by the reaction (at the
292+
reaction-wide level), while the original keeps its own level for thermochemistry.
292293

293294

294295
Control job memory allocation

0 commit comments

Comments
 (0)