Skip to content

Commit d4d8938

Browse files
alongdclaude
andcommitted
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. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2a4236a commit d4d8938

4 files changed

Lines changed: 70 additions & 28 deletions

File tree

arc/scheduler_test.py

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

12861286
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')]
1287+
"""Test that with thermo_at_own_level=True, wells on a different grain get relabeled copies"""
1288+
r = [ARCSpecies(label='CH4', smiles='C', thermo_at_own_level=True),
1289+
ARCSpecies(label='OH', smiles='[OH]', thermo_at_own_level=True)]
1290+
p = [ARCSpecies(label='CH3', smiles='[CH3]', thermo_at_own_level=True),
1291+
ARCSpecies(label='H2O', smiles='O', thermo_at_own_level=True)]
12901292
rxn = ARCReaction(label='CH4 + OH <=> CH3 + H2O', r_species=r, p_species=p)
12911293
sched = self.build_scheduler(rxn, r + p, 'adaptive_bimol')
12921294

@@ -1312,19 +1314,59 @@ def test_bimolecular_creates_copies(self):
13121314
self.assertIsNone(original.adaptive_lot_n_heavy)
13131315
self.assertTrue(original.compute_thermo)
13141316

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)]
1317+
def test_thermo_at_own_level_default_no_copy(self):
1318+
"""Test that by default (thermo_at_own_level=False) the species itself takes the reaction-wide level, no copy"""
1319+
r = [ARCSpecies(label='CH4', smiles='C'), ARCSpecies(label='OH', smiles='[OH]')]
1320+
p = [ARCSpecies(label='CH3', smiles='[CH3]'), ARCSpecies(label='H2O', smiles='O')]
13211321
rxn = ARCReaction(label='CH4 + OH <=> CH3 + H2O', r_species=r, p_species=p)
1322-
sched = self.build_scheduler(rxn, r + p, 'adaptive_noflag')
1322+
sched = self.build_scheduler(rxn, r + p, 'adaptive_default')
13231323

13241324
self.assertEqual(rxn.label, 'CH4 + OH <=> CH3 + H2O')
13251325
self.assertFalse(any('_TS' in label for label in sched.species_dict))
13261326
self.assertEqual(sched.species_dict['CH4'].adaptive_lot_n_heavy, 2)
13271327

1328+
def test_shared_species_across_grains_gets_copy(self):
1329+
"""Test that a no-copy species shared by reactions on different grains gets a copy for the second reaction"""
1330+
oh = ARCSpecies(label='OH', smiles='[OH]')
1331+
h2o = ARCSpecies(label='H2O', smiles='O')
1332+
rxn1 = ARCReaction(label='CH4 + OH <=> CH3 + H2O',
1333+
r_species=[ARCSpecies(label='CH4', smiles='C'), oh],
1334+
p_species=[ARCSpecies(label='CH3', smiles='[CH3]'), h2o])
1335+
rxn2 = ARCReaction(label='C3H8 + OH <=> nC3H7 + H2O',
1336+
r_species=[ARCSpecies(label='C3H8', smiles='CCC'), oh],
1337+
p_species=[ARCSpecies(label='nC3H7', smiles='[CH2]CC'), h2o])
1338+
project_directory = os.path.join(ARC_PATH, 'Projects', 'adaptive_shared_delete')
1339+
self.addCleanup(shutil.rmtree, project_directory, ignore_errors=True)
1340+
species_list = rxn1.r_species + rxn1.p_species + [rxn2.r_species[0], rxn2.p_species[0]]
1341+
sched = Scheduler(project='adaptive_shared',
1342+
ess_settings=self.ess_settings,
1343+
species_list=species_list,
1344+
rxn_list=[rxn1, rxn2],
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={(1, 1): {('sp',): Level(repr='ccsd(t)-f12/cc-pvtz-f12')},
1349+
(2, 3): {('sp',): Level(repr='dlpno-ccsd(t)/def2-tzvp')},
1350+
(4, 'inf'): {('sp',): Level(repr='b3lyp/6-311+g(d,p)')}},
1351+
project_directory=project_directory,
1352+
job_types=initialize_job_types(),
1353+
testing=True)
1354+
1355+
# rxn1 (2 heavy atoms) set the shared wells' overrides; rxn1 itself is unchanged.
1356+
self.assertEqual(rxn1.label, 'CH4 + OH <=> CH3 + H2O')
1357+
self.assertEqual(sched.species_dict['OH'].adaptive_lot_n_heavy, 2)
1358+
# rxn2 (4 heavy atoms) lands on a different grain, so the shared wells got dedicated copies.
1359+
self.assertEqual(set(rxn2.reactants), {'C3H8', 'OH_TS1'})
1360+
self.assertEqual(set(rxn2.products), {'nC3H7', 'H2O_TS1'})
1361+
self.assertEqual(rxn2.label,
1362+
rxn2.arrow.join([rxn2.plus.join(rxn2.reactants), rxn2.plus.join(rxn2.products)]))
1363+
for copy_label in ['OH_TS1', 'H2O_TS1']:
1364+
self.assertEqual(sched.species_dict[copy_label].adaptive_lot_n_heavy, 4)
1365+
self.assertFalse(sched.species_dict[copy_label].compute_thermo)
1366+
# Unshared rxn2 wells just took the rxn2 override, no copies.
1367+
self.assertEqual(sched.species_dict['C3H8'].adaptive_lot_n_heavy, 4)
1368+
self.assertEqual(sched.species_dict['nC3H7'].adaptive_lot_n_heavy, 4)
1369+
13281370
def test_unimolecular_no_copy(self):
13291371
"""Test that a reaction whose well shares the reaction's grain gets no copies"""
13301372
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: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -283,12 +283,12 @@ levels (defined separately, e.g., via ``opt_level``, or using ARC's defaults).
283283
When a species participates in a reaction, all of the reaction's species (the TS and the
284284
reactant/product wells) are energy-evaluated at a single, reaction-consistent level keyed
285285
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).
286+
By default (the per-species ``thermo_at_own_level`` flag, ``False``) a well whose own size
287+
falls on a finer grain than its reaction's is evaluated directly at the reaction-wide
288+
(coarser) level, and its thermochemistry uses that same level. Set ``thermo_at_own_level=True`` on a species
289+
to instead compute its thermochemistry at its own size-appropriate (granular) adaptive level:
290+
an autonomous, relabeled copy of the species is then created and used by the reaction (at the
291+
reaction-wide level), while the original keeps its own level for thermochemistry.
292292

293293

294294
Control job memory allocation

0 commit comments

Comments
 (0)