Skip to content

Commit

Permalink
Gaussian Troubleshoot Combinations Improvement (#670)
Browse files Browse the repository at this point in the history
The current version of ARC cannot fully troubleshoot Gaussian due to
lack of combination troubleshooting. This PR attempts to rectify this
issue.

For example, if a job receives a first error of L401/301, and so we
remove the chkfile by changing `guess=read` to `guess=mix` but then it
its next failed run it gives another error where we need to add
`scf=(nosymm)`, then this modification will create an `input.gjf` file
that uses BOTH troubleshooting methods.
  • Loading branch information
alongd authored Nov 19, 2023
2 parents 7f5190d + 66dc921 commit 26b200b
Show file tree
Hide file tree
Showing 14 changed files with 1,250 additions and 90 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,10 @@ timer.dat

# .vscode
.vscode

# .trunk folder
.trunk

# ignore files created from tests
nul
run.out
2 changes: 1 addition & 1 deletion arc/imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import settings as local_settings
except ImportError:
pass
if local_settings and 'pytest' not in sys.modules:
if local_settings:
local_settings_dict = {key: val for key, val in vars(local_settings).items() if '__' not in key}
settings.update(local_settings_dict)
# Set global_ess_settings to None if using a local settings file (ARC's defaults are dummies)
Expand Down
2 changes: 1 addition & 1 deletion arc/job/adapter_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ def test_add_to_args(self):
'trsh': {}}
self.assertEqual(self.job_1.args, expected_args)
self.job_1.write_input_file()
new_expected_args = {'keyword': {'general': 'val_tst_1 val_tst_2 val_tst_3 scf=xqc'}, # scf=xqc
new_expected_args = {'keyword': {'general': 'val_tst_1 val_tst_2 val_tst_3'},
'block': {'specific_key_2': 'val_tst_4\nval_tst_5'},
'trsh': {}}
self.assertEqual(self.job_1.args, new_expected_args)
Expand Down
159 changes: 151 additions & 8 deletions arc/job/adapters/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import os
import shutil
import sys
import re

from pprint import pformat
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
Expand Down Expand Up @@ -220,8 +221,13 @@ def _initialize_adapter(obj: 'JobAdapter',
if obj.execution_type != 'incore' and obj.job_adapter in obj.ess_settings.keys():
obj.determine_job_array_parameters()

obj.scan_res = obj.args['trsh']['scan_res'] \
if obj.args and 'trsh' in obj.args.keys() and 'scan_res' in obj.args['trsh'].keys() else rotor_scan_resolution
# Set scan_res if required by trsh
if obj.args and 'trsh' in obj.args.keys() and 'scan_res' in obj.args['trsh'].keys():
obj.scan_res = obj.args['trsh']['scan_res']
# Remove it from the args dict
obj.args['trsh'].pop('scan_res')
else:
obj.scan_res = rotor_scan_resolution

obj.set_files()
check_argument_consistency(obj)
Expand Down Expand Up @@ -280,30 +286,131 @@ def update_input_dict_with_args(args: dict,
"""
Update the job input_dict attribute with keywords and blocks from the args attribute.
The function iterates over each key in the 'args' dictionary. Depending on the key type
('block', 'keyword', 'trsh'), it updates the 'input_dict' accordingly. For 'block' and
'keyword' types, it appends the corresponding values to the 'input_dict'. For 'trsh',
it handles both list and non-list values.
Args:
args (dict): The job arguments.
input_dict (dict): The job input dict.
args (dict): The job arguments with the following structure:
args = {
'block': {key1: value1, key2: value2, ...},
'keyword': {key1: value1, key2: value2, ...},
'trsh': {key1: value1, key2: value2, ... or key: [value1, value2, ...]},
...
}
input_dict (dict): The job input dict with a structure that may be updated:
input_dict = {
'block': <block string>,
'keywords': <keywords string>,
'scan_trsh': <scan_trsh string>,
'trsh': <trsh string>,
...
}
Returns:
dict: The updated input_dict.
dict: The updated input_dict with appended or modified values based on the args.
Example:
Input args structure:
args = {
'block': {'1': 'block text 1', '2': 'block text 2'},
'keyword': {'opt': 'keyword opt', 'freq': 'keyword freq'},
'trsh': ['trsh value 1', 'trsh value 2']
}
Input input_dict structure:
input_dict = {
'block': 'existing block text',
'keywords': 'existing keywords',
'scan_trsh': 'existing scan_trsh',
'trsh': 'existing trsh'
}
Output input_dict structure:
input_dict = {
'block': 'existing block text\n\nblock text 1\nblock text 2\n',
'keywords': 'existing keywords keyword opt keyword freq ',
'scan_trsh': 'existing scan_trsh',
'trsh': 'existing trsh trsh value 1 trsh value 2 '
}
"""
for arg_type, arg_dict in args.items():
if arg_type == 'block' and arg_dict:
input_dict['block'] = '\n\n' if not input_dict['block'] else input_dict['block']
for block in arg_dict.values():
input_dict['block'] += f'{block}\n'
# Chek if input_dict['block'] already contains a value and that it ends with a newline
if input_dict['block'] and not input_dict['block'].endswith('\n'):
input_dict['block'] += '\n'
input_dict['block'] += f'{block}'
elif arg_type == 'keyword' and arg_dict:
for key, value in arg_dict.items():
if key == 'scan_trsh':
if 'scan_trsh' not in input_dict.keys():
input_dict['scan_trsh'] = ''
input_dict['scan_trsh'] += f'{value} '
# Check if input_dict['scan_trsh'] already contains a value
if input_dict['scan_trsh']:
input_dict['scan_trsh'] += f' {value}'
else:
input_dict['scan_trsh'] += f'{value}'
else:
if 'keywords' not in input_dict.keys():
input_dict['keywords'] = ''
input_dict['keywords'] += f'{value} '
# Check if input_dict['keywords'] already contains a value
if input_dict['keywords']:
input_dict['keywords'] += f' {value}'
else:
input_dict['keywords'] += f'{value}'
elif arg_type == 'trsh':
if isinstance(arg_dict, list):
# arg_dict is a list, iterate through its elements
for val in arg_dict:
if 'trsh' not in input_dict.keys():
input_dict['trsh'] = ''
# Check if input_dict['trsh'] already contains a value
if input_dict['trsh']:
input_dict['trsh'] += f' {val}'
else:
input_dict['trsh'] += f'{val}'
else:
# arg_dict is a dictionary, proceed as before
for key, value in arg_dict.items():
# Check if value is a list
if isinstance(value, list):
for val in value:
if key not in input_dict.keys():
input_dict[key] = ''
if input_dict[key]:
input_dict[key] += f' {val}'
else:
input_dict[key] += f'{val}'
else:
if key not in input_dict.keys():
input_dict[key] = ''
if input_dict[key]:
input_dict[key] += f' {value}'
else:
input_dict[key] += f'{value}'

return input_dict

def input_dict_strip(input_dict: dict) -> dict:
"""
Strip all values in the input dict of leading and trailing whitespace.
"""
stripped_dict = {
key: (
val.rstrip() if isinstance(val, str) and val.startswith('\n') and not val.endswith('\n') else
val.lstrip() if isinstance(val, str) and not val.startswith('\n') and val.endswith('\n') else
val.strip() if isinstance(val, str) and not val.startswith('\n') and not val.endswith('\n') else val
)
for key, val in input_dict.items() if val is not None
}

return stripped_dict


def set_job_args(args: Optional[dict],
level: Optional['Level'],
Expand Down Expand Up @@ -380,3 +487,39 @@ def which(command: Union[str, list],
return bool(ans)
else:
return ans

def combine_parameters(input_dict: dict, terms: list) -> Tuple[dict, List]:
"""
Extract and combine specific parameters from a dictionary's string values based on a list of terms.
This function iterates over each key-value pair in the input dictionary. If a value is a string,
it searches for occurrences of each term in the 'terms' list within the string. Each found term is
appended to a 'parameters' list. The terms found are then removed from the string value in the dictionary.
The function returns a modified copy of the input dictionary with the terms removed from the string values,
and a list of extracted parameters.
Args:
input_dict (dict): A dictionary with string values from which terms are to be extracted.
terms (list): A list of string terms to be searched for within the dictionary's values.
Returns:
tuple: A tuple containing two elements:
- A dictionary with the same structure as 'input_dict', but with the terms removed from the string values.
- A list of extracted parameters (terms found in the string values of the input dictionary).
"""

input_dict_copy = input_dict.copy()
parameters = []

for key, value in input_dict_copy.items():
if isinstance(value, str):
for term in terms:
matches = re.findall(term, value)
for match in matches:
if match:
parameters.append(match)
value = re.sub(term, '', value)
input_dict_copy[key] = value

return input_dict_copy, parameters
85 changes: 81 additions & 4 deletions arc/job/adapters/common_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,19 +110,36 @@ def test_update_input_dict_with_args(self):

input_dict = common.update_input_dict_with_args(args={'block': {'1': """a\nb"""}},
input_dict={'block': ''})
self.assertEqual(input_dict, {'block': """\n\na\nb\n"""})
self.assertEqual(input_dict, {'block': """\n\na\nb"""})

input_dict = common.update_input_dict_with_args(args={'block': {'1': """a\nb"""}},
input_dict={'block': """x\ny\n"""})
self.assertEqual(input_dict, {'block': """x\ny\na\nb\n"""})
self.assertEqual(input_dict, {'block': """x\ny\na\nb"""})

input_dict = common.update_input_dict_with_args(args={'keyword': {'scan_trsh': 'keyword 1'}},
input_dict={})
self.assertEqual(input_dict, {'scan_trsh': 'keyword 1 '})
self.assertEqual(input_dict, {'scan_trsh': 'keyword 1'})

input_dict = common.update_input_dict_with_args(args={'keyword': {'opt': 'keyword 2'}},
input_dict={})
self.assertEqual(input_dict, {'keywords': 'keyword 2 '})
self.assertEqual(input_dict, {'keywords': 'keyword 2'})

def test_update_input_dict_with_multiple_args(self):
"""Test the update_input_dict_with_args() function with multiple arguments in args."""
args = {
'block': {'1': 'block text 1'},
'keyword': {'opt': 'keyword opt'},
'trsh': ['trsh value 1']
}
input_dict = {'block': 'existing block', 'keywords': 'existing keywords', 'trsh': 'existing trsh'}
expected_dict = {
'block': 'existing block\nblock text 1',
'keywords': 'existing keywords keyword opt',
'trsh': 'existing trsh trsh value 1'
}

result_dict = common.update_input_dict_with_args(args=args, input_dict=input_dict)
self.assertEqual(result_dict, expected_dict)

def test_set_job_args(self):
"""Test the set_job_args() function"""
Expand Down Expand Up @@ -151,6 +168,66 @@ def test_which(self):

ans = common.which(command=['python'], return_bool=False, raise_error=False)
self.assertIn('bin/python', ans)

def test_combine_parameters(self):
"""Test the combine_parameters function for normal input."""
input_dict = {'param1': 'value1 term1 value2', 'param2': 'another value term2'}
terms = ['term1', 'term2']
expected_dict = {'param1': 'value1 value2', 'param2': 'another value '}
expected_parameters = ['term1', 'term2']

modified_dict, parameters = common.combine_parameters(input_dict, terms)

self.assertEqual(modified_dict, expected_dict)
self.assertEqual(parameters, expected_parameters)

def test_combine_parameters_empty_input(self):
"""Test the combine_parameters function with empty input."""
input_dict = {}
terms = ['term1', 'term2']
expected_dict = {}
expected_parameters = []

modified_dict, parameters = common.combine_parameters(input_dict, terms)

self.assertEqual(modified_dict, expected_dict)
self.assertEqual(parameters, expected_parameters)

def test_combine_parameters_no_match(self):
"""Test the combine_parameters function with no matching terms."""
input_dict = {'param1': 'value1 value2', 'param2': 'another value'}
terms = ['nonexistent']
expected_dict = {'param1': 'value1 value2', 'param2': 'another value'}
expected_parameters = []

modified_dict, parameters = common.combine_parameters(input_dict, terms)

self.assertEqual(modified_dict, expected_dict)
self.assertEqual(parameters, expected_parameters)

def test_input_dict_strip(self):
"""Test the input_dict_strip() function"""
input_dict = {
'key1': ' value1 ',
'key2': ' value2 ',
'key3': '\nvalue3\n',
'key4': ' value4\n',
'key5': None,
'key6': 42, # Not a string, should remain unchanged
'key7': '\nvalue7 '
}

expected_stripped_dict = {
'key1': 'value1',
'key2': 'value2',
'key3': '\nvalue3\n',
'key4': 'value4\n',
'key6': 42, # Should not be stripped
'key7': '\nvalue7'
}

stripped_dict = common.input_dict_strip(input_dict)
self.assertEqual(stripped_dict, expected_stripped_dict)

@classmethod
def tearDownClass(cls):
Expand Down
Loading

0 comments on commit 26b200b

Please sign in to comment.