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

Improved workflow sets #625

Merged
merged 10 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
.tox
.pytest_cache
*.vscode

docs/_build
build/
dist/
Expand Down
10 changes: 8 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ classifiers = [
keywords = ["aiida", "plugin"]
requires-python = ">=3.9"
dependencies = [
"aiida-core[atomic_tools]~=2.4",
"aiida-core[atomic_tools]~=2.3",
"lxml",
"packaging",
"parsevasp~=3.2"
Expand All @@ -41,7 +41,7 @@ Source = "https://github.com/aiida-vasp/aiida-vasp"

[project.optional-dependencies]
tests = [
"aiida-core[tests]~=2.4",
"aiida-core[tests]~=2.3",
"tox>=3.23.0",
"virtualenv>20"
]
Expand Down Expand Up @@ -93,12 +93,18 @@ graphs = [
"vasp.relax" = "aiida_vasp.workchains.relax:RelaxWorkChain"
"vasp.neb" = "aiida_vasp.workchains.neb:VaspNEBWorkChain"
"vasp.immigrant" = "aiida_vasp.workchains.immigrant:VaspImmigrantWorkChain"
"vasp.v2.vasp" = "aiida_vasp.workchains.v2.vasp:VaspWorkChain"
"vasp.v2.converge" = "aiida_vasp.workchains.v2.converge:VaspConvergenceWorkChain"
"vasp.v2.bands" = "aiida_vasp.workchains.v2.bands:VaspBandsWorkChain"
"vasp.v2.hybrid_bands" = "aiida_vasp.workchains.v2.bands:VaspHybridBandsWorkChain"
"vasp.v2.relax" = "aiida_vasp.workchains.v2.relax:VaspRelaxWorkChain"

[project.entry-points."aiida.groups"]
"vasp.potcar" = "aiida_vasp.data.potcar:PotcarGroup"

[project.scripts]
"mock-vasp" = "aiida_vasp.commands.mock_vasp:mock_vasp"
"dryrun-vasp" = "aiida_vasp.commands.dryrun_vasp:cmd_dryrun_vasp"
"mock-vasp-strict" = "aiida_vasp.commands.mock_vasp:mock_vasp_strict"

[tool.flit.module]
Expand Down
178 changes: 178 additions & 0 deletions src/aiida_vasp/commands/dryrun_vasp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
"""
Module for dry-running a VASP calculation
"""

import shutil
import subprocess as sb
import tempfile
import time
from pathlib import Path

Check warning on line 9 in src/aiida_vasp/commands/dryrun_vasp.py

View check run for this annotation

Codecov / codecov/patch

src/aiida_vasp/commands/dryrun_vasp.py#L5-L9

Added lines #L5 - L9 were not covered by tests

import click
import yaml
from parsevasp.kpoints import Kpoints

Check warning on line 13 in src/aiida_vasp/commands/dryrun_vasp.py

View check run for this annotation

Codecov / codecov/patch

src/aiida_vasp/commands/dryrun_vasp.py#L11-L13

Added lines #L11 - L13 were not covered by tests

# pylint:disable=too-many-branches,consider-using-with


@click.command('dryrun-vasp')
@click.option(

Check warning on line 19 in src/aiida_vasp/commands/dryrun_vasp.py

View check run for this annotation

Codecov / codecov/patch

src/aiida_vasp/commands/dryrun_vasp.py#L18-L19

Added lines #L18 - L19 were not covered by tests
'--input-dir',
help='Where the VASP input is, default to the current working directory.',
type=click.Path(exists=True, file_okay=False, dir_okay=True),
default='.',
show_default=True,
)
@click.option('--vasp-exe', help='Executable for VASP', default='vasp_std', show_default=True)
@click.option(

Check warning on line 27 in src/aiida_vasp/commands/dryrun_vasp.py

View check run for this annotation

Codecov / codecov/patch

src/aiida_vasp/commands/dryrun_vasp.py#L26-L27

Added lines #L26 - L27 were not covered by tests
'--timeout',
help='Timeout in seconds to terminate VASP',
default=10,
show_default=True,
)
@click.option('--work-dir', help='Working directory for running', show_default=True)
@click.option('--keep', help='Wether to the dryrun files', is_flag=True, show_default=True)
@click.option(

Check warning on line 35 in src/aiida_vasp/commands/dryrun_vasp.py

View check run for this annotation

Codecov / codecov/patch

src/aiida_vasp/commands/dryrun_vasp.py#L33-L35

Added lines #L33 - L35 were not covered by tests
'--force',
help='Force the run even if the working directory exists.',
is_flag=True,
show_default=True,
)
def cmd_dryrun_vasp(input_dir, vasp_exe, timeout, work_dir, keep, force):

Check warning on line 41 in src/aiida_vasp/commands/dryrun_vasp.py

View check run for this annotation

Codecov / codecov/patch

src/aiida_vasp/commands/dryrun_vasp.py#L41

Added line #L41 was not covered by tests
"""
A simple tool to dryrun a VASP calculation. The calculation will be run for
up to <timeout> seconds. The underlying VASP process will be terminated once it enters
the main loop, which is signalled by the appearance of a `INWAV` keyword in the OUTCAR.
"""
result = dryrun_vasp(

Check warning on line 47 in src/aiida_vasp/commands/dryrun_vasp.py

View check run for this annotation

Codecov / codecov/patch

src/aiida_vasp/commands/dryrun_vasp.py#L47

Added line #L47 was not covered by tests
input_dir=input_dir,
vasp_exe=vasp_exe,
timeout=timeout,
work_dir=work_dir,
keep=keep,
force=force,
)
with open(Path(input_dir) / 'dryrun.yaml', 'w', encoding='utf-8') as fhandle:
yaml.dump(result, fhandle, Dumper=yaml.SafeDumper)

Check warning on line 56 in src/aiida_vasp/commands/dryrun_vasp.py

View check run for this annotation

Codecov / codecov/patch

src/aiida_vasp/commands/dryrun_vasp.py#L55-L56

Added lines #L55 - L56 were not covered by tests


def dryrun_vasp(input_dir, vasp_exe='vasp_std', timeout=10, work_dir=None, keep=False, force=False):

Check warning on line 59 in src/aiida_vasp/commands/dryrun_vasp.py

View check run for this annotation

Codecov / codecov/patch

src/aiida_vasp/commands/dryrun_vasp.py#L59

Added line #L59 was not covered by tests
"""
Perform a "dryrun" for a VASP calculation - get the number of kpoints, bands and
estimated memory usage.
"""
input_dir = Path(input_dir)
if not work_dir:
tmpdir = tempfile.mkdtemp() # tmpdir is the one to remove when finished
work_dir = Path(tmpdir) / 'vasp_dryrun'

Check warning on line 67 in src/aiida_vasp/commands/dryrun_vasp.py

View check run for this annotation

Codecov / codecov/patch

src/aiida_vasp/commands/dryrun_vasp.py#L64-L67

Added lines #L64 - L67 were not covered by tests
else:
work_dir = Path(work_dir)
if work_dir.resolve() == input_dir.resolve():
raise ValueError('The working directory cannot be the input directory!')
if work_dir.exists():
if not force:
raise FileExistsError(f'Working directory {work_dir} exists already! Please remove it first.')
shutil.rmtree(work_dir)
tmpdir = str(work_dir)
shutil.copytree(str(input_dir), str(work_dir))

Check warning on line 77 in src/aiida_vasp/commands/dryrun_vasp.py

View check run for this annotation

Codecov / codecov/patch

src/aiida_vasp/commands/dryrun_vasp.py#L69-L77

Added lines #L69 - L77 were not covered by tests

# Add the DRYRUNCAR for triggering the dryrun interface
(Path(work_dir) / 'DRYRUNCAR').write_text('LDRYRUN = .TRUE.\n')

Check warning on line 80 in src/aiida_vasp/commands/dryrun_vasp.py

View check run for this annotation

Codecov / codecov/patch

src/aiida_vasp/commands/dryrun_vasp.py#L80

Added line #L80 was not covered by tests

process = sb.Popen(vasp_exe, cwd=str(work_dir))
launch_start = time.time()
outcar = work_dir / 'OUTCAR'
time.sleep(3.0) # Sleep for 3 seconds to wait for VASP creating the file
dryrun_finish = False
try:
while (time.time() - launch_start < timeout) and not dryrun_finish:
with open(outcar, encoding='utf-8') as fhandle:
for line in fhandle:
if 'INWAV' in line or 'Terminating' in line:
dryrun_finish = True
break

Check warning on line 93 in src/aiida_vasp/commands/dryrun_vasp.py

View check run for this annotation

Codecov / codecov/patch

src/aiida_vasp/commands/dryrun_vasp.py#L82-L93

Added lines #L82 - L93 were not covered by tests
# Stop if VASP is terminated or crashed
if process.poll() is not None:
break
time.sleep(0.2)
except Exception as error:
raise error

Check warning on line 99 in src/aiida_vasp/commands/dryrun_vasp.py

View check run for this annotation

Codecov / codecov/patch

src/aiida_vasp/commands/dryrun_vasp.py#L95-L99

Added lines #L95 - L99 were not covered by tests
finally:
# Once we are out side the loop, kill VASP process
process.kill()
result = parse_outcar(outcar)
ibzkpt = parse_ibzkpt(work_dir / 'IBZKPT')
result['kpoints_and_weights_ibzkpt'] = ibzkpt

Check warning on line 105 in src/aiida_vasp/commands/dryrun_vasp.py

View check run for this annotation

Codecov / codecov/patch

src/aiida_vasp/commands/dryrun_vasp.py#L102-L105

Added lines #L102 - L105 were not covered by tests

if not keep:
shutil.rmtree(tmpdir)

Check warning on line 108 in src/aiida_vasp/commands/dryrun_vasp.py

View check run for this annotation

Codecov / codecov/patch

src/aiida_vasp/commands/dryrun_vasp.py#L107-L108

Added lines #L107 - L108 were not covered by tests

return result

Check warning on line 110 in src/aiida_vasp/commands/dryrun_vasp.py

View check run for this annotation

Codecov / codecov/patch

src/aiida_vasp/commands/dryrun_vasp.py#L110

Added line #L110 was not covered by tests


def parse_ibzkpt(ibzkpt_path):

Check warning on line 113 in src/aiida_vasp/commands/dryrun_vasp.py

View check run for this annotation

Codecov / codecov/patch

src/aiida_vasp/commands/dryrun_vasp.py#L113

Added line #L113 was not covered by tests
"""
Parsing the IBZKPT file
"""

kpoints = Kpoints(file_path=str(ibzkpt_path))
tmp = kpoints.get_dict()['points']
kpoints_and_weights = [elem[0].tolist() + [elem[1]] for elem in tmp]
total_weight = sum(tmp[3] for tmp in kpoints_and_weights)

Check warning on line 121 in src/aiida_vasp/commands/dryrun_vasp.py

View check run for this annotation

Codecov / codecov/patch

src/aiida_vasp/commands/dryrun_vasp.py#L118-L121

Added lines #L118 - L121 were not covered by tests

# Normalise the kpoint weights
normalised = []
for entry in kpoints_and_weights:
normalised.append(entry[:3] + [entry[3] / total_weight])

Check warning on line 126 in src/aiida_vasp/commands/dryrun_vasp.py

View check run for this annotation

Codecov / codecov/patch

src/aiida_vasp/commands/dryrun_vasp.py#L124-L126

Added lines #L124 - L126 were not covered by tests

return normalised

Check warning on line 128 in src/aiida_vasp/commands/dryrun_vasp.py

View check run for this annotation

Codecov / codecov/patch

src/aiida_vasp/commands/dryrun_vasp.py#L128

Added line #L128 was not covered by tests


def parse_outcar(outcar_path):

Check warning on line 131 in src/aiida_vasp/commands/dryrun_vasp.py

View check run for this annotation

Codecov / codecov/patch

src/aiida_vasp/commands/dryrun_vasp.py#L131

Added line #L131 was not covered by tests
"""
Parse the header part of the OUTCAR

Returns:
A dictionary of the parsed information
"""
output_dict = {

Check warning on line 138 in src/aiida_vasp/commands/dryrun_vasp.py

View check run for this annotation

Codecov / codecov/patch

src/aiida_vasp/commands/dryrun_vasp.py#L138

Added line #L138 was not covered by tests
'POTCARS': [],
}
with open(outcar_path, encoding='utf-8') as fhandle:
lines = fhandle.readlines()
for line_number, line in enumerate(lines):
if 'POTCAR:' in line:
content = line.split(maxsplit=1)[1].strip()
if content not in output_dict['POTCARS']:
output_dict['POTCARS'].append(content)
elif 'NKPTS' in line:
tokens = line.strip().split()
output_dict['num_kpoints'] = int(tokens[tokens.index('NKPTS') + 2])
output_dict['num_bands'] = int(tokens[-1])
elif 'dimension x,y,z NGX =' in line:
tokens = line.strip().split()
output_dict['NGX'] = int(tokens[tokens.index('NGX') + 2])
output_dict['NGY'] = int(tokens[tokens.index('NGY') + 2])
output_dict['NGZ'] = int(tokens[tokens.index('NGZ') + 2])
elif 'FFT grid for exact exchange' in line:
tokens = lines[line_number + 1].replace(';', '').strip().split()
output_dict['EX NGX'] = int(tokens[tokens.index('NGX') + 2])
output_dict['EX NGY'] = int(tokens[tokens.index('NGY') + 2])
output_dict['EX NGZ'] = int(tokens[tokens.index('NGZ') + 2])
elif 'NPLWV' in line:
try:
output_dict['num_plane_waves'] = int(line.split()[-1])
except ValueError:
pass
elif 'k-points in reciprocal lattice and weights:' in line:
kblock = lines[line_number + 1 : line_number + 1 + output_dict['num_kpoints']]
k_list = [[float(token) for token in subline.strip().split()] for subline in kblock]
output_dict['kpoints_and_weights'] = k_list
elif 'maximum and minimum number of plane-waves per node :' in line:
output_dict['plane_waves_min_max'] = [float(token) for token in line.split()[-2:]]
elif 'total amount of memory used by VASP MPI-rank0' in line:
output_dict['max_ram_rank0'] = float(line.split()[-2])
for subline in lines[line_number + 3 : line_number + 9]:
tokens = subline.replace(':', '').split()
output_dict['mem_' + tokens[0]] = float(tokens[-2])
return output_dict

Check warning on line 178 in src/aiida_vasp/commands/dryrun_vasp.py

View check run for this annotation

Codecov / codecov/patch

src/aiida_vasp/commands/dryrun_vasp.py#L141-L178

Added lines #L141 - L178 were not covered by tests
Empty file.