diff --git a/.travis.yml b/.travis.yml index 51b66ff..2b60153 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,23 @@ language: python - python: - '2.7' - '3.8' - install: - pip install -r requirements.txt - pip install -r test_requirements.txt - pip install -e . - script: -- pytest --cov=src/ --cov-report term-missing --cov-report term:skip-covered --cov-config=tox.ini --cov-fail-under=100 -svv . -- flake8 src \ No newline at end of file +- pytest --cov=src/ --cov-report term-missing --cov-report term:skip-covered --cov-config=tox.ini + --cov-fail-under=100 -svv . +- flake8 src +deploy: +- provider: pypi + user: CitrineInformatics + password: "$PYPI_PASSWORD" + distributions: sdist bdist_wheel + skip_existing: true + on: + tags: true +env: + global: + secure: M9HI5bpY5t4Rd17rjdrSo6QF4jV2JMi3hOgMzMGgly4jP1NoafOBSpJHy4TisBeE8eyrkJPJurMnF0Fw7kACtpCMT6h8tLE4r6Ss97542MJnFLV33hILdcwJZjAYfaQ0q5lrWmtNr420y5kvD1EjL3jQnYYVJC2wjAb4b5C8Ji5i/43n5Vuk3aO2BG8IzNXF0buqtAWIwyclOpr/QjkSWxf+ptj4Fv64Cy84aVTFMOGL523DRVkZgCl5vdq6gSpFTk9S/a2G16LM6GPU3ohKRzIGRzRv8HsGL4oyi2c+NjzysIdOq7yx/8crM4fADAl+xG3bh8biYJY6PVfxrR4ttNdL6UFxg0xUwPVpmngL+YjUS6DOi7cVs9VoaqsPq5mrrcZ5HvL+RaJt43o0okxBqvmZW/SRJs0fAdDLu1UKIxzH9MTpdQmuUhCyyh6muePnnzvNkSzXnZRSO2z/DUmLsRiA3qMJkY/CSIWvxqj9+tVvAT9jRjb+loWBrra3FR5+sMikmoT/Qd27xCYSrwTMBy7jgw4mOh6xNK6FmT1nS/JDpH0KrZDXwjsB2uHSw9G1EnrBF7fhXCTx7SodD9ypwKrB0aIUrRj2iUcsAKGN4mVz92KvF/OhhuLmH2+hiivSM98k2R36bZfhEuAqFTSN5DM/UoqLXckdr63vahxHlEA= diff --git a/MANIFEST.in b/MANIFEST.in index 11dad2a..d5c2f9c 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,6 @@ -include src/dftinpgen/*.txt -include src/dftinpgen/data/*.json -include src/dftinpgen/qe/settings/*.json -include src/dftinpgen/qe/settings/calculation_presets/*.json +include src/dftinputgen/*.txt +include src/dftinputgen/data/*.json +include src/dftinputgen/qe/settings/*.json +include src/dftinputgen/qe/settings/calculation_presets/*.json +include src/dftinputgen/gpaw/settings/*.json +include src/dftinputgen/gpaw/settings/calculation_presets/*.json diff --git a/README.md b/README.md index 6ac8cf1..6f21c20 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,10 @@ documentation. 1. [pw.x](https://www.quantum-espresso.org/Doc/INPUT_PW.html) from the [Quantum Espresso package](https://www.quantum-espresso.org/) -2. (under development) Post-processing utilities for pw.x: +2. [GPAW](https://wiki.fysik.dtu.dk/gpaw/index.html) including functions for +`surface relaxation`, `adsorbate vibration calculations`, and `lattice +parameter optimization` (fcc, bcc, and hcp) +3. (under development) Post-processing utilities for pw.x: [dos.x](https://www.quantum-espresso.org/Doc/INPUT_DOS.html), [bands.x](https://www.quantum-espresso.org/Doc/INPUT_BANDS.html), [projwfc.x](https://www.quantum-espresso.org/Doc/INPUT_PROJWFC.html) diff --git a/docs/src/conf.py b/docs/src/conf.py index 81cbe9e..4f535a3 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -23,7 +23,9 @@ author = "Vinay Hegde <vhegde@citrine.io>" # The full version, including alpha/beta/rc tags -release = "0.1.0" +version_file = os.path.join("..", "..", "src", "dftinputgen", "VERSION.txt") +with open(version_file, "r") as fr: + release = fr.read().strip() # -- General configuration --------------------------------------------------- diff --git a/docs/src/developer_notes/index.rst b/docs/src/developer_notes/index.rst index 2a179a9..8b1a0cd 100644 --- a/docs/src/developer_notes/index.rst +++ b/docs/src/developer_notes/index.rst @@ -20,10 +20,20 @@ Code Organization settings/ tags_and_groups.json ... - base_recipes/ + calculation_presets/ scf.json vc-relax.json ... + gpaw/ + gpaw.py + ... + settings/ + tags_and_groups.json + ... + calculation_presets/ + bulk_opt.json + bulk_opt_hcp.json + ... vasp/ ... @@ -33,6 +43,8 @@ Code Organization - ``dftinputgen.qe``: derived classes that can generate input files for various DFT-based and postprocessing codes in the Quantum Espresso suite (more :ref:`here <ssec-qe>`) +- ``dftinputgen.gpaw``: derived classes that can generate input python scripts + for the GPAW package (more :ref:`here <ssec-gpaw>`). - ``dftinputgen.vasp``: [under development] derived classes that can generate input files for the VASP package. - ``dftinputgen.utils``: general-purpose helper functions, e.g. chemical formula diff --git a/docs/src/module_reference/gpaw/gpaw.rst b/docs/src/module_reference/gpaw/gpaw.rst new file mode 100644 index 0000000..cd33043 --- /dev/null +++ b/docs/src/module_reference/gpaw/gpaw.rst @@ -0,0 +1,49 @@ +.. _sssec-gpaw: + +Input GPAW +++++++++++ + +The :class:`GPAWInputGenerator <dftinputgen.gpaw.GPAWInputGenerator>` class +(derived from :class:`DftInputGenerator <dftinputgen.base.DftInputGenerator>`) +implements functionality to generate python scripts for the `GPAW`_ +package. + +Python scripts are written that use `ASE`_ with GPAW specified via a +calculator object. This calculator object is where all settings for +the DFT calculation is given. An example of the calculator object +that is automatically written is given below. + +.. code-block:: python + + slab.calc = GPAW( + h=0.16, + kpts={'size': [4, 4, 1]}, + occupations={'name': 'fermi-dirac', 'width': 0.05}, + poissonsolver={'dipolelayer': 'xy'}, + xc='BEEF-vdW' + ) + +For each calculator setting, a list of valid parameters is looked up from +a ``GPAW_TAGS`` dictionary which is then defined within the written +calculator object (more information about the valid calculator parameters +and defaults provided as ``calculation_presets`` is :ref:`here +<sssec-gpaw-input-settings>`). + +Currently supported calculations include relaxations, bulk optimizations, +and total energy. + +**Note:** The scripts are written assuming that the crystal structure +is stored in an `ASE readable format`_ (e.g. traj, cif, etc..) with +the desired initial magnetic moments defined. + +.. _`GPAW`: https://wiki.fysik.dtu.dk/gpaw/index.html +.. _`ASE`: https://wiki.fysik.dtu.dk/ase/ +.. _`ASE readable format`: https://wiki.fysik.dtu.dk/ase/ase/io/io.html + +Interfaces +========== + +.. automodule:: dftinputgen.gpaw.gpaw + :members: + :inherited-members: + :undoc-members: diff --git a/docs/src/module_reference/gpaw/index.rst b/docs/src/module_reference/gpaw/index.rst new file mode 100644 index 0000000..b72945e --- /dev/null +++ b/docs/src/module_reference/gpaw/index.rst @@ -0,0 +1,21 @@ +.. _ssec-gpaw: + +Input generators for GPAW ++++++++++++++++++++++++++ + +``dftinputgen`` provides an input generator class for the `GPAW`_ package. +More information :ref:`here <sssec-gpaw>` + +.. _`GPAW`: https://wiki.fysik.dtu.dk/gpaw/index.html + + +.. automodule:: dftinputgen.gpaw + :members: + :undoc-members: + +.. toctree:: + :maxdepth: 2 + :hidden: + + gpaw + settings diff --git a/docs/src/module_reference/gpaw/settings.rst b/docs/src/module_reference/gpaw/settings.rst new file mode 100644 index 0000000..a46288f --- /dev/null +++ b/docs/src/module_reference/gpaw/settings.rst @@ -0,0 +1,33 @@ +.. _sssec-gpaw-input-settings: + +Input settings +++++++++++++++ + +All input settings have been parsed from the `GPAW manual`_ +(last updated January 2021). +The calculator parameter names are stored in `tags_and_groups.json`_. +These parameter names are then made available to the user via +a module level variable ``GPAW_TAGS``. + +The settings module also makes available a few sets of default settings +to be used for common DFT calculations such as ``surface_relax``, +and ``bulk_opt``. +The defaults are stored in JSON files in the `calculation_presets`_ module. +These can be accessed by the user via a module level variable ``GPAW_PRESETS``. +Note that these presets are only reasonable defaults and are not meant to be +prescriptive. + +.. _`GPAW manual`: https://wiki.fysik.dtu.dk/gpaw/documentation/manual.html#parameters +.. _`tags_and_groups.json`: https://github.com/CitrineInformatics/dft-input-gen/blob/master/src/dftinputgen/qe/settings/tags_and_groups.json +.. _`calculation_presets`: https://github.com/CitrineInformatics/dft-input-gen/tree/master/src/dftinputgen/qe/settings/calculation_presets + +.. automodule:: dftinputgen.gpaw.settings + :members: + :undoc-members: + :special-members: + +.. automodule:: dftinputgen.gpaw.settings.calculation_presets + :members: + :undoc-members: + :special-members: + diff --git a/docs/src/module_reference/index.rst b/docs/src/module_reference/index.rst index 2ac7eb8..630831a 100644 --- a/docs/src/module_reference/index.rst +++ b/docs/src/module_reference/index.rst @@ -9,5 +9,6 @@ Module reference base qe/index + gpaw/index utils data diff --git a/setup.py b/setup.py index 3996cd5..794b1bb 100644 --- a/setup.py +++ b/setup.py @@ -20,6 +20,7 @@ author_email="vhegde@citrine.io", packages=find_packages(where="src"), package_dir={"": "src"}, + include_package_data=True, install_requires=["six", "numpy", "ase <= 3.17"], entry_points={"console_scripts": ["dftinputgen = dftinputgen.cli:driver"]}, classifiers=[ diff --git a/src/dftinputgen/VERSION.txt b/src/dftinputgen/VERSION.txt index 6e8bf73..d917d3e 100644 --- a/src/dftinputgen/VERSION.txt +++ b/src/dftinputgen/VERSION.txt @@ -1 +1 @@ -0.1.0 +0.1.2 diff --git a/src/dftinputgen/cli.py b/src/dftinputgen/cli.py index f5a719d..0e883fa 100644 --- a/src/dftinputgen/cli.py +++ b/src/dftinputgen/cli.py @@ -2,6 +2,8 @@ from dftinputgen.demo.pwx import build_pwx_parser from dftinputgen.demo.pwx import generate_pwx_input_files +from dftinputgen.demo.gpaw import build_gpaw_parser +from dftinputgen.demo.gpaw import generate_gpaw_input_files def get_parser(): @@ -28,6 +30,12 @@ def get_parser(): # other subparsers, to be added similarly, go here # e.g. ones for gpaw/vasp + # gpaw subparser + gpaw_help = "Generate a python input file for GPAW" + gpaw_parser = subparsers.add_parser("gpaw", help=gpaw_help) + build_gpaw_parser(gpaw_parser) + gpaw_parser.set_defaults(func=generate_gpaw_input_files) + return parser diff --git a/src/dftinputgen/demo/gpaw.py b/src/dftinputgen/demo/gpaw.py new file mode 100644 index 0000000..a6c9e02 --- /dev/null +++ b/src/dftinputgen/demo/gpaw.py @@ -0,0 +1,117 @@ +"""Demo generating input files for doing a calculation with pw.x.""" + +import json +import argparse + +from dftinputgen.utils import read_crystal_structure +from dftinputgen.gpaw import GPAWInputGenerator + + +def _get_default_parser(): + description = "Input file generation for gpaw." + return argparse.ArgumentParser(description=description) + + +def build_gpaw_parser(parser): + """Adds GPAW arguments to the input `argparse.ArgumentParser` object.""" + # Required: + crystal_structure = "(REQUIRED) File with the input crystal structure" + parser.add_argument( + "-i", + "--crystal-structure", + type=read_crystal_structure, + help=crystal_structure, + required=True, + ) + + # Optional: + calculation_presets = "Preset group of tags and default values to use" + parser.add_argument( + "-pre", + "--calculation-presets", + choices=[ + "surface_relax", + "bulk_opt", + "bulk_opt_hcp", + "molecule", + "surface_adsorbate_vibrations", + ], + default=None, + help=calculation_presets, + ) + + custom_settings_file = "JSON file with custom DFT settings to use" + parser.add_argument( + "-file", + "--custom-settings-file", + default=None, + help=custom_settings_file, + ) + + custom_settings_dict = """JSON string with a dictionary of custom DFT + settings to use. Example: '{"pseudo_dir": "/path/to/pseudo_dir/"}'""" + parser.add_argument( + "-dict", + "--custom-settings-dict", + default="{}", + type=json.loads, + help=custom_settings_dict, + ) + + write_location = "Directory to write the input file(s) in" + parser.add_argument("-loc", "--write-location", help=write_location) + + gpaw_input_file = "Name of the GPAW input file to be written" + parser.add_argument("-o", "--gpaw-input-file", help=gpaw_input_file) + + gpaw_restart_file = "Name of the GPAW restart file that the written script\ + will read from" + parser.add_argument("-r", "--gpaw-restart-file", help=gpaw_restart_file) + + input_struct_filename = "Name of the input structure that the written\ + script will read from" + parser.add_argument( + "-is", "--input_struct_filename", help=input_struct_filename + ) + + overwrite_files = "To overwrite files or not, that is the question" + parser.add_argument("-ov", "--overwrite_files", help=overwrite_files) + + from_scratch = "Whether the calculation should be run from scratch" + parser.add_argument("-fs", "--from-scratch", help=from_scratch) + + +def generate_gpaw_input_files(args): + """Write input files for the input crystal structure.""" + gig = GPAWInputGenerator( + crystal_structure=args.crystal_structure, + calculation_presets=args.calculation_presets, + custom_sett_file=args.custom_settings_file, + custom_sett_dict=args.custom_settings_dict, + write_location=args.write_location, + gpaw_input_file=args.gpaw_input_file, + gpaw_restart_file=args.gpaw_restart_file, + input_struct_filename=args.input_struct_filename, + overwrite_files=args.overwrite_files, + from_scratch=args.from_scratch, + ) + gig.write_input_files() + + +def run_demo(*sys_args): + """End-to-end run of pw.x input file generation.""" + parser = _get_default_parser() + build_gpaw_parser(parser) + args = parser.parse_args(*sys_args) + generate_gpaw_input_files(args) + + +if __name__ == "__main__": + """ + When run as a script, this module will generate input files to use with + GPAW, for a specified crystal structure, calculation presets, and any + custom DFT settings on top of preset defaults. + + For a list of optional arguments, run this script with "-h" argument. + """ + run_demo() # pragma: no cover diff --git a/src/dftinputgen/gpaw/__init__.py b/src/dftinputgen/gpaw/__init__.py new file mode 100644 index 0000000..0dad217 --- /dev/null +++ b/src/dftinputgen/gpaw/__init__.py @@ -0,0 +1 @@ +from dftinputgen.gpaw.gpaw import GPAWInputGenerator # noqa: F401 diff --git a/src/dftinputgen/gpaw/gpaw.py b/src/dftinputgen/gpaw/gpaw.py new file mode 100644 index 0000000..133178e --- /dev/null +++ b/src/dftinputgen/gpaw/gpaw.py @@ -0,0 +1,389 @@ +import os + +from dftinputgen.gpaw.settings import GPAW_TAGS +from dftinputgen.gpaw.settings.calculation_presets import GPAW_PRESETS + +from dftinputgen.base import DftInputGenerator +from dftinputgen.base import DftInputGeneratorError + + +class GPAWInputGeneratorError(DftInputGeneratorError): + pass + + +class GPAWInputGenerator(DftInputGenerator): + """Base class to generate python scripts for GPAW""" + + def __init__( + self, + crystal_structure=None, + calculation_presets=None, + custom_sett_file=None, + custom_sett_dict=None, + write_location=None, + gpaw_input_file=None, + gpaw_restart_file=None, + input_struct_filename=None, + overwrite_files=None, + from_scratch=None, + **kwargs, + ): + """ + Constructor. + + Parameters + ---------- + + crystal_structure: :class:`ase.Atoms` object + :class:`ase.Atoms` object from `ase.io.read([crystal structure + file])`. + + calculation_presets: str, optional + The "base" calculation settings to use--must be one of the + pre-defined groups of tags and values provided for GPAW. + + Pre-defined settings for some common calculation types are in + INSTALL_PATH/qe/settings/calculation_presets/ + + custom_sett_file: str, optional + Location of a JSON file with custom calculation settings as a + dictionary of tags and values. + + NB: Custom settings specified here always OVERRIDE those in + `calculation_presets` in case of overlap. + + custom_sett_dict: dict, optional + Dictionary with custom calculation settings as tags and values/ + + NB: Custom settings specified here always OVERRIDE those in + `calculation_presets` and `custom_sett_file`. + + write_location: str, optional + Path to the directory in which to write the input file(s). + + Default: Current working directory. + + gpaw_input_file: str, optional + Name of the file in which to write the GPAW python script + + Default: "[`calculation_presets`]_in.py" if `calculation_presets` + is specified by the user, else "gpaw_in.py". + + gpaw_restart_file: str, optional + Name of the gpaw restart file that the written gpaw script will + read from. + + Default: "output.gpw" + + input_struct_filename: str, optional + Name of the input structure file readable by `ase.io.read` that the + written gpaw script will call from if no restart files present. + + Default: "input.traj" + + overwrite_files: bool, optional + To overwrite files or not, that is the question. + + Default: True + + from_scratch: bool, optional + Even if restart files are found, whether to restart + the job from scratch (ie. fresh GPAW calculator) + each time the job is resubmitted + + **kwargs: + Arbitrary keyword arguments. + + """ + + super(GPAWInputGenerator, self).__init__( + crystal_structure=crystal_structure, + calculation_presets=calculation_presets, + custom_sett_file=custom_sett_file, + custom_sett_dict=custom_sett_dict, + write_location=write_location, + overwrite_files=overwrite_files, + **kwargs, + ) + + self._calculation_settings = self._get_calculation_settings() + + self._gpaw_input_file = self._get_default_input_filename() + self.gpaw_input_file = gpaw_input_file + + self._gpaw_restart_file = "output.gpw" + self.gpaw_restart_file = gpaw_restart_file + + self._struct_filename = "input.traj" + self.struct_filename = input_struct_filename + + self._from_scratch = False + self.from_scratch = from_scratch + + @property + def dft_package(self): + return "GPAW" + + @property + def gpaw_input_file(self): + """Name of the gpaw input file to write to.""" + return self._gpaw_input_file + + @gpaw_input_file.setter + def gpaw_input_file(self, gpaw_input_file): + if gpaw_input_file is not None: + self._gpaw_input_file = gpaw_input_file + + @property + def gpaw_restart_file(self): + """Name of gpaw output file to read from if restarting a calculation""" + return self._gpaw_restart_file + + @gpaw_restart_file.setter + def gpaw_restart_file(self, gpaw_restart_file): + if gpaw_restart_file is not None: + self._gpaw_restart_file = gpaw_restart_file + + @property + def struct_filename(self): + """Name of the structure file that the gpaw script will read from.""" + return self._struct_filename + + @struct_filename.setter + def struct_filename(self, struct_filename): + if struct_filename is not None: + self._struct_filename = struct_filename + + @property + def from_scratch(self): + """Whether will always start jobs from scratch, even in presence of output file""" + return self._from_scratch + + @from_scratch.setter + def from_scratch(self, from_scratch): + if from_scratch is not None: + self._from_scratch = from_scratch + + @property + def calculation_settings(self): + """Dictionary of all calculation settings to use as input for gpaw.""" + return self._get_calculation_settings() + + def _get_calculation_settings(self): + """Load all calculation settings: user-input and auto-determined.""" + calc_sett = {} + if self.calculation_presets is not None: + calc_sett.update(GPAW_PRESETS[self.calculation_presets]) + if self.custom_sett_from_file is not None: + calc_sett.update(self.custom_sett_from_file) + if self.custom_sett_dict is not None: + calc_sett.update(self.custom_sett_dict) + return calc_sett + + def _get_default_input_filename(self): + if self.calculation_presets is None: + return "gpaw_in.py" + return "{}_in.py".format(self.calculation_presets) + + @property + def calculator_object_as_str(self): + top = "GPAW(" + + calc_sett = self.calculation_settings + + params = [] + for p in GPAW_TAGS["parameters"]: + if p in calc_sett: + if type(calc_sett[p]) is str: + params.append(f" {p}='{str(calc_sett[p])}'") + else: + params.append(f" {p}={str(calc_sett[p])}") + params.append(f' txt="output.txt"') + + return "\n".join([top, ",\n".join(params), ")"]) + + @property + def gpaw_input_as_str(self): + header = f"""import os +import numpy as np + +from gpaw import GPAW +from gpaw import restart +from ase.io import read +from ase.io import write +from ase.optimize import BFGS +from ase.parallel import barrier +from ase.eos import EquationOfState +from ase.vibrations import Vibrations + +from_scratch = {self.from_scratch} +slab = None + +if os.path.isfile("{self.gpaw_restart_file}"): + try: + slab, _ = restart("{self.gpaw_restart_file}", txt="output.txt") + except: + from_scratch = True + if os.path.isfile("output.traj") and os.path.getsize("output.traj") > 0: + slab = read("output.traj") + else: + slab = read("{self.struct_filename}") +else: + slab = read("{self.struct_filename}") + from_scratch = True + +if from_scratch: + slab.calc = {self.calculator_object_as_str} +""" + + calc_sett = self.calculation_settings + try: + calc_type = calc_sett["calculation"] + except KeyError: + # if no input settings found will return calc object + # without any settings defined + calc_type = None + if calc_type == "relax": + define_relax_fn = f"""def relax(atoms, fmax=0.05, step=0.04): + atoms.calc.attach(atoms.calc.write, 5, "{self.gpaw_restart_file}") + + def _check_file_exists(filename): + #Check if file exists and is not empty + if os.path.isfile(filename): + return os.path.getsize(filename) > 0 + else: + return False + + # check if it is a restart + barrier() + if _check_file_exists("output.traj"): + latest = read("output.traj", index=":") + # check if already restarted previously and extend history if needed + if not (_check_file_exists("history.traj")): + barrier() + write("history.traj", latest) + else: + hist = read("history.traj", index=":") + hist.extend(latest) + write("history.traj", hist) + + dyn = BFGS(atoms=atoms, trajectory="output.traj", logfile="qn.log", maxstep=step) + # if history exists, read in hessian + if _check_file_exists("history.traj"): + dyn.replay_trajectory("history.traj") + # optimize + dyn.run(fmax=fmax) +""" + return "\n".join([header, define_relax_fn, "relax(slab)",]) + + elif calc_type == "bulk_opt": + define_bulk_opt_fn = """def optimize_bulk(atoms, step=0.05): + cell = atoms.get_cell() + name = atoms.get_chemical_formula(mode='hill') + vol=atoms.get_volume() + volumes =[] + energies=[] + for x in np.linspace(1-2*step,1+2*step,5): + atoms.set_cell(cell*x, scale_atoms=True) + atoms.calc.set(txt=name+'_'+str(x)+'.txt') + energies.append(atoms.get_potential_energy()) + volumes.append(atoms.get_volume()) + eos = EquationOfState(volumes, energies) + v0,e0,B= eos.fit() + atoms.set_cell(np.cbrt(v0/vol)*cell,scale_atoms=True) + x0=np.cbrt(v0/vol) + atoms.calc.set(txt='output.txt') + dyn=BFGS(atoms=atoms,trajectory='output.traj',logfile = 'qn.log') + dyn.run(fmax=0.05) + atoms.calc.write('output.gpw') +""" + return "\n".join( + [header, define_bulk_opt_fn, "optimize_bulk(slab)",] + ) + elif calc_type == "bulk_opt_hcp": + define_bulk_opt_hcp_fn = """def optimize_bulk_hcp(atoms, step=0.05, nstep=5): + # Get initial cell + cell = atoms.get_cell() + a_in = cell[0] + b_in = cell[1] + c_in = cell[2] + name = atoms.get_chemical_formula(mode='hill') + vol=atoms.get_volume() + + # Optimize in ab first + volumes_ab =[] + energies_ab=[] + scale_factors = np.linspace(1-2*step,1+2*step,nstep) + for x in scale_factors: + atoms.set_cell([a_in*x,b_in*x,c_in], scale_atoms=True) + atoms.calc.set(txt=name+'_'+'ab'+'_'+str(x)+'.txt') + energies_ab.append(atoms.get_potential_energy()) + volumes_ab.append(atoms.get_volume()) + + # Fit in ab + eos = EquationOfState(volumes_ab, energies_ab) + v0,e0,B= eos.fit() + a_fit = a_in*np.sqrt(v0/vol) + b_fit = b_in*np.sqrt(v0/vol) + + # Generate new cell with fit ab parameters + atoms.set_cell([a_fit,b_fit,c_in], scale_atoms=True) + vol_ab = atoms.get_volume() + + # Optimize c + energies_c = [] + volumes_c = [] + for x in scale_factors: + atoms.set_cell([a_fit,b_fit,c_in*x], scale_atoms=True) + atoms.calc.set(txt=name+'_'+'c'+'_'+str(x)+'.txt') + energies_c.append(atoms.get_potential_energy()) + volumes_c.append(atoms.get_volume()) + + # Fit in c + eos_c = EquationOfState(volumes_c,energies_c,eos='birchmurnaghan') + v0, e0, B = eos_c.fit() + c_scaling = v0/vol_ab + c_fit = c_in*c_scaling + + # Update cell with optimized parameters and relax atoms + atoms.set_cell([a_fit,b_fit,c_fit], scale_atoms=True) + atoms.calc.set(txt='output.txt') + dyn=BFGS(atoms=atoms,trajectory='output.traj',logfile = 'qn.log') + dyn.run(fmax=0.05) + atoms.calc.write("output.gpw") +""" + return "\n".join( + [header, define_bulk_opt_hcp_fn, "optimize_bulk_hcp(slab)",] + ) + + elif calc_type == "surface_adsorbate_vibrations": + define_surf_ads_vib = """def calculate_vibrations(atoms): + indices_to_perturb = np.where(atoms.get_tags() <= 0)[0].tolist() + barrier() + vib = Vibrations(atoms, indices = indices_to_perturb, name = "vib.log") + vib.clean(empty_files=True) + vib.run() + vib.summary(log="vib.summary") +""" + return "\n".join( + [header, define_surf_ads_vib, "calculate_vibrations(slab)"] + ) + + # if not a relax or bulk_opt calculation, + # defaults to getting total energy of static structure + return "\n".join([header, "slab.get_total_energy()",]) + + def write_gpaw_input(self, write_location=None, filename=None): + if write_location is None: + msg = "Location to write files not specified" + raise GPAWInputGeneratorError(msg) + if filename is None: + msg = "Name of the input file to write into not specified" + raise GPAWInputGeneratorError(msg) + with open(os.path.join(write_location, filename), "w") as fw: + fw.write(self.gpaw_input_as_str) + + def write_input_files(self): + self.write_gpaw_input( + write_location=self.write_location, filename=self.gpaw_input_file, + ) diff --git a/src/dftinputgen/gpaw/settings/__init__.py b/src/dftinputgen/gpaw/settings/__init__.py new file mode 100644 index 0000000..bbbdc13 --- /dev/null +++ b/src/dftinputgen/gpaw/settings/__init__.py @@ -0,0 +1,11 @@ +import json +import pkg_resources + +__all__ = ["GPAW_TAGS"] + +tags_file = pkg_resources.resource_filename( + "dftinputgen.gpaw.settings", "tags_and_groups.json" +) + +with open(tags_file, "r") as fr: + GPAW_TAGS = json.load(fr) diff --git a/src/dftinputgen/gpaw/settings/calculation_presets/__init__.py b/src/dftinputgen/gpaw/settings/calculation_presets/__init__.py new file mode 100644 index 0000000..3191b35 --- /dev/null +++ b/src/dftinputgen/gpaw/settings/calculation_presets/__init__.py @@ -0,0 +1,23 @@ +import os +import json +import pkg_resources + + +__all__ = ["GPAW_PRESETS"] + + +GPAW_PRESETS = {} + + +preset_listdir = pkg_resources.resource_listdir( + "dftinputgen.gpaw.settings", "calculation_presets" +) +for filename in preset_listdir: + root, ext = os.path.splitext(filename) + if not ext == ".json": + continue + resource = pkg_resources.resource_filename( + "dftinputgen.gpaw.settings.calculation_presets", filename + ) + with open(resource, "r") as fr: + GPAW_PRESETS[root] = json.load(fr) diff --git a/src/dftinputgen/gpaw/settings/calculation_presets/bulk_opt.json b/src/dftinputgen/gpaw/settings/calculation_presets/bulk_opt.json new file mode 100644 index 0000000..c9344ed --- /dev/null +++ b/src/dftinputgen/gpaw/settings/calculation_presets/bulk_opt.json @@ -0,0 +1,12 @@ +{ + "kpts": { + "size": [12,12,12] + }, + "xc": "BEEF-vdW", + "h": 0.16, + "occupations": { + "name": "fermi-dirac", + "width": 0.05 + }, + "calculation": "bulk_opt" +} diff --git a/src/dftinputgen/gpaw/settings/calculation_presets/bulk_opt_hcp.json b/src/dftinputgen/gpaw/settings/calculation_presets/bulk_opt_hcp.json new file mode 100644 index 0000000..039f8de --- /dev/null +++ b/src/dftinputgen/gpaw/settings/calculation_presets/bulk_opt_hcp.json @@ -0,0 +1,12 @@ +{ + "kpts": { + "size": [12,12,6] + }, + "xc": "BEEF-vdW", + "h": 0.16, + "occupations": { + "name": "fermi-dirac", + "width": 0.05 + }, + "calculation": "bulk_opt_hcp" +} diff --git a/src/dftinputgen/gpaw/settings/calculation_presets/molecule.json b/src/dftinputgen/gpaw/settings/calculation_presets/molecule.json new file mode 100644 index 0000000..aff579f --- /dev/null +++ b/src/dftinputgen/gpaw/settings/calculation_presets/molecule.json @@ -0,0 +1,9 @@ +{ + "xc": "BEEF-vdW", + "h": 0.16, + "occupations": { + "name": "fermi-dirac", + "width": 0.05 + }, + "calculation": "relax" +} diff --git a/src/dftinputgen/gpaw/settings/calculation_presets/surface_adsorbate_vibrations.json b/src/dftinputgen/gpaw/settings/calculation_presets/surface_adsorbate_vibrations.json new file mode 100644 index 0000000..d047cbb --- /dev/null +++ b/src/dftinputgen/gpaw/settings/calculation_presets/surface_adsorbate_vibrations.json @@ -0,0 +1,19 @@ +{ + "kpts": { + "size": [ + 4, + 4, + 1 + ] + }, + "xc": "BEEF-vdW", + "h": 0.16, + "occupations": { + "name": "fermi-dirac", + "width": 0.05 + }, + "poissonsolver": { + "dipolelayer": "xy" + }, + "calculation": "surface_adsorbate_vibrations" +} \ No newline at end of file diff --git a/src/dftinputgen/gpaw/settings/calculation_presets/surface_relax.json b/src/dftinputgen/gpaw/settings/calculation_presets/surface_relax.json new file mode 100644 index 0000000..4194010 --- /dev/null +++ b/src/dftinputgen/gpaw/settings/calculation_presets/surface_relax.json @@ -0,0 +1,15 @@ +{ + "kpts": { + "size": [4,4,1] + }, + "xc": "BEEF-vdW", + "h": 0.16, + "occupations": { + "name": "fermi-dirac", + "width": 0.05 + }, + "poissonsolver": { + "dipolelayer": "xy" + }, + "calculation": "relax" +} diff --git a/src/dftinputgen/gpaw/settings/tags_and_groups.json b/src/dftinputgen/gpaw/settings/tags_and_groups.json new file mode 100644 index 0000000..e6105cb --- /dev/null +++ b/src/dftinputgen/gpaw/settings/tags_and_groups.json @@ -0,0 +1,28 @@ +{ + "parameters" : [ + "basis", + "charge", + "communicator", + "convergence", + "eigensolver", + "external", + "fixdensity", + "gpts", + "h", + "hund", + "idiotproof", + "kpts", + "maxiter", + "mode", + "nbands", + "occupations", + "parallel", + "poissonsolver", + "random", + "setups", + "spinpol", + "symmetry", + "txt", + "xc" + ] +} diff --git a/tests/demo/files/gpaw_li_relax_custom.py b/tests/demo/files/gpaw_li_relax_custom.py new file mode 100644 index 0000000..0784078 --- /dev/null +++ b/tests/demo/files/gpaw_li_relax_custom.py @@ -0,0 +1,70 @@ +# fmt: off +import os +import numpy as np + +from gpaw import GPAW +from gpaw import restart +from ase.io import read +from ase.io import write +from ase.optimize import BFGS +from ase.parallel import barrier +from ase.eos import EquationOfState +from ase.vibrations import Vibrations + +from_scratch = True +slab = None + +if os.path.isfile("Li.gpw"): + try: + slab, _ = restart("Li.gpw", txt="output.txt") + except: + from_scratch = True + if os.path.isfile("output.traj") and os.path.getsize("output.traj") > 0: + slab = read("output.traj") + else: + slab = read("Li110.traj") +else: + slab = read("Li110.traj") + from_scratch = True + +if from_scratch: + slab.calc = GPAW( + h=0.18, + kpts={'size': [6, 6, 1]}, + occupations={'name': 'fermi-dirac', 'width': 0.05}, + poissonsolver={'dipolelayer': 'xy'}, + xc='PBE', + txt="output.txt" +) + +def relax(atoms, fmax=0.05, step=0.04): + atoms.calc.attach(atoms.calc.write, 5, "Li.gpw") + + def _check_file_exists(filename): + #Check if file exists and is not empty + if os.path.isfile(filename): + return os.path.getsize(filename) > 0 + else: + return False + + # check if it is a restart + barrier() + if _check_file_exists("output.traj"): + latest = read("output.traj", index=":") + # check if already restarted previously and extend history if needed + if not (_check_file_exists("history.traj")): + barrier() + write("history.traj", latest) + else: + hist = read("history.traj", index=":") + hist.extend(latest) + write("history.traj", hist) + + dyn = BFGS(atoms=atoms, trajectory="output.traj", logfile="qn.log", maxstep=step) + # if history exists, read in hessian + if _check_file_exists("history.traj"): + dyn.replay_trajectory("history.traj") + # optimize + dyn.run(fmax=fmax) + +relax(slab) diff --git a/tests/demo/files/li110.traj b/tests/demo/files/li110.traj new file mode 100644 index 0000000..8eb08e8 Binary files /dev/null and b/tests/demo/files/li110.traj differ diff --git a/tests/demo/files/test_sett_gpaw.json b/tests/demo/files/test_sett_gpaw.json new file mode 100644 index 0000000..d11dfa1 --- /dev/null +++ b/tests/demo/files/test_sett_gpaw.json @@ -0,0 +1,6 @@ +{ + "h": 0.18, + "kpts": {"size": [6,6,1]}, + "occupations": {"name": "fermi-dirac", "width": 0.05}, + "poissonsolver": {"dipolelayer": "xy"} +} diff --git a/tests/demo/test_gpaw_demo.py b/tests/demo/test_gpaw_demo.py new file mode 100644 index 0000000..303919a --- /dev/null +++ b/tests/demo/test_gpaw_demo.py @@ -0,0 +1,107 @@ +import os +import json +import pytest +import argparse + +from dftinputgen.utils import read_crystal_structure +from dftinputgen.demo.gpaw import _get_default_parser +from dftinputgen.demo.gpaw import build_gpaw_parser +from dftinputgen.demo.gpaw import run_demo + + +files_dir = os.path.join(os.path.dirname(__file__), "files") +li_file = os.path.join(files_dir, "li110.traj") +li_struct = read_crystal_structure(li_file) +sett_file = os.path.join(files_dir, "test_sett_gpaw.json") +li_gpaw_script = os.path.join(files_dir, "gpaw_li_relax_custom.py") + + +def test_get_parser_required_missing(capsys): + parser = _get_default_parser() + build_gpaw_parser(parser) + + # input structure (required) missing: error + with pytest.raises(SystemExit): + parser.parse_args() + stderr = capsys.readouterr().err + assert "required" in stderr + + +def test_get_parser_default_args(): + parser = _get_default_parser() + build_gpaw_parser(parser) + args = parser.parse_args(["-i", li_file]) + assert args.crystal_structure == li_struct + assert args.calculation_presets is None + assert args.custom_settings_file is None + assert args.custom_settings_dict == {} + assert not args.overwrite_files + + +def test_get_parser_input_args(capsys): + parser = _get_default_parser() + build_gpaw_parser(parser) + + # invalid choice for `calculation_presets` + with pytest.raises(SystemExit): + parser.parse_args(["-i", li_file, "-pre", "unsupported"]) + stderr = capsys.readouterr().err + assert "invalid choice" in stderr + + # all ok + args = parser.parse_args( + [ + "-i", + li_file, + "-o", + "gpaw.in", + "-loc", + "/path/to/location", + "-dict", + '{"key_1": "val", "key_2": 0}', + "-file", + "some_file", + "-pre", + "bulk_opt_hcp", + ] + ) + assert args.calculation_presets == "bulk_opt_hcp" + assert args.custom_settings_file == "some_file" + assert args.custom_settings_dict == {"key_1": "val", "key_2": 0} + assert args.write_location == "/path/to/location" + assert args.gpaw_input_file == "gpaw.in" + + +def test_run_demo(): + import tempfile + + _tmp_file = tempfile.NamedTemporaryFile(mode="w", delete=True) + filename = _tmp_file.name + write_location = os.path.dirname(filename) + args = [ + "-i", + li_file, + "-pre", + "surface_relax", + "-file", + sett_file, + "-dict", + '{"xc": "PBE"}', + "-loc", + write_location, + "-o", + os.path.basename(filename), + "-is", + "Li110.traj", + "-r", + "Li.gpw", + "-fs", + "True", + ] + run_demo(args) + + with open(filename, "r") as fr: + test = fr.read() + with open(li_gpaw_script, "r") as fr: + reference = "\n".join(fr.read().splitlines()[1:]) + assert test == reference diff --git a/tests/gpaw/files/TEST_bulk_opt.py b/tests/gpaw/files/TEST_bulk_opt.py new file mode 100644 index 0000000..660eae5 --- /dev/null +++ b/tests/gpaw/files/TEST_bulk_opt.py @@ -0,0 +1,59 @@ +# fmt: off +import os +import numpy as np + +from gpaw import GPAW +from gpaw import restart +from ase.io import read +from ase.io import write +from ase.optimize import BFGS +from ase.parallel import barrier +from ase.eos import EquationOfState +from ase.vibrations import Vibrations + +from_scratch = False +slab = None + +if os.path.isfile("output.gpw"): + try: + slab, _ = restart("output.gpw", txt="output.txt") + except: + from_scratch = True + if os.path.isfile("output.traj") and os.path.getsize("output.traj") > 0: + slab = read("output.traj") + else: + slab = read("input.traj") +else: + slab = read("input.traj") + from_scratch = True + +if from_scratch: + slab.calc = GPAW( + h=0.16, + kpts={'size': [12, 12, 12]}, + occupations={'name': 'fermi-dirac', 'width': 0.05}, + xc='BEEF-vdW', + txt="output.txt" +) + +def optimize_bulk(atoms, step=0.05): + cell = atoms.get_cell() + name = atoms.get_chemical_formula(mode='hill') + vol=atoms.get_volume() + volumes =[] + energies=[] + for x in np.linspace(1-2*step,1+2*step,5): + atoms.set_cell(cell*x, scale_atoms=True) + atoms.calc.set(txt=name+'_'+str(x)+'.txt') + energies.append(atoms.get_potential_energy()) + volumes.append(atoms.get_volume()) + eos = EquationOfState(volumes, energies) + v0,e0,B= eos.fit() + atoms.set_cell(np.cbrt(v0/vol)*cell,scale_atoms=True) + x0=np.cbrt(v0/vol) + atoms.calc.set(txt='output.txt') + dyn=BFGS(atoms=atoms,trajectory='output.traj',logfile = 'qn.log') + dyn.run(fmax=0.05) + atoms.calc.write('output.gpw') + +optimize_bulk(slab) diff --git a/tests/gpaw/files/TEST_bulk_opt_hcp.py b/tests/gpaw/files/TEST_bulk_opt_hcp.py new file mode 100644 index 0000000..9e9d478 --- /dev/null +++ b/tests/gpaw/files/TEST_bulk_opt_hcp.py @@ -0,0 +1,90 @@ +# fmt: off +import os +import numpy as np + +from gpaw import GPAW +from gpaw import restart +from ase.io import read +from ase.io import write +from ase.optimize import BFGS +from ase.parallel import barrier +from ase.eos import EquationOfState +from ase.vibrations import Vibrations + +from_scratch = False +slab = None + +if os.path.isfile("output.gpw"): + try: + slab, _ = restart("output.gpw", txt="output.txt") + except: + from_scratch = True + if os.path.isfile("output.traj") and os.path.getsize("output.traj") > 0: + slab = read("output.traj") + else: + slab = read("input.traj") +else: + slab = read("input.traj") + from_scratch = True + +if from_scratch: + slab.calc = GPAW( + h=0.16, + kpts={'size': [12, 12, 6]}, + occupations={'name': 'fermi-dirac', 'width': 0.05}, + xc='BEEF-vdW', + txt="output.txt" +) + +def optimize_bulk_hcp(atoms, step=0.05, nstep=5): + # Get initial cell + cell = atoms.get_cell() + a_in = cell[0] + b_in = cell[1] + c_in = cell[2] + name = atoms.get_chemical_formula(mode='hill') + vol=atoms.get_volume() + + # Optimize in ab first + volumes_ab =[] + energies_ab=[] + scale_factors = np.linspace(1-2*step,1+2*step,nstep) + for x in scale_factors: + atoms.set_cell([a_in*x,b_in*x,c_in], scale_atoms=True) + atoms.calc.set(txt=name+'_'+'ab'+'_'+str(x)+'.txt') + energies_ab.append(atoms.get_potential_energy()) + volumes_ab.append(atoms.get_volume()) + + # Fit in ab + eos = EquationOfState(volumes_ab, energies_ab) + v0,e0,B= eos.fit() + a_fit = a_in*np.sqrt(v0/vol) + b_fit = b_in*np.sqrt(v0/vol) + + # Generate new cell with fit ab parameters + atoms.set_cell([a_fit,b_fit,c_in], scale_atoms=True) + vol_ab = atoms.get_volume() + + # Optimize c + energies_c = [] + volumes_c = [] + for x in scale_factors: + atoms.set_cell([a_fit,b_fit,c_in*x], scale_atoms=True) + atoms.calc.set(txt=name+'_'+'c'+'_'+str(x)+'.txt') + energies_c.append(atoms.get_potential_energy()) + volumes_c.append(atoms.get_volume()) + + # Fit in c + eos_c = EquationOfState(volumes_c,energies_c,eos='birchmurnaghan') + v0, e0, B = eos_c.fit() + c_scaling = v0/vol_ab + c_fit = c_in*c_scaling + + # Update cell with optimized parameters and relax atoms + atoms.set_cell([a_fit,b_fit,c_fit], scale_atoms=True) + atoms.calc.set(txt='output.txt') + dyn=BFGS(atoms=atoms,trajectory='output.traj',logfile = 'qn.log') + dyn.run(fmax=0.05) + atoms.calc.write("output.gpw") + +optimize_bulk_hcp(slab) diff --git a/tests/gpaw/files/TEST_defaults.py b/tests/gpaw/files/TEST_defaults.py new file mode 100644 index 0000000..8143e36 --- /dev/null +++ b/tests/gpaw/files/TEST_defaults.py @@ -0,0 +1,35 @@ +# fmt: off +import os +import numpy as np + +from gpaw import GPAW +from gpaw import restart +from ase.io import read +from ase.io import write +from ase.optimize import BFGS +from ase.parallel import barrier +from ase.eos import EquationOfState +from ase.vibrations import Vibrations + +from_scratch = False +slab = None + +if os.path.isfile("output.gpw"): + try: + slab, _ = restart("output.gpw", txt="output.txt") + except: + from_scratch = True + if os.path.isfile("output.traj") and os.path.getsize("output.traj") > 0: + slab = read("output.traj") + else: + slab = read("input.traj") +else: + slab = read("input.traj") + from_scratch = True + +if from_scratch: + slab.calc = GPAW( + txt="output.txt" +) + +slab.get_total_energy() diff --git a/tests/gpaw/files/TEST_relax.py b/tests/gpaw/files/TEST_relax.py new file mode 100644 index 0000000..f175e3e --- /dev/null +++ b/tests/gpaw/files/TEST_relax.py @@ -0,0 +1,70 @@ +# fmt: off +import os +import numpy as np + +from gpaw import GPAW +from gpaw import restart +from ase.io import read +from ase.io import write +from ase.optimize import BFGS +from ase.parallel import barrier +from ase.eos import EquationOfState +from ase.vibrations import Vibrations + +from_scratch = False +slab = None + +if os.path.isfile("output.gpw"): + try: + slab, _ = restart("output.gpw", txt="output.txt") + except: + from_scratch = True + if os.path.isfile("output.traj") and os.path.getsize("output.traj") > 0: + slab = read("output.traj") + else: + slab = read("input.traj") +else: + slab = read("input.traj") + from_scratch = True + +if from_scratch: + slab.calc = GPAW( + h=0.16, + kpts={'size': [4, 4, 1]}, + occupations={'name': 'fermi-dirac', 'width': 0.05}, + poissonsolver={'dipolelayer': 'xy'}, + xc='BEEF-vdW', + txt="output.txt" +) + +def relax(atoms, fmax=0.05, step=0.04): + atoms.calc.attach(atoms.calc.write, 5, "output.gpw") + + def _check_file_exists(filename): + #Check if file exists and is not empty + if os.path.isfile(filename): + return os.path.getsize(filename) > 0 + else: + return False + + # check if it is a restart + barrier() + if _check_file_exists("output.traj"): + latest = read("output.traj", index=":") + # check if already restarted previously and extend history if needed + if not (_check_file_exists("history.traj")): + barrier() + write("history.traj", latest) + else: + hist = read("history.traj", index=":") + hist.extend(latest) + write("history.traj", hist) + + dyn = BFGS(atoms=atoms, trajectory="output.traj", logfile="qn.log", maxstep=step) + # if history exists, read in hessian + if _check_file_exists("history.traj"): + dyn.replay_trajectory("history.traj") + # optimize + dyn.run(fmax=fmax) + +relax(slab) diff --git a/tests/gpaw/files/TEST_relax_custom.py b/tests/gpaw/files/TEST_relax_custom.py new file mode 100644 index 0000000..61bf54a --- /dev/null +++ b/tests/gpaw/files/TEST_relax_custom.py @@ -0,0 +1,70 @@ +# fmt: off +import os +import numpy as np + +from gpaw import GPAW +from gpaw import restart +from ase.io import read +from ase.io import write +from ase.optimize import BFGS +from ase.parallel import barrier +from ase.eos import EquationOfState +from ase.vibrations import Vibrations + +from_scratch = True +slab = None + +if os.path.isfile("C3N4.gpw"): + try: + slab, _ = restart("C3N4.gpw", txt="output.txt") + except: + from_scratch = True + if os.path.isfile("output.traj") and os.path.getsize("output.traj") > 0: + slab = read("output.traj") + else: + slab = read("C3N4.traj") +else: + slab = read("C3N4.traj") + from_scratch = True + +if from_scratch: + slab.calc = GPAW( + h=0.18, + kpts={'size': [6, 6, 1]}, + occupations={'name': 'fermi-dirac', 'width': 0.05}, + poissonsolver={'dipolelayer': 'xy'}, + xc='PBE', + txt="output.txt" +) + +def relax(atoms, fmax=0.05, step=0.04): + atoms.calc.attach(atoms.calc.write, 5, "C3N4.gpw") + + def _check_file_exists(filename): + #Check if file exists and is not empty + if os.path.isfile(filename): + return os.path.getsize(filename) > 0 + else: + return False + + # check if it is a restart + barrier() + if _check_file_exists("output.traj"): + latest = read("output.traj", index=":") + # check if already restarted previously and extend history if needed + if not (_check_file_exists("history.traj")): + barrier() + write("history.traj", latest) + else: + hist = read("history.traj", index=":") + hist.extend(latest) + write("history.traj", hist) + + dyn = BFGS(atoms=atoms, trajectory="output.traj", logfile="qn.log", maxstep=step) + # if history exists, read in hessian + if _check_file_exists("history.traj"): + dyn.replay_trajectory("history.traj") + # optimize + dyn.run(fmax=fmax) + +relax(slab) diff --git a/tests/gpaw/files/TEST_surface_adsorbate_vibrations.py b/tests/gpaw/files/TEST_surface_adsorbate_vibrations.py new file mode 100644 index 0000000..f0f7724 --- /dev/null +++ b/tests/gpaw/files/TEST_surface_adsorbate_vibrations.py @@ -0,0 +1,48 @@ +# fmt: off +import os +import numpy as np + +from gpaw import GPAW +from gpaw import restart +from ase.io import read +from ase.io import write +from ase.optimize import BFGS +from ase.parallel import barrier +from ase.eos import EquationOfState +from ase.vibrations import Vibrations + +from_scratch = False +slab = None + +if os.path.isfile("output.gpw"): + try: + slab, _ = restart("output.gpw", txt="output.txt") + except: + from_scratch = True + if os.path.isfile("output.traj") and os.path.getsize("output.traj") > 0: + slab = read("output.traj") + else: + slab = read("input.traj") +else: + slab = read("input.traj") + from_scratch = True + +if from_scratch: + slab.calc = GPAW( + h=0.16, + kpts={'size': [4, 4, 1]}, + occupations={'name': 'fermi-dirac', 'width': 0.05}, + poissonsolver={'dipolelayer': 'xy'}, + xc='BEEF-vdW', + txt="output.txt" +) + +def calculate_vibrations(atoms): + indices_to_perturb = np.where(atoms.get_tags() <= 0)[0].tolist() + barrier() + vib = Vibrations(atoms, indices = indices_to_perturb, name = "vib.log") + vib.clean(empty_files=True) + vib.run() + vib.summary(log="vib.summary") + +calculate_vibrations(slab) diff --git a/tests/gpaw/files/cu_bulk.traj b/tests/gpaw/files/cu_bulk.traj new file mode 100644 index 0000000..c6c344c Binary files /dev/null and b/tests/gpaw/files/cu_bulk.traj differ diff --git a/tests/gpaw/test_gpaw.py b/tests/gpaw/test_gpaw.py new file mode 100644 index 0000000..674c3fc --- /dev/null +++ b/tests/gpaw/test_gpaw.py @@ -0,0 +1,186 @@ +"""Unit tests for the `GPAWInputGenerator` class.""" + +import os +import pytest + +from ase import io as ase_io + +from dftinputgen.gpaw.gpaw import GPAWInputGenerator, GPAWInputGeneratorError + +test_data_dir = os.path.join(os.path.dirname(__file__), "files") +cu_bulk_struct = ase_io.read(os.path.join(test_data_dir, "cu_bulk.traj")) +with open(os.path.join(test_data_dir, "TEST_bulk_opt.py"), "r") as fr: + bulk_opt_in = fr.read() +with open(os.path.join(test_data_dir, "TEST_bulk_opt_hcp.py"), "r") as fr: + bulk_opt_hcp_in = fr.read() +with open(os.path.join(test_data_dir, "TEST_relax.py"), "r") as fr: + relax_in = fr.read() +with open(os.path.join(test_data_dir, "TEST_relax_custom.py"), "r") as fr: + relax_custom_in = fr.read() +with open(os.path.join(test_data_dir, "TEST_defaults.py"), "r") as fr: + defaults_in = fr.read() +with open( + os.path.join(test_data_dir, "TEST_surface_adsorbate_vibrations.py"), "r" +) as fr: + vib_in = fr.read() + + +def test_dft_package_name(): + gig = GPAWInputGenerator(crystal_structure=cu_bulk_struct) + assert gig.dft_package == "GPAW" + + +def test_gpaw_input_file(): + # default "[calculation_presets]_in.py" + gig = GPAWInputGenerator( + crystal_structure=cu_bulk_struct, calculation_presets="bulk_opt" + ) + assert gig.gpaw_input_file == "bulk_opt_in.py" + # otherwise use any user-input file name + gig = GPAWInputGenerator( + crystal_structure=cu_bulk_struct, calculation_presets="bulk_opt" + ) + gig.gpaw_input_file = "test.py" + assert gig.gpaw_input_file == "test.py" + + +def test_bulk_opt_calculation_presets_settings(): + # Tests bulk_opt settings (fcc/bcc) + gig = GPAWInputGenerator( + crystal_structure=cu_bulk_struct, calculation_presets="bulk_opt" + ) + cs = gig.calculation_settings + assert cs["calculation"] == "bulk_opt" + assert cs["kpts"]["size"] == [12, 12, 12] + assert cs["xc"] == "BEEF-vdW" + assert cs["h"] == 0.16 + assert cs["occupations"]["name"] == "fermi-dirac" + assert cs["occupations"]["width"] == 0.05 + + +def test_bulk_opt_hcp_calculation_presets_settings(): + # Tests bulk_opt_hcp settings (hcp) + gig = GPAWInputGenerator( + crystal_structure=cu_bulk_struct, calculation_presets="bulk_opt_hcp" + ) + cs = gig.calculation_settings + assert cs["calculation"] == "bulk_opt_hcp" + assert cs["kpts"]["size"] == [12, 12, 6] + assert cs["xc"] == "BEEF-vdW" + assert cs["h"] == 0.16 + assert cs["occupations"]["name"] == "fermi-dirac" + assert cs["occupations"]["width"] == 0.05 + + +def test_molecule_calculation_presets_settings(): + # Tests molecule settings + gig = GPAWInputGenerator( + crystal_structure=cu_bulk_struct, calculation_presets="molecule" + ) + cs = gig.calculation_settings + assert cs["xc"] == "BEEF-vdW" + assert cs["h"] == 0.16 + assert cs["occupations"]["name"] == "fermi-dirac" + assert cs["occupations"]["width"] == 0.05 + assert cs["calculation"] == "relax" + + +def test_surface_relax_calculation_presets_settings(): + # Tests relax presets + gig = GPAWInputGenerator( + crystal_structure=cu_bulk_struct, calculation_presets="surface_relax" + ) + cs = gig.calculation_settings + assert cs["xc"] == "BEEF-vdW" + assert cs["h"] == 0.16 + assert cs["occupations"]["name"] == "fermi-dirac" + assert cs["occupations"]["width"] == 0.05 + assert cs["calculation"] == "relax" + assert cs["poissonsolver"]["dipolelayer"] == "xy" + + +def test_calculator_object_as_str(): + # Tests generation of calculator object as string + gig = GPAWInputGenerator(crystal_structure=cu_bulk_struct) + assert gig.calculator_object_as_str == 'GPAW(\n txt="output.txt"\n)' + gig.calculation_presets = "bulk_opt" + calc_obj = "\n".join(["GPAW(", "\n".join(bulk_opt_in.splitlines()[31:37])]) + assert gig.calculator_object_as_str == calc_obj + + +def test_gpaw_input_as_str(): + # Test defaults + gig = GPAWInputGenerator(crystal_structure=cu_bulk_struct) + assert gig.gpaw_input_as_str == "\n".join(defaults_in.splitlines()[1:]) + # Test generation of full bulk opt script + gig = GPAWInputGenerator( + crystal_structure=cu_bulk_struct, calculation_presets="bulk_opt" + ) + assert gig.gpaw_input_as_str == "\n".join(bulk_opt_in.splitlines()[1:]) + gig = GPAWInputGenerator( + crystal_structure=cu_bulk_struct, calculation_presets="bulk_opt_hcp" + ) + assert gig.gpaw_input_as_str == "\n".join(bulk_opt_hcp_in.splitlines()[1:]) + gig = GPAWInputGenerator( + crystal_structure=cu_bulk_struct, calculation_presets="surface_relax" + ) + assert gig.gpaw_input_as_str == "\n".join(relax_in.splitlines()[1:]) + gig = GPAWInputGenerator( + crystal_structure=cu_bulk_struct, + calculation_presets="surface_adsorbate_vibrations", + ) + assert gig.gpaw_input_as_str == "\n".join(vib_in.splitlines()[1:]) + + +def test_write_gpaw_input(): + gig = GPAWInputGenerator(crystal_structure=cu_bulk_struct) + gig.calculation_presets = "surface_relax" + # no `write_location` input: error + with pytest.raises(GPAWInputGeneratorError, match="Location to write"): + gig.write_gpaw_input() + # no input filename: error + with pytest.raises(GPAWInputGeneratorError, match="file to write"): + gig.write_gpaw_input(write_location="/path/to/write_location") + + import tempfile + + _tmp_file = tempfile.NamedTemporaryFile(mode="w", delete=True) + filename = _tmp_file.name + write_location = os.path.dirname(filename) + gig.gpaw_restart_file = "C3N4.gpw" + gig.struct_filename = "C3N4.traj" + gig.from_scratch = True + gig.custom_sett_dict = { + "kpts": {"size": [6, 6, 1]}, + "xc": "PBE", + "h": 0.18, + } + gig.write_gpaw_input(write_location=write_location, filename=filename) + with open(filename, "r") as fr: + written_file = fr.read() + assert written_file == "\n".join(relax_custom_in.splitlines()[1:]) + + +def test_write_input_files(): + import tempfile + + _tmp_file = tempfile.NamedTemporaryFile(mode="w", delete=True) + filename = _tmp_file.name + write_location = os.path.dirname(filename) + gig = GPAWInputGenerator(crystal_structure=cu_bulk_struct) + gig.calculation_presets = "surface_relax" + gig.struct_filename = "C3N4.traj" + gig.gpaw_restart_file = "C3N4.gpw" + gig.from_scratch = True + gig.custom_sett_dict = { + "kpts": {"size": [6, 6, 1]}, + "xc": "PBE", + "h": 0.18, + } + gig.write_location = write_location + gig.gpaw_input_file = filename + gig.write_input_files() + with open(filename, "r") as fr: + written_file = fr.read() + check = "\n".join(relax_custom_in.splitlines()[1:]) + assert written_file == check diff --git a/tests/test_cli.py b/tests/test_cli.py index 727cfeb..3822461 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -27,7 +27,7 @@ def test_driver(capsys): # subparser: invalid dft package choice error with pytest.raises(SystemExit): - driver(["gpaw"]) + driver(["vasp"]) assert "invalid choice" in capsys.readouterr().err # pw.x package: missing required arguments error @@ -55,3 +55,25 @@ def test_driver(capsys): filename, ] ) + + # gpaw package: missing required arguments error + # only tests that the arguments have been passed on to the pw.x subparser + with pytest.raises(SystemExit): + driver(["gpaw"]) + assert "required" in capsys.readouterr().err + + # gpaw package: minimal working example + + driver( + [ + "gpaw", + "-i", + test_struct, + "-pre", + "surface_relax", + "-loc", + write_location, + "-o", + filename, + ] + ) diff --git a/tox.ini b/tox.ini index c473d4a..d39df15 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,8 @@ doctests = True # D107: __init__ is self explanatory # D301: backslash is used in making docstrings for sphinx to parse # D401: Imperative mood requirement basically gets in the way -ignore = D100,D104,D105,D107,D301,D401 +# W503: Line break before binary operator is now PEP-8 compliant +ignore = D100,D104,D105,D107,D301,D401,W503 exclude = tests/*