Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow ARCSpecies to be initialized with a conformer list file #104

Merged
merged 12 commits into from
Mar 28, 2019
Merged
5 changes: 4 additions & 1 deletion arc/job/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,10 @@ def __init__(self, project, settings, species_name, xyz, job_type, level_of_theo
self.submit = ''
self.input = ''
self.server_nodes = list()
self._write_initiated_job_to_csv_file()
if job_num is None:
# this checks jon_num and not self.job_num on purpose
# if job_num was given, then don't save as initiated jobs, this is a restarted job
self._write_initiated_job_to_csv_file()

def as_dict(self):
"""A helper function for dumping this object as a dictionary in a YAML file for restarting ARC"""
Expand Down
6 changes: 3 additions & 3 deletions arc/job/submit.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@
SubmitDir=`pwd`

GAUSS_SCRDIR=/scratch/users/{un}/g09/$SLURM_JOB_NAME-$SLURM_JOB_ID
export GAUSS_SCRDIR
export GAUSS_SCRDIR

mkdir -p $GAUSS_SCRDIR
mkdir -p $WorkDir

cd $WorkDir
cd $WorkDir
. $g09root/g09/bsd/g09.profile

cp $SubmitDir/input.gjf .
Expand Down Expand Up @@ -79,7 +79,7 @@
SubmitDir=`pwd`

mkdir -p $WorkDir
cd $WorkDir
cd $WorkDir

cp $SubmitDir/input.inp .

Expand Down
3 changes: 3 additions & 0 deletions arc/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from __future__ import (absolute_import, division, print_function, unicode_literals)
import logging
import warnings
import sys
import os
import time
Expand Down Expand Up @@ -776,6 +777,8 @@ def log_header(self, level=logging.INFO):
logging.log(level, 'The current git HEAD for ARC is:')
logging.log(level, ' {0}\n {1}\n'.format(head, date))
logging.info('Starting project {0}'.format(self.project))
# ignore Paramiko warnings:
warnings.filterwarnings(action='ignore', module='.*paramiko.*')

def log_footer(self, level=logging.INFO):
"""
Expand Down
1 change: 0 additions & 1 deletion arc/mainTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ def test_as_dict(self):
'multiplicity': 1,
'neg_freqs_trshed': [],
'number_of_rotors': 0,
'opt_level': '',
'rotors_dict': {},
't1': None}],
}
Expand Down
130 changes: 78 additions & 52 deletions arc/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ def __init__(self, project, settings, species_list, composite_method, conformer_
# restart-related check are performed in run_scan_jobs()
self.run_scan_jobs(species.label)
elif not self.species_dict[species.label].is_ts and self.generate_conformers\
and 'geo' not in self.output[species.label]:
and 'geo' not in self.output[species.label] and not species.conformers:
self.species_dict[species.label].generate_conformers()
else:
# Species is loaded from a YAML file
Expand All @@ -325,6 +325,13 @@ def schedule_jobs(self):
"""
The main job scheduling block
"""
for species in self.species_dict.values():
if not species.initial_xyz and not species.final_xyz and species.conformers and species.conformer_energies:
self.determine_most_stable_conformer(species.label)
if self.composite_method:
self.run_composite_job(species.label)
else:
self.run_opt_job(species.label)
if self.generate_conformers:
self.run_conformer_jobs()
while self.running_jobs != {}: # loop while jobs are still running
Expand Down Expand Up @@ -442,7 +449,8 @@ def schedule_jobs(self):
folder_name = 'rxns' if self.species_dict[label].is_ts else 'Species'
orbitals_path = os.path.join(self.project_directory, 'output', folder_name, label, 'geometry',
'orbitals.fchk')
shutil.copyfile(job.local_path_to_orbitals_file, orbitals_path)
if os.path.isfile(job.local_path_to_orbitals_file):
shutil.copyfile(job.local_path_to_orbitals_file, orbitals_path)
self.timer = False
break

Expand Down Expand Up @@ -549,15 +557,8 @@ def run_conformer_jobs(self):
"""
for label in self.unique_species_labels:
if not self.species_dict[label].is_ts and 'opt converged' not in self.output[label]['status']\
and 'opt' not in self.job_dict[label]:
geo_dir = os.path.join(self.project_directory, 'output', 'Species', label, 'geometry')
if not os.path.exists(geo_dir):
os.makedirs(geo_dir)
conf_path = os.path.join(geo_dir, 'conformers_before_optimization.txt')
with open(conf_path, 'w') as f:
for conf in self.species_dict[label].conformers:
f.write(conf)
f.write('\n\n')
and 'opt' not in self.job_dict[label] and not self.species_dict[label].conformer_energies:
self.save_conformers_file(label)
if not self.testing:
if len(self.species_dict[label].conformers) > 1:
self.job_dict[label]['conformers'] = dict()
Expand Down Expand Up @@ -714,13 +715,13 @@ def run_orbitals_job(self, label):

def parse_conformer_energy(self, job, label, i):
"""
Parse E0 (Hartree) from the conformer opt output file, and save it in the 'conformer_energies' attribute.
Parse E0 (J/mol) from the conformer opt output file, and save it in the 'conformer_energies' attribute.
"""
if job.job_status[1] == 'done':
log = Log(path='')
log.determine_qm_software(fullpath=job.local_path_to_output_file)
e0 = log.software_log.loadEnergy()
self.species_dict[label].conformer_energies[i] = e0 # in J/mol
self.species_dict[label].conformer_energies[i] = log.software_log.loadEnergy() # in J/mol
logging.debug('energy for {0} is {1}'.format(i, self.species_dict[label].conformer_energies[i]))
else:
logging.warn('Conformer {i} for {label} did not converge!'.format(i=i, label=label))

Expand All @@ -733,7 +734,7 @@ def determine_most_stable_conformer(self, label):
if self.species_dict[label].is_ts:
raise SchedulerError('The determine_most_stable_conformer() method does not deal with transition'
' state guesses.')
if all(e == 0.0 for e in self.species_dict[label].conformer_energies):
if 'conformers' in self.job_dict[label] and all(e is None for e in self.species_dict[label].conformer_energies):
logging.error('No conformer converged for species {0}! Trying to troubleshoot conformer jobs...'.format(
label))
for i, job in self.job_dict[label]['conformers'].items():
Expand All @@ -743,34 +744,19 @@ def determine_most_stable_conformer(self, label):
conformer_xyz = None
xyzs = list()
log = Log(path='')
for job in self.job_dict[label]['conformers'].values():
log.determine_qm_software(fullpath=job.local_path_to_output_file)
try:
coord, number, _ = log.software_log.loadGeometry()
except RMGInputError:
xyzs.append(None)
else:
xyzs.append(get_xyz_string(xyz=coord, number=number))
energies, xyzs = (list(t) for t in zip(*sorted(zip(self.species_dict[label].conformer_energies, xyzs))))
smiles_list = list()
for xyz in xyzs:
b_mol = molecules_from_xyz(xyz, multiplicity=self.species_dict[label].multiplicity,
charge=self.species_dict[label].charge)[1]
smiles = b_mol.toSMILES() if b_mol is not None else 'no 2D structure'
smiles_list.append(smiles)
geo_dir = os.path.join(self.project_directory, 'output', 'Species', label, 'geometry')
if not os.path.exists(geo_dir):
os.makedirs(geo_dir)
conf_path = os.path.join(geo_dir, 'conformers_after_optimization.txt')
with open(conf_path, 'w') as f:
for i, xyz in enumerate(xyzs):
f.write('conformer {0}:\n'.format(i))
if xyz is not None:
f.write(xyz + '\n')
f.write('SMILES: ' + smiles_list[i] + '\n')
f.write('Relative Energy: {0} kJ/mol\n\n\n'.format((energies[i] - min(energies)) * 0.001))
if self.species_dict[label].conformer_energies:
xyzs = self.species_dict[label].conformers
else:
for job in self.job_dict[label]['conformers'].values():
log.determine_qm_software(fullpath=job.local_path_to_output_file)
try:
coord, number, _ = log.software_log.loadGeometry()
except RMGInputError:
xyzs.append(None)
else:
f.write('Failed to converge')
xyzs.append(get_xyz_string(xyz=coord, number=number))
energies, xyzs = (list(t) for t in zip(*sorted(zip(self.species_dict[label].conformer_energies, xyzs))))
self.save_conformers_file(label, xyzs=xyzs, energies=energies)
# Run isomorphism checks if a 2D representation is available
if self.species_dict[label].mol is not None:
for i, xyz in enumerate(xyzs):
Expand All @@ -781,11 +767,14 @@ def determine_most_stable_conformer(self, label):
is_isomorphic = check_isomorphism(self.species_dict[label].mol, b_mol)
except ValueError as e:
if self.species_dict[label].charge:
logging.error('Could not determine isomorphism for charged species. Got the '
'following error:\n{0}'.format(e.message))
logging.error('Could not determine isomorphism for charged species {0}. '
'Optimizing the most stable conformer anyway. Got the '
'following error:\n{1}'.format(label, e))
else:
logging.error('Could not determine isomorphism for (non-charged) species. Got the '
'following error:\n{0}'.format(e.message))
logging.error('Could not determine isomorphism for (non-charged) species {0}. '
'Optimizing the most stable conformer anyway. Got the '
'following error:\n{1}'.format(label, e))
conformer_xyz = xyzs[0]
break
if is_isomorphic:
if i == 0:
Expand Down Expand Up @@ -818,7 +807,7 @@ def determine_most_stable_conformer(self, label):
smiles_list.append(molecules_from_xyz(xyz, multiplicity=self.species_dict[label].multiplicity,
charge=self.species_dict[label].charge)[1])
if self.allow_nonisomorphic_2d or self.species_dict[label].charge:
# we'll optimize the most stable conformer even if it not isomorphic to the 2D graph
# we'll optimize the most stable conformer even if it is not isomorphic to the 2D graph
logging.error('No conformer for {0} was found to be isomorphic with the 2D graph representation'
' {1} (got: {2}). Optimizing the most stable conformer anyway.'.format(
label, self.species_dict[label].mol.toSMILES(), smiles_list))
Expand Down Expand Up @@ -846,7 +835,7 @@ def determine_most_likely_ts_conformer(self, label):
if not self.species_dict[label].is_ts:
raise SchedulerError('The determine_most_likely_ts_conformer() method only deals with transition'
' state guesses.')
if all(e == 0.0 for e in self.species_dict[label].conformer_energies):
if all(e is None for e in self.species_dict[label].conformer_energies):
logging.error('No guess converged for TS {0}!')
# for i, job in self.job_dict[label]['conformers'].items():
# self.troubleshoot_ess(label, job, level_of_theory=job.level_of_theory, job_type='conformer',
Expand Down Expand Up @@ -893,6 +882,7 @@ def parse_composite_geo(self, label, job):
self.species_dict[label].final_xyz = get_xyz_string(xyz=coord, number=number)
self.output[label]['status'] += 'composite converged; '
self.output[label]['composite'] = os.path.join(job.local_path, 'output.out')
self.species_dict[label].opt_level = self.composite_method
rxn_str = ''
if self.species_dict[label].is_ts:
rxn_str = ' of reaction {0}'.format(self.species_dict[label].rxn_label)
Expand All @@ -914,7 +904,6 @@ def parse_composite_geo(self, label, job):
return True # run freq / scan jobs on this optimized geometry
elif not self.species_dict[label].is_ts:
self.troubleshoot_negative_freq(label=label, job=job)
self.species_dict[label].opt_level = self.composite_method
if job.job_status[1] != 'done' or not freq_ok:
self.troubleshoot_ess(label=label, job=job, level_of_theory=job.level_of_theory, job_type='composite')
return False # return ``False``, so no freq / scan jobs are initiated for this unoptimized geometry
Expand Down Expand Up @@ -1314,7 +1303,7 @@ def troubleshoot_negative_freq(self, label, job):
xyz2 = atomcoords - factor * displacement
self.species_dict[label].conformers.append(get_xyz_string(xyz=xyz1, number=atomnos))
self.species_dict[label].conformers.append(get_xyz_string(xyz=xyz2, number=atomnos))
self.species_dict[label].conformer_energies.extend([0.0, 0.0]) # a placeholder (lists are synced)
self.species_dict[label].conformer_energies.extend([None, None]) # a placeholder (lists are synced)
self.job_dict[label]['conformers'] = dict() # initialize the conformer job dictionary
for i, xyz in enumerate(self.species_dict[label].conformers):
self.run_job(label=label, xyz=xyz, level_of_theory=self.conformer_level, job_type='conformer', conformer=i)
Expand Down Expand Up @@ -1736,16 +1725,53 @@ def make_reaction_labels_info_file(self):
f.write('Reaction labels and respective TS labels:\n\n')
return rxn_info_path

def save_conformers_file(self, label, xyzs=None, energies=None):
"""
A helper function for saving the conformers for species `label` before and after optimization
If `xyzs` is given, then it is used as the conformers xyz list, otherwise it is taken from the species.conformer
attribute. If `energies`, a list of respective conformer energies in J/mol, is given, it will also be reported.
"""
geo_dir = os.path.join(self.project_directory, 'output', 'Species', label, 'geometry')
if not os.path.exists(geo_dir):
os.makedirs(geo_dir)
smiles_list = list()
xyzs = xyzs or self.species_dict[label].conformers
for xyz in xyzs:
b_mol = molecules_from_xyz(xyz, multiplicity=self.species_dict[label].multiplicity,
charge=self.species_dict[label].charge)[1]
smiles = b_mol.toSMILES() if b_mol is not None else 'no 2D structure'
smiles_list.append(smiles)
if energies is not None:
conf_path = os.path.join(geo_dir, 'conformers_after_optimization.txt')
else:
conf_path = os.path.join(geo_dir, 'conformers_before_optimization.txt')
with open(conf_path, 'w') as f:
if energies is not None:
f.write('conformers optimized at {0}\n\n'.format(self.conformer_level))
for i, conf in enumerate(xyzs):
f.write('conformer {0}:\n'.format(i))
if conf is not None:
f.write(conf + '\n')
f.write('SMILES: ' + smiles_list[i] + '\n')
if energies is not None:
if energies[i] == min(energies):
f.write('Relative Energy: 0 kJ/mol (lowest)')
else:
f.write('Relative Energy: {0:.3f} kJ/mol'.format((energies[i] - min(energies)) * 0.001))
else:
f.write('Failed to converge')
f.write('\n\n\n')


# Add a custom string representer to use block literals for multiline strings
def string_representer(dumper, data):
"""Add a custom string representer to use block literals for multiline strings"""
if len(data.splitlines()) > 1:
return dumper.represent_scalar(tag='tag:yaml.org,2002:str', value=data, style='|')
return dumper.represent_scalar(tag='tag:yaml.org,2002:str', value=data)


# Add a custom unicode representer to use block literals for multiline strings
def unicode_representer(dumper, data):
"""Add a custom unicode representer to use block literals for multiline strings"""
if len(data.splitlines()) > 1:
return yaml.ScalarNode(tag='tag:yaml.org,2002:str', value=data, style='|')
return yaml.ScalarNode(tag='tag:yaml.org,2002:str', value=data)
17 changes: 11 additions & 6 deletions arc/schedulerTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,14 @@ def test_conformers(self):
self.sched1.job_dict[label]['conformers'] = dict()
self.sched1.job_dict[label]['conformers'][0] = self.job1
self.sched1.job_dict[label]['conformers'][1] = self.job2
self.sched1.species_dict[label].conformer_energies = [None, None]
self.sched1.species_dict[label].conformers = [None, None]
self.sched1.parse_conformer_energy(job=self.job1, label=label, i=0)
self.sched1.parse_conformer_energy(job=self.job2, label=label, i=1)
expecting = [-251596443.5088726, -254221943.3698632]
self.assertEqual(self.sched1.species_dict[label].conformer_energies, expecting)
self.sched1.species_dict[label].conformers[0] = parser.parse_xyz_from_file(self.job1.local_path_to_output_file)
self.sched1.species_dict[label].conformers[1] = parser.parse_xyz_from_file(self.job2.local_path_to_output_file)

self.sched1.determine_most_stable_conformer(label=label)
expecting = """N -0.75555952 -0.12937106 0.00000000
Expand All @@ -83,20 +87,21 @@ def test_conformers(self):
self.assertTrue(os.path.isfile(methylamine_conf_path))
with open(methylamine_conf_path, 'r') as f:
lines = f.readlines()
self.assertEqual(lines[0], 'conformer 0:\n')
self.assertEqual(lines[9], 'SMILES: CN\n')
self.assertTrue('Relative Energy:' in lines[10])
self.assertEqual(lines[14][0], 'N')
self.assertTrue('conformers optimized at' in lines[0])
self.assertEqual(lines[11], 'SMILES: CN\n')
self.assertTrue('Relative Energy:' in lines[12])
self.assertEqual(lines[16][0], 'N')

self.sched1.run_conformer_jobs()
self.sched1.save_conformers_file(label='C2H6')
c2h6_conf_path = os.path.join(self.sched1.project_directory, 'output', 'Species', 'C2H6', 'geometry',
'conformers_before_optimization.txt')
self.assertTrue(os.path.isfile(c2h6_conf_path))
with open(c2h6_conf_path, 'r') as f:
lines = f.readlines()
self.assertEqual(lines[0][0], 'C')
self.assertEqual(lines[1][0], 'C')
self.assertEqual(lines[9], '\n')
self.assertEqual(lines[12][0], 'H')
self.assertEqual(lines[17][0], 'H')

def test_check_negative_freq(self):
"""Test the check_negative_freq() method"""
Expand Down
Loading