Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions arc/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2952,16 +2952,16 @@ def check_negative_freq(self,
def check_rxn_e0_by_spc(self, label: str):
"""
Check the E0 (electronic energy + ZPE) of reactions related to a specific species.
Requires all opt + freq computations to be converged for all species (and TS) participating in each reaction.
Requires SP energies for all participants and frequencies for all non-monoatomic participants.

Args:
label (str): A label representing a species.
"""
for rxn in self.rxn_list:
labels = rxn.reactants + rxn.products + [rxn.ts_label]
if label in labels and rxn.ts_species.ts_checks['E0'] is None \
and all([species_has_sp_and_freq(output_dict, self.species_dict[spc_label].yml_path)
for spc_label, output_dict in self.output.items() if spc_label in labels]):
and all([species_is_ready_for_e0(self.output[spc_label], self.species_dict[spc_label])
for spc_label in set(labels)]):
check_ts(reaction=rxn,
checks=['energy'],
species_dict=self.species_dict,
Expand Down Expand Up @@ -3090,7 +3090,7 @@ def post_sp_actions(self,
self.output[label]['paths']['sp_no_sol'] = sp_path
self.output[label]['paths']['sp'] = original_sp_path # restore the original path

if species_has_freq(self.output[label], self.species_dict[label].yml_path):
if species_is_ready_for_e0(self.output[label], self.species_dict[label]):
self.check_rxn_e0_by_spc(label)

if self.report_e_elect:
Expand Down Expand Up @@ -4516,3 +4516,12 @@ def species_has_sp_and_freq(species_output_dict: dict,
Whether a species has a valid converged single-point energy and frequencies.
"""
return species_has_sp(species_output_dict, yml_path) and species_has_freq(species_output_dict, yml_path)


def species_is_ready_for_e0(species_output_dict: dict,
species: ARCSpecies,
) -> bool:
"""Check whether a species has the SP energy and, when applicable, frequencies needed to compute its E0."""
if species.is_monoatomic():
return species_has_sp(species_output_dict, species.yml_path)
return species_has_sp_and_freq(species_output_dict, species.yml_path)
102 changes: 101 additions & 1 deletion arc/scheduler_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
from arc.level import Level
from arc.plotter import save_conformers_file
from arc.scheduler import (Scheduler, SchedulerError, species_has_freq, species_has_geo, species_has_sp,
species_has_sp_and_freq, tsg_method_matches_adapter)
species_has_sp_and_freq, species_is_ready_for_e0,
tsg_method_matches_adapter)
from arc.imports import settings
from arc.reaction import ARCReaction
from arc.species.converter import str_to_xyz
Expand Down Expand Up @@ -894,6 +895,105 @@ def test_species_has_geo_sp_freq(self):
self.assertTrue(species_has_sp(species_output_dict=species_output_dict, yml_path=yml_path))
self.assertTrue(species_has_sp_and_freq(species_output_dict=species_output_dict, yml_path=yml_path))

def test_species_is_ready_for_e0(self):
"""Monoatomic species require an SP energy but legitimately have no frequencies."""
output = {'paths': {'sp': 'sp.out', 'freq': '', 'composite': ''}}
monoatomic = ARCSpecies(label='O', smiles='[O]')
molecular = ARCSpecies(label='OH', smiles='[OH]')
self.assertTrue(species_is_ready_for_e0(output, monoatomic))
self.assertFalse(species_is_ready_for_e0(output, molecular))
output['paths']['sp'] = ''
self.assertFalse(species_is_ready_for_e0(output, monoatomic))

@patch('arc.scheduler.check_ts')
def test_monoatomic_participant_reaches_e0_check_and_switches_ts(self, mock_check_ts):
"""A failed E0 check switches guesses even when one reaction participant is monoatomic."""
scheduler = object.__new__(Scheduler)
species = {
'R': ARCSpecies(label='R', smiles='OO'),
'O': ARCSpecies(label='O', smiles='[O]'),
'P': ARCSpecies(label='P', smiles='O=O'),
'TS0': ARCSpecies(label='TS0', is_ts=True),
}
species['TS0'].ts_guesses_exhausted = False
species['TS0'].chosen_ts = 1
rxn = MagicMock()
rxn.reactants = ['R', 'O']
rxn.products = ['P']
rxn.ts_label = 'TS0'
rxn.ts_species = species['TS0']
rxn.label = 'R + O <=> P'
scheduler.rxn_list = [rxn]
scheduler.species_dict = species
scheduler.output = {
label: {'paths': {'sp': 'sp.out', 'freq': '' if label == 'O' else 'freq.out', 'composite': ''},
'convergence': True}
for label in species
}
scheduler.project_directory = '/tmp'
scheduler.kinetics_adapter = 'arkane'
scheduler.sp_level = Level('gfn2')
scheduler.composite_method = None
scheduler.freq_scale_factor = 1.0
scheduler.switch_ts = MagicMock()

def fail_e0(**kwargs):
kwargs['reaction'].ts_species.ts_checks['E0'] = False

mock_check_ts.side_effect = fail_e0
scheduler.check_rxn_e0_by_spc('O')

mock_check_ts.assert_called_once()
scheduler.switch_ts.assert_called_once_with('TS0')

@patch('arc.scheduler.check_ts')
@patch('arc.scheduler.parser.parse_e_elect', return_value=-75.0)
def test_atomic_sp_completion_triggers_e0_check_and_switches_ts(self, mock_parse_e_elect, mock_check_ts):
"""Completing the last atomic SP triggers the E0 check and switches a failed TS."""
scheduler = object.__new__(Scheduler)
species = {
'R': ARCSpecies(label='R', smiles='OO'),
'O': ARCSpecies(label='O', smiles='[O]'),
'P': ARCSpecies(label='P', smiles='O=O'),
'TS0': ARCSpecies(label='TS0', is_ts=True),
}
species['TS0'].ts_guesses_exhausted = False
species['TS0'].chosen_ts = 1
rxn = MagicMock()
rxn.reactants = ['R', 'O']
rxn.products = ['P']
rxn.ts_label = 'TS0'
rxn.ts_species = species['TS0']
rxn.label = 'R + O <=> P'
scheduler.rxn_list = [rxn]
scheduler.species_dict = species
scheduler.output = {
label: {'paths': {'sp': '' if label == 'O' else 'sp.out',
'freq': '' if label == 'O' else 'freq.out',
'composite': ''},
'job_types': {'sp': False},
'info': '',
'convergence': True}
for label in species
}
scheduler.project_directory = '/tmp'
scheduler.kinetics_adapter = 'arkane'
scheduler.sp_level = Level('gfn2')
scheduler.composite_method = None
scheduler.freq_scale_factor = 1.0
scheduler.report_e_elect = False
scheduler.switch_ts = MagicMock()

def fail_e0(**kwargs):
kwargs['reaction'].ts_species.ts_checks['E0'] = False

mock_check_ts.side_effect = fail_e0
scheduler.post_sp_actions('O', 'atomic-sp.out')

mock_parse_e_elect.assert_called_once_with('atomic-sp.out')
mock_check_ts.assert_called_once()
scheduler.switch_ts.assert_called_once_with('TS0')

def test_add_label_to_unique_species_labels(self):
"""Test the add_label_to_unique_species_labels() method."""
self.assertEqual(self.sched2.unique_species_labels, ['methylamine', 'C2H6', 'CtripCO'])
Expand Down
Loading