From e1bc816419a8b82636e1d91d8818925f43084359 Mon Sep 17 00:00:00 2001 From: Sophie Gerits Date: Tue, 3 Sep 2024 16:19:58 -0600 Subject: [PATCH] Jobs.py uses Custodian's Job class and should be stored there when ready. It takes a .in file and runs the jdftx calculation using a command (Docker command is used atm) and outputs an out file and any requested output. Example and testing are in sample-move-later folder. --- src/atomate2/jdftx/jobs/base.py | 14 +-- src/atomate2/jdftx/jobs/core.py | 2 +- src/atomate2/jdftx/jobs/jobs.py | 100 ++++++++++++++++++ .../jdftx/jobs/sample-move-later/Dockerfile | 21 ++++ .../jobs/sample-move-later/input-tutorial.in | 27 +++++ .../jdftx/jobs/sample-move-later/test_run.py | 13 +++ 6 files changed, 170 insertions(+), 7 deletions(-) create mode 100644 src/atomate2/jdftx/jobs/jobs.py create mode 100644 src/atomate2/jdftx/jobs/sample-move-later/Dockerfile create mode 100644 src/atomate2/jdftx/jobs/sample-move-later/input-tutorial.in create mode 100644 src/atomate2/jdftx/jobs/sample-move-later/test_run.py diff --git a/src/atomate2/jdftx/jobs/base.py b/src/atomate2/jdftx/jobs/base.py index c0b6e5c126..a7f642b29c 100644 --- a/src/atomate2/jdftx/jobs/base.py +++ b/src/atomate2/jdftx/jobs/base.py @@ -21,9 +21,9 @@ from atomate2 import SETTINGS from atomate2.common.files import gzip_output_folder +from atomate2.jdftx.sets.base import JdftxInputGenerator from atomate2.vasp.files import copy_vasp_outputs, write_vasp_input_set from atomate2.vasp.run import run_vasp, should_stop_children -from atomate2.jdftx.sets.base import JdftxInputGenerator if TYPE_CHECKING: from pymatgen.core import Structure @@ -36,7 +36,7 @@ or which("chargemol") ) -_DATA_OBJECTS = [ # TODO just store trajectory +_DATA_OBJECTS = [ # TODO just store trajectory BandStructure, BandStructureSymmLine, DOS, @@ -53,7 +53,7 @@ # Input files. Partially from https://www.vasp.at/wiki/index.php/Category:Input_files # Exclude those that are also outputs -_INPUT_FILES = [ # TODO implement file names from Jacob +_INPUT_FILES = [ # TODO implement file names from Jacob "DYNMATFULL", "ICONST", "INCAR", @@ -68,7 +68,7 @@ ] # Output files. Partially from https://www.vasp.at/wiki/index.php/Category:Output_files -_OUTPUT_FILES = [ # TODO implement file names from Jacob +_OUTPUT_FILES = [ # TODO implement file names from Jacob "AECCAR0", "AECCAR1", "AECCAR2", @@ -180,7 +180,9 @@ class BaseVaspMaker(Maker): """ name: str = "base vasp job" - input_set_generator: JdftxInputGenerator = field(default_factory=JdftxInputGenerator) + input_set_generator: JdftxInputGenerator = field( + default_factory=JdftxInputGenerator + ) write_input_set_kwargs: dict = field(default_factory=dict) copy_vasp_kwargs: dict = field(default_factory=dict) run_vasp_kwargs: dict = field(default_factory=dict) @@ -288,4 +290,4 @@ def get_vasp_task_document(path: Path | str, **kwargs) -> TaskDoc: kwargs.setdefault("store_volumetric_data", SETTINGS.VASP_STORE_VOLUMETRIC_DATA) - return TaskDoc.from_directory(path, **kwargs) \ No newline at end of file + return TaskDoc.from_directory(path, **kwargs) diff --git a/src/atomate2/jdftx/jobs/core.py b/src/atomate2/jdftx/jobs/core.py index dd8b133155..a3f3ddbf84 100644 --- a/src/atomate2/jdftx/jobs/core.py +++ b/src/atomate2/jdftx/jobs/core.py @@ -1 +1 @@ -from pymatgen.io.core import \ No newline at end of file +from pymatgen.io.core import diff --git a/src/atomate2/jdftx/jobs/jobs.py b/src/atomate2/jdftx/jobs/jobs.py new file mode 100644 index 0000000000..50402b1540 --- /dev/null +++ b/src/atomate2/jdftx/jobs/jobs.py @@ -0,0 +1,100 @@ +"""This module implements basic kinds of jobs for JDFTx runs.""" + +import logging +import os +import subprocess + +from custodian.custodian import Job + +logger = logging.getLogger(__name__) + + +class JDFTxJob(Job): + """ + A basic JDFTx job. Runs whatever is in the working directory. + """ + + # If testing, use something like: + # job = JDFTxJob() + # job.run() # assumes input files already written to directory + + # Used Cp2kJob developed by Nick Winner as a template. + + def __init__( + self, + jdftx_cmd, + input_file="jdftx.in", + output_file="jdftx.out", + stderr_file="std_err.txt", + ) -> None: + """ + This constructor is necessarily complex due to the need for + flexibility. For standard kinds of runs, it's often better to use one + of the static constructors. The defaults are usually fine too. + + Args: + jdftx_cmd (str): Command to run JDFTx as a string. + input_file (str): Name of the file to use as input to JDFTx + executable. Defaults to "input.in" + output_file (str): Name of file to direct standard out to. + Defaults to "jdftx.out". + stderr_file (str): Name of file to direct standard error to. + Defaults to "std_err.txt". + """ + self.jdftx_cmd = jdftx_cmd + self.input_file = input_file + self.output_file = output_file + self.stderr_file = stderr_file + + def setup(self, directory="./") -> None: + """ + No setup required. + """ + pass + + def run(self, directory="./"): + """ + Perform the actual JDFTx run. + + Returns: + (subprocess.Popen) Used for monitoring. + """ + cmd = self.jdftx_cmd + " -i " + self.input_file + logger.info(f"Running {cmd}") + with ( + open(os.path.join(directory, self.output_file), "w") as f_std, + open(os.path.join(directory, self.stderr_file), "w", buffering=1) as f_err, + ): + result = subprocess.run([cmd], cwd=directory, stdout=f_std, stderr=f_err, shell=True) + + # Review the return code + if result.returncode == 0: + logger.info(f"Command executed successfully with return code {result.returncode}.") + else: + logger.error(f"Command failed with return code {result.returncode}.") + # Optionally, you can log or print additional information here + with open(os.path.join(directory, self.stderr_file), 'r') as f_err: + error_output = f_err.read() + logger.error(f"Standard Error Output:\n{error_output}") + + return result + + # use line buffering for stderr + # return subprocess.run([cmd], cwd=directory, stdout=f_std, stderr=f_err, shell=True) + + + def postprocess(self, directory="./") -> None: + """No post-processing required.""" + pass + + def terminate(self, directory="./") -> None: + """Terminate JDFTx.""" + # This will kill any running process with "jdftx" in the name, + # this might have unintended consequences if running multiple jdftx processes + # on the same node. + for cmd in self.jdftx_cmd: + if "jdftx" in cmd: + try: + os.system(f"killall {cmd}") + except Exception: + pass \ No newline at end of file diff --git a/src/atomate2/jdftx/jobs/sample-move-later/Dockerfile b/src/atomate2/jdftx/jobs/sample-move-later/Dockerfile new file mode 100644 index 0000000000..b61cc1ced9 --- /dev/null +++ b/src/atomate2/jdftx/jobs/sample-move-later/Dockerfile @@ -0,0 +1,21 @@ +# install Docker +# run "docker build . -t jdftx" from directory containing Dockefile + +FROM ubuntu:24.04 + +RUN apt-get -y update && apt-get -y --no-install-recommends install g++ cmake libgsl0-dev libopenmpi-dev openmpi-bin libfftw3-dev libatlas-base-dev liblapack-dev wget unzip ca-certificates make && \ + cd /root && \ + wget https://github.com/shankar1729/jdftx/archive/refs/heads/master.zip && unzip master.zip && rm master.zip && \ + cd jdftx-master && mkdir build && cd build && \ + cmake ../jdftx && make all && make install && \ +# make test && \ + cd /root && rm -rf /root/jdftx-master && \ + echo 'export PATH="$PATH:/usr/local/share/jdftx/scripts"' >> /root/.bashrc && mkdir /root/research + +WORKDIR /root/research + +#Use it like this: +#docker run -it --rm -v $PWD:/root/research jdftx + +#Or even better, put the following line at the end of your .bashrc and/or .zshrc so that you can just run 'jdftx' : +#function jdftx () { docker run -it --rm -v $PWD:/root/research jdftx ; } \ No newline at end of file diff --git a/src/atomate2/jdftx/jobs/sample-move-later/input-tutorial.in b/src/atomate2/jdftx/jobs/sample-move-later/input-tutorial.in new file mode 100644 index 0000000000..60d1a5214a --- /dev/null +++ b/src/atomate2/jdftx/jobs/sample-move-later/input-tutorial.in @@ -0,0 +1,27 @@ +# The input file is a list of commands, one per line +# The commands may appear in any order; group them to your liking +# Everything on a line after a # is treated as a comment and ignored +# Whitespace separates words; extra whitespace is ignored +# --------------- Water molecule example ---------------- + +# Set up the unit cell - each column is a bravais lattice vector in bohrs +# Hence this is a cubic box of side 10 bohr (Note that \ continues lines) +lattice \ + 10 0 0 \ + 0 10 0 \ + 0 0 10 + +elec-cutoff 20 100 #Plane-wave kinetic energy cutoff for wavefunctions and charge density in Hartrees + +# Specify the pseudopotentials (this defines species O and H): +ion-species GBRV/h_pbe.uspp +ion-species GBRV/o_pbe.uspp + +# Specify coordinate system and atom positions: +coords-type cartesian #the other option is lattice (suitable for solids) +ion O 0.00 0.00 0.00 0 # The last 0 holds this atom fixed +ion H 0.00 1.13 +1.45 1 # while the 1 allows this one to move +ion H 0.00 1.13 -1.45 1 # during ionic minimization + +dump-name water.$VAR #Filename pattern for outputs +dump End Ecomponents ElecDensity #Output energy components and electron density at the end \ No newline at end of file diff --git a/src/atomate2/jdftx/jobs/sample-move-later/test_run.py b/src/atomate2/jdftx/jobs/sample-move-later/test_run.py new file mode 100644 index 0000000000..bb5a4d5a72 --- /dev/null +++ b/src/atomate2/jdftx/jobs/sample-move-later/test_run.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python + +# Optional for debugging, use via "pip install stackprinter" first: +import stackprinter +stackprinter.set_excepthook(style='darkbg2') + +# if running from directory containing jobs.py for testing, can also use "from jobs import JDFTxJob" +from atomate2.jdftx.jobs.jobs import JDFTxJob + +# assumes running this script from directory containing already-generated input files +# if input files are in a different directory, change "$PWD" below +job = JDFTxJob(jdftx_cmd="docker run -t --rm -v $PWD:/root/research jdftx jdftx", input_file="input-tutorial.in") +job.run()