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

29 read vibration data from dftb calcs #33

Closed
wants to merge 31 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0775867
Added squeeze_list. Opposite of ensure_list, reduces list of length 1…
YHordijk Sep 14, 2023
bf59e0d
Added a function to convert user input into a Result object.
YHordijk Sep 14, 2023
59ebe09
Added function to convert user-input into a human-readable format
YHordijk Sep 14, 2023
5514524
Added get_level_of_theory to the reading for adf calculations
YHordijk Sep 14, 2023
5e68e0d
Added a new print to the ethanol example
YHordijk Sep 14, 2023
a2d9066
Added missing MP2 and HartreeFock functional categories. For MP2-SOS …
YHordijk Sep 15, 2023
f0dfaa3
Merge pull request #27 from TheoChem-VU/main
YHordijk Sep 15, 2023
5b1176b
NumericalQuality now defaults to Normal
YHordijk Sep 15, 2023
830447c
Fixed an oversight where dispersion was not split before being merged
YHordijk Sep 15, 2023
2430c03
Added the MP2 and HF functionals along with the empiricalscaling vari…
YHordijk Sep 15, 2023
05d94bf
Fixed an issue where dispersion options were not lowercase
YHordijk Sep 15, 2023
df00a37
Made block names uppercase for readability and added new ones
YHordijk Sep 15, 2023
d1ffc3f
Added new test-cases for determining the level-of-theories for variou…
YHordijk Sep 15, 2023
383052b
Added new test-cases for determining the level-of-theories for variou…
YHordijk Sep 15, 2023
931cf39
Added more documentation and comments to clarify some unclear code
YHordijk Sep 15, 2023
050890f
Added a missing block
YHordijk Sep 15, 2023
16a5560
Update test_results.py
SiebeLeDe Sep 15, 2023
391cca9
Rewrote get_ams_input to use all blocks defined on the SCM website
YHordijk Sep 15, 2023
eb3cf50
Rewrite get_ams_input again, this time it implements all features des…
YHordijk Sep 16, 2023
0597af0
Added test cases to test reading and converting ams inputfiles
YHordijk Sep 16, 2023
dc51980
Removing unnecessary code and fixed wrong path
YHordijk Sep 16, 2023
0b161d4
Fixing Ruff issues
YHordijk Sep 16, 2023
221e462
Rmoved unnecessary import
YHordijk Sep 16, 2023
8c44807
Added NMR blocks
YHordijk Sep 16, 2023
cc60dfb
Moved reading of vibrational properties to a new function
YHordijk Sep 17, 2023
3e24c0a
Removed the check for adf.rkf presence and moved definition of reader…
YHordijk Sep 17, 2023
eb6cd39
Intensities are not written by default for some calculations, so we h…
YHordijk Sep 23, 2023
76ca8d1
Merge pull request #30 from TheoChem-VU/28-read-vibrationalanalysis-j…
YHordijk Sep 23, 2023
c167af7
Added reading of properties and vibration data
YHordijk Sep 24, 2023
7e72583
Removed property reading from get_calc_settings, as it should not hav…
YHordijk Sep 24, 2023
e39f5fc
Removed unnecessary variable
YHordijk Sep 24, 2023
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
1 change: 1 addition & 0 deletions TCutility/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
ensure_list = lambda x: [x] if not isinstance(x, (list, tuple, set)) else x # noqa: E731
squeeze_list = lambda x: x[0] if len(x) == 1 else x # noqa: E731

from TCutility import results, constants # noqa: F401, E402
1 change: 1 addition & 0 deletions TCutility/results/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def read(calc_dir: str) -> Result:
if ret.engine == 'adf':
ret.adf = adf.get_calc_settings(ret)
ret.properties = adf.get_properties(ret)
ret.level = adf.get_level_of_theory(ret)
elif ret.engine == 'dftb':
ret.dftb = dftb.get_calc_settings(ret)
ret.properties = dftb.get_properties(ret)
Expand Down
121 changes: 108 additions & 13 deletions TCutility/results/adf.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ def get_calc_settings(info: Result) -> Result:
'''

assert info.engine == 'adf', f'This function reads ADF data, not {info.engine} data'
assert 'adf.rkf' in info.files, f'Missing adf.rkf file, [{", ".join([": ".join(item) for item in info.files.items()])}]'
# assert 'adf.rkf' in info.files, f'Missing adf.rkf file, [{", ".join([": ".join(item) for item in info.files.items()])}]'

reader_adf = cache.get(info.files['adf.rkf'])
reader_ams = cache.get(info.files['ams.rkf'])
ret = Result()

Expand All @@ -41,6 +40,14 @@ def get_calc_settings(info: Result) -> Result:
ret.task = words[i+1]
break

# the VibrationalAnalysis task does not produce adf.rkf files
# in that case we end the reading here, since the following
# variables do not apply
if ret.task.lower() == 'vibrationalanalysis':
return ret

reader_adf = cache.get(info.files['adf.rkf'])

# determine if calculation used relativistic corrections
# if it did, variable 'escale' will be present in 'SFOs'
# if it didnt, only variable 'energy' will be present
Expand Down Expand Up @@ -90,12 +97,32 @@ def get_properties(info: Result) -> Result:
- **vdd.charges (list[float])** - list of Voronoi Deformation Denisty (VDD) charges in [electrons], being the difference between the final (SCF) and initial VDD charges.
'''

def read_vibrations(reader: cache.TrackKFReader) -> Result:
ret = Result()
ret.number_of_modes = reader.read('Vibrations', 'nNormalModes')
ret.frequencies = ensure_list(reader.read('Vibrations', 'Frequencies[cm-1]'))
if ('Vibrations', 'Intensities[km/mol]') in reader:
ret.intensities = ensure_list(reader.read('Vibrations', 'Intensities[km/mol]'))
ret.number_of_imag_modes = len([freq for freq in ret.frequencies if freq < 0])
ret.character = 'minimum' if ret.number_of_imag_modes == 0 else 'transitionstate'
ret.modes = []
for i in range(ret.number_of_modes):
ret.modes.append(reader.read('Vibrations', f'NoWeightNormalMode({i+1})'))
return ret


assert info.engine == 'adf', f'This function reads ADF data, not {info.engine} data'
assert 'adf.rkf' in info.files, f'Missing adf.rkf file, [{", ".join([": ".join(item) for item in info.files.items()])}]'

reader_adf = cache.get(info.files['adf.rkf'])

ret = Result()

if info.adf.task.lower() == 'vibrationalanalysis':
reader_ams = cache.get(info.files['ams.rkf'])
ret.vibrations = read_vibrations(reader_ams)
return ret

reader_adf = cache.get(info.files['adf.rkf'])

# read energies (given in Ha in rkf files)
ret.energy.bond = reader_adf.read('Energy', 'Bond Energy') * constants.HA2KCALMOL
ret.energy.elstat = reader_adf.read('Energy', 'elstat') * constants.HA2KCALMOL
Expand All @@ -108,15 +135,7 @@ def get_properties(info: Result) -> Result:

# vibrational information
if ('Vibrations', 'nNormalModes') in reader_adf:
ret.vibrations.number_of_modes = reader_adf.read('Vibrations', 'nNormalModes')
freqs = reader_adf.read('Vibrations', 'Frequencies[cm-1]')
ints = reader_adf.read('Vibrations', 'Intensities[km/mol]')
ret.vibrations.frequencies = freqs if isinstance(freqs, list) else [freqs]
ret.vibrations.intensities = ints if isinstance(ints, list) else [ints]
ret.vibrations.number_of_imag_modes = len([freq for freq in freqs if freq < 0])
ret.vibrations.modes = []
for i in range(ret.vibrations.number_of_modes):
ret.vibrations.modes.append(reader_adf.read('Vibrations', f'NoWeightNormalMode({i+1})'))
ret.vibrations = read_vibrations(reader_adf)

# read the Voronoi Deformation Charges Deformation (VDD) before and after SCF convergence (being "inital" and "SCF")
vdd_scf: List[float] = ensure_list(reader_adf.read('Properties', 'AtomCharge_SCF Voronoi')) # type: ignore since plams does not include typing for KFReader. List[float] is returned
Expand All @@ -129,3 +148,79 @@ def get_properties(info: Result) -> Result:
# The ordering is not very straightfoward so this is a suggestion for the future with keys: ret.vdd.[IRREP]

return ret


def get_level_of_theory(info: Result) -> Result:
'''Function to get the level-of-theory from an input-file.

Args:
inp_path: Path to the input file. Can be a .run or .in file create for AMS

Returns:
:Dictionary containing information about the level-of-theory:

- **summary (str)** - a summary string that gives the level-of-theory in a human-readable format.
- **xc.functional (str)** - XC-functional used in the calculation.
- **xc.category (str)** - category the XC-functional belongs to. E.g. GGA, MetaHybrid, etc ...
- **xc.dispersion (str)** - the dispersion correction method used during the calculation.
- **xc.summary (str)** - a summary string that gives the XC-functional information in a human-readable format.
- **xc.empiricalScaling (str)** - which empirical scaling parameter was used. Useful for MP2 calculations.
- **basis.type (str)** - the size/type of the basis-set.
- **basis.core (str)** - the size of the frozen-core approximation.
- **quality (str)** - the numerical quality setting.
'''
sett = info.input
ret = Result()
# print(json.dumps(sett, indent=4))
xc_categories = ['GGA', 'LDA', 'MetaGGA', 'MetaHybrid', 'Model', 'LibXC', 'DoubleHybrid', 'Hybrid', 'MP2', 'HartreeFock']
ret.xc.functional = 'VWN'
ret.xc.category = 'LDA'
for cat in xc_categories:
if cat.lower() in [key.lower() for key in sett.adf.xc]:
ret.xc.functional = sett.adf.xc[cat]
ret.xc.category = cat

ret.basis.type = sett.adf.basis.type
ret.basis.core = sett.adf.basis.core
ret.quality = sett.adf.NumericalQuality or 'Normal'

ret.xc.dispersion = None
if 'dispersion' in [key.lower() for key in sett.adf.xc]:
ret.xc.dispersion = " ".join(sett.adf.xc.dispersion.split())

# the empirical scaling value is used for MP2 calculations
ret.xc.empirical_scaling = None
if 'empiricalscaling' in [key.lower() for key in sett.adf.xc]:
ret.xc.empiricalscaling = sett.adf.xc.empiricalscaling

# MP2 and HF are a little bit different from the usual xc categories. They are not key-value pairs but only the key
# we start building the ret.xc.summary string here already. This will contain the human-readable functional name
if ret.xc.category == 'MP2':
ret.xc.summary = 'MP2'
if ret.xc.empiricalscaling:
ret.xc.summary += f'-{ret.xc.empiricalscaling}'
elif ret.xc.category == 'HartreeFock':
ret.xc.summary = 'HF'
else:
ret.xc.summary = ret.xc.functional

# If dispersion was used, we want to add it to the ret.xc.summary
if ret.xc.dispersion:
if ret.xc.dispersion.lower() == 'grimme3':
ret.xc.summary += '-D3'
if ret.xc.dispersion.lower() == 'grimme3 bjdamp':
ret.xc.summary += '-D3(BJ)'
if ret.xc.dispersion.lower() == 'grimme4':
ret.xc.summary += '-D4'
if ret.xc.dispersion.lower() == 'ddsc':
ret.xc.summary += '-dDsC'
if ret.xc.dispersion.lower() == 'uff':
ret.xc.summary += '-dUFF'
if ret.xc.dispersion.lower() == 'mbd':
ret.xc.summary += '-MBD@rsSC'
if ret.xc.dispersion.lower() == 'default':
ret.xc.summary += '-D'

# ret.summary is simply the ret.xc.summary plus the basis set type
ret.summary = f'{ret.xc.summary}/{ret.basis.type}'
return ret
153 changes: 153 additions & 0 deletions TCutility/results/ams.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from TCutility import ensure_list
import os
from datetime import datetime
import re

j = os.path.join

Expand Down Expand Up @@ -124,6 +125,9 @@ def get_ams_info(calc_dir: str) -> Result:
else:
ret.engine = 'adf'

# store the input of the calculation
ret.input = get_ams_input(reader_ams.read('General', 'user input'))

# store the job id, which should be unique for the job
ret.job_id = reader_ams.read('General', 'jobid') if ('General', 'jobid') in reader_ams else None

Expand Down Expand Up @@ -375,3 +379,152 @@ def get_history(calc_dir: str) -> Result:
ret[item.lower()].append(val)

return ret


def get_input_blocks():
'''
This function reads input_blocks and decomposes its content into a list of blocks and a list of non-standard blocks
The general format is as follows:

parentblock
- subblock
- - subsubblock
- subblock !nonstandard
parentblock
- subblock
- - subsubblock
- - - subsubsubblock
- - - subsubsubblock !nonstandard

Each subblock has to be defined within its parent block. !nonstandard indicates that the block is a non-standard block
These blocks are special in that they can contain multiple of the same entry
'''
blocks = []
nonstandard_blocks = []
parent_blocks = [] # this list tracks the parent blocks of the current block
with open(j(os.path.split(__file__)[0], 'input_blocks')) as inpblx:
lines = inpblx.readlines()

for line in lines:
line = line.strip().lower()
# we can ignore some lines
if line == '' or line.startswith('#'):
continue

# block_depth indicates how many parents the block has
block_depth = line.count('- ')
# we reduce the amount of parent_blocks using block_depth
# if we move from a subsubblock to a subblock we remove the last-added block
parent_blocks = parent_blocks[:block_depth]

# remove the "- " substrings
block = line.split('- ')[-1].strip().lower()
# check if the block is non-standard. If it is, remove the !nonstandard substring
# and add it to the nonstandard_blocks list
if block.endswith('!nonstandard'):
block = block.split()[0]
nonstandard_blocks.append(parent_blocks.copy() + [block])
# in both standard and nonstandard cases add the block to blocks list
blocks.append(parent_blocks.copy() + [block])
# add the current block to parent_blocks for the next line
parent_blocks.append(block.lower())

return blocks, nonstandard_blocks


def get_ams_input(inp: str) -> Result:
def get_possible_blocks():
# check which blocks are openable given the current open blocks
# we start by considering all blocks
possible_blocks = blocks
# we iterate through the open blocks and check if the first element in the possible_blocks is the same.
# this way we move down through the blocks
for open_block in open_blocks:
possible_blocks = [block[1:] for block in possible_blocks if len(block) > 0 and block[0].lower() == open_block.lower()]
# we want only the first element in the possible blocks, not the tails
possible_blocks = set([block[0] for block in possible_blocks if len(block) > 0])
return possible_blocks

sett = Result()

blocks, nonstandard_blocks = get_input_blocks()

open_blocks = ['ams']
for line in inp.splitlines():
line = line.strip()

# we remove comments from the line
# comments can be either at the start of a line or after a real statement
# so we have to search the line for comment starters and remove the part after it
for comment_start in ['#', '!', '::']:
if comment_start in line:
idx = line.index(comment_start)
line = line[:idx]

# skip empty lines
if not line:
continue

# if we encounter an end statement we can close the last openblock
if line.lower().startswith('end'):
open_blocks.pop(-1)
continue

# check if we are opening a block
# We have to store the parts of a compact block (if it is compact)
# it will be a list of tuples of key-value pairs
compact_parts = None
skip = False
# check if the current line corresponds to a possible block
for possible_block in get_possible_blocks():
if line.lower().startswith(possible_block.lower()):
# a block opening can either span multiple lines or can be use with compact notation
# compact notation will always have = signs
if '=' in line:
# split the key-values using some regex magic
compact = line[len(possible_block):].strip() # remove the block name
compact_parts = re.findall(r"""(\S+)=['"]{0,1}([^'"\n]*)['"]{0,1}""", compact)
else:
skip = True
# add the new block to the open_blocks
open_blocks.append(possible_block)

# if we are not in a compact block we can just skip
if skip:
continue

# get the sett to the correct level
# first check if the block is nonstandard
is_nonstandard = [block.lower() for block in open_blocks] in nonstandard_blocks
sett_ = sett
# go to the layer one above the lowest
for open_block in open_blocks[:-1]:
sett_ = sett_[open_block]
# at the lowest level we have to check if the block is nonstandard. If it is, we add the whole line to the settings object
if is_nonstandard and not sett_[open_blocks[-1]]:
sett_[open_blocks[-1]] = []
# then finally go to the lowest sett layer
sett_ = sett_[open_blocks[-1]]

# if we are in a compact block we just add all the key-value pairs
if compact_parts:
for key, val in compact_parts:
sett_[key] = val
# compact blocks do not have end statements, so we can remove it again from the open_blocks
open_blocks.pop(-1)
# in a normal block we split the line and add them to the settings
else:
# for nonstandard blocks we add the line to the list
if is_nonstandard:
sett_.append(line.strip())
# for normal blocks we split the line and set it as a key, the rest of the line is the value
else:
sett_[line.split()[0]] = line[len(line.split()[0]):].strip()

for engine_block in ['engine adf', 'engine dftb', 'engine band']:
if engine_block not in sett['ams']:
continue
sett['ams'][engine_block.split()[1]] = sett['ams'][engine_block]
del sett['ams'][engine_block]

return sett['ams']
Loading
Loading