diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c0a71cee..c48f08bb 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -31,12 +31,7 @@ jobs: run: | python -m pip install --upgrade pip pip install setuptools setuptools_scm wheel - pip install numpy - pip install pydocstyle - pip install black - pip install flake8 - pip install isort - pip install -e . + pip install pydocstyle black flake8 isort pip install -e .[tests] - name: sort imports diff --git a/.github/workflows/pip_install_test.yml b/.github/workflows/pip_install_test.yml index a52aea04..5ae629c3 100644 --- a/.github/workflows/pip_install_test.yml +++ b/.github/workflows/pip_install_test.yml @@ -6,11 +6,14 @@ on: branches: [main] types: - completed # only test when new release has been deployed to PyPI + workflow_dispatch: jobs: build-linux: runs-on: ubuntu-latest - if: ${{ github.event.workflow_run.conclusion == 'success' }} + if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') }} + # only run when tests have passed (or manually triggered) + strategy: max-parallel: 5 @@ -30,17 +33,23 @@ jobs: run: | sleep 10m # wait 10 minutes for PyPI to update with the new release python -m pip install --upgrade pip - pip install shakenbreak # install only from PyPI - pip install shakenbreak[tests] + pip install shakenbreak[tests] # install only from PyPI - name: Test run: | - for test in tests/test_*py - do if [[ "$test" != *"test_local"* ]] - then pytest $test - fi - done # Ignore local tests file, which tests INCAR and POTCAR file writing but not possible on GitHub Actions - pytest --mpl tests/test_shakenbreak.py # test output plots - pytest --mpl tests/test_plotting.py # test output plots - # pytest --mpl-generate-path=tests/remote_baseline tests/test_plotting.py # generate output plots - # pytest --mpl-generate-path=tests/remote_baseline tests/test_shakenbreak.py # generate output plots + pytest --mpl -vv tests # test everything + + - name: Generate GH Actions test plots + if: always() # always generate the plots, even if the tests fail + run: | + # Generate the test plots in case there were any failures: + pytest --mpl-generate-path=tests/remote_baseline tests/test_plotting.py + pytest --mpl-generate-path=tests/remote_baseline tests/test_shakenbreak.py + + # Upload test plots + - name: Archive test plots + if: always() # always upload the plots, even if the tests fail + uses: actions/upload-artifact@v3 + with: + name: output-plots + path: tests/remote_baseline diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f95e00ae..e1dbb4ab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,6 +6,7 @@ on: - main tags: - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 + workflow_dispatch: jobs: release: @@ -21,20 +22,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install setuptools setuptools_scm wheel - pip install numpy - pip install -e . pip install -e .[tests] - name: Test run: | - for test in tests/test_*py - do if [[ "$test" != *"test_local"* ]] - then pytest $test - fi - done # Ignore local tests file, which tests INCAR and POTCAR file writing but not possible on GitHub Actions - pytest --mpl tests/test_shakenbreak.py # test output plots - pytest --mpl tests/test_plotting.py # test output plots + pytest --mpl tests # test everything - name: Build packages run: | diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/test.yml similarity index 50% rename from .github/workflows/build_and_test.yml rename to .github/workflows/test.yml index 55f26dfb..2487c66f 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/test.yml @@ -32,34 +32,32 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install setuptools setuptools_scm wheel - pip install numpy - pip install -e . pip install -e .[tests] + - name: Check package versions run: | pip show -V pymatgen-analysis-defects pip show -V pymatgen pip show -V pytest + - name: Test run: | - for test in tests/test_*py - do if [[ "$test" != *"test_local"* ]] - then pytest $test - fi - done # Ignore local tests file, which tests INCAR and POTCAR file writing but not possible on GitHub Actions - pytest --mpl tests/test_shakenbreak.py # test output plots - pytest --mpl tests/test_plotting.py # test output plots - # To generate the test plots on GA: - # pytest --mpl-generate-path=tests/remote_baseline tests/test_plotting.py # generate output plots - # pytest --mpl-generate-path=tests/remote_baseline tests/test_shakenbreak.py # generate output plots + pytest --mpl -vv tests # test everything + + - name: Generate GH Actions test plots + if: always() # always generate the plots, even if the tests fail + run: | + # Generate the test plots in case there were any failures: + pytest --mpl-generate-path=tests/remote_baseline tests/test_plotting.py + pytest --mpl-generate-path=tests/remote_baseline tests/test_shakenbreak.py - # Download test plots - # - name: Archive test plots - # uses: actions/upload-artifact@v3 - # with: - # name: output-plots - # path: tests/remote_baseline + # Upload test plots + - name: Archive test plots + if: always() # always upload the plots, even if the tests fail + uses: actions/upload-artifact@v3 + with: + name: output-plots + path: tests/remote_baseline # - name: Download a single artifact # uses: actions/download-artifact@v3 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1887f897..87e06a61 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,13 @@ Change Log ========== +v3.2.1 +---------- +- Update CLI config handling. +- Remove `shakenbreak.vasp` module and use `doped` VASP file writing functions directly. +- Add INCAR/KPOINTS/POTCAR file writing tests. `test_local.py` now deleted as these tests are now + automatically run in `test_input.py`/`test_cli.py` if `POTCAR`s available. + v3.2.0 ---------- - Following the major release of `doped` `v2.0`, now compatible with the new `pymatgen` diff --git a/README.md b/README.md index 03bb7ee5..111636e5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build status](https://github.com/SMTG-UCL/ShakeNBreak/actions/workflows/build_and_test.yml/badge.svg)](https://github.com/SMTG-UCL/ShakeNBreak/actions) +[![Build status](https://github.com/SMTG-UCL/ShakeNBreak/actions/workflows/test.yml/badge.svg)](https://github.com/SMTG-UCL/ShakeNBreak/actions) [![Documentation Status](https://readthedocs.org/projects/shakenbreak/badge/?version=latest&style=flat)](https://shakenbreak.readthedocs.io/en/latest/) [![JOSS](https://joss.theoj.org/papers/10.21105/joss.04817/status.svg)](https://doi.org/10.21105/joss.04817) [![PyPI](https://img.shields.io/pypi/v/shakenbreak)](https://pypi.org/project/shakenbreak) diff --git a/SnB_input_files/incar.yaml b/SnB_input_files/incar.yaml index 5c4be3af..5f5e2820 100644 --- a/SnB_input_files/incar.yaml +++ b/SnB_input_files/incar.yaml @@ -5,8 +5,8 @@ ALGO: "Normal # Change to All if ZHEGV, FEXCP/F or ZBRENT errors encountered (d EDIFFG: -0.01 ENCUT: 300 HFSCREEN: 0.208 # correct HSE screening parameter; see https://aip.scitation.org/doi/10.1063/1.2404663 -# Note this 👆 differs from the Materials Project MPHSERelaxSet default of 0.2! (Will cause systematic -# energy shifts in HSE supercell calculations.) +# Note this HFSCREEN value differs from the Materials Project MPHSERelaxSet default of 0.2! This should +# be consistent between all your defect/bulk/competing-phase calculations. IBRION: '2 # Typically more stable / reliable than "1" (RMM-DIIS), but change if ionic convergence is poor (done automatically by snb-run)' ISIF: 2 ISMEAR: 0 diff --git a/docs/ShakeNBreak_Example_Workflow.ipynb b/docs/ShakeNBreak_Example_Workflow.ipynb index 769a2ed5..589c22e1 100644 --- a/docs/ShakeNBreak_Example_Workflow.ipynb +++ b/docs/ShakeNBreak_Example_Workflow.ipynb @@ -2523,7 +2523,7 @@ "source": [ "```{note} \n", "\n", - "Using the `incar_settings` optional argument for `Distortions.write_vasp_files()` above, we can also specify some custom `INCAR` tags to match our converged `ENCUT` for this system and optimal `NCORE` for the HPC we will run the calculations on. Note that any `INCAR` flags that aren't numbers (e.g. `{\"IBRION\": 1}`) or True/False (e.g. `{\"LREAL\": False}`) need to be input as strings with quotation marks (e.g. `{\"ALGO\": \"All\"}`).\n", + "Using the `user_incar_settings` optional argument for `Distortions.write_vasp_files()` above, we can also specify some custom `INCAR` tags to match our converged `ENCUT` for this system and optimal `NCORE` for the HPC we will run the calculations on. Note that any `INCAR` flags that aren't numbers (e.g. `{\"IBRION\": 1}`) or True/False (e.g. `{\"LREAL\": False}`) need to be input as strings with quotation marks (e.g. `{\"ALGO\": \"All\"}`).\n", "\n", "For the recommended default coarse structure-searching `INCAR` settings, either have a look at the `incar.yaml` file in the `SnB_input_files` folder or at the generated files: \n", "```" @@ -2589,7 +2589,7 @@ "Note that the `NELECT` `INCAR` tag (number of electrons) is automatically determined based on the choice\n", " of `POTCAR`s. The default in `ShakeNBreak` (and `doped`) is to use the\n", "[`MPRelaxSet` `POTCAR` choices](https://github.com/materialsproject/pymatgen/blob/master/pymatgen/io/vasp/MPRelaxSet.yaml), but if you're using\n", - "different ones, make sure to set `potcar_settings` in `write_vasp_files()`, so that `NELECT` is then set\n", + "different ones, make sure to set `user_potcar_settings` in `write_vasp_files()`, so that `NELECT` is then set\n", "accordingly.\n", "This requires the `pymatgen` config file `$HOME/.pmgrc.yaml` to be properly set up as detailed on the [Installation](https://shakenbreak.readthedocs.io/en/latest/Installation.html) docs page.\n", "```" diff --git a/docs/conf.py b/docs/conf.py index 85d06163..e6dc239d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,7 +25,7 @@ author = 'Irea Mosquera-Lois, Seán R. Kavanagh' # The full version, including alpha/beta/rc tags -release = '3.2.0' +release = '3.2.1' # -- General configuration --------------------------------------------------- diff --git a/docs/index.rst b/docs/index.rst index 4aa89311..c038136d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,7 +3,7 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -.. image:: https://github.com/SMTG-UCL/ShakeNBreak/actions/workflows/build_and_test.yml/badge.svg +.. image:: https://github.com/SMTG-UCL/ShakeNBreak/actions/workflows/test.yml/badge.svg :target: https://github.com/SMTG-UCL/ShakeNBreak/actions .. image:: https://readthedocs.org/projects/shakenbreak/badge/?version=latest&style=flat diff --git a/docs/shakenbreak.rst b/docs/shakenbreak.rst index b3714908..231028cb 100644 --- a/docs/shakenbreak.rst +++ b/docs/shakenbreak.rst @@ -21,5 +21,4 @@ Submodules shakenbreak.analysis shakenbreak.plotting shakenbreak.energy_lowering_distortions - shakenbreak.vasp shakenbreak.cli \ No newline at end of file diff --git a/docs/shakenbreak.vasp.rst b/docs/shakenbreak.vasp.rst deleted file mode 100644 index f04df886..00000000 --- a/docs/shakenbreak.vasp.rst +++ /dev/null @@ -1,6 +0,0 @@ -shakenbreak.vasp module -============================ -.. automodule:: shakenbreak.vasp - :members: - :undoc-members: - :show-inheritance: diff --git a/setup.py b/setup.py index 7e2f4d22..e6434c21 100644 --- a/setup.py +++ b/setup.py @@ -130,7 +130,7 @@ def package_files(directory): setup( name="shakenbreak", - version="3.2.0", + version="3.2.1", description="Package to generate and analyse distorted defect structures, in order to " "identify ground-state and metastable defect configurations.", long_description=long_description, diff --git a/shakenbreak/cli.py b/shakenbreak/cli.py index 2a66bb72..cd9ef6a8 100755 --- a/shakenbreak/cli.py +++ b/shakenbreak/cli.py @@ -29,10 +29,8 @@ def _parse_defect_dirs(path) -> list: for dir in os.listdir(path) if os.path.isdir(f"{path}/{dir}") and any( - [ - fnmatch.filter(os.listdir(f"{path}/{dir}"), f"{dist}*") - for dist in ["Rattled", "Unperturbed", "Bond_Distortion"] - ] + fnmatch.filter(os.listdir(f"{path}/{dir}"), f"{dist}*") + for dist in ["Rattled", "Unperturbed", "Bond_Distortion"] ) # only parse defect directories that contain distortion folders ] @@ -196,8 +194,12 @@ def generate( for a given defect. """ user_settings = loadfn(config) if config is not None else {} + # Parse POTCARs/pseudopotentials from config file, if specified + user_potcar_functional = user_settings.pop("POTCAR_FUNCTIONAL", "PBE") + user_potcar_settings = user_settings.pop("POTCAR", None) + pseudopotentials = user_settings.pop("pseudopotentials", None) + func_args = list(locals().keys()) - pseudopotentials = None if user_settings: valid_args = [ "defect", @@ -233,13 +235,7 @@ def generate( for key in func_args: if key in user_settings: user_settings.pop(key, None) - # Parse pseudopotentials from config file, if specified - if "POTCAR" in user_settings.keys(): - pseudopotentials = {"POTCAR": deepcopy(user_settings["POTCAR"])} - user_settings.pop("POTCAR", None) - if "pseudopotentials" in user_settings.keys(): - pseudopotentials = deepcopy(user_settings["pseudopotentials"]) - user_settings.pop("pseudopotentials", None) + for key in list(user_settings.keys()): # remove non-sense keys from user_settings if key not in valid_args: @@ -320,19 +316,19 @@ def generate( if code.lower() == "vasp": if input_file: incar = Incar.from_file(input_file) - incar_settings = incar.as_dict() - [incar_settings.pop(key, None) for key in ["@class", "@module"]] - if not incar_settings: + user_incar_settings = incar.as_dict() + [user_incar_settings.pop(key, None) for key in ["@class", "@module"]] + if not user_incar_settings: warnings.warn( f"Input file {input_file} specified but no valid INCAR tags found. " f"Should be in the format of VASP INCAR file." ) else: - incar_settings = None + user_incar_settings = None distorted_defects_dict, distortion_metadata = Dist.write_vasp_files( verbose=verbose, - potcar_settings=pseudopotentials, - incar_settings=incar_settings, + user_potcar_settings=user_potcar_settings, + user_incar_settings=user_incar_settings, ) elif code.lower() == "cp2k": if input_file: @@ -487,9 +483,13 @@ def generate_all( else: defect_settings, user_settings = {}, {} + # Parse POTCARs/pseudopotentials from config file, if specified + user_potcar_functional = user_settings.pop("POTCAR_FUNCTIONAL", "PBE") + user_potcar_settings = user_settings.pop("POTCAR", None) + pseudopotentials = user_settings.pop("pseudopotentials", None) + func_args = list(locals().keys()) # Specified options take precedence over the ones in the config file - pseudopotentials = None if user_settings: valid_args = [ "defects", @@ -521,13 +521,7 @@ def generate_all( for key in func_args: if key in user_settings: user_settings.pop(key, None) - # Parse pseudopotentials from config file, if specified - if "POTCAR" in user_settings.keys(): - pseudopotentials = {"POTCAR": deepcopy(user_settings["POTCAR"])} - user_settings.pop("POTCAR", None) - if "pseudopotentials" in user_settings.keys(): - pseudopotentials = deepcopy(user_settings["pseudopotentials"]) - user_settings.pop("pseudopotentials", None) + for key in list(user_settings.keys()): # remove non-sense keys from user_settings if key not in valid_args: @@ -684,19 +678,19 @@ def parse_defect_position(defect_name, defect_settings): if code.lower() == "vasp": if input_file: incar = Incar.from_file(input_file) - incar_settings = incar.as_dict() - [incar_settings.pop(key, None) for key in ["@class", "@module"]] - if incar_settings == {}: + user_incar_settings = incar.as_dict() + [user_incar_settings.pop(key, None) for key in ["@class", "@module"]] + if user_incar_settings == {}: warnings.warn( f"Input file {input_file} specified but no valid INCAR tags found. " f"Should be in the format of VASP INCAR file." ) else: - incar_settings = None + user_incar_settings = None distorted_defects_dict, distortion_metadata = Dist.write_vasp_files( verbose=verbose, - potcar_settings=pseudopotentials, - incar_settings=incar_settings, + user_potcar_settings=user_potcar_settings, + user_incar_settings=user_incar_settings, ) elif code.lower() == "cp2k": if input_file: diff --git a/shakenbreak/energy_lowering_distortions.py b/shakenbreak/energy_lowering_distortions.py index 69335e36..26179365 100644 --- a/shakenbreak/energy_lowering_distortions.py +++ b/shakenbreak/energy_lowering_distortions.py @@ -958,31 +958,39 @@ def _copy_vasp_files( """ distorted_structure.to(fmt="poscar", filename=f"{distorted_dir}/POSCAR") - if os.path.exists(f"{output_path}/{defect_species}/Unperturbed/INCAR"): - for i in ["INCAR", "KPOINTS", "POTCAR"]: + for i in ["INCAR", "KPOINTS", "POTCAR"]: + if os.path.exists(f"{output_path}/{defect_species}/Unperturbed/{i}"): shutil.copyfile( f"{output_path}/{defect_species}/Unperturbed/{i}", f"{distorted_dir}/{i}", ) # copy input files from Unperturbed directory - else: - subfolders_with_input_files = [] + + file_dict = { + filename: os.path.exists(f"{distorted_dir}/{filename}") + for filename in ["INCAR", "KPOINTS", "POTCAR"] + } + if not all(file_dict.values()): + # try other distortion directories for subfolder in os.listdir(f"{output_path}/{defect_species}"): - if os.path.exists(f"{output_path}/{defect_species}/{subfolder}/INCAR"): - subfolders_with_input_files.append(subfolder) - if len(subfolders_with_input_files) > 0: - for i in ["INCAR", "KPOINTS", "POTCAR"]: - shutil.copyfile( - f"{output_path}/{defect_species}/" - f"{subfolders_with_input_files[0]}/" - f"{i}", - f"{distorted_dir}/{i}", - ) - else: - print( - f"No subfolders with VASP input files found in " - f"{output_path}/{defect_species}, so just writing distorted " - f"POSCAR file to {distorted_dir} directory." - ) + for filename in ["INCAR", "KPOINTS", "POTCAR"]: + if ( + os.path.exists( + f"{output_path}/{defect_species}/{subfolder}/{filename}" + ) + and not file_dict[filename] + ): + shutil.copyfile( + f"{output_path}/{defect_species}/{subfolder}/{filename}", + f"{distorted_dir}/{filename}", + ) + + if not all(file_dict.values()): + warnings.warn( + f"Subfolders with VASP input files ({[k for k,v in file_dict.items() if not v]} not found in " + f"{output_path}/{defect_species}, so just writing distorted POSCAR file" + f"{f' and {[k for k,v in file_dict.items() if v]}' if any(file_dict.values()) else ''} to " + f"{distorted_dir} directory." + ) def _copy_espresso_files( diff --git a/shakenbreak/input.py b/shakenbreak/input.py index 8dbbb933..88d9f7be 100755 --- a/shakenbreak/input.py +++ b/shakenbreak/input.py @@ -19,11 +19,13 @@ from ase.calculators.aims import Aims from ase.calculators.castep import Castep from ase.calculators.espresso import Espresso +from doped import _ignore_pmg_warnings from doped.generation import ( DefectsGenerator, get_defect_name_from_entry, name_defect_entries, ) +from doped.vasp import DefectDictSet from monty.json import MontyDecoder from monty.serialization import dumpfn, loadfn from pymatgen.analysis.defects.core import Defect @@ -34,22 +36,24 @@ from pymatgen.entries.computed_entries import ComputedStructureEntry from pymatgen.io.ase import AseAtomsAdaptor from pymatgen.io.cp2k.inputs import Cp2kInput -from pymatgen.io.vasp.inputs import UnknownPotcarWarning -from pymatgen.io.vasp.sets import BadInputSetWarning +from pymatgen.io.vasp.inputs import Kpoints, UnknownPotcarWarning +from pymatgen.io.vasp.sets import BadInputSetWarning, UserPotcarFunctional from pymatgen.util.coord import pbc_diff from scipy.cluster.hierarchy import fcluster, linkage from scipy.spatial import Voronoi from scipy.spatial.distance import squareform -from shakenbreak import analysis, distortions, io, vasp +from shakenbreak import analysis, distortions, io MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) +default_potcar_dict = loadfn(f"{MODULE_DIR}/../SnB_input_files/default_POTCARs.yaml") +# Load default INCAR settings for the ShakeNBreak geometry relaxations +default_incar_settings = loadfn( + os.path.join(MODULE_DIR, "../SnB_input_files/incar.yaml") +) -warnings.filterwarnings( - "ignore", category=UnknownPotcarWarning -) # Ignore pymatgen POTCAR warnings -warnings.filterwarnings("ignore", message=".*Ignoring unknown variable type.*") +_ignore_pmg_warnings() # Ignore pymatgen POTCAR warnings def _warning_on_one_line( @@ -178,9 +182,11 @@ def _write_distortion_metadata( def _create_vasp_input( defect_name: str, distorted_defect_dict: dict, - incar_settings: dict, - potcar_settings: Optional[dict] = None, + user_incar_settings: Optional[dict] = None, + user_potcar_functional: Optional[UserPotcarFunctional] = "PBE", + user_potcar_settings: Optional[dict] = None, output_path: str = ".", + **kwargs, ) -> None: """ Creates folders for storing VASP ShakeNBreak files. @@ -190,26 +196,34 @@ def _create_vasp_input( Folder name distorted_defect_dict (:obj:`dict`): Dictionary with the distorted structures of charged defect - incar_settings (:obj:`dict`): + user_incar_settings (:obj:`dict`): Dictionary of user VASP INCAR settings, to overwrite/update the `doped` defaults. - potcar_settings (:obj:`dict`): - Dictionary of user VASP POTCAR settings, to overwrite/update the - `doped` defaults. Using `pymatgen` syntax (e.g. {'POTCAR': - {'Fe': 'Fe_pv', 'O': 'O'}}). + user_potcar_functional (str): + POTCAR functional to use. Default is "PBE" and if this fails, + tries "PBE_52", then "PBE_54". + user_potcar_settings (:obj:`dict`): + Dictionary of user VASP POTCAR settings, to overwrite/update + the `doped` defaults (e.g. {'Fe': 'Fe_pv', 'O': 'O'}}). Highly + recommended to look at output `POTCAR`s, or `shakenbreak` + `SnB_input_files/default_POTCARs.yaml`, to see what the default + `POTCAR` settings are. (Default: None) output_path (:obj:`str`): Path to directory in which to write distorted defect structures and calculation inputs. (Default is current directory = "./") + **kwargs: + Keyword arguments to pass to `DefectDictSet.write_input()` (e.g. + `potcar_spec`). Returns: None """ # create folder for defect - defect_name_wout_charge, charge = defect_name.rsplit( + defect_name_wout_charge, charge_state = defect_name.rsplit( "_", 1 ) # `defect_name` includes charge - charge = int(charge) + charge_state = int(charge_state) test_letters = [ "h", "g", @@ -228,9 +242,10 @@ def _create_vasp_input( for letter in test_letters for dir in os.listdir(output_path) if dir - == f"{defect_name_wout_charge}{letter}_{'+' if charge > 0 else ''}{charge}" + == f"{defect_name_wout_charge}{letter}_{'+' if charge_state > 0 else ''}{charge_state}" and os.path.isdir( - f"{output_path}/{defect_name_wout_charge}{letter}_{'+' if charge > 0 else ''}{charge}" + f"{output_path}/{defect_name_wout_charge}{letter}_{'+' if charge_state > 0 else ''}" + f"{charge_state}" ) ] except Exception: @@ -270,15 +285,20 @@ def _create_vasp_input( for letter in test_letters for dir in matching_dirs if dir - == f"{defect_name_wout_charge}{letter}_{'+' if charge > 0 else ''}{charge}" + == f"{defect_name_wout_charge}{letter}_{'+' if charge_state > 0 else ''}{charge_state}" ][0] - prev_dir_name = f"{defect_name_wout_charge}{last_letter}_{'+' if charge > 0 else ''}{charge}" + prev_dir_name = ( + f"{defect_name_wout_charge}{last_letter}_{'+' if charge_state > 0 else ''}" + f"{charge_state}" + ) if last_letter == "": # rename prev defect folder new_prev_dir_name = ( - f"{defect_name_wout_charge}a_{'+' if charge > 0 else ''}{charge}" + f"{defect_name_wout_charge}a_{'+' if charge_state > 0 else ''}" + f"{charge_state}" ) new_current_dir_name = ( - f"{defect_name_wout_charge}b_{'+' if charge > 0 else ''}{charge}" + f"{defect_name_wout_charge}b_{'+' if charge_state > 0 else ''}" + f"{charge_state}" ) warnings.warn( f"A previously-generated defect folder {prev_dir_name} exists in " @@ -298,7 +318,7 @@ def _create_vasp_input( next_letter = test_letters[test_letters.index(last_letter) - 1] new_current_dir_name = ( f"{defect_name_wout_charge}{next_letter}" - f"_{'+' if charge > 0 else ''}{charge}" + f"_{'+' if charge_state > 0 else ''}{charge_state}" ) warnings.warn( f"Previously-generated defect folders ({prev_dir_name}...) exist in " @@ -311,22 +331,59 @@ def _create_vasp_input( defect_name = new_current_dir_name _create_folder(os.path.join(output_path, defect_name)) + + potcar_settings = copy.deepcopy(default_potcar_dict)["POTCAR"] + potcar_settings.update(user_potcar_settings or {}) + incar_settings = copy.deepcopy(default_incar_settings) + incar_settings.update(user_incar_settings or {}) + single_defect_dict = list(distorted_defect_dict.values())[0] + + num_elements = len(single_defect_dict["Defect Structure"].composition.elements) + incar_settings.update({"ROPT": ("1e-3 " * num_elements).rstrip()}) + + dds = DefectDictSet( # create one DefectDictSet first, then just edit structure & comment for each + single_defect_dict["Defect Structure"], + charge_state=single_defect_dict["Charge State"], + user_incar_settings=incar_settings, + user_kpoints_settings=Kpoints().from_dict( + { + "comment": "Γ-only KPOINTS from ShakeNBreak", + "generation_style": "Gamma", + } + ), + user_potcar_functional=user_potcar_functional, + user_potcar_settings=potcar_settings, + poscar_comment=None, + ) + for ( distortion, single_defect_dict, ) in ( distorted_defect_dict.items() ): # for each distortion, create sub-subfolder folder - potcar_settings_copy = copy.deepcopy( - potcar_settings - ) # files empties `potcar_settings dict` (via pop()), so make a - # deepcopy each time - vasp.write_vasp_gam_files( - single_defect_dict=single_defect_dict, - input_dir=f"{output_path}/{defect_name}/{distortion}", - incar_settings=incar_settings, - potcar_settings=potcar_settings_copy, - ) + dds._structure = single_defect_dict["Defect Structure"] + dds.poscar_comment = single_defect_dict.get("POSCAR Comment", None) + + try: + dds._check_user_potcars(unperturbed_poscar=False) + dds.write_input(f"{output_path}/{defect_name}/{distortion}", **kwargs) + + except ValueError: + # POTCARs not set up, warn and write other files + warnings.warn( + "POTCAR directory not set up with pymatgen (see the doped docs Installation page: " + "https://doped.readthedocs.io/en/latest/Installation.html for instructions on setting " + "this up). This is required to generate `POTCAR` files and set `NELECT` (i.e. charge " + "state) and `NUPDOWN` in the `INCAR` files!\n" + "No `POTCAR` files will be written, and `NELECT` and `NUPDOWN` will not be set in " + "`INCAR`s. Beware!" + ) + + os.makedirs(f"{output_path}/{defect_name}/{distortion}", exist_ok=True) + dds.incar.write_file(f"{output_path}/{defect_name}/{distortion}/INCAR") + dds.poscar.write_file(f"{output_path}/{defect_name}/{distortion}/POSCAR") + dds.kpoints.write_file(f"{output_path}/{defect_name}/{distortion}/KPOINTS") def _get_bulk_comp(defect_object) -> Composition: @@ -1640,9 +1697,7 @@ def apply_snb_distortions( "defect_site_index" ] = bond_distorted_defect["defect_site_index"] - elif ( - num_nearest_neighbours == 0 - ): # when no extra/missing electrons, just rattle the structure. + else: # when no extra/missing electrons, just rattle the structure # Likely to be a shallow defect. if defect_type == "vacancy": defect_site_index = None @@ -2207,7 +2262,7 @@ def _generate_structure_comment( approx_coords = ( f"~[{frac_coords[0]:.1f},{frac_coords[1]:.1f},{frac_coords[2]:.1f}]" ) - poscar_comment = ( + return ( str( key_distortion.split("_")[-1] ) # Get distortion factor (-60.%) or 'Rattled' @@ -2219,7 +2274,6 @@ def _generate_structure_comment( ) + f" {approx_coords}" ) - return poscar_comment def _setup_distorted_defect_dict( self, @@ -2326,7 +2380,7 @@ def apply_distortions( for each charge state of each defect, in the format: {'defect_name': { 'charges': { - 'charge_state': { + {charge_state}: { 'structures': {...}, }, }, @@ -2404,8 +2458,8 @@ def apply_distortions( ): sorted_distances = np.sort(struct.distance_matrix.flatten()) shortest_interatomic_distance = sorted_distances[len(struct)] - if shortest_interatomic_distance < 1.0 and not any( - [el.symbol == "H" for el in struct.composition.elements] + if shortest_interatomic_distance < 1.0 and all( + el.symbol != "H" for el in struct.composition.elements ): if verbose: warnings.warn( @@ -2421,12 +2475,9 @@ def apply_distortions( "Unperturbed": defect_distorted_structures[ "Unperturbed" ].sc_entry.structure, - "distortions": { - dist: struct - for dist, struct in defect_distorted_structures[ - "distortions" - ].items() - }, + "distortions": dict( + defect_distorted_structures["distortions"].items() + ), } # Store distortion parameters/info in self.distortion_metadata @@ -2448,33 +2499,35 @@ def apply_distortions( def write_vasp_files( self, - incar_settings: Optional[dict] = None, - potcar_settings: Optional[dict] = None, + user_incar_settings: Optional[dict] = None, + user_potcar_functional: Optional[UserPotcarFunctional] = "PBE", + user_potcar_settings: Optional[dict] = None, output_path: str = ".", verbose: bool = False, + **kwargs, ) -> Tuple[dict, dict]: """ Generates the input files for `vasp_gam` relaxations of all output structures. Args: - incar_settings (:obj:`dict`): + user_incar_settings (:obj:`dict`): Dictionary of user VASP INCAR settings (e.g. {"ENCUT": 300, ...}), to overwrite the `ShakenBreak` defaults for those tags. Highly recommended to look at output `INCAR`s, or `SnB_input_files/incar.yaml` to see what the default `INCAR` settings are. Note that any flags that aren't numbers or True/False need to be input as strings with quotation marks - (e.g. `{"ALGO": "All"}`). - (Default: None) - potcar_settings (:obj:`dict`): + (e.g. `{"ALGO": "All"}`). (Default: None) + user_potcar_functional (str): + POTCAR functional to use. Default is "PBE" and if this fails, + tries "PBE_52", then "PBE_54". + user_potcar_settings (:obj:`dict`): Dictionary of user VASP POTCAR settings, to overwrite/update - the `doped` defaults. Using `pymatgen` syntax - (e.g. {'POTCAR': {'Fe': 'Fe_pv', 'O': 'O'}}). Highly + the `doped` defaults (e.g. {'Fe': 'Fe_pv', 'O': 'O'}}). Highly recommended to look at output `POTCAR`s, or `shakenbreak` `SnB_input_files/default_POTCARs.yaml`, to see what the default - `POTCAR` settings are. - (Default: None) + `POTCAR` settings are. (Default: None) write_files (:obj:`bool`): Whether to write output files (Default: True) output_path (:obj:`str`): @@ -2483,8 +2536,10 @@ def write_vasp_files( (Default is current directory = ".") verbose (:obj:`bool`): Whether to print distortion information (bond atoms and - distances). - (Default: False) + distances). (Default: False) + kwargs: + Additional keyword arguments to pass to `_create_vasp_input()` + (Mainly for testing purposes). Returns: :obj:`tuple`: @@ -2501,51 +2556,46 @@ def write_vasp_files( # loop for each defect in dict for defect_name, defect_dict in distorted_defects_dict.items(): - dict_transf = { - k: v for k, v in defect_dict.items() if k != "charges" - } # Single defect dict - # loop for each charge state of defect - for charge in defect_dict["charges"]: - charged_defect = {} + for charge_state in defect_dict["charges"]: + charged_defect_dict = {} for key_distortion, struct in zip( [ "Unperturbed", ] + list( - defect_dict["charges"][charge]["structures"][ + defect_dict["charges"][charge_state]["structures"][ "distortions" ].keys() ), - [defect_dict["charges"][charge]["structures"]["Unperturbed"]] + [defect_dict["charges"][charge_state]["structures"]["Unperturbed"]] + list( - defect_dict["charges"][charge]["structures"][ + defect_dict["charges"][charge_state]["structures"][ "distortions" ].values() ), ): poscar_comment = self._generate_structure_comment( defect_name=defect_name, - charge=charge, + charge=charge_state, key_distortion=key_distortion, ) - charged_defect[key_distortion] = { + charged_defect_dict[key_distortion] = { "Defect Structure": struct, "POSCAR Comment": poscar_comment, - "Transformation Dict": copy.deepcopy(dict_transf), + "Charge State": charge_state, } - charged_defect[key_distortion]["Transformation Dict"].update( - {"charge": charge} - ) # Add charge state to transformation dict _create_vasp_input( - defect_name=f"{defect_name}_{'+' if charge > 0 else ''}{charge}", - distorted_defect_dict=charged_defect, - incar_settings=incar_settings, - potcar_settings=potcar_settings, + defect_name=f"{defect_name}_{'+' if charge_state > 0 else ''}{charge_state}", + distorted_defect_dict=charged_defect_dict, + user_incar_settings=user_incar_settings, + user_potcar_functional=user_potcar_functional, + user_potcar_settings=user_potcar_settings, output_path=output_path, + **kwargs, ) self.write_distortion_metadata(output_path=output_path) @@ -2657,7 +2707,7 @@ def write_espresso_files( images=atoms, format="espresso-in", ) - elif pseudopotentials and not write_structures_only: + else: # write complete input file default_input_parameters["SYSTEM"][ "tot_charge" diff --git a/shakenbreak/plotting.py b/shakenbreak/plotting.py index 14128819..0c31944c 100644 --- a/shakenbreak/plotting.py +++ b/shakenbreak/plotting.py @@ -72,6 +72,23 @@ def _install_custom_font(): warnings.warn(warning_msg) +def _get_backend(save_format: str) -> Optional[str]: + """Try use pycairo as backend if installed, and save_format is pdf.""" + backend = None + if "pdf" in save_format: + try: + import cairo # noqa: F401 + + backend = "cairo" + except ImportError: + warnings.warn( + "pycairo not installed. Defaulting to matplotlib's pdf backend, so default " + "ShakeNBreak fonts may not be used – try setting `save_format` to 'png' or " + "`pip install pycairo` if you want ShakeNBreak's default font." + ) + return backend + + # Helper functions for formatting plots def _verify_data_directories_exist( output_path: str, @@ -501,18 +518,7 @@ def _save_plot( ) # use pycairo as backend if installed and save_format is pdf: - backend = None - if "pdf" in save_format: - try: - import cairo - - backend = "cairo" - except ImportError: - warnings.warn( - "pycairo not installed. Defaulting to matplotlib's pdf backend, so default " - "ShakeNBreak fonts may not be used – try setting `save_format` to 'png' or " - "`pip install pycairo` if you want ShakeNBreak's default font." - ) + backend = _get_backend(save_format) fig.savefig( plot_filepath, diff --git a/shakenbreak/vasp.py b/shakenbreak/vasp.py deleted file mode 100644 index 35d2df37..00000000 --- a/shakenbreak/vasp.py +++ /dev/null @@ -1,347 +0,0 @@ -"""Module to generate VASP input files for defect calculations""" -import os -import warnings -from copy import deepcopy # See https://stackoverflow.com/a/22341377/14020960 why -from typing import TYPE_CHECKING - -import numpy as np -from monty.io import zopen -from monty.os.path import zpath -from monty.serialization import loadfn -from pymatgen.io.vasp import Incar, Kpoints -from pymatgen.io.vasp.inputs import BadIncarWarning, Potcar, PotcarSingle, incar_params -from pymatgen.io.vasp.sets import BadInputSetWarning, MPRelaxSet - -if TYPE_CHECKING: - import pymatgen.core.periodic_table - import pymatgen.core.structure - - -MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) -default_potcar_dict = loadfn(f"{MODULE_DIR}/../SnB_input_files/default_POTCARs.yaml") -# Load default INCAR settings for the ShakenBreak geometry relaxations -default_incar_settings = loadfn( - os.path.join(MODULE_DIR, "../SnB_input_files/incar.yaml") -) - - -def _check_psp_dir(): # Provided by Katarina Brlec, from github.com/SMTG-UCL/surfaxe - """ - Helper function to check if potcars are set up correctly for use with - pymatgen, to be compatible across pymatgen versions (breaking changes in v2022) - """ - potcar = False - try: - import pymatgen.settings - - pmg_settings = pymatgen.settings.SETTINGS - if "PMG_VASP_PSP_DIR" in pmg_settings: - potcar = True - except ModuleNotFoundError: - try: - import pymatgen - - pmg_settings = pymatgen.SETTINGS - if "PMG_VASP_PSP_DIR" in pmg_settings: - potcar = True - except AttributeError: - from pymatgen.core import SETTINGS - - pmg_settings = SETTINGS - if "PMG_VASP_PSP_DIR" in pmg_settings: - potcar = True - return potcar - - -def _import_psp(): - """Import pmg settings for _PotcarSingleMod. - Duplicated code from doped (from github.com/SMTG-UCL/doped). - """ - pmg_settings = None - try: - import pymatgen.settings - - pmg_settings = pymatgen.settings.SETTINGS - except ModuleNotFoundError: - try: - import pymatgen - - pmg_settings = pymatgen.SETTINGS - except AttributeError: - from pymatgen.core import SETTINGS - - pmg_settings = SETTINGS - - if pmg_settings is None: - raise ValueError("pymatgen settings not found?") - else: - return pmg_settings - - -class _PotcarSingleMod(PotcarSingle): - """Modified PotcarSingle class.""" - - def __init__(self, *args, **kwargs): - super(self.__class__, self).__init__(*args, **kwargs) - - @staticmethod - def from_symbol_and_functional(symbol, functional=None): - settings = _import_psp() - if functional is None: - functional = settings.get("PMG_DEFAULT_FUNCTIONAL", "PBE") - funcdir = PotcarSingle.functional_dir[functional] - - if not os.path.isdir(os.path.join(settings.get("PMG_VASP_PSP_DIR"), funcdir)): - functional_dir = { - "LDA_US": "pot", - "PW91_US": "pot_GGA", - "LDA": "potpaw", - "PW91": "potpaw_GGA", - "LDA_52": "potpaw_LDA.52", - "LDA_54": "potpaw_LDA.54", - "PBE": "potpaw_PBE", - "PBE_52": "potpaw_PBE.52", - "PBE_54": "potpaw_PBE.54", - } - funcdir = functional_dir[functional] - - d = settings.get("PMG_VASP_PSP_DIR") - if d is None: - raise ValueError( - "No POTCAR directory found. Please set " - "the VASP_PSP_DIR environment variable" - ) - - paths_to_try = [ - os.path.join(d, funcdir, "POTCAR.{}".format(symbol)), - os.path.join(d, funcdir, symbol, "POTCAR.Z"), - os.path.join(d, funcdir, symbol, "POTCAR"), - ] - for p in paths_to_try: - p = os.path.expanduser(p) - p = zpath(p) - if os.path.exists(p): - return _PotcarSingleMod.from_file(p) - raise IOError( - "You do not have the right POTCAR with functional " - + f"{functional} and label {symbol} in your VASP_PSP_DIR" - ) - - -class _PotcarMod(Potcar): - """Modified Potcar class.""" - - def __init__(self, **kwargs): - super(self.__class__, self).__init__(**kwargs) - - def set_symbols(self, symbols, functional=None, sym_potcar_map=None): - """ - Initialize the POTCAR from a set of symbols. Currently, the POTCARs can - be fetched from a location specified in .pmgrc.yaml. Use pmg config - to add this setting. - - Args: - symbols ([str]): A list of element symbols - functional (str): The functional to use. If None, the setting - PMG_DEFAULT_FUNCTIONAL in .pmgrc.yaml is used, or if this is - not set, it will default to PBE. - sym_potcar_map (dict): A map of symbol:raw POTCAR string. If - sym_potcar_map is specified, POTCARs will be generated from - the given map data rather than the config file location. - """ - del self[:] - if sym_potcar_map: - for el in symbols: - self.append(_PotcarSingleMod(sym_potcar_map[el])) - else: - for el in symbols: - p = _PotcarSingleMod.from_symbol_and_functional(el, functional) - self.append(p) - - -class DefectRelaxSet(MPRelaxSet): - """ - Extension to MPRelaxSet which modifies some parameters appropriate - for defect calculations. - - Args: - charge (:obj:`int`): - Charge of the defect structure - """ - - def __init__(self, structure, **kwargs): - charge = kwargs.pop("charge", 0) - super(self.__class__, self).__init__(structure, **kwargs) - self.charge = charge - - @property - def incar(self): - """Get Incar object""" - inc = super(self.__class__, self).incar - try: - inc["NELECT"] = self.nelect - self.charge - except Exception: - print("NELECT flag is not set due to non-availability of POTCARs") - - return inc - - @property - def potcar(self): - """Potcar object.""" - return _PotcarMod( - symbols=self.potcar_symbols, functional=self.potcar_functional - ) - - @property - def all_input(self): - """ - Returns all input files as a dict of {filename: vasp object} - - Returns: - dict of {filename: object}, e.g., {'INCAR': Incar object, ...} - """ - try: - return super(DefectRelaxSet, self).all_input - except Exception: # Expecting the error to be POTCAR related, its ignored - kpoints = self.kpoints - incar = self.incar - if np.product(kpoints.kpts) < 4 and incar.get("ISMEAR", 0) == -5: - incar["ISMEAR"] = 0 - - return {"INCAR": incar, "KPOINTS": kpoints, "POSCAR": self.poscar} - - -def _scaled_ediff(natoms): # 1e-5 for 50 atoms, up to max 1e-4 - ediff = float(f"{((natoms/50)*1e-5):.1g}") - return ediff if ediff <= 1e-4 else 1e-4 - - -def write_vasp_gam_files( - single_defect_dict: dict, - input_dir: str = None, - incar_settings: dict = None, - potcar_settings: dict = None, -) -> None: - """ - Generates input files for vasp Gamma-point-only relaxation. - - Args: - single_defect_dict (:obj:`dict`): - Single defect-dictionary from prepare_vasp_defect_inputs() - output dictionary of defect calculations (see example notebook) - input_dir (:obj:`str`): - Folder in which to create vasp_gam calculation inputs folder - (Recommended to set as the key of the prepare_vasp_defect_inputs() - output directory) - (default: None) - incar_settings (:obj:`dict`): - Dictionary of user INCAR settings (AEXX, NCORE etc.) to override - default settings. Highly recommended to look at - `/SnB_input_files/incar.yaml`, or output INCARs or doped.vasp_input - source code, to see what the default INCAR settings are. - Note that any flags that aren't numbers or True/False need to be - input as strings with quotation marks (e.g. `{"ALGO": "All"}`). - (default: None) - potcar_settings (:obj:`dict`): - Dictionary of user POTCAR settings to override default settings. - Highly recommended to look at `default_potcar_dict` from - doped.vasp_input to see what the (Pymatgen) syntax and doped - default settings are. - (default: None) - - Returns: - None - """ - supercell = single_defect_dict["Defect Structure"] - num_elements = len(supercell.composition.elements) # for ROPT setting in INCAR - poscar_comment = ( - single_defect_dict["POSCAR Comment"] - if "POSCAR Comment" in single_defect_dict - else None - ) - - # Directory - vaspgaminputdir = input_dir + "/" if input_dir else "VASP_Files/" - if not os.path.exists(vaspgaminputdir): - os.makedirs(vaspgaminputdir) - - warnings.filterwarnings( - "ignore", category=BadInputSetWarning - ) # Ignore POTCAR warnings because Pymatgen incorrectly detecting POTCAR types - potcar_dict = deepcopy(default_potcar_dict) - if potcar_settings: - if "POTCAR_FUNCTIONAL" in potcar_settings.keys(): - potcar_dict["POTCAR_FUNCTIONAL"] = potcar_settings["POTCAR_FUNCTIONAL"] - if "POTCAR" in potcar_settings.keys(): - potcar_dict["POTCAR"].update(potcar_settings.pop("POTCAR")) - - defect_relax_set = DefectRelaxSet( - supercell, - charge=single_defect_dict["Transformation Dict"]["charge"], - user_potcar_settings=potcar_dict["POTCAR"], - user_potcar_functional=potcar_dict["POTCAR_FUNCTIONAL"], - ) - potcars = _check_psp_dir() - if potcars: - defect_relax_set.potcar.write_file(vaspgaminputdir + "POTCAR") - else: # make the folders without POTCARs - warnings.warn( - "POTCAR directory not set up with pymatgen, so only POSCAR files " - "will be generated (POTCARs also needed to determine appropriate " - "NELECT setting in INCAR files)" - ) - vaspgamposcar = defect_relax_set.poscar - if poscar_comment: - vaspgamposcar.comment = poscar_comment - vaspgamposcar.write_file(vaspgaminputdir + "POSCAR") - return # exit here - - relax_set_incar = defect_relax_set.incar - try: - # Only set if change in NELECT - nelect = relax_set_incar.as_dict()["NELECT"] - except KeyError: - # Get NELECT if no change (-dNELECT = 0) - nelect = defect_relax_set.nelect - - # Update system dependent parameters - default_incar_settings_copy = default_incar_settings.copy() - default_incar_settings_copy.update( - { - "NELECT": nelect, - "NUPDOWN": f"{nelect % 2:.0f} # But could be {nelect % 2 + 2:.0f} " - + "if strong spin polarisation or magnetic behaviour present", - "EDIFF": f"{_scaled_ediff(supercell.num_sites)} # May need to reduce for tricky relaxations", - "ROPT": ("1e-3 " * num_elements).rstrip(), - } - ) - if incar_settings: - for ( - k - ) in ( - incar_settings.keys() - ): # check INCAR flags and warn if they don't exist (typos) - if k not in incar_params.keys() and not k.startswith( - "#" - ): # comment tag # this code is taken from pymatgen.io.vasp.inputs - warnings.warn( # but only checking keys, not values so we can add comments etc - f"Cannot find {k} from your incar_settings in the list of " - "INCAR flags", - BadIncarWarning, - ) - default_incar_settings_copy.update(incar_settings) - - vaspgamincar = Incar.from_dict(default_incar_settings_copy) - - # kpoints - vaspgamkpts = Kpoints().from_dict( - {"comment": "Gamma-only KPOINTS from ShakeNBreak", "generation_style": "Gamma"} - ) - - vaspgamposcar = defect_relax_set.poscar - if poscar_comment: - vaspgamposcar.comment = poscar_comment - vaspgamposcar.write_file(vaspgaminputdir + "POSCAR") - with zopen(vaspgaminputdir + "INCAR", "wt") as incar_file: - incar_file.write(vaspgamincar.get_str()) - vaspgamkpts.write_file(vaspgaminputdir + "KPOINTS") diff --git a/tests/data/vasp/CdTe/vac_1_Cd_0/default_INCAR b/tests/data/vasp/CdTe/vac_1_Cd_0/default_INCAR new file mode 100644 index 00000000..9a186a55 --- /dev/null +++ b/tests/data/vasp/CdTe/vac_1_Cd_0/default_INCAR @@ -0,0 +1,35 @@ +# KPAR = # no kpar, only one kpoint +# May want to change NCORE, KPAR, AEXX, ENCUT, NUPDOWN, ISPIN, POTIM = +# ShakeNBreak INCAR with coarse settings to maximise speed with sufficient accuracy for qualitative structure searching = +AEXX = 0.25 +ALGO = Normal # change to all if zhegv, fexcp/f or zbrent errors encountered (done automatically by snb-run) +EDIFF = 1e-05 +EDIFFG = -0.01 +ENCUT = 300 +GGA = Pe +HFSCREEN = 0.208 +IBRION = 2 +ICHARG = 1 +ICORELEVEL = 0 # needed if using the kumagai-oba (efnv) anisotropic charge correction scheme +ISIF = 2 +ISMEAR = 0 +ISPIN = 2 +ISYM = 0 # symmetry breaking extremely likely for defects +LASPH = True +LCHARG = False +LHFCALC = True +LMAXMIX = 4 +LORBIT = 11 +LREAL = Auto +LVHAR = True +LWAVE = False +NCORE = 16 +NEDOS = 2000 +NELECT = 564.0 +NELM = 40 +NSW = 300 +NUPDOWN = 0 +PREC = Accurate +PRECFOCK = Fast +ROPT = 1e-3 1e-3 +SIGMA = 0.05 diff --git a/tests/test_cli.py b/tests/test_cli.py index 9176561c..60ddedb1 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,10 +1,12 @@ import copy import datetime +import filecmp import json import os import re import shutil import subprocess +import inspect import unittest import warnings @@ -17,7 +19,9 @@ # Pymatgen from pymatgen.core.structure import Structure -from pymatgen.io.vasp.inputs import Poscar, UnknownPotcarWarning +from pymatgen.io.vasp.inputs import Poscar, UnknownPotcarWarning, Kpoints, Potcar, Incar + +from doped.vasp import _test_potcar_functional_choice from shakenbreak.cli import snb from shakenbreak.distortions import rattle @@ -25,6 +29,17 @@ file_path = os.path.dirname(__file__) +def _potcars_available() -> bool: + """ + Check if the POTCARs are available for the tests (i.e. testing locally). + + If not (testing on GitHub Actions), POTCAR testing will be skipped. + """ + try: + _test_potcar_functional_choice("PBE") + return True + except ValueError: + return False def if_present_rm(path): if os.path.exists(path): @@ -88,6 +103,12 @@ def setUp(self): warnings.filterwarnings("ignore", category=DeprecationWarning) warnings.filterwarnings("ignore", category=UnknownPotcarWarning) + # get example INCAR: + self.V_Cd_INCAR_file = os.path.join( + self.VASP_CDTE_DATA_DIR, "vac_1_Cd_0/default_INCAR" + ) + self.V_Cd_INCAR = Incar.from_file(self.V_Cd_INCAR_file) + def tearDown(self): os.chdir(os.path.dirname(__file__)) for i in [ @@ -184,6 +205,9 @@ def tearDown(self): if_present_rm(f"{self.VASP_TIO2_DATA_DIR}/Bond_Distortion_20.0%") if_present_rm(f"{self.EXAMPLE_RESULTS}/v_Ti_0/Unperturbed/INCAR") + for i in ["castep", "cp2k", "fhi_aims", "quantum_espresso"]: + if_present_rm(f"{self.DATA_DIR}/{i}/vac_1_Cd_0/default_INCAR") + def copy_v_Ti_OUTCARs(self): """ Copy the OUTCAR files from the `v_Ti_0` `example_results` directory to the `vac_1_Ti_0` `vasp` @@ -261,18 +285,26 @@ def test_snb_generate(self): # check if correct files were created: V_Cd_Bond_Distortion_folder = f"{defect_name}_0/Bond_Distortion_-50.0%" self.assertTrue(os.path.exists(V_Cd_Bond_Distortion_folder)) - V_Cd_minus0pt5_rattled_POSCAR = Poscar.from_file( - V_Cd_Bond_Distortion_folder + "/POSCAR" - ) + V_Cd_minus0pt5_rattled_POSCAR = Poscar.from_file(f"{V_Cd_Bond_Distortion_folder}/POSCAR") self.assertEqual( V_Cd_minus0pt5_rattled_POSCAR.comment, - f"-50.0% N(Distort)=2 ~[0.0,0.0,0.0]", + "-50.0% N(Distort)=2 ~[0.0,0.0,0.0]", ) # default self.assertEqual( V_Cd_minus0pt5_rattled_POSCAR.structure, self.V_Cd_minus0pt5_struc_rattled, ) + kpoints = Kpoints.from_file(f"{V_Cd_Bond_Distortion_folder}/KPOINTS") + self.assertEqual(kpoints.kpts, [[1, 1, 1]]) + + if _potcars_available(): + assert filecmp.cmp(f"{V_Cd_Bond_Distortion_folder}/INCAR", self.V_Cd_INCAR_file) + + # check if POTCARs have been written: + potcar = Potcar.from_file(f"{V_Cd_Bond_Distortion_folder}/POTCAR") + assert set(potcar.as_dict()["symbols"]) == {"Cd", "Te"} + # Test recognises distortion_metadata.json: if_present_rm(f"{defect_name}_0") # but distortion_metadata.json still present result = runner.invoke( @@ -833,7 +865,10 @@ def test_snb_generate_config(self): max_attempts: 10000 max_disp: 1.0 seed: 20 -local_rattle: False""" +local_rattle: False +POTCAR_FUNCTIONAL: PBE_52 +POTCAR: + Te: Te_GW""" with open("test_config.yml", "w+") as fp: fp.write(test_yml) runner = CliRunner() @@ -859,6 +894,16 @@ def test_snb_generate_config(self): self.assertEqual( V_Cd_kwarged_POSCAR.structure, self.V_Cd_minus0pt5_struc_kwarged ) + kpoints = Kpoints.from_file(f"{defect_name}_0/Bond_Distortion_-50.0%/KPOINTS") + self.assertEqual(kpoints.kpts, [[1, 1, 1]]) + + if _potcars_available(): + assert filecmp.cmp(f"{defect_name}_0/Bond_Distortion_-50.0%/INCAR", self.V_Cd_INCAR_file) + + # check if POTCARs have been written: + potcar = Potcar.from_file(f"{defect_name}_0/Bond_Distortion_-50.0%/POTCAR") + assert set(potcar.as_dict()["symbols"]) == {"Cd", "Te_GW"} + test_yml = """ oxidation_states: @@ -1202,7 +1247,9 @@ def test_snb_generate_all(self): test_yml = """bond_distortions: [0.3,] local_rattle: True stdev: 0.25 -seed: 42""" # previous default +seed: 42 +POTCAR: + Cd: Cd_sv_GW""" # previous default rattle settings with open("test_config.yml", "w+") as fp: fp.write(test_yml) @@ -1286,6 +1333,23 @@ def test_snb_generate_all(self): Structure.from_file(f"{defect_name}_0/Bond_Distortion_30.0%/POSCAR"), self.V_Cd_0pt3_local_rattled, ) + kpoints = Kpoints.from_file(f"{defect_name}_0/Bond_Distortion_30.0%/KPOINTS") + self.assertEqual(kpoints.kpts, [[1, 1, 1]]) + + if _potcars_available(): + assert not filecmp.cmp(f"{defect_name}_0/Bond_Distortion_30.0%/INCAR", self.V_Cd_INCAR_file) + # NELECT has changed due to POTCARs + + v_Cd_INCAR = Incar.from_file(f"{defect_name}_0/Bond_Distortion_30.0%/INCAR") + v_Cd_INCAR.pop("NELECT") + test_INCAR = self.V_Cd_INCAR.copy() + test_INCAR.pop("NELECT") + assert v_Cd_INCAR == test_INCAR + + # check if POTCARs have been written: + potcar = Potcar.from_file(f"{defect_name}_0/Bond_Distortion_30.0%/POTCAR") + assert set(potcar.as_dict()["symbols"]) == {"Cd_sv", "Te"} + if_present_rm(defects_dir) for charge in range(-2, 3): if_present_rm(f"{defect_name}_{'+' if charge > 0 else ''}{charge}") @@ -3482,8 +3546,9 @@ def test_regenerate(self): catch_exceptions=False, ) defect = "v_Cd" # in example results + non_ignored_warnings = [warning for warning in w if "Subfolders with" not in str(warning.message)] self.assertEqual( - len([warning for warning in w if warning.category == UserWarning]), 0 + len([warning for warning in non_ignored_warnings if warning.category == UserWarning]), 0 ) self.assertIn( @@ -3565,9 +3630,18 @@ def test_regenerate(self): ], catch_exceptions=False, ) + non_ignored_warnings = [warning for warning in w if "Subfolders with" not in str(warning.message)] self.assertEqual( - len([warning for warning in w if warning.category == UserWarning]), 0 + len([warning for warning in non_ignored_warnings if warning.category == UserWarning]), 0 + ) + assert any( + f"Subfolders with VASP input files (['INCAR', 'KPOINTS', 'POTCAR'] not found in " + f"{self.EXAMPLE_RESULTS}/{defect}_-2, so just writing distorted POSCAR file to " + f"{self.EXAMPLE_RESULTS}/{defect}_-2/Bond_Distortion_-60.0%_from_0 directory." + in str(warning.message) + for warning in w ) + self.assertIn( "Comparing structures to specified ref_structure (Cd31 Te32)...", result.output, @@ -3581,12 +3655,6 @@ def test_regenerate(self): f" {self.EXAMPLE_RESULTS}/{defect}_0/Bond_Distortion_20.0%_from_-1\n", result.output, ) - self.assertIn( - f"No subfolders with VASP input files found in {self.EXAMPLE_RESULTS}/{defect}_-2," - f" so just writing distorted POSCAR file to " - f"{self.EXAMPLE_RESULTS}/{defect}_-2/Bond_Distortion_-60.0%_from_0 directory.\n", - result.output, - ) self.assertFalse("High_Energy" in result.output) # test FileNotFoundError raised when no defect folders found diff --git a/tests/test_energy_lowering_distortions.py b/tests/test_energy_lowering_distortions.py index ca32d16c..6c54bf80 100644 --- a/tests/test_energy_lowering_distortions.py +++ b/tests/test_energy_lowering_distortions.py @@ -950,7 +950,9 @@ def test_compare_struct_to_distortions(self): os.path.join( self.VASP_CDTE_DATA_DIR, "vac_1_Cd_0/Bond_Distortion_-55.0%/CONTCAR" ), - os.path.join(self.VASP_CDTE_DATA_DIR, "vac_1_Cd_-1/Rattled_from_+1/CONTCAR"), + os.path.join( + self.VASP_CDTE_DATA_DIR, "vac_1_Cd_-1/Rattled_from_+1/CONTCAR" + ), ) shutil.copy( os.path.join( @@ -1010,28 +1012,19 @@ def test_write_retest_inputs(self): ) ) with patch("builtins.print") as mock_print: - energy_lowering_distortions.write_retest_inputs( - low_energy_defects=low_energy_defects_dict, - output_path=self.VASP_CDTE_DATA_DIR, - ) + with warnings.catch_warnings(record=True) as w: + energy_lowering_distortions.write_retest_inputs( + low_energy_defects=low_energy_defects_dict, + output_path=self.VASP_CDTE_DATA_DIR, + ) mock_print.assert_any_call( "Writing low-energy distorted structure to" f" {self.VASP_CDTE_DATA_DIR}/vac_1_Cd_-1/Bond_Distortion_-55.0%_from_0" ) - mock_print.assert_any_call( - f"No subfolders with VASP input files found in {self.VASP_CDTE_DATA_DIR}/vac_1_Cd_-1, " - "so just writing distorted POSCAR file to " - f"{self.VASP_CDTE_DATA_DIR}/vac_1_Cd_-1/Bond_Distortion_-55.0%_from_0 directory." - ) # No VASP input files in distortion directories mock_print.assert_any_call( "Writing low-energy distorted structure to" f" {self.VASP_CDTE_DATA_DIR}/vac_1_Cd_-2/Bond_Distortion_-55.0%_from_0" ) - mock_print.assert_any_call( - f"No subfolders with VASP input files found in {self.VASP_CDTE_DATA_DIR}/vac_1_Cd_-2, " - "so just writing distorted POSCAR file to " - f"{self.VASP_CDTE_DATA_DIR}/vac_1_Cd_-2/Bond_Distortion_-55.0%_from_0 directory." - ) self.assertEqual( self.V_Cd_minus_0pt55_structure, Structure.from_file( @@ -1039,6 +1032,21 @@ def test_write_retest_inputs(self): ), ) + assert any( + f"Subfolders with VASP input files (['INCAR', 'KPOINTS', 'POTCAR'] not found in " + f"{self.VASP_CDTE_DATA_DIR}/vac_1_Cd_-1, so just writing distorted POSCAR file to " + f"{self.VASP_CDTE_DATA_DIR}/vac_1_Cd_-1/Bond_Distortion_-55.0%_from_0 directory." + in str(warning.message) + for warning in w + ) + assert any( + f"Subfolders with VASP input files (['INCAR', 'KPOINTS', 'POTCAR'] not found in " + f"{self.VASP_CDTE_DATA_DIR}/vac_1_Cd_-2, so just writing distorted POSCAR file to " + f"{self.VASP_CDTE_DATA_DIR}/vac_1_Cd_-2/Bond_Distortion_-55.0%_from_0 directory." + in str(warning.message) + for warning in w + ) + # Test for copying over VASP input files (INCAR, KPOINTS and (empty) # POTCAR files) if_present_rm( diff --git a/tests/test_input.py b/tests/test_input.py index fbdbd155..4823ff19 100755 --- a/tests/test_input.py +++ b/tests/test_input.py @@ -1,5 +1,6 @@ import copy import datetime +import filecmp import os import shutil import unittest @@ -9,6 +10,8 @@ import numpy as np from ase.build import bulk, make_supercell from ase.calculators.aims import Aims +from doped import _ignore_pmg_warnings +from doped.vasp import _test_potcar_functional_choice from monty.serialization import dumpfn, loadfn from pymatgen.analysis.defects.generators import VacancyGenerator from pymatgen.analysis.defects.thermo import DefectEntry @@ -16,10 +19,10 @@ from pymatgen.core.structure import Composition, PeriodicSite, Structure from pymatgen.entries.computed_entries import ComputedStructureEntry from pymatgen.io.ase import AseAtomsAdaptor -from pymatgen.io.vasp.inputs import Poscar, UnknownPotcarWarning +from pymatgen.io.vasp.inputs import Poscar, UnknownPotcarWarning, Incar, Kpoints, Potcar -from shakenbreak import distortions, input, vasp -from shakenbreak.distortions import rattle +from shakenbreak import input +from shakenbreak.distortions import rattle, distort def if_present_rm(path): @@ -27,6 +30,19 @@ def if_present_rm(path): shutil.rmtree(path) +def _potcars_available() -> bool: + """ + Check if the POTCARs are available for the tests (i.e. testing locally). + + If not (testing on GitHub Actions), POTCAR testing will be skipped. + """ + try: + _test_potcar_functional_choice("PBE") + return True + except ValueError: + return False + + def _update_struct_defect_dict( defect_dict: dict, structure: Structure, poscar_comment: str ) -> dict: @@ -200,6 +216,12 @@ def setUp(self): ) ) + # get example INCAR: + self.V_Cd_INCAR_file = os.path.join( + self.VASP_CDTE_DATA_DIR, "vac_1_Cd_0/default_INCAR" + ) + self.V_Cd_INCAR = Incar.from_file(self.V_Cd_INCAR_file) + # Setup distortion parameters self.V_Cd_distortion_parameters = { "unique_site": np.array([0.0, 0.0, 0.0]), @@ -508,7 +530,7 @@ def test_apply_rattle_bond_distortions_V_Cd(self): verbose=True, ) vac_coords = np.array([0, 0, 0]) # Cd vacancy fractional coordinates - output = distortions.distort(self.V_Cd_struc, 2, 0.5, frac_coords=vac_coords) + output = distort(self.V_Cd_struc, 2, 0.5, frac_coords=vac_coords) np.testing.assert_raises( AssertionError, np.testing.assert_array_equal, V_Cd_distorted_dict, output ) # Shouldn't match because rattling not done yet @@ -518,9 +540,7 @@ def test_apply_rattle_bond_distortions_V_Cd(self): rattling_atom_indices = rattling_atom_indices[ ~idx ] # removed distorted Te indices - output[ - "distorted_structure" - ] = distortions.rattle( # overwrite with distorted and rattle + output["distorted_structure"] = rattle( # overwrite with distorted and rattle # structure output["distorted_structure"], d_min=d_min, @@ -560,7 +580,7 @@ def test_apply_rattle_bond_distortions_Int_Cd_2(self): stdev=0.28333683853583164, # 10% of CdTe bond length, default seed=40, # distortion_factor * 100, default ) - output = distortions.distort(self.Int_Cd_2_struc, 2, 0.4, site_index=65) + output = distort(self.Int_Cd_2_struc, 2, 0.4, site_index=65) np.testing.assert_raises( AssertionError, np.testing.assert_array_equal, @@ -575,9 +595,7 @@ def test_apply_rattle_bond_distortions_Int_Cd_2(self): rattling_atom_indices = rattling_atom_indices[ ~idx ] # removed distorted Cd indices - output[ - "distorted_structure" - ] = distortions.rattle( # overwrite with distorted and rattle + output["distorted_structure"] = rattle( # overwrite with distorted and rattle output["distorted_structure"], d_min=d_min, active_atoms=rattling_atom_indices, @@ -874,30 +892,20 @@ def test_apply_snb_distortions_kwargs(self, mock_print): # test create_folder and create_vasp_input simultaneously: def test_create_vasp_input(self): """Test create_vasp_input function""" + # Create doped/PyCDT-style defect dict: supercell = self.V_Cd_dict["supercell"] - V_Cd_defect_relax_set = vasp.DefectRelaxSet(supercell["structure"], charge=0) - poscar = V_Cd_defect_relax_set.poscar - struct = V_Cd_defect_relax_set.structure - dict_transf = { - "defect_type": self.V_Cd_dict["name"], - "defect_site": self.V_Cd_dict["unique_site"], - "defect_supercell_site": self.V_Cd_dict["bulk_supercell_site"], - "defect_multiplicity": self.V_Cd_dict["site_multiplicity"], - "charge": 0, - "supercell": supercell["size"], - } - poscar.comment = ( + poscar_comment = ( self.V_Cd_dict["name"] - + str(dict_transf["defect_supercell_site"].frac_coords) + + str(self.V_Cd_dict["bulk_supercell_site"].frac_coords) + "_-dNELECT=" # change in NELECT from bulk supercell + str(0) ) vasp_defect_inputs = { "vac_1_Cd_0": { - "Defect Structure": struct, - "POSCAR Comment": poscar.comment, - "Transformation Dict": dict_transf, + "Defect Structure": supercell, + "POSCAR Comment": poscar_comment, + "Charge State": 0, } } V_Cd_updated_charged_defect_dict = _update_struct_defect_dict( @@ -909,70 +917,135 @@ def test_create_vasp_input(self): "Bond_Distortion_-50.0%": V_Cd_updated_charged_defect_dict } self.assertFalse(os.path.exists("vac_1_Cd_0")) - input._create_vasp_input( - "vac_1_Cd_0", - distorted_defect_dict=V_Cd_charged_defect_dict, - incar_settings=vasp.default_incar_settings, + with warnings.catch_warnings(record=True) as w: + _ignore_pmg_warnings() + input._create_vasp_input( + "vac_1_Cd_0", + distorted_defect_dict=V_Cd_charged_defect_dict, + ) + V_Cd_POSCAR = self._check_V_Cd_rattled_poscar( + "vac_1_Cd_0/Bond_Distortion_-50.0%" ) - V_Cd_Bond_Distortion_folder = "vac_1_Cd_0/Bond_Distortion_-50.0%" - self.assertTrue(os.path.exists(V_Cd_Bond_Distortion_folder)) - V_Cd_POSCAR = Poscar.from_file(V_Cd_Bond_Distortion_folder + "/POSCAR") - self.assertEqual(V_Cd_POSCAR.comment, "V_Cd Rattled") - self.assertEqual(V_Cd_POSCAR.structure, self.V_Cd_minus0pt5_struc_rattled) - # only test POSCAR as INCAR, KPOINTS and POTCAR not written on GitHub actions, - # but tested locally + kpoints = Kpoints.from_file("vac_1_Cd_0/Bond_Distortion_-50.0%/KPOINTS") + self.assertEqual(kpoints.kpts, [[1, 1, 1]]) + + if _potcars_available(): + assert filecmp.cmp( + "vac_1_Cd_0/Bond_Distortion_-50.0%/INCAR", self.V_Cd_INCAR_file + ) + + # check if POTCARs have been written: + potcar = Potcar.from_file("vac_1_Cd_0/Bond_Distortion_-50.0%/POTCAR") + assert set(potcar.as_dict()["symbols"]) == { + input.default_potcar_dict["POTCAR"][el_symbol] + for el_symbol in V_Cd_POSCAR.structure.symbol_set + } + else: # test POTCAR warning + assert ( + len(w) == 2 + ) # general POTCAR warning and NELECT/NUPDOWN INCAR warning + assert any( + str(warning.message) + == "POTCAR directory not set up with pymatgen (see the doped docs Installation page: " + "https://doped.readthedocs.io/en/latest/Installation.html for instructions on setting " + "this up). This is required to generate `POTCAR` files and set `NELECT` (i.e. charge " + "state) and `NUPDOWN` in the `INCAR` files!\nNo `POTCAR` files will be written, and " + "`NELECT` and `NUPDOWN` will not be set in `INCAR`s. Beware!" + for warning in w + ) - # test with kwargs: (except POTCAR settings because we can't have this on the GitHub test - # server) + # test with kwargs: kwarg_incar_settings = { - "NELECT": 3, "IBRION": 42, "LVHAR": True, "LWAVE": True, "LCHARG": True, + "AEXX": 0.35, } - kwarged_incar_settings = vasp.default_incar_settings.copy() - kwarged_incar_settings.update(kwarg_incar_settings) with warnings.catch_warnings(record=True) as w: input._create_vasp_input( "vac_1_Cd_0", distorted_defect_dict=V_Cd_charged_defect_dict, - incar_settings=kwarged_incar_settings, - ) - self.assertTrue( - any( # here we get this warning because no Unperturbed structures were - # written so couldn't be compared - f"A previously-generated defect folder vac_1_Cd_0 exists in " - f"{os.path.basename(os.path.abspath('.'))}, and the Unperturbed defect structure " - f"could not be matched to the current defect species: vac_1_Cd_0. These are assumed " - f"to be inequivalent defects, so the previous vac_1_Cd_0 will be renamed to " - f"vac_1_Cda_0 and ShakeNBreak files for the current defect will be saved to " - f"vac_1_Cdb_0, to prevent overwriting." in str(warning.message) - for warning in w + user_incar_settings=kwarg_incar_settings, + user_potcar_settings={"Cd": "Cd_sv_GW", "Te": "Te_GW"}, ) + self._check_V_Cd_folder_renaming( + w, + "A previously-generated defect folder vac_1_Cd_0 exists in ", + ", and the Unperturbed defect structure could not be matched to the current defect species: " + "vac_1_Cd_0. These are assumed to be inequivalent defects, so the previous vac_1_Cd_0 will " + "be renamed to vac_1_Cda_0 and ShakeNBreak files for the current defect will be saved to " + "vac_1_Cdb_0, to prevent overwriting.", ) - self.assertFalse(os.path.exists("vac_1_Cd_0")) - self.assertTrue(os.path.exists("vac_1_Cda_0")) - self.assertTrue(os.path.exists("vac_1_Cdb_0")) V_Cd_kwarg_folder = "vac_1_Cdb_0/Bond_Distortion_-50.0%" - V_Cd_POSCAR = Poscar.from_file(V_Cd_kwarg_folder + "/POSCAR") - self.assertEqual(V_Cd_POSCAR.comment, "V_Cd Rattled") - self.assertEqual(V_Cd_POSCAR.structure, self.V_Cd_minus0pt5_struc_rattled) - # only test POSCAR as INCAR, KPOINTS and POTCAR not written on GitHub actions, - # but tested locally + V_Cd_POSCAR = self._check_V_Cd_rattled_poscar(V_Cd_kwarg_folder) + kpoints = Kpoints.from_file(f"{V_Cd_kwarg_folder}/KPOINTS") + self.assertEqual(kpoints.kpts, [[1, 1, 1]]) + + if _potcars_available(): + assert not filecmp.cmp( # INCAR settings changed now + f"{V_Cd_kwarg_folder}/INCAR", self.V_Cd_INCAR_file + ) + assert self.V_Cd_INCAR != Incar.from_file(f"{V_Cd_kwarg_folder}/INCAR") + kwarged_INCAR = self.V_Cd_INCAR.copy() + kwarged_INCAR.update(kwarg_incar_settings) + kwarged_INCAR["NELECT"] = 812.0 # changed POTCARs + assert kwarged_INCAR == Incar.from_file(f"{V_Cd_kwarg_folder}/INCAR") + + # check if POTCARs have been written: + potcar = Potcar.from_file(f"{V_Cd_kwarg_folder}/POTCAR") + assert set(potcar.as_dict()["symbols"]) == { + "Cd_sv", + "Te_GW", + } # Cd_sv_GW POTCAR has Cd_sv symbol, checked + else: # test POTCAR warning + assert any( + str(warning.message) + == "POTCAR directory not set up with pymatgen (see the doped docs Installation page: " + "https://doped.readthedocs.io/en/latest/Installation.html for instructions on setting " + "this up). This is required to generate `POTCAR` files and set `NELECT` (i.e. charge " + "state) and `NUPDOWN` in the `INCAR` files!\nNo `POTCAR` files will be written, and " + "`NELECT` and `NUPDOWN` will not be set in `INCAR`s. Beware!" + for warning in w + ) # test output_path option input._create_vasp_input( "vac_1_Cd_0", distorted_defect_dict=V_Cd_charged_defect_dict, - incar_settings=kwarged_incar_settings, + user_incar_settings=kwarg_incar_settings, output_path="test_path", ) - V_Cd_kwarg_folder = "test_path/vac_1_Cd_0/Bond_Distortion_-50.0%" - self.assertTrue(os.path.exists(V_Cd_kwarg_folder)) - V_Cd_POSCAR = Poscar.from_file(V_Cd_kwarg_folder + "/POSCAR") - self.assertEqual(V_Cd_POSCAR.comment, "V_Cd Rattled") - self.assertEqual(V_Cd_POSCAR.structure, self.V_Cd_minus0pt5_struc_rattled) + V_Cd_POSCAR = self._check_V_Cd_rattled_poscar( + "test_path/vac_1_Cd_0/Bond_Distortion_-50.0%" + ) + kpoints = Kpoints.from_file( + "test_path/vac_1_Cd_0/Bond_Distortion_-50.0%/KPOINTS" + ) + self.assertEqual(kpoints.kpts, [[1, 1, 1]]) + + if _potcars_available(): + assert not filecmp.cmp( # INCAR settings changed now + "test_path/vac_1_Cd_0/Bond_Distortion_-50.0%/INCAR", + self.V_Cd_INCAR_file, + ) + assert self.V_Cd_INCAR != Incar.from_file( + "test_path/vac_1_Cd_0/Bond_Distortion_-50.0%/INCAR" + ) + kwarged_INCAR = self.V_Cd_INCAR.copy() + kwarged_INCAR.update(kwarg_incar_settings) + assert kwarged_INCAR == Incar.from_file( + "test_path/vac_1_Cd_0/Bond_Distortion_-50.0%/INCAR" + ) + + # check if POTCARs have been written: + potcar = Potcar.from_file( + "test_path/vac_1_Cd_0/Bond_Distortion_-50.0%/POTCAR" + ) + assert set(potcar.as_dict()["symbols"]) == { + input.default_potcar_dict["POTCAR"][el_symbol] + for el_symbol in V_Cd_POSCAR.structure.symbol_set + } # Test correct handling of cases where defect folders with the same name have previously # been written: @@ -997,20 +1070,26 @@ def test_create_vasp_input(self): input._create_vasp_input( "vac_1_Cd_0", distorted_defect_dict=V_Cd_charged_defect_dict, - incar_settings={}, - ) - self.assertTrue( - any( - f"The previously-generated defect folder vac_1_Cdb_0 in " - f"{os.path.basename(os.path.abspath('.'))} has the same Unperturbed defect " - f"structure as the current defect species: vac_1_Cd_0. ShakeNBreak files in " - f"vac_1_Cdb_0 will be overwritten." in str(warning.message) + user_incar_settings={}, + user_potcar_functional="PBE_54", # check setting POTCAR functional to one that isn't + # present locally + ) + self._check_V_Cd_folder_renaming( + w, + "The previously-generated defect folder vac_1_Cdb_0 in ", + " has the same Unperturbed defect structure as the current defect species: vac_1_Cd_0. ShakeNBreak files in vac_1_Cdb_0 will be overwritten.", + ) + if not _potcars_available(): # test POTCAR warning + assert any( + str(warning.message) + == "POTCAR directory not set up with pymatgen (see the doped docs Installation page: " + "https://doped.readthedocs.io/en/latest/Installation.html for instructions on setting " + "this up). This is required to generate `POTCAR` files and set `NELECT` (i.e. charge " + "state) and `NUPDOWN` in the `INCAR` files!\nNo `POTCAR` files will be written, and " + "`NELECT` and `NUPDOWN` will not be set in `INCAR`s. Beware!" for warning in w ) - ) - self.assertFalse(os.path.exists("vac_1_Cd_0")) - self.assertTrue(os.path.exists("vac_1_Cda_0")) - self.assertTrue(os.path.exists("vac_1_Cdb_0")) + self.assertFalse(os.path.exists("vac_1_Cdc_0")) V_Cd_POSCAR = Poscar.from_file("vac_1_Cdb_0/Unperturbed/POSCAR") self.assertEqual(V_Cd_POSCAR.comment, "V_Cd Unperturbed, Overwritten") @@ -1027,15 +1106,31 @@ def test_create_vasp_input(self): input._create_vasp_input( "vac_1_Cd_0", distorted_defect_dict=V_Cd_charged_defect_dict, - incar_settings={}, + user_incar_settings={}, ) + self._check_V_Cd_folder_renaming( + w, + "Previously-generated defect folders (vac_1_Cdb_0...) exist in ", + ", and the Unperturbed defect structures could not be matched to the current defect species: vac_1_Cd_0. These are assumed to be inequivalent defects, so ShakeNBreak files for the current defect will be saved to vac_1_Cdc_0 to prevent overwriting.", + ) + self.assertTrue(os.path.exists("vac_1_Cdc_0")) + self.assertFalse(os.path.exists("vac_1_Cdd_0")) + V_Cd_prev_POSCAR = Poscar.from_file("vac_1_Cdb_0/Unperturbed/POSCAR") + self.assertEqual(V_Cd_prev_POSCAR.comment, "V_Cd Unperturbed, Overwritten") + V_Cd_new_POSCAR = Poscar.from_file("vac_1_Cdc_0/Unperturbed/POSCAR") + self.assertEqual(V_Cd_new_POSCAR.comment, "V_Cd Rattled, New Folder") + self.assertEqual(V_Cd_new_POSCAR.structure, self.V_Cd_minus0pt5_struc_rattled) + + def _check_V_Cd_rattled_poscar(self, defect_dir): + result = Poscar.from_file(f"{defect_dir}/POSCAR") + self.assertEqual(result.comment, "V_Cd Rattled") + self.assertEqual(result.structure, self.V_Cd_minus0pt5_struc_rattled) + return result + + def _check_V_Cd_folder_renaming(self, w, top_dir, defect_dir): self.assertTrue( any( - f"Previously-generated defect folders (vac_1_Cdb_0...) exist in " - f"{os.path.basename(os.path.abspath('.'))}, and the Unperturbed defect structures " - f"could not be matched to the current defect species: vac_1_Cd_0. These are " - f"assumed to be inequivalent defects, so ShakeNBreak files for the current defect " - f"will be saved to vac_1_Cdc_0 to prevent overwriting." + f"{top_dir}{os.path.basename(os.path.abspath('.'))}{defect_dir}" in str(warning.message) for warning in w ) @@ -1043,13 +1138,6 @@ def test_create_vasp_input(self): self.assertFalse(os.path.exists("vac_1_Cd_0")) self.assertTrue(os.path.exists("vac_1_Cda_0")) self.assertTrue(os.path.exists("vac_1_Cdb_0")) - self.assertTrue(os.path.exists("vac_1_Cdc_0")) - self.assertFalse(os.path.exists("vac_1_Cdd_0")) - V_Cd_prev_POSCAR = Poscar.from_file("vac_1_Cdb_0/Unperturbed/POSCAR") - self.assertEqual(V_Cd_prev_POSCAR.comment, "V_Cd Unperturbed, Overwritten") - V_Cd_new_POSCAR = Poscar.from_file("vac_1_Cdc_0/Unperturbed/POSCAR") - self.assertEqual(V_Cd_new_POSCAR.comment, "V_Cd Rattled, New Folder") - self.assertEqual(V_Cd_new_POSCAR.structure, self.V_Cd_minus0pt5_struc_rattled) def test_generate_defect_object(self): """Test generate_defect_object""" @@ -1315,7 +1403,6 @@ def test_write_vasp_files(self): bond_distortions = list(np.arange(-0.6, 0.601, 0.05)) # Use customised names for defects - dist = input.Distortions( self.cdte_defects, oxidation_states=oxidation_states, @@ -1325,10 +1412,11 @@ def test_write_vasp_files(self): seed=42, # old default ) with patch("builtins.print") as mock_print: - _, distortion_metadata = dist.write_vasp_files( - incar_settings={"ENCUT": 212, "IBRION": 0, "EDIFF": 1e-4}, - verbose=False, - ) + with warnings.catch_warnings(record=True) as w: + _, distortion_metadata = dist.write_vasp_files( + user_incar_settings={"ENCUT": 212, "IBRION": 0, "EDIFF": 1e-4}, + verbose=False, + ) # check if expected folders were created: self.assertTrue( @@ -1391,8 +1479,38 @@ def test_write_vasp_files(self): "-50.0% N(Distort)=2 ~[0.0,0.0,0.0]", ) # default self.assertEqual(V_Cd_POSCAR.structure, self.V_Cd_minus0pt5_struc_rattled) - # only test POSCAR as INCAR, KPOINTS and POTCAR not written on GitHub actions, - # but tested locally + kpoints = Kpoints.from_file(f"{V_Cd_Bond_Distortion_folder}/KPOINTS") + self.assertEqual(kpoints.kpts, [[1, 1, 1]]) + + if _potcars_available(): + assert not filecmp.cmp( # INCAR settings changed now + f"{V_Cd_Bond_Distortion_folder}/INCAR", self.V_Cd_INCAR_file + ) + assert self.V_Cd_INCAR != Incar.from_file( + f"{V_Cd_Bond_Distortion_folder}/INCAR" + ) + kwarged_INCAR = self.V_Cd_INCAR.copy() + kwarged_INCAR.update({"ENCUT": 212, "IBRION": 0, "EDIFF": 1e-4}) + assert kwarged_INCAR == Incar.from_file( + f"{V_Cd_Bond_Distortion_folder}/INCAR" + ) + + # check if POTCARs have been written: + potcar = Potcar.from_file(f"{V_Cd_Bond_Distortion_folder}/POTCAR") + assert set(potcar.as_dict()["symbols"]) == { + input.default_potcar_dict["POTCAR"][el_symbol] + for el_symbol in V_Cd_POSCAR.structure.symbol_set + } + else: # test POTCAR warning + assert any( + str(warning.message) + == "POTCAR directory not set up with pymatgen (see the doped docs Installation page: " + "https://doped.readthedocs.io/en/latest/Installation.html for instructions on setting " + "this up). This is required to generate `POTCAR` files and set `NELECT` (i.e. charge " + "state) and `NUPDOWN` in the `INCAR` files!\nNo `POTCAR` files will be written, and " + "`NELECT` and `NUPDOWN` will not be set in `INCAR`s. Beware!" + for warning in w + ) Int_Cd_2_Bond_Distortion_folder = "Int_Cd_2_0/Bond_Distortion_-60.0%" self.assertTrue(os.path.exists(Int_Cd_2_Bond_Distortion_folder)) @@ -1404,8 +1522,26 @@ def test_write_vasp_files(self): self.assertNotEqual( # Int_Cd_2_minus0pt6_struc_rattled is with new default `stdev` & `seed` Int_Cd_2_POSCAR.structure, self.Int_Cd_2_minus0pt6_struc_rattled ) - # only test POSCAR as INCAR, KPOINTS and POTCAR not written on GitHub actions, - # but tested locally + kpoints = Kpoints.from_file(f"{Int_Cd_2_Bond_Distortion_folder}/KPOINTS") + self.assertEqual(kpoints.kpts, [[1, 1, 1]]) + + if _potcars_available(): + assert not filecmp.cmp( # INCAR settings changed now + f"{Int_Cd_2_Bond_Distortion_folder}/INCAR", self.V_Cd_INCAR_file + ) + kwarged_INCAR = self.V_Cd_INCAR.copy() + kwarged_INCAR.update({"ENCUT": 212, "IBRION": 0, "EDIFF": 1e-4}) + kwarged_INCAR.pop("NELECT") # different NELECT for Cd_i_+2 + Int_Cd_2_INCAR = Incar.from_file(f"{Int_Cd_2_Bond_Distortion_folder}/INCAR") + Int_Cd_2_INCAR.pop("NELECT") + assert kwarged_INCAR == Int_Cd_2_INCAR + + # check if POTCARs have been written: + potcar = Potcar.from_file(f"{Int_Cd_2_Bond_Distortion_folder}/POTCAR") + assert set(potcar.as_dict()["symbols"]) == { + input.default_potcar_dict["POTCAR"][el_symbol] + for el_symbol in V_Cd_POSCAR.structure.symbol_set + } # Test `Rattled` folder not generated for non-fully-ionised defects, # and only `Rattled` and `Unperturbed` folders generated for fully-ionised defects @@ -1500,19 +1636,22 @@ def test_write_vasp_files(self): ] with patch("builtins.print") as mock_Int_Cd_2_print: - dist = input.Distortions( - {"Int_Cd_2": reduced_Int_Cd_2_entries}, - oxidation_states=oxidation_states, - distortion_increment=0.25, - distorted_elements={"Int_Cd_2": ["Cd"]}, - dict_number_electrons_user={"Int_Cd_2": 3}, - local_rattle=False, - stdev=0.25, # old default - seed=42, # old default - ) - _, distortion_metadata = dist.write_vasp_files( - verbose=True, - ) + with warnings.catch_warnings(record=True) as w: + dist = input.Distortions( + {"Int_Cd_2": reduced_Int_Cd_2_entries}, + oxidation_states=oxidation_states, + distortion_increment=0.25, + distorted_elements={"Int_Cd_2": ["Cd"]}, + dict_number_electrons_user={"Int_Cd_2": 3}, + local_rattle=False, + stdev=0.25, # old default + seed=42, # old default + ) + _, distortion_metadata = dist.write_vasp_files( + verbose=True, + user_potcar_settings={"Cd": "Cd_sv_GW", "Te": "Te_GW"}, + user_potcar_functional="PBE_52", + ) kwarged_Int_Cd_2_dict = { "distortion_parameters": { @@ -1685,6 +1824,37 @@ def test_write_vasp_files(self): ) # Defect added at index 0, so atom indexing + 1 wrt original structure # check correct folder was created: self.assertTrue(os.path.exists("Int_Cd_2_+1/Unperturbed")) + _int_Cd_2_POSCAR = Poscar.from_file( + "Int_Cd_2_+1/Unperturbed/POSCAR" + ) # test POSCAR loaded fine + kpoints = Kpoints.from_file("Int_Cd_2_+1/Unperturbed/KPOINTS") + self.assertEqual(kpoints.kpts, [[1, 1, 1]]) + + if _potcars_available(): + assert not filecmp.cmp( # INCAR settings changed now + "Int_Cd_2_+1/Unperturbed/INCAR", self.V_Cd_INCAR_file + ) + int_Cd_2_INCAR = Incar.from_file("Int_Cd_2_+1/Unperturbed/INCAR") + v_Cd_INCAR = self.V_Cd_INCAR.copy() + v_Cd_INCAR.pop("NELECT") # NELECT and NUPDOWN differs for the two defects + v_Cd_INCAR.pop("NUPDOWN") + int_Cd_2_INCAR.pop("NELECT") + int_Cd_2_INCAR.pop("NUPDOWN") + assert v_Cd_INCAR == int_Cd_2_INCAR + + # check if POTCARs have been written: + potcar = Potcar.from_file("Int_Cd_2_+1/Unperturbed/POTCAR") + assert set(potcar.as_dict()["symbols"]) == {"Cd_sv", "Te_GW"} + else: # test POTCAR warning + assert any( + str(warning.message) + == "POTCAR directory not set up with pymatgen (see the doped docs Installation page: " + "https://doped.readthedocs.io/en/latest/Installation.html for instructions on setting " + "this up). This is required to generate `POTCAR` files and set `NELECT` (i.e. charge " + "state) and `NUPDOWN` in the `INCAR` files!\nNo `POTCAR` files will be written, and " + "`NELECT` and `NUPDOWN` will not be set in `INCAR`s. Beware!" + for warning in w + ) # check correct output for "extra" electrons and positive charge state: with patch("builtins.print") as mock_Int_Cd_2_print: @@ -1988,7 +2158,7 @@ def test_write_vasp_files_from_list(self): mock_print.assert_any_call( "\nDefect Te_i_Td_Te2.83 in charge state: 0. Number of distorted " "neighbours: 2" - ) # TODO: this is not created + ) # check if correct files were created: V_Cd_Bond_Distortion_folder = "v_Cd_0/Bond_Distortion_-50.0%" diff --git a/tests/test_local.py b/tests/test_local.py deleted file mode 100644 index d84060ed..00000000 --- a/tests/test_local.py +++ /dev/null @@ -1,1004 +0,0 @@ -""" -Python test file only to be run locally, when POTCARs are available and the .pmgrc.yaml file is -set up. This cannot be run on GitHub actions as it does not have the POTCARs, preventing POTCAR -and INCAR files from being written. -""" - -import copy -import json -import os -import shutil -import unittest -import warnings -from unittest.mock import patch - -import numpy as np - -# Click -from click.testing import CliRunner -from doped import vasp_input -from matplotlib.testing.compare import compare_images -from monty.serialization import dumpfn, loadfn -from pymatgen.analysis.defects.core import StructureMatcher -from pymatgen.core.structure import Structure -from pymatgen.io.vasp.inputs import Incar, Kpoints, Poscar, UnknownPotcarWarning - -from shakenbreak import cli, input, vasp -from shakenbreak.cli import snb - -_file_path = os.path.dirname(__file__) -_DATA_DIR = os.path.join(_file_path, "data") - - -def if_present_rm(path): - if os.path.exists(path): - if os.path.isfile(path): - os.remove(path) - elif os.path.isdir(path): - shutil.rmtree(path) - - -def _update_struct_defect_dict( - defect_dict: dict, structure: Structure, poscar_comment: str -) -> dict: - """ - Given a Structure object and POSCAR comment, update the folders dictionary - (generated with `doped.vasp_input.prepare_vasp_defect_inputs()`) with - the given values. - Args: - defect_dict (:obj:`dict`): - Dictionary with defect information, as generated with doped - `prepare_vasp_defect_inputs()` - structure (:obj:`~pymatgen.core.structure.Structure`): - Defect structure as a pymatgen object - poscar_comment (:obj:`str`): - Comment to include in the top line of the POSCAR file - Returns: - single defect dict in the `doped` format. - """ - defect_dict_copy = copy.deepcopy(defect_dict) - defect_dict_copy["Defect Structure"] = structure - defect_dict_copy["POSCAR Comment"] = poscar_comment - return defect_dict_copy - - -class DistortionLocalTestCase(unittest.TestCase): - """Test ShakeNBreak structure distortion helper functions""" - - def setUp(self): - warnings.filterwarnings("ignore", category=UnknownPotcarWarning) - self.DATA_DIR = os.path.join(os.path.dirname(__file__), "data") - self.VASP_CDTE_DATA_DIR = os.path.join(self.DATA_DIR, "vasp/CdTe") - self.EXAMPLE_RESULTS = os.path.join(self.DATA_DIR, "example_results") - - # Refactor doped defect dict to dict of Defect() objects - self.cdte_doped_defect_dict = loadfn( - os.path.join(self.VASP_CDTE_DATA_DIR, "CdTe_defects_dict.json") - ) - self.cdte_defects = { - defect_dict["name"]: input.generate_defect_object( - single_defect_dict=defect_dict, - bulk_dict=self.cdte_doped_defect_dict["bulk"], - ) - for defects_type, defect_dict_list in self.cdte_doped_defect_dict.items() - if "bulk" not in defects_type - for defect_dict in defect_dict_list - } # with doped/PyCDT names - - self.V_Cd_dict = self.cdte_doped_defect_dict["vacancies"][0] - self.Int_Cd_2_dict = self.cdte_doped_defect_dict["interstitials"][1] - # Refactor to Defect() objects - self.V_Cd = input.generate_defect_object( - self.V_Cd_dict, self.cdte_doped_defect_dict["bulk"] - ) - self.Int_Cd_2 = input.generate_defect_object( - self.Int_Cd_2_dict, self.cdte_doped_defect_dict["bulk"] - ) - - self.V_Cd_struc = Structure.from_file( - os.path.join(self.VASP_CDTE_DATA_DIR, "CdTe_V_Cd_POSCAR") - ) - self.V_Cd_minus0pt5_struc_rattled = Structure.from_file( - os.path.join( - self.VASP_CDTE_DATA_DIR, "CdTe_V_Cd_-50%_Distortion_Rattled_POSCAR" - ) - ) - self.V_Cd_minus0pt5_struc_0pt1_rattled = Structure.from_file( - os.path.join( - self.VASP_CDTE_DATA_DIR, - "CdTe_V_Cd_-50%_Distortion_stdev0pt1_Rattled_POSCAR", - ) - ) - self.V_Cd_minus0pt5_struc_kwarged = Structure.from_file( - os.path.join(self.VASP_CDTE_DATA_DIR, "CdTe_V_Cd_-50%_Kwarged_POSCAR") - ) - self.V_Cd_distortion_parameters = { - "unique_site": np.array([0.0, 0.0, 0.0]), - "num_distorted_neighbours": 2, - "distorted_atoms": [(33, "Te"), (42, "Te")], - } - self.Int_Cd_2_struc = Structure.from_file( - os.path.join(self.VASP_CDTE_DATA_DIR, "CdTe_Int_Cd_2_POSCAR") - ) - self.Int_Cd_2_minus0pt6_struc_rattled = Structure.from_file( - os.path.join( - self.VASP_CDTE_DATA_DIR, "CdTe_Int_Cd_2_-60%_Distortion_Rattled_POSCAR" - ) - ) - self.Int_Cd_2_minus0pt6_NN_10_struc_rattled = Structure.from_file( - os.path.join( - self.VASP_CDTE_DATA_DIR, "CdTe_Int_Cd_2_-60%_Distortion_NN_10_POSCAR" - ) - ) - self.Int_Cd_2_normal_distortion_parameters = { - "unique_site": self.Int_Cd_2_dict["unique_site"].frac_coords, - "num_distorted_neighbours": 2, - "distorted_atoms": [(10, "Cd"), (22, "Cd")], - "defect_site_index": 65, - } - self.Int_Cd_2_NN_10_distortion_parameters = { - "unique_site": self.Int_Cd_2_dict["unique_site"].frac_coords, - "num_distorted_neighbours": 10, - "distorted_atoms": [ - (10, "Cd"), - (22, "Cd"), - (29, "Cd"), - (1, "Cd"), - (14, "Cd"), - (24, "Cd"), - (30, "Cd"), - (38, "Te"), - (54, "Te"), - (62, "Te"), - ], - "defect_site_index": 65, - } - - # Note that Int_Cd_2 has been chosen as a test case, because the first few nonzero bond - # distances are the interstitial bonds, rather than the bulk bond length, so here we are - # also testing that the package correctly ignores these and uses the bulk bond length of - # 2.8333... for d_min in the structure rattling functions. - - self.cdte_defect_folders = [ - "as_1_Cd_on_Te_-1", - "as_1_Cd_on_Te_-2", - "as_1_Cd_on_Te_0", - "as_1_Cd_on_Te_1", - "as_1_Cd_on_Te_2", - "as_1_Cd_on_Te_3", - "as_1_Cd_on_Te_4", - "as_1_Te_on_Cd_-1", - "as_1_Te_on_Cd_-2", - "as_1_Te_on_Cd_0", - "as_1_Te_on_Cd_1", - "as_1_Te_on_Cd_2", - "as_1_Te_on_Cd_3", - "as_1_Te_on_Cd_4", - "Int_Cd_1_0", - "Int_Cd_1_1", - "Int_Cd_1_2", - "Int_Cd_2_0", - "Int_Cd_2_1", - "Int_Cd_2_2", - "Int_Cd_3_0", - "Int_Cd_3_1", - "Int_Cd_3_2", - "Int_Te_1_-1", - "Int_Te_1_-2", - "Int_Te_1_0", - "Int_Te_1_1", - "Int_Te_1_2", - "Int_Te_1_3", - "Int_Te_1_4", - "Int_Te_1_5", - "Int_Te_1_6", - "Int_Te_2_-1", - "Int_Te_2_-2", - "Int_Te_2_0", - "Int_Te_2_1", - "Int_Te_2_2", - "Int_Te_2_3", - "Int_Te_2_4", - "Int_Te_2_5", - "Int_Te_2_6", - "Int_Te_3_-1", - "Int_Te_3_-2", - "Int_Te_3_0", - "Int_Te_3_1", - "Int_Te_3_2", - "Int_Te_3_3", - "Int_Te_3_4", - "Int_Te_3_5", - "Int_Te_3_6", - "vac_1_Cd_-1", - "vac_1_Cd_-2", - "vac_1_Cd_0", - "vac_1_Cd_1", - "vac_1_Cd_2", - "vac_2_Te_-1", - "vac_2_Te_-2", - "vac_2_Te_0", - "vac_2_Te_1", - "vac_2_Te_2", - ] - - self.parsed_default_incar_settings = { - k: v for k, v in vasp.default_incar_settings.items() if "#" not in k - } # pymatgen doesn't parsed commented lines - self.parsed_incar_settings_wo_comments = { - k: v - for k, v in self.parsed_default_incar_settings.items() - if "#" not in str(v) - } # pymatgen ignores comments after values - - def tearDown(self) -> None: - for i in self.cdte_defect_folders: - if_present_rm(i) # remove test-generated vac_1_Cd_0 folder if present - if os.path.exists("distortion_metadata.json"): - os.remove("distortion_metadata.json") - - for i in [ - "parsed_defects_dict.json", - "distortion_metadata.json", - "test_config.yml", - ]: - if_present_rm(i) - - for i in os.listdir("."): - if "distortion_metadata" in i: - os.remove(i) - if ".png" in i: - os.remove(i) - elif ( - "Vac_Cd" in i - or "v_Cd" in i - or "vac_1_Cd" in i - or "Int_Cd" in i - or "Wally_McDoodle" in i - or "pesky_defects" in i - ): - shutil.rmtree(i) - - for defect_folder in [ - dir for dir in os.listdir(self.EXAMPLE_RESULTS) - if os.path.isdir(f"{self.EXAMPLE_RESULTS}/{dir}") - ]: - for file in os.listdir(f"{self.EXAMPLE_RESULTS}/{defect_folder}"): - if file.endswith(".png"): - os.remove(f"{self.EXAMPLE_RESULTS}/{defect_folder}/{file}") - - # test create_folder and create_vasp_input simultaneously: - def test_create_vasp_input(self): - """Test create_vasp_input function for INCARs and POTCARs""" - vasp_defect_inputs = vasp_input.prepare_vasp_defect_inputs( - copy.deepcopy(self.cdte_doped_defect_dict) - ) - V_Cd_updated_charged_defect_dict = _update_struct_defect_dict( - vasp_defect_inputs["vac_1_Cd_0"], - self.V_Cd_minus0pt5_struc_rattled, - "V_Cd Rattled", - ) - # make unperturbed defect entry: - V_Cd_unperturbed_dict = _update_struct_defect_dict( - vasp_defect_inputs["vac_1_Cd_0"], - self.V_Cd_struc, - "V_Cd Unperturbed", - ) - V_Cd_charged_defect_dict = { - "Unperturbed": V_Cd_unperturbed_dict, - "Bond_Distortion_-50.0%": V_Cd_updated_charged_defect_dict - } - self.assertFalse(os.path.exists("vac_1_Cd_0")) - input._create_vasp_input( - "vac_1_Cd_0", - distorted_defect_dict=V_Cd_charged_defect_dict, - incar_settings=vasp.default_incar_settings, - ) - V_Cd_minus50_folder = "vac_1_Cd_0/Bond_Distortion_-50.0%" - self.assertTrue(os.path.exists(V_Cd_minus50_folder)) - V_Cd_POSCAR = Poscar.from_file(V_Cd_minus50_folder + "/POSCAR") - self.assertEqual(V_Cd_POSCAR.comment, "V_Cd Rattled") - self.assertEqual(V_Cd_POSCAR.structure, self.V_Cd_minus0pt5_struc_rattled) - - V_Cd_INCAR = Incar.from_file(V_Cd_minus50_folder + "/INCAR") - # check if default INCAR is subset of INCAR: - self.assertTrue( - self.parsed_incar_settings_wo_comments.items() <= V_Cd_INCAR.items() - ) - - V_Cd_KPOINTS = Kpoints.from_file(V_Cd_minus50_folder + "/KPOINTS") - self.assertEqual(V_Cd_KPOINTS.kpts, [[1, 1, 1]]) - - # check if POTCARs have been written: - self.assertTrue(os.path.isfile(V_Cd_minus50_folder + "/POTCAR")) - - # test with kwargs: (except POTCAR settings because we can't have this on the GitHub test - # server) - kwarg_incar_settings = { - "NELECT": 3, - "IBRION": 42, - "LVHAR": True, - "LWAVE": True, - "LCHARG": True, - "ENCUT": 200, - } - kwarged_incar_settings = self.parsed_incar_settings_wo_comments.copy() - kwarged_incar_settings.update(kwarg_incar_settings) - input._create_vasp_input( - "vac_1_Cd_0", - distorted_defect_dict=V_Cd_charged_defect_dict, - incar_settings=kwarged_incar_settings, - ) - V_Cd_kwarg_minus50_folder = "vac_1_Cd_0/Bond_Distortion_-50.0%" - self.assertTrue(os.path.exists(V_Cd_kwarg_minus50_folder)) - V_Cd_POSCAR = Poscar.from_file(V_Cd_kwarg_minus50_folder + "/POSCAR") - self.assertEqual(V_Cd_POSCAR.comment, "V_Cd Rattled") - self.assertEqual(V_Cd_POSCAR.structure, self.V_Cd_minus0pt5_struc_rattled) - - V_Cd_INCAR = Incar.from_file(V_Cd_kwarg_minus50_folder + "/INCAR") - # check if default INCAR is subset of INCAR: - self.assertFalse( - self.parsed_incar_settings_wo_comments.items() <= V_Cd_INCAR.items() - ) - self.assertTrue(kwarged_incar_settings.items() <= V_Cd_INCAR.items()) - - V_Cd_KPOINTS = Kpoints.from_file(V_Cd_kwarg_minus50_folder + "/KPOINTS") - self.assertEqual(V_Cd_KPOINTS.kpts, [[1, 1, 1]]) - - # check if POTCARs have been written: - self.assertTrue(os.path.isfile(V_Cd_kwarg_minus50_folder + "/POTCAR")) - - @patch("builtins.print") - def test_write_vasp_files(self, mock_print): - """Test write_vasp_files method""" - oxidation_states = {"Cd": +2, "Te": -2} - bond_distortions = list(np.arange(-0.6, 0.601, 0.05)) - - dist = input.Distortions( - self.cdte_defects, - oxidation_states=oxidation_states, - bond_distortions=bond_distortions, - local_rattle=False, - ) - distorted_defect_dict, _ = dist.write_vasp_files( - incar_settings={"ENCUT": 212, "IBRION": 0, "EDIFF": 1e-4}, - verbose=False, - ) - - # check if expected folders were created: - self.assertTrue(set(self.cdte_defect_folders).issubset(set(os.listdir()))) - # check expected info printing: - mock_print.assert_any_call( - "Applying ShakeNBreak...", - "Will apply the following bond distortions:", - "['-0.6', '-0.55', '-0.5', '-0.45', '-0.4', '-0.35', '-0.3', " - "'-0.25', '-0.2', '-0.15', '-0.1', '-0.05', '0.0', '0.05', " - "'0.1', '0.15', '0.2', '0.25', '0.3', '0.35', '0.4', '0.45', " - "'0.5', '0.55', '0.6'].", - "Then, will rattle with a std dev of 0.28 Å \n", - ) - mock_print.assert_any_call( - "\033[1m" + "\nDefect: vac_1_Cd" + "\033[0m" - ) # bold print - mock_print.assert_any_call( - "\033[1m" + "Number of missing electrons in neutral state: 2" + "\033[0m" - ) - mock_print.assert_any_call( - "\nDefect vac_1_Cd in charge state: -2. Number of distorted " - "neighbours: 0" - ) - mock_print.assert_any_call( - "\nDefect vac_1_Cd in charge state: -1. Number of distorted " - "neighbours: 1" - ) - mock_print.assert_any_call( - "\nDefect vac_1_Cd in charge state: 0. Number of distorted " "neighbours: 2" - ) - # test correct distorted neighbours based on oxidation states: - mock_print.assert_any_call( - "\nDefect vac_2_Te in charge state: -2. Number of distorted " - "neighbours: 4" - ) - mock_print.assert_any_call( - "\nDefect as_1_Cd_on_Te in charge state: -2. Number of " - "distorted neighbours: 2" - ) - mock_print.assert_any_call( - "\nDefect as_1_Te_on_Cd in charge state: -2. Number of " - "distorted neighbours: 2" - ) - mock_print.assert_any_call( - "\nDefect Int_Cd_1 in charge state: 0. Number of distorted " "neighbours: 2" - ) - mock_print.assert_any_call( - "\nDefect Int_Te_1 in charge state: -2. Number of distorted " - "neighbours: 0" - ) - - # check if correct files were created: - V_Cd_minus50_folder = "vac_1_Cd_0/Bond_Distortion_-50.0%" - self.assertTrue(os.path.exists(V_Cd_minus50_folder)) - V_Cd_POSCAR = Poscar.from_file(V_Cd_minus50_folder + "/POSCAR") - self.assertEqual( - V_Cd_POSCAR.comment, - "-50.0%__num_neighbours=2__vac_1_Cd", - ) # default - V_Cd_POSCAR.structure.remove_oxidation_states() - self.assertNotEqual(V_Cd_POSCAR.structure, self.V_Cd_minus0pt5_struc_rattled) - # V_Cd_minus0pt5_struc_rattled was with old default seed = 42 and stdev = 0.25 - - # Check INCAR - V_Cd_INCAR = Incar.from_file(V_Cd_minus50_folder + "/INCAR") - # check if default INCAR is subset of INCAR: (not here because we set ENCUT) - self.assertFalse( - self.parsed_incar_settings_wo_comments.items() <= V_Cd_INCAR.items() - ) - self.assertEqual(V_Cd_INCAR.pop("ENCUT"), 212) - self.assertEqual(V_Cd_INCAR.pop("IBRION"), 0) - self.assertEqual(V_Cd_INCAR.pop("EDIFF"), 1e-4) - self.assertEqual(V_Cd_INCAR.pop("ROPT"), "1e-3 1e-3") - parsed_settings = self.parsed_incar_settings_wo_comments.copy() - parsed_settings.pop("ENCUT") - self.assertTrue( - parsed_settings.items() - <= V_Cd_INCAR.items() # matches after - # removing kwarg settings - ) - # Check KPOINTS - V_Cd_KPOINTS = Kpoints.from_file(V_Cd_minus50_folder + "/KPOINTS") - self.assertEqual(V_Cd_KPOINTS.kpts, [[1, 1, 1]]) - - # check if POTCARs have been written: - self.assertTrue(os.path.isfile(V_Cd_minus50_folder + "/POTCAR")) - - # Check POSCARs - Int_Cd_2_minus60_folder = "Int_Cd_2_0/Bond_Distortion_-60.0%" - self.assertTrue(os.path.exists(Int_Cd_2_minus60_folder)) - Int_Cd_2_POSCAR = Poscar.from_file(Int_Cd_2_minus60_folder + "/POSCAR") - self.assertEqual( - Int_Cd_2_POSCAR.comment, - "-60.0%__num_neighbours=2__Int_Cd_2", - ) - struc = Int_Cd_2_POSCAR.structure - struc.remove_oxidation_states() - self.assertEqual(struc, self.Int_Cd_2_minus0pt6_struc_rattled) - - # check INCAR - V_Cd_INCAR = Incar.from_file(V_Cd_minus50_folder + "/INCAR") - Int_Cd_2_INCAR = Incar.from_file(Int_Cd_2_minus60_folder + "/INCAR") - # neutral even-electron INCARs the same except for NELECT: - for incar in [V_Cd_INCAR, Int_Cd_2_INCAR]: - incar.pop("NELECT") # https://tenor.com/bgVv9.gif - self.assertEqual(V_Cd_INCAR, Int_Cd_2_INCAR) - # Kpoints - Int_Cd_2_KPOINTS = Kpoints.from_file(Int_Cd_2_minus60_folder + "/KPOINTS") - self.assertEqual(Int_Cd_2_KPOINTS.kpts, [[1, 1, 1]]) - # check if POTCARs have been written: - self.assertTrue(os.path.isfile(Int_Cd_2_minus60_folder + "/POTCAR")) - - def test_plot(self): - """ - Test plot() function. - The plots used for comparison have been generated with the Montserrat font - (available in the fonts directory). - """ - # Test the following options: - # --defect, --path, --format, --units, --colorbar, --metric, --no_title, --verbose - defect = "v_Ti_0" - dumpfn( - { - "distortions": {-0.4: -1176.28458753}, - "Unperturbed": -1173.02056574, - }, - f"{self.EXAMPLE_RESULTS}/{defect}/{defect}.yaml", - ) - if os.path.exists(f"{self.EXAMPLE_RESULTS}/distortion_metadata.json"): - os.remove(f"{self.EXAMPLE_RESULTS}/distortion_metadata.json") - runner = CliRunner() - with warnings.catch_warnings(record=True) as w: - result = runner.invoke( - snb, - [ - "plot", - "-d", - defect, - "-p", - self.EXAMPLE_RESULTS, - "--units", - "meV", - "--format", - "png", - "--colorbar", - "--metric", - "disp", - "-nt", # No title - "-v", - ], - catch_exceptions=False, - ) - self.assertTrue( - os.path.exists(os.path.join(self.EXAMPLE_RESULTS, f"{defect}/{defect}.png")) - ) - compare_images( - os.path.join(self.EXAMPLE_RESULTS, f"{defect}/{defect}.png"), - f"{_DATA_DIR}/local_baseline_plots/vac_1_Ti_0_cli_colorbar_disp.png", - tol=2.0, - ) # only locally (on Github Actions, saved image has a different size) - self.tearDown() - [ - os.remove(os.path.join(self.EXAMPLE_RESULTS, defect, file)) - for file in os.listdir(os.path.join(self.EXAMPLE_RESULTS, defect)) - if "yaml" in file or "png" in file - ] - - # Test --all option, with the distortion_metadata.json file present to parse number of - # distorted neighbours and their identities - fake_distortion_metadata = { - "defects": { - "v_Cd": { - "charges": { - "0": { - "num_nearest_neighbours": 2, - "distorted_atoms": [[33, "Te"], [42, "Te"]], - }, - "-1": { - "num_nearest_neighbours": 1, - "distorted_atoms": [ - [33, "Te"], - ], - }, - } - }, - "v_Ti": { - "charges": { - "0": { - "num_nearest_neighbours": 3, - "distorted_atoms": [[33, "O"], [42, "O"], [40, "O"]], - }, - } - }, - } - } - with open(f"{self.EXAMPLE_RESULTS}/distortion_metadata.json", "w") as f: - f.write(json.dumps(fake_distortion_metadata, indent=4)) - result = runner.invoke( - snb, - [ - "plot", - "--all", - "-p", - self.EXAMPLE_RESULTS, - "-f", - "png", - ], - catch_exceptions=False, - ) - self.assertTrue( - os.path.exists(os.path.join(self.EXAMPLE_RESULTS, f"{defect}/{defect}.png")) - ) - self.assertTrue( - os.path.exists(os.path.join(self.EXAMPLE_RESULTS, "v_Cd_0/v_Cd_0.png")) - ) - self.assertTrue( - os.path.exists(os.path.join(self.EXAMPLE_RESULTS, "v_Cd_-1/v_Cd_-1.png")) - ) - compare_images( - os.path.join(self.EXAMPLE_RESULTS, "v_Cd_0/v_Cd_0.png"), - f"{_DATA_DIR}/local_baseline_plots/vac_1_Cd_0_cli_default.png", - tol=2.0, - ) # only locally (on Github Actions, saved image has a different size) - [ - os.remove(os.path.join(self.EXAMPLE_RESULTS, defect, file)) - for file in os.listdir(os.path.join(self.EXAMPLE_RESULTS, defect)) - if "yaml" in file or "png" in file - ] - - # generate docs example plots: - shutil.copytree( - f"{self.EXAMPLE_RESULTS}/v_Cd_0", f"{self.EXAMPLE_RESULTS}/orig_v_Cd_0" - ) - for i in range(1,7): - shutil.copyfile( - f"{self.EXAMPLE_RESULTS}/v_Cd_0/Unperturbed/CONTCAR", - f"{self.EXAMPLE_RESULTS}/v_Cd_0/Bond_Distortion_{i}0.0%/CONTCAR", - ) - energies_dict = loadfn(f"{self.EXAMPLE_RESULTS}/v_Cd_0/v_Cd_0.yaml") - energies_dict["distortions"][-0.5] = energies_dict["distortions"][-0.6] - dumpfn(energies_dict, f"{self.EXAMPLE_RESULTS}/v_Cd_0/v_Cd_0.yaml") - shutil.copyfile( - f"{self.EXAMPLE_RESULTS}/v_Cd_0/Bond_Distortion_-60.0%/CONTCAR", - f"{self.EXAMPLE_RESULTS}/v_Cd_0/Bond_Distortion_-50.0%/CONTCAR", - ) - - result = runner.invoke( - snb, - [ - "plot", - "-d", - "v_Cd_0", - "-cb", - "-p", - self.EXAMPLE_RESULTS, - "-f", - "svg", - ], - ) - shutil.copyfile(f"{self.EXAMPLE_RESULTS}/v_Cd_0/v_Cd_0.svg", - "../docs/v_Cd_0_colorbar.svg") - result = runner.invoke( - snb, - [ - "plot", - "-d", - "v_Cd_0", - "-p", - self.EXAMPLE_RESULTS, - "-f", - "svg", - ], - ) - shutil.copyfile(f"{self.EXAMPLE_RESULTS}/v_Cd_0/v_Cd_0.svg", "../docs/v_Cd_0.svg") - shutil.rmtree(f"{self.EXAMPLE_RESULTS}/v_Cd_0") - shutil.move( - f"{self.EXAMPLE_RESULTS}/orig_v_Cd_0", f"{self.EXAMPLE_RESULTS}/v_Cd_0" - ) - os.remove(f"{self.EXAMPLE_RESULTS}/distortion_metadata.json") - self.tearDown() - - def test_generate_all_input_file(self): - """Test generate_all() function when user specifies input_file""" - defects_dir = f"pesky_defects" - defect_name = "vac_1_Cd" - os.mkdir(defects_dir) - os.mkdir(f"{defects_dir}/{defect_name}") # non-standard defect name - shutil.copyfile( - f"{self.VASP_CDTE_DATA_DIR}/CdTe_V_Cd_POSCAR", - f"{defects_dir}/{defect_name}/POSCAR", - ) - test_yml = f""" - defects: - {defect_name}: - charges: [0,] - defect_coords: [0.0, 0.0, 0.0] - bond_distortions: [0.3,] - POTCAR: - Cd: Cd_GW - """ - with open("test_config.yml", "w") as fp: - fp.write(test_yml) - - # Test VASP - with open("INCAR", "w") as fp: - fp.write("IBRION = 1 \n GGA = PS") - runner = CliRunner() - result = runner.invoke( - snb, - [ - "generate_all", - "-d", - f"{defects_dir}/", - "-b", - f"{self.VASP_CDTE_DATA_DIR}/CdTe_Bulk_Supercell_POSCAR", - "--code", - "vasp", - "--input_file", - "INCAR", - "--config", - "test_config.yml", - ], - catch_exceptions=True, - ) - dist = "Unperturbed" - incar_dict = Incar.from_file(f"{defect_name}_0/{dist}/INCAR").as_dict() - self.assertEqual(incar_dict["GGA"].lower(), "PS".lower()) - self.assertEqual(incar_dict["IBRION"], 1) - for file in ["KPOINTS", "POTCAR", "POSCAR"]: - self.assertTrue(os.path.exists(f"{defect_name}_0/{dist}/{file}")) - # Check POTCAR generation - with open(f"{defect_name}_0/{dist}/POTCAR") as myfile: - first_line = myfile.readline() - self.assertIn("PAW_PBE Cd_GW", first_line) - - shutil.rmtree(f"{defect_name}_0") - os.remove("INCAR") - - # test warning when input file doesn't match expected format: - os.remove("distortion_metadata.json") - with warnings.catch_warnings(record=True) as w: - result = runner.invoke( - snb, - [ - "generate_all", - "-d", - f"{defects_dir}/", - "-b", - f"{self.VASP_CDTE_DATA_DIR}/CdTe_Bulk_Supercell_POSCAR", - "--code", - "vasp", - "--input_file", - "test_config.yml", - "--config", - "test_config.yml", - ], - catch_exceptions=True, - ) - dist = "Unperturbed" - incar_dict = Incar.from_file(f"{defect_name}_0/{dist}/INCAR").as_dict() - self.assertEqual(incar_dict["IBRION"], 2) # default setting - # assert UserWarning about unparsed input file - user_warnings = [warning for warning in w if warning.category == UserWarning] - self.assertEqual(len(user_warnings), 1) - self.assertEqual( - "Input file test_config.yml specified but no valid INCAR tags found. " - "Should be in the format of VASP INCAR file.", - str(user_warnings[-1].message), - ) - for file in ["KPOINTS", "POTCAR", "POSCAR"]: - self.assertTrue(os.path.exists(f"{defect_name}_0/{dist}/{file}")) - shutil.rmtree(f"{defect_name}_0") - - # Test CASTEP - with open("castep.param", "w") as fp: - fp.write("XC_FUNCTIONAL: PBE \n MAX_SCF_CYCLES: 100 \n CHARGE: 0") - runner = CliRunner() - result = runner.invoke( - snb, - [ - "generate_all", - "-d", - f"{defects_dir}/", - "-b", - f"{self.VASP_CDTE_DATA_DIR}/CdTe_Bulk_Supercell_POSCAR", - "--code", - "castep", - "--input_file", - "castep.param", - "--config", - "test_config.yml", - ], - catch_exceptions=True, - ) - dist = "Unperturbed" - with open(f"{defect_name}_0/{dist}/castep.param") as fp: - castep_lines = [line.strip() for line in fp.readlines()[-3:]] - self.assertEqual( - ["XC_FUNCTIONAL: PBE", "MAX_SCF_CYCLES: 100", "CHARGE: 0"], castep_lines - ) - shutil.rmtree(f"{defect_name}_0") - os.remove("castep.param") - - # Test CP2K - runner = CliRunner() - result = runner.invoke( - snb, - [ - "generate_all", - "-d", - f"{defects_dir}/", - "-b", - f"{self.VASP_CDTE_DATA_DIR}/CdTe_Bulk_Supercell_POSCAR", - "--code", - "cp2k", - "--input_file", - f"{self.DATA_DIR}/cp2k/cp2k_input_mod.inp", - "--config", - "test_config.yml", - ], - catch_exceptions=False, - ) - dist = "Unperturbed" - self.assertTrue(os.path.exists(f"{defect_name}_0/{dist}")) - with open(f"{defect_name}_0/{dist}/cp2k_input.inp") as fp: - input_cp2k = fp.readlines() - self.assertEqual( - "CUTOFF [eV] 800 ! PW cutoff", - input_cp2k[15].strip(), - ) - shutil.rmtree(f"{defect_name}_0") - - # Test Quantum Espresso - test_yml = f""" - defects: - {defect_name}: - charges: [0,] - defect_coords: [0.0, 0.0, 0.0] - bond_distortions: [0.3,] - pseudopotentials: - 'Cd': 'Cd_pbe_v1.uspp.F.UPF' - 'Te': 'Te.pbe-n-rrkjus_psl.1.0.0.UPF' - """ - with open("test_config.yml", "w") as fp: - fp.write(test_yml) - runner = CliRunner() - result = runner.invoke( - snb, - [ - "generate_all", - "-d", - f"{defects_dir}/", - "-b", - f"{self.VASP_CDTE_DATA_DIR}/CdTe_Bulk_Supercell_POSCAR", - "--code", - "espresso", - "--input_file", - f"{self.DATA_DIR}/quantum_espresso/qe.in", - "--config", - "test_config.yml", - ], - catch_exceptions=False, - ) - dist = "Unperturbed" - with open(f"{defect_name}_0/{dist}/espresso.pwi") as fp: - input_qe = fp.readlines() - self.assertEqual( - "title = 'Si bulk'", - input_qe[2].strip(), - ) - shutil.rmtree(f"{defect_name}_0") - - # Test FHI-aims - runner = CliRunner() - result = runner.invoke( - snb, - [ - "generate_all", - "-d", - f"{defects_dir}/", - "-b", - f"{self.VASP_CDTE_DATA_DIR}/CdTe_Bulk_Supercell_POSCAR", - "--code", - "fhiaims", - "--input_file", - f"{self.DATA_DIR}/fhi_aims/control.in", - "--config", - "test_config.yml", - ], - catch_exceptions=False, - ) - dist = "Unperturbed" - with open(f"{defect_name}_0/{dist}/control.in") as fp: - input_aims = fp.readlines() - self.assertEqual( - "xc pbe", - input_aims[6].strip(), - ) - self.assertEqual( - "sc_iter_limit 100.0", - input_aims[10].strip(), - ) - shutil.rmtree(f"{defect_name}_0") - self.tearDown() - - def test_generate(self): - "Test generate command" - - test_yml = """ -bond_distortions: [-0.5,] -stdev: 0.15 -d_min: 2.1250262890187375 # 0.75 * 2.8333683853583165 -nbr_cutoff: 3.4 -n_iter: 3 -active_atoms: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30] # np.arange(0,31) -width: 0.3 -max_attempts: 10000 -max_disp: 1.0 -seed: 20 -local_rattle: False -POTCAR: - Cd: Cd_GW -""" - with open("test_config.yml", "w+") as fp: - fp.write(test_yml) - defect_name = "v_Cd" # SnB default name - runner = CliRunner() - result = runner.invoke( - snb, - [ - "generate", - "-d", - f"{self.VASP_CDTE_DATA_DIR}/CdTe_V_Cd_POSCAR", - "-b", - f"{self.VASP_CDTE_DATA_DIR}/CdTe_Bulk_Supercell_POSCAR", - "-c 0", - "-v", - "--config", - f"test_config.yml", - ], - catch_exceptions=False, - ) - self.assertEqual(result.exit_code, 0) - self.assertTrue(os.path.exists(f"./{defect_name}_0")) - self.assertTrue(os.path.exists(f"./{defect_name}_0/Bond_Distortion_-50.0%")) - V_Cd_kwarged_POSCAR = Poscar.from_file( - f"./{defect_name}_0/Bond_Distortion_-50.0%/POSCAR" - ) - self.assertEqual( - V_Cd_kwarged_POSCAR.structure, self.V_Cd_minus0pt5_struc_kwarged - ) - for file in ["KPOINTS", "POTCAR", "INCAR"]: - self.assertTrue( - os.path.exists(f"{defect_name}_0/Bond_Distortion_-50.0%/{file}") - ) - # Check POTCAR file - with open(f"{defect_name}_0/Bond_Distortion_-50.0%/POTCAR") as myfile: - first_line = myfile.readline() - self.assertIn("PAW_PBE Cd_GW", first_line) - # Check KPOINTS file - kpoints = Kpoints.from_file( - f"{defect_name}_0/Bond_Distortion_-50.0%/" + "KPOINTS" - ) - self.assertEqual(kpoints.kpts, [[1, 1, 1]]) - # Check INCAR - incar = Incar.from_file(f"{defect_name}_0/Bond_Distortion_-50.0%/" + "INCAR") - self.assertEqual(incar.pop("IBRION"), 2) - self.assertEqual(incar.pop("EDIFF"), 1e-5) - self.assertEqual(incar.pop("ROPT"), "1e-3 1e-3") - - # Test custom name - defect_name = "vac_1_Cd" - result = runner.invoke( - snb, - [ - "generate", - "-d", - f"{self.VASP_CDTE_DATA_DIR}/CdTe_V_Cd_POSCAR", - "-b", - f"{self.VASP_CDTE_DATA_DIR}/CdTe_Bulk_Supercell_POSCAR", - "-c 0", - "-n", - "vac_1_Cd", - "--config", - f"test_config.yml", - ], - catch_exceptions=False, - ) - cwd = os.getcwd() - self.assertEqual(result.exit_code, 0) - # self.assertTrue(os.path.exists(f"{cwd}/vac_1_Cd_0")) - self.assertTrue(os.path.exists(f"{cwd}/vac_1_Cd_0/Bond_Distortion_-50.0%")) - - # test warning when input file doesn't match expected format: - os.remove("distortion_metadata.json") - with warnings.catch_warnings(record=True) as w: - result = runner.invoke( - snb, - [ - "generate", - "-d", - f"{self.VASP_CDTE_DATA_DIR}/CdTe_V_Cd_POSCAR", - "-b", - f"{self.VASP_CDTE_DATA_DIR}/CdTe_Bulk_Supercell_POSCAR", - "-c 0", - "-v", - "--input_file", - f"test_config.yml", - ], - catch_exceptions=False, - ) - incar_dict = Incar.from_file( - f"{defect_name}_0/Bond_Distortion_-50.0%/INCAR" - ).as_dict() - self.assertEqual(incar_dict["IBRION"], 2) # default setting - # assert UserWarning about unparsed input file - user_warnings = [warning for warning in w if warning.category == UserWarning] - self.assertEqual(len(user_warnings), 2) # wrong INCAR format and overwriting folder - self.assertTrue( - any("Input file test_config.yml specified but no valid INCAR tags found. " - "Should be in the format of VASP INCAR file." - in str(warning.message) for warning in user_warnings) - ) - self.assertTrue( # here we get this warning because no Unperturbed structures were - # written so couldn't be compared - any(f"The previously-generated defect folder v_Cd_0 in " - f"{os.path.basename(os.path.abspath('.'))} has the same Unperturbed defect structure " - f"as the current defect species: v_Cd_0. ShakeNBreak files in v_Cd_0 will be " - f"overwritten." in str(warning.message) for warning in user_warnings) - ) - for file in ["KPOINTS", "POTCAR", "POSCAR"]: - self.assertTrue( - os.path.exists(f"{defect_name}_0/Bond_Distortion_-50.0%/{file}") - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_shakenbreak.py b/tests/test_shakenbreak.py index 90427fe8..6d039c3d 100644 --- a/tests/test_shakenbreak.py +++ b/tests/test_shakenbreak.py @@ -105,7 +105,7 @@ def test_SnB_integration(self): oxidation_states=oxidation_states, ) distortion_defect_dict, structures_defect_dict = dist.write_vasp_files( - incar_settings={"ENCUT": 212, "IBRION": 0, "EDIFF": 1e-4}, + user_incar_settings={"ENCUT": 212, "IBRION": 0, "EDIFF": 1e-4}, verbose=False, ) shutil.rmtree("vac_1_Cd_0") diff --git a/tutorials/ShakeNBreak_Example_Workflow.ipynb b/tutorials/ShakeNBreak_Example_Workflow.ipynb index a22e905a..c9d0023b 100644 --- a/tutorials/ShakeNBreak_Example_Workflow.ipynb +++ b/tutorials/ShakeNBreak_Example_Workflow.ipynb @@ -2466,7 +2466,7 @@ "id": "771afcb5-1fcd-4a77-99ea-63899513992d", "metadata": {}, "source": [ - "Using the `incar_settings` optional argument for `Distortions.write_vasp_files()` above, we can also specify some custom `INCAR` tags to match our converged `ENCUT` for this system and optimal `NCORE` for the HPC we will run the calculations on. More information on the distortions generated can be obtained by setting `verbose = True`. Note that any `INCAR` flags that aren't numbers (e.g. `{\"IBRION\": 1}`) or True/False (e.g. `{\"LREAL\": False}`) need to be input as strings with quotation marks (e.g. `{\"ALGO\": \"All\"}`)." + "Using the `user_incar_settings` optional argument for `Distortions.write_vasp_files()` above, we can also specify some custom `INCAR` tags to match our converged `ENCUT` for this system and optimal `NCORE` for the HPC we will run the calculations on. More information on the distortions generated can be obtained by setting `verbose = True`. Note that any `INCAR` flags that aren't numbers (e.g. `{\"IBRION\": 1}`) or True/False (e.g. `{\"LREAL\": False}`) need to be input as strings with quotation marks (e.g. `{\"ALGO\": \"All\"}`)." ] }, { @@ -2536,7 +2536,7 @@ "Note that the `NELECT` `INCAR` tag (number of electrons) is automatically determined based on the choice\n", " of `POTCAR`s. The default in `ShakeNBreak` (and `doped`) is to use the\n", "[`MPRelaxSet` `POTCAR` choices](https://github.com/materialsproject/pymatgen/blob/master/pymatgen/io/vasp/MPRelaxSet.yaml), but if you're using\n", - "different ones, make sure to set `potcar_settings` in `write_vasp_files()`, so that `NELECT` is then set\n", + "different ones, make sure to set `user_potcar_settings` in `write_vasp_files()`, so that `NELECT` is then set\n", "accordingly.\n", "This requires the `pymatgen` config file `$HOME/.pmgrc.yaml` to be properly set up as detailed on the [Installation](https://shakenbreak.readthedocs.io/en/latest/Installation.html) docs page." ]