diff --git a/docs/Analysis.rst b/docs/Analysis.rst index 3bcdd048..3c3c57c9 100644 --- a/docs/Analysis.rst +++ b/docs/Analysis.rst @@ -24,15 +24,11 @@ Where ``defects_folder`` is the path to the top level directory containing the d different from the current directory. Instead of a single defect, we can parse the results for **all** defects present -in a given/current directory using the ``-a``/``--all`` flag: - -.. code:: bash - - $ snb-parse -a - -This generates a ``yaml`` file for each defect, mapping each distortion to the -final energy of the relaxed structures (in eV). These files are saved to the -corresponding defect directory (e.g. ``defects_folder/v_Cd_0/v_Cd_0.yaml``). +in a given/current directory by running ``snb-parse`` from the top-level directory +containing our defect folders. This generates a ``yaml`` file for each defect, +mapping each distortion to the final energy of the relaxed structures (in eV). +These files are saved to the corresponding defect directory +(e.g. ``defects_folder/v_Cd_0/v_Cd_0.yaml``). .. code:: yaml @@ -63,12 +59,8 @@ was used (if not :code:`VASP`) and which reference structure to use (default = ` $ snb-analyse --defect v_Cd_0 --code FHI-aims --path defects_folder --ref_struct -0.4 --verbose -Again if we want to analyse the results for **all** defects present in a given/current directory, we can use the -``-a``/``--all`` flag: - -.. code:: bash - - $ snb-analyse -a +Again if we want to analyse the results for **all** defects present in a given/current directory, +we can just run ``snb-analyse`` from the top-level directory containing the defect folders. .. NOTE:: Further analysis tools are provided through the python API. These are documented in @@ -116,12 +108,8 @@ was used (if not :code:`VASP`) and other options (what ``metric`` to use for col $ snb-plot --defect v_Cd_0 --code FHI-aims --path defects_folder --colorbar -0.4 --metric disp --units meV --verbose -Again if we want to plot the results for **all** defects present in a given/current directory, we can use the -``-a``/``--all`` flag: - -.. code:: bash - - $ snb-plot -a +Again if we want to plot the results for **all** defects present in a given/current directory, we can +just run ``snb-plot`` from the top-level directory containing the defect folders. .. TIP:: See ``snb-plot -h`` or `the CLI docs `_ diff --git a/docs/Generation.rst b/docs/Generation.rst index 3e0943e8..c6b86d54 100644 --- a/docs/Generation.rst +++ b/docs/Generation.rst @@ -198,12 +198,16 @@ Submitting the geometry optimisations ======================================= Once the input files have been generated, we can submit the geometry optimisations -for a single or all defects using the ``snb-run`` command. -To submit all defects present in the current directory: +for a single or all defects using the ``snb-run`` command: .. code:: bash - $ snb-run -a + $ snb-run + +If ``snb-run`` is run in the top-level directory (i.e. the directory containing the defect folders), +it will loop through all defect folders present and attempt to submit the distortion calculations for +each. Alternatively, it can be run within a single defect folder to just submit the calculations for +that defect. This assumes the ``SGE`` queuing system (i.e. ``qsub`` = job submission command) for the HPC and a job script name of ``job`` by default, but again can be controlled with the ``--submit-command`` and ``--job-script`` flags @@ -212,14 +216,7 @@ script file name of ``my_job_script.sh``, we would use: .. code:: bash - $ snb-run --submit-command sbatch --job-script my_job_script.sh --all - - -To submit a single defect, we can simply run the command :code:`snb-run` within the defect folder: - -.. code:: bash - - $ snb-run + $ snb-run --submit-command sbatch --job-script my_job_script.sh ``snb-run`` can be used to submit the initial geometry optimisation calculations, as well as automatically continuing and resubmitting calculations that have not yet converged (and handle calculations which have failed) as discussed in diff --git a/shakenbreak/cli.py b/shakenbreak/cli.py index 00d72dd3..48464bda 100644 --- a/shakenbreak/cli.py +++ b/shakenbreak/cli.py @@ -761,15 +761,6 @@ def parse_defect_position(defect_name, defect_settings): type=str, default=None, ) -@click.option( - "--all", - "-a", - help="Loop through all defect folders (then through their distortion subfolders) in the " - "current directory", - default=False, - is_flag=True, - show_default=True, -) @click.option( "--verbose", "-v", @@ -778,18 +769,22 @@ def parse_defect_position(defect_name, defect_settings): is_flag=True, show_default=True, ) -def run(submit_command, job_script, job_name_option, all, verbose): +def run(submit_command, job_script, job_name_option, verbose): """ Loop through distortion subfolders for a defect, when run within a defect folder, or for all - defect folders in the current (top-level) directory if the --all (-a) flag is set, and submit - jobs to the HPC scheduler. + defect folders in the current (top-level) directory, and submit jobs to the HPC scheduler. As well as submitting the initial geometry optimisations, can automatically continue and resubmit calculations that have not yet converged (and handle those which have failed), see: https://shakenbreak.readthedocs.io/en/latest/Generation.html#submitting-the-geometry-optimisations """ optional_flags = "-" - if all: + # determine if running from within a defect directory or from the top level directory: + if not _running_in_defect_dir( + path=".", + warning_substring="calculations will only be submitted for the distortion folders in this " + "directory.", + ): optional_flags += "a" if verbose: optional_flags += "v" @@ -822,14 +817,6 @@ def run(submit_command, job_script, job_name_option, all, verbose): type=str, default=None, ) -@click.option( - "--all", - "-a", - help="Parse energies for all defects present in the specified/current directory", - default=False, - is_flag=True, - show_default=True, -) @click.option( "--path", "-p", @@ -854,24 +841,22 @@ def run(submit_command, job_script, job_name_option, all, verbose): is_flag=True, show_default=True, ) -def parse(defect, all, path, code, verbose): +def parse(defect, path, code, verbose): """ Parse final energies of defect structures from relaxation output files. Parsed energies are written to a `yaml` file in the corresponding defect directory. + + Can be run within a single defect folder, or in the top-level directory (either + specifying `defect` or looping through all defect folders). """ if defect: _ = io.parse_energies(defect, path, code, verbose=verbose) - elif all: - defect_dirs = _parse_defect_dirs(path) - _ = [io.parse_energies(defect, path, code, verbose=verbose) for defect in defect_dirs] - else: + elif _running_in_defect_dir( + path=path, + warning_substring="calculations will only be parsed for the distortion folders in this directory.", + ): # assume current directory is the defect folder try: - if path != ".": - warnings.warn( - "`--path` option ignored when running from within defect folder (i.e. " - "when `--defect` is not specified." - ) cwd = os.getcwd() defect = cwd.split("/")[-1] path = cwd.rsplit("/", 1)[0] @@ -880,10 +865,14 @@ def parse(defect, all, path, code, verbose): raise Exception( f"Could not parse defect '{defect}' in directory '{path}'. Please either specify " f"a defect to parse (with option --defect), run from within a single defect " - f"directory (without setting --defect) or use the --all flag to parse all " - f"defects in the specified/current directory." + f"directory (without setting --defect) or run from the top-level directory to " + f"analyse all defects in the specified/current directory." ) from exc + else: + defect_dirs = _parse_defect_dirs(path) + _ = [io.parse_energies(defect, path, code, verbose=verbose) for defect in defect_dirs] + @snb.command( name="analyse", @@ -899,14 +888,6 @@ def parse(defect, all, path, code, verbose): type=str, default=None, ) -@click.option( - "--all", - "-a", - help="Analyse all defects present in specified directory", - default=False, - is_flag=True, - show_default=True, -) @click.option( "--path", "-p", @@ -941,10 +922,13 @@ def parse(defect, all, path, code, verbose): is_flag=True, show_default=True, ) -def analyse(defect, all, path, code, ref_struct, verbose): +def analyse(defect, path, code, ref_struct, verbose): """ Generate `csv` file mapping each distortion to its final energy (in eV) and its mean displacement (in Angstrom and relative to `ref_struct`). + + Can be run within a single defect folder, or in the top-level directory (either + specifying `defect` or looping through all defect folders). """ def analyse_single_defect(defect, path, code, ref_struct, verbose): @@ -970,23 +954,22 @@ def analyse_single_defect(defect, path, code, ref_struct, verbose): dataframe.to_csv(f"{path}/{defect}/{defect}.csv") # change name to results.csv? print(f"Saved results to {path}/{defect}/{defect}.csv") - if all: - defect_dirs = _parse_defect_dirs(path) - for defect in defect_dirs: - print(f"\nAnalysing {defect}...") - analyse_single_defect(defect, path, code, ref_struct, verbose) - - elif defect is None: + if defect is None and _running_in_defect_dir( + path=path, + warning_substring="calculations will only be analysed for the distortion folders in this " + "directory.", + ): # assume current directory is the defect folder - if path != ".": - warnings.warn( - "`--path` option ignored when running from within defect folder (i.e. when `--defect` is " - "not specified." - ) cwd = os.getcwd() defect = cwd.split("/")[-1] path = cwd.rsplit("/", 1)[0] + if defect is None: # then all + defect_dirs = _parse_defect_dirs(path) + for defect in defect_dirs: + print(f"\nAnalysing {defect}...") + analyse_single_defect(defect, path, code, ref_struct, verbose) + defect = defect.strip("/") # Remove trailing slash if present # Check if defect present in path: if path == ".": @@ -1004,9 +987,9 @@ def analyse_single_defect(defect, path, code, ref_struct, verbose): except Exception as exc: raise Exception( f"Could not analyse defect '{defect}' in directory '{path}'. Please either specify a " - f"defect to analyse (with option --defect), run from within a single defect directory (" - f"without setting --defect) or use the --all flag to analyse all defects in the " - f"specified/current directory." + f"defect to analyse (with option --defect), run from within a single defect directory " + f"(without setting --defect) or run from the top-level directory to " + f"analyse all defects in the specified/current directory." ) from exc @@ -1024,20 +1007,12 @@ def analyse_single_defect(defect, path, code, ref_struct, verbose): type=str, default=None, ) -@click.option( - "--all", - "-a", - help="Analyse all defects present in current/specified directory", - default=False, - is_flag=True, - show_default=True, -) @click.option( "--min_energy", "-min", help="Minimum energy difference (in eV) between the ground-state " "distortion and the `Unperturbed` structure to generate the " - "distortion plot, when `--all` is set.", + "distortion plot, when running from the top-level folder.", default=0.05, type=float, show_default=True, @@ -1124,14 +1099,12 @@ def analyse_single_defect(defect, path, code, ref_struct, verbose): @click.option( "--style-file", "-s", - help="Path to a mplstyle file to use for the plot(s).", - default="shakenbreak.mplstyle", + help="Path to a mplstyle file to use for the plot(s). Default is 'shakenbreak.mplstyle'.", + default=None, type=click.Path(exists=True, dir_okay=False), - show_default=True, ) def plot( defect, - all, min_energy, path, code, @@ -1147,16 +1120,24 @@ def plot( """ Generate energy vs distortion plots. Optionally, the structural similarity between configurations can be illustrated with a colorbar. + + Can be run within a single defect folder, or in the top-level directory + (either specifying `defect` or looping through all defect folders). """ - if style_file == "shakenbreak": + if style_file is None: style_file = f"{os.path.dirname(os.path.abspath(__file__))}/shakenbreak.mplstyle" - if all: - if defect is not None: - warnings.warn( - "The option `--defect` is ignored when using the `--all` flag. (All defects in " - f"`--path` = {path} will be plotted)." - ) + if defect is None and _running_in_defect_dir( + path=path, + warning_substring="calculations will only be analysed and plotted for the distortion folders in " + "this directory.", + ): + # assume current directory is the defect folder + cwd = os.getcwd() + defect = cwd.split("/")[-1] + path = cwd.rsplit("/", 1)[0] + + if defect is None: # then all defect_dirs = _parse_defect_dirs(path) for defect in defect_dirs: if verbose: @@ -1182,17 +1163,6 @@ def plot( close_figures=True, # reduce memory usage with snb-plot with many defects at once ) - if defect is None: - # assume current directory is the defect folder - if path != ".": - warnings.warn( - "`--path` option ignored when running from within defect folder (" - "i.e. when `--defect` is not specified." - ) - cwd = os.getcwd() - defect = cwd.split("/")[-1] - path = cwd.rsplit("/", 1)[0] - defect = defect.strip("/") # Remove trailing slash if present # Check if defect present in path: if path == ".": @@ -1249,8 +1219,8 @@ def plot( raise Exception( f"Could not analyse & plot defect '{defect}' in directory '{path}'. Please either " f"specify a defect to analyse (with option --defect), run from within a single " - f"defect directory (without setting --defect) or use the --all flag to analyse all " - f"defects in the specified/current directory." + f"defect directory (without setting --defect) or run from the top-level directory to " + f"analyse all defects in the specified/current directory." ) from exc @@ -1342,6 +1312,44 @@ def regenerate(path, code, filename, min_energy, metastable, verbose): ) +def _running_in_defect_dir(path: str = ".", warning_substring: str = ""): + warning_substring = warning_substring or ( + "the groundstate structure from the distortion folders in this directory will be generated." + ) + if any( + dir + for dir in os.listdir() + if os.path.isdir(dir) + and any(substring in dir for substring in ["Bond_Distortion", "Rattled", "Unperturbed", "Dimer"]) + ): # distortion subfolders in cwd + # check if defect folders also in cwd + for dir in [dir for dir in os.listdir() if os.path.isdir(dir)]: + defect_name = None + try: + defect_name = format_defect_name(dir, include_site_info_in_name=False) + except Exception: + with contextlib.suppress(Exception): + defect_name = format_defect_name(f"{dir}_0", include_site_info_in_name=False) + if defect_name: # recognised defect folder found in cwd, warn user and proceed + # assuming they want to just parse the distortion folders in cwd + warnings.warn( + f"Both distortion folders and defect folders (i.e. {dir}) were found in the current " + f"directory. The defect folders will be ignored and {warning_substring}" + ) + break + + # assume current directory is the defect folder + if path != ".": + warnings.warn( + "`--path` option ignored when running from within defect folder (assumed to be " + "the case here as distortion folders found in current directory)." + ) + + return True + + return False + + @snb.command( name="groundstate", context_settings=CONTEXT_SETTINGS, @@ -1404,37 +1412,12 @@ def groundstate( please specify the name of the structure/output files. """ # determine if running from within a defect directory or from the top level directory - if any( - dir - for dir in os.listdir() - if os.path.isdir(dir) - and any(substring in dir for substring in ["Bond_Distortion", "Rattled", "Unperturbed", "Dimer"]) - ): # distortion subfolders in cwd - # check if defect folders also in cwd - for dir in [dir for dir in os.listdir() if os.path.isdir(dir)]: - defect_name = None - try: - defect_name = format_defect_name(dir, include_site_info_in_name=False) - except Exception: - with contextlib.suppress(Exception): - defect_name = format_defect_name(f"{dir}_0", include_site_info_in_name=False) - if defect_name: # recognised defect folder found in cwd, warn user and proceed - # assuming they want to just parse the distortion folders in cwd - warnings.warn( - f"Both distortion folders and defect folders (i.e. {dir}) were " - f"found in the current directory. The defect folders will be " - f"ignored and the groundstate structure from the distortion folders " - f"in this directory will be generated." - ) - break - - # assume current directory is the defect folder - if path != ".": - warnings.warn( - "`--path` option ignored when running from within defect folder (assumed to be " - "the case here as distortion folders found in current directory)." - ) - + if _running_in_defect_dir( + path, + warning_substring=( + "the groundstate structure from the distortion folders in this directory will be generated." + ), + ): energy_lowering_distortions.write_groundstate_structure( all=False, output_path=os.getcwd(), diff --git a/tests/data/vasp/CdTe/vac_1_Cd_0/default_INCAR b/tests/data/vasp/CdTe/vac_1_Cd_0/default_INCAR index 90321236..3c586c56 100644 --- a/tests/data/vasp/CdTe/vac_1_Cd_0/default_INCAR +++ b/tests/data/vasp/CdTe/vac_1_Cd_0/default_INCAR @@ -23,7 +23,7 @@ LREAL = Auto LVHAR = True LWAVE = False NCORE = 16 -NEDOS = 2000 +NEDOS = 3000 NELECT = 564.0 NELM = 40 NSW = 300 diff --git a/tests/test_cli.py b/tests/test_cli.py index df7f78af..a0d1cf11 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -74,9 +74,7 @@ def setUp(self): "CdTe_V_Cd_-50%_Distortion_local_rattle_POSCAR", ) ) # Local rattled - self.CdTe_distortion_config = os.path.join( - self.VASP_CDTE_DATA_DIR, "distortion_config.yml" - ) + self.CdTe_distortion_config = os.path.join(self.VASP_CDTE_DATA_DIR, "distortion_config.yml") self.V_Cd_minus0pt5_struc_kwarged = Structure.from_file( os.path.join(self.VASP_CDTE_DATA_DIR, "CdTe_V_Cd_-50%_Kwarged_POSCAR") ) @@ -91,9 +89,7 @@ def setUp(self): ) self.Int_Cd_2_dict = self.cdte_doped_defect_dict["interstitials"][1] 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" - ) + os.path.join(self.VASP_CDTE_DATA_DIR, "CdTe_Int_Cd_2_-60%_Distortion_Rattled_POSCAR") ) previous_default_rattle_settings = {"stdev": 0.25, "seed": 42} with open("previous_default_rattle_settings.yaml", "w") as fp: @@ -105,9 +101,7 @@ def setUp(self): 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_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): @@ -157,6 +151,7 @@ def tearDown(self): ] elif os.path.isfile(f"{self.EXAMPLE_RESULTS}/{defect}"): os.remove(f"{self.EXAMPLE_RESULTS}/{defect}") + if_present_rm(f"{self.EXAMPLE_RESULTS}/{defect}/{defect}.csv") for i in os.listdir(f"{self.EXAMPLE_RESULTS}"): if any( @@ -176,30 +171,16 @@ def tearDown(self): # Remove re-generated files folder = "Bond_Distortion_-60.0%_from_0" for charge in [-1, -2]: - if os.path.exists( - os.path.join(self.EXAMPLE_RESULTS, f"v_Cd_{charge}", folder) - ): - shutil.rmtree( - os.path.join(self.EXAMPLE_RESULTS, f"v_Cd_{charge}", folder) - ) + if os.path.exists(os.path.join(self.EXAMPLE_RESULTS, f"v_Cd_{charge}", folder)): + shutil.rmtree(os.path.join(self.EXAMPLE_RESULTS, f"v_Cd_{charge}", folder)) folder = "Bond_Distortion_20.0%_from_-1" for charge in [0, -2]: - if os.path.exists( - os.path.join(self.EXAMPLE_RESULTS, f"v_Cd_{charge}", folder) - ): - shutil.rmtree( - os.path.join(self.EXAMPLE_RESULTS, f"v_Cd_{charge}", folder) - ) + if os.path.exists(os.path.join(self.EXAMPLE_RESULTS, f"v_Cd_{charge}", folder)): + shutil.rmtree(os.path.join(self.EXAMPLE_RESULTS, f"v_Cd_{charge}", folder)) if_present_rm( - os.path.join( - self.VASP_CDTE_DATA_DIR, "vac_1_Cd_0/Bond_Distortion_-48.0%_High_Energy" - ) - ) - if_present_rm( - os.path.join( - self.EXAMPLE_RESULTS, "vac_1_Cd_0/Bond_Distortion_-48.0%_High_Energy" - ) + os.path.join(self.VASP_CDTE_DATA_DIR, "vac_1_Cd_0/Bond_Distortion_-48.0%_High_Energy") ) + if_present_rm(os.path.join(self.EXAMPLE_RESULTS, "vac_1_Cd_0/Bond_Distortion_-48.0%_High_Energy")) if_present_rm("Rattled_Bulk_CdTe_POSCAR") # Remove parsed vac_1_Ti_0 energies file @@ -224,6 +205,10 @@ def tearDown(self): for i in ["castep", "cp2k", "fhi_aims", "quantum_espresso"]: if_present_rm(f"{self.DATA_DIR}/{i}/vac_1_Cd_0/default_INCAR") + v_Ti_0_distortion_10pct_dir = os.path.join(self.VASP_TIO2_DATA_DIR, "Bond_Distortion_10.0%") + if os.path.exists(f"{v_Ti_0_distortion_10pct_dir}_High_Energy"): + os.rename(f"{v_Ti_0_distortion_10pct_dir}_High_Energy", v_Ti_0_distortion_10pct_dir) + 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,9 +246,7 @@ def test_snb_generate(self): catch_exceptions=False, ) print([str(warning.message) for warning in w]) # for debugging - non_potcar_warnings = [ - warning for warning in w if "POTCAR" not in str(warning.message) - ] + non_potcar_warnings = [warning for warning in w if "POTCAR" not in str(warning.message)] assert not non_potcar_warnings # no warnings other than POTCAR warnings self.assertEqual(result.exit_code, 0) self.assertIn( @@ -308,9 +291,7 @@ 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( - f"{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, "-50.0% N(Distort)=2 ~[0.0,0.0,0.0]", @@ -332,9 +313,7 @@ def test_snb_generate(self): 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, but different + if_present_rm(f"{defect_name}_0") # but distortion_metadata.json still present, but different with warnings.catch_warnings(record=True) as w: result = runner.invoke( snb, @@ -391,15 +370,10 @@ def test_snb_generate(self): # skipping minutes comparison in case this changes between test and check result.output, ) - self.assertIn( - "Combining old and new metadata in distortion_metadata.json", result.output - ) + self.assertIn("Combining old and new metadata in distortion_metadata.json", result.output) self.assertTrue(os.path.exists("distortion_metadata.json")) self.assertTrue( - any( - f"distortion_metadata_{current_datetime_wo_minutes}" in i - for i in os.listdir(".") - ) + any(f"distortion_metadata_{current_datetime_wo_minutes}" in i for i in os.listdir(".")) ) self.assertIn( "Previous and new metadata show different distortion parameters for v_Cd_Td_Te2.83 in " @@ -410,9 +384,7 @@ def test_snb_generate(self): new_distortion_metadata = loadfn("distortion_metadata.json") self.assertTrue( np.isclose( - new_distortion_metadata["distortion_parameters"][ - "mc_rattle_parameters" - ]["stdev"], + new_distortion_metadata["distortion_parameters"]["mc_rattle_parameters"]["stdev"], 0.28333683853583164, ) ) @@ -441,13 +413,9 @@ def test_snb_generate(self): catch_exceptions=False, ) print([str(warning.message) for warning in w]) # for debugging - non_potcar_warnings = [ - warning for warning in w if "POTCAR" not in str(warning.message) - ] + non_potcar_warnings = [warning for warning in w if "POTCAR" not in str(warning.message)] assert len(non_potcar_warnings) == 1 # only overwriting structures warning - assert "has the same Unperturbed defect structure" in str( - non_potcar_warnings[0].message - ) + assert "has the same Unperturbed defect structure" in str(non_potcar_warnings[0].message) self.assertEqual(result.exit_code, 0) self.assertIn(f"Defect: {defect_name}", result.output) self.assertIn("Number of missing electrons in neutral state: 2", result.output) @@ -463,23 +431,17 @@ def test_snb_generate(self): # test distortion_metadata file: reloaded_distortion_metadata = loadfn("distortion_metadata.json") self.assertEqual(len(reloaded_distortion_metadata["defects"]), 1) - self.assertEqual( - len(reloaded_distortion_metadata["defects"]["v_Cd_Td_Te2.83"]["charges"]), 1 - ) + self.assertEqual(len(reloaded_distortion_metadata["defects"]["v_Cd_Td_Te2.83"]["charges"]), 1) self.assertEqual( len( - reloaded_distortion_metadata["defects"]["v_Cd_Td_Te2.83"]["charges"][ - "0" - ]["distortion_parameters"]["bond_distortions"] + reloaded_distortion_metadata["defects"]["v_Cd_Td_Te2.83"]["charges"]["0"][ + "distortion_parameters" + ]["bond_distortions"] ), 13, ) # no duplication of bond distortions - defect_folder_distortion_metadata = loadfn( - "v_Cd_Td_Te2.83_0/distortion_metadata.json" - ) - self.assertDictEqual( - reloaded_distortion_metadata, defect_folder_distortion_metadata - ) + defect_folder_distortion_metadata = loadfn("v_Cd_Td_Te2.83_0/distortion_metadata.json") + self.assertDictEqual(reloaded_distortion_metadata, defect_folder_distortion_metadata) # test rerunning with same settings but with more charge states, so previous distortion_metadata # is a subset of current one, so no warnings / info messages (about distortion_metadata): @@ -496,13 +458,9 @@ def test_snb_generate(self): catch_exceptions=False, ) print([str(warning.message) for warning in w]) # for debugging - non_potcar_warnings = [ - warning for warning in w if "POTCAR" not in str(warning.message) - ] + non_potcar_warnings = [warning for warning in w if "POTCAR" not in str(warning.message)] assert len(non_potcar_warnings) == 1 # only overwriting structures warning - assert "has the same Unperturbed defect structure" in str( - non_potcar_warnings[0].message - ) + assert "has the same Unperturbed defect structure" in str(non_potcar_warnings[0].message) self.assertEqual(result.exit_code, 0) self.assertIn(f"Defect: {defect_name}", result.output) self.assertIn("Number of missing electrons in neutral state: 2", result.output) @@ -524,23 +482,17 @@ def test_snb_generate(self): ) self.assertEqual( len( - reloaded_distortion_metadata["defects"]["v_Cd_Td_Te2.83"]["charges"][ - "0" - ]["distortion_parameters"]["bond_distortions"] + reloaded_distortion_metadata["defects"]["v_Cd_Td_Te2.83"]["charges"]["0"][ + "distortion_parameters" + ]["bond_distortions"] ), 13, ) # no duplication of bond distortions - defect_folder_distortion_metadata = loadfn( - "v_Cd_Td_Te2.83_0/distortion_metadata.json" - ) - self.assertNotEqual( - reloaded_distortion_metadata, defect_folder_distortion_metadata - ) + defect_folder_distortion_metadata = loadfn("v_Cd_Td_Te2.83_0/distortion_metadata.json") + self.assertNotEqual(reloaded_distortion_metadata, defect_folder_distortion_metadata) self.assertDictEqual( reloaded_distortion_metadata["defects"]["v_Cd_Td_Te2.83"]["charges"]["0"], - defect_folder_distortion_metadata["defects"]["v_Cd_Td_Te2.83"]["charges"][ - "0" - ], + defect_folder_distortion_metadata["defects"]["v_Cd_Td_Te2.83"]["charges"]["0"], ) # test rerunning with same settings but with less charge states, so new distortion_metadata @@ -560,13 +512,9 @@ def test_snb_generate(self): catch_exceptions=False, ) print([str(warning.message) for warning in w]) # for debugging - non_potcar_warnings = [ - warning for warning in w if "POTCAR" not in str(warning.message) - ] + non_potcar_warnings = [warning for warning in w if "POTCAR" not in str(warning.message)] assert len(non_potcar_warnings) == 1 # only overwriting structures warning - assert "has the same Unperturbed defect structure" in str( - non_potcar_warnings[0].message - ) + assert "has the same Unperturbed defect structure" in str(non_potcar_warnings[0].message) self.assertEqual(result.exit_code, 0) self.assertIn(f"Defect: {defect_name}", result.output) self.assertIn("Number of missing electrons in neutral state: 2", result.output) @@ -587,15 +535,13 @@ def test_snb_generate(self): ) # combined metadata so still has 5 charges self.assertEqual( len( - reloaded_distortion_metadata["defects"]["v_Cd_Td_Te2.83"]["charges"][ - "0" - ]["distortion_parameters"]["bond_distortions"] + reloaded_distortion_metadata["defects"]["v_Cd_Td_Te2.83"]["charges"]["0"][ + "distortion_parameters" + ]["bond_distortions"] ), 13, ) # no duplication of bond distortions - defect_folder_distortion_metadata = loadfn( - "v_Cd_Td_Te2.83_0/distortion_metadata.json" - ) + defect_folder_distortion_metadata = loadfn("v_Cd_Td_Te2.83_0/distortion_metadata.json") self.assertNotEqual( # combined metadata so has 5 charges, rather than just neutral reloaded_distortion_metadata, defect_folder_distortion_metadata ) @@ -620,13 +566,9 @@ def test_snb_generate(self): catch_exceptions=False, ) print([str(warning.message) for warning in w]) # for debugging - non_potcar_warnings = [ - warning for warning in w if "POTCAR" not in str(warning.message) - ] + non_potcar_warnings = [warning for warning in w if "POTCAR" not in str(warning.message)] assert len(non_potcar_warnings) == 1 # only overwriting structures warning - assert "has the same Unperturbed defect structure" in str( - non_potcar_warnings[0].message - ) + assert "has the same Unperturbed defect structure" in str(non_potcar_warnings[0].message) self.assertEqual(result.exit_code, 0) self.assertIn(f"Defect: {defect_name}", result.output) self.assertIn("Number of missing electrons in neutral state: 2", result.output) @@ -642,23 +584,17 @@ def test_snb_generate(self): # test distortion_metadata file: reloaded_distortion_metadata = loadfn("distortion_metadata.json") self.assertEqual(len(reloaded_distortion_metadata["defects"]), 1) - self.assertEqual( - len(reloaded_distortion_metadata["defects"]["v_Cd_Td_Te2.83"]["charges"]), 5 - ) + self.assertEqual(len(reloaded_distortion_metadata["defects"]["v_Cd_Td_Te2.83"]["charges"]), 5) self.assertEqual( len( - reloaded_distortion_metadata["defects"]["v_Cd_Td_Te2.83"]["charges"][ - "0" - ]["distortion_parameters"]["bond_distortions"] + reloaded_distortion_metadata["defects"]["v_Cd_Td_Te2.83"]["charges"]["0"][ + "distortion_parameters" + ]["bond_distortions"] ), 25, ) # no duplication of bond distortions - defect_folder_distortion_metadata = loadfn( - "v_Cd_Td_Te2.83_0/distortion_metadata.json" - ) - self.assertNotEqual( - reloaded_distortion_metadata, defect_folder_distortion_metadata - ) + defect_folder_distortion_metadata = loadfn("v_Cd_Td_Te2.83_0/distortion_metadata.json") + self.assertNotEqual(reloaded_distortion_metadata, defect_folder_distortion_metadata) # test defect_index option: self.tearDown() @@ -821,12 +757,8 @@ def test_snb_generate(self): self.assertEqual(result.exit_code, 0) if w: # Check no problems in identifying the defect site - self.assertFalse( - any(str(warning.message) == warning_message for warning in w) - ) - self.assertFalse( - any("Coordinates" in str(warning.message) for warning in w) - ) + self.assertFalse(any(str(warning.message) == warning_message for warning in w)) + self.assertFalse(any("Coordinates" in str(warning.message) for warning in w)) self.assertNotIn("Auto site-matching", result.output) self.assertIn("--Distortion -60.0%", result.output) @@ -845,9 +777,7 @@ def test_snb_generate(self): # with rattled bulk self.tearDown() with warnings.catch_warnings(record=True) as w: - rattled_bulk = rattle( - self.CdTe_bulk_struc, stdev=0.25, d_min=2.25 - ) # previous default + rattled_bulk = rattle(self.CdTe_bulk_struc, stdev=0.25, d_min=2.25) # previous default rattled_bulk.to(filename="./Rattled_Bulk_CdTe_POSCAR", fmt="POSCAR") result = runner.invoke( snb, @@ -874,9 +804,7 @@ def test_snb_generate(self): # Bond_Distortion_-60.0% for defect Int_Cd_mult1 gives an interatomic # distance less than 1.0 Å (1.0 Å), which is likely to give explosive forces. # Omitting this distortion. - self.assertFalse( - any("Coordinates" in str(warning.message) for warning in w) - ) + self.assertFalse(any("Coordinates" in str(warning.message) for warning in w)) self.assertNotIn("Auto site-matching", result.output) self.assertIn("--Distortion -60.0%", result.output) @@ -891,9 +819,7 @@ def test_snb_generate(self): # test defect_coords working even when slightly off correct site with V_Cd and rattled bulk self.tearDown() with warnings.catch_warnings(record=True) as w: - rattled_bulk = rattle( - self.CdTe_bulk_struc, stdev=0.25, d_min=2.25 - ) # previous default + rattled_bulk = rattle(self.CdTe_bulk_struc, stdev=0.25, d_min=2.25) # previous default rattled_bulk.to(filename="./Rattled_Bulk_CdTe_POSCAR", fmt="POSCAR") result = runner.invoke( snb, @@ -1183,19 +1109,13 @@ def test_snb_generate_config(self): ) defect_name = "v_Cd_Td_Te2.83" self.assertEqual(result.exit_code, 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 - ) + 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) 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 - ) + 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") @@ -1237,12 +1157,8 @@ def test_snb_generate_config(self): f"Defect {defect_name} in charge state: 0. Number of distorted neighbours: 3", result.output, ) - V_Cd_ox3_POSCAR = Poscar.from_file( - f"{defect_name}_0/Bond_Distortion_-50.0%/POSCAR" - ) - self.assertNotEqual( - V_Cd_ox3_POSCAR.structure, self.V_Cd_minus0pt5_struc_local_rattled - ) + V_Cd_ox3_POSCAR = Poscar.from_file(f"{defect_name}_0/Bond_Distortion_-50.0%/POSCAR") + self.assertNotEqual(V_Cd_ox3_POSCAR.structure, self.V_Cd_minus0pt5_struc_local_rattled) self.tearDown() test_yml = """ @@ -1380,9 +1296,7 @@ def test_snb_generate_config(self): V_Cd_Bond_Distortion_folder = "Wally_McDoodle_0/Bond_Distortion_-50.0%" self.assertTrue(os.path.exists(V_Cd_Bond_Distortion_folder)) self.assertTrue(os.path.exists("Wally_McDoodle_0/Unperturbed")) - V_Cd_minus0pt5_rattled_POSCAR = Poscar.from_file( - V_Cd_Bond_Distortion_folder + "/POSCAR" - ) + V_Cd_minus0pt5_rattled_POSCAR = Poscar.from_file(V_Cd_Bond_Distortion_folder + "/POSCAR") self.assertEqual( V_Cd_minus0pt5_rattled_POSCAR.comment, "-50.0% N(Distort)=2 ~[0.0,0.0,0.0]", @@ -1613,17 +1527,13 @@ def test_snb_generate_all(self): + " Distorted Neighbour Distances:\n\t[(3.68, 33, 'Te'), (3.68, 42, 'Te'), (3.68, 52, 'Te')]", result.output, ) - self.assertNotIn( - f"Defect {defect_name} in charge state: +2.", result.output - ) # old default + self.assertNotIn(f"Defect {defect_name} in charge state: +2.", result.output) # old default for charge in [ 1, ] + list(range(-1, 2)): for dist in ["Unperturbed", "Bond_Distortion_30.0%"]: self.assertTrue( - os.path.exists( - f"{defect_name}_{'+' if charge > 0 else ''}{charge}/{dist}/POSCAR" - ) + os.path.exists(f"{defect_name}_{'+' if charge > 0 else ''}{charge}/{dist}/POSCAR") ) for dist in ["Unperturbed", "Rattled"]: # -2 has 0 electron change -> only Unperturbed & rattled folders @@ -1970,9 +1880,7 @@ def test_run(self): """Test snb-run function""" os.chdir(self.VASP_TIO2_DATA_DIR) self.copy_v_Ti_OUTCARs() - proc = subprocess.Popen( - ["snb-run", "-v"], stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) + proc = subprocess.Popen(["snb-run", "-v"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) out = str(proc.communicate()[0]) self.assertIn( "Job file 'job' not found, so will only save files and submit jobs in folders with 'job' " @@ -2000,9 +1908,7 @@ def test_run(self): ) self.assertIn("Bond_Distortion_-40.0% fully relaxed", out) self.assertIn("Unperturbed fully relaxed", out) - self.assertNotIn( - "Bond_Distortion_10.0% fully relaxed", out - ) # also present but no OUTCAR + self.assertNotIn("Bond_Distortion_10.0% fully relaxed", out) # also present but no OUTCAR self.assertIn("Running job for Bond_Distortion_10.0%", out) self.assertIn("this vac_1_Ti_0_10.0% job_file", out) # job submit command self.assertTrue(os.path.exists("Bond_Distortion_10.0%/job_file")) @@ -2026,9 +1932,7 @@ def test_run(self): ) self.assertIn("Bond_Distortion_-40.0% fully relaxed", out) self.assertIn("Unperturbed fully relaxed", out) - self.assertNotIn( - "Bond_Distortion_10.0% fully relaxed", out - ) # also present but no OUTCAR + self.assertNotIn("Bond_Distortion_10.0% fully relaxed", out) # also present but no OUTCAR self.assertIn("Running job for Bond_Distortion_10.0%", out) self.assertIn("this vac_1_Ti_0_10.0% job_file", out) # job submit command self.assertTrue(os.path.exists("Bond_Distortion_10.0%/job_file")) @@ -2038,49 +1942,31 @@ def test_run(self): # test save_vasp_files: car_files = [ - file - for file in os.listdir("Bond_Distortion_10.0%") - if "CAR_" in file and "on" in file + file for file in os.listdir("Bond_Distortion_10.0%") if "CAR_" in file and "on" in file ] for file in car_files: # remove os.remove(f"Bond_Distortion_10.0%/{file}") with open("Bond_Distortion_10.0%/OUTCAR", "w") as fp: fp.write("Test pop") - proc = subprocess.Popen( - ["snb-run", "-v"], stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) + proc = subprocess.Popen(["snb-run", "-v"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) out = str(proc.communicate()[0]) - self.assertIn( - "Bond_Distortion_10.0% not (fully) relaxed, saving files and rerunning", out - ) + self.assertIn("Bond_Distortion_10.0% not (fully) relaxed, saving files and rerunning", out) saved_files = [ - file - for file in os.listdir("Bond_Distortion_10.0%") - if "on" in file and "CAR_" in file + file for file in os.listdir("Bond_Distortion_10.0%") if "on" in file and "CAR_" in file ] # contcar and outcar - self.assertEqual( - len(saved_files), 0 - ) # no saved files, because submit command will fail + self.assertEqual(len(saved_files), 0) # no saved files, because submit command will fail # with job file present, but failing job submit command (no file saving) with open("job", "w") as fp: fp.write("Test pop") - proc = subprocess.Popen( - ["snb-run", "-v"], stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) + proc = subprocess.Popen(["snb-run", "-v"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) out = str(proc.communicate()[0]) - self.assertIn( - "Bond_Distortion_10.0% not (fully) relaxed, saving files and rerunning", out - ) + self.assertIn("Bond_Distortion_10.0% not (fully) relaxed, saving files and rerunning", out) saved_files = [ - file - for file in os.listdir("Bond_Distortion_10.0%") - if "on" in file and "CAR_" in file + file for file in os.listdir("Bond_Distortion_10.0%") if "on" in file and "CAR_" in file ] - self.assertEqual( - len(saved_files), 0 - ) # no saved files, because submit command fails + self.assertEqual(len(saved_files), 0) # no saved files, because submit command fails # with working (fake) job submit command: proc = subprocess.Popen( @@ -2089,13 +1975,9 @@ def test_run(self): stderr=subprocess.PIPE, ) out = str(proc.communicate()[0]) - self.assertIn( - "Bond_Distortion_10.0% not (fully) relaxed, saving files and rerunning", out - ) + self.assertIn("Bond_Distortion_10.0% not (fully) relaxed, saving files and rerunning", out) saved_files = [ - file - for file in os.listdir("Bond_Distortion_10.0%") - if "on" in file and "CAR_" in file + file for file in os.listdir("Bond_Distortion_10.0%") if "on" in file and "CAR_" in file ] # poscar, contcar and outcar self.assertEqual(len(saved_files), 3) # POSCAR, CONTCAR, OUTCAR self.assertEqual(len([i for i in saved_files if "POSCAR" in i]), 1) @@ -2108,16 +1990,14 @@ def test_run(self): if_present_rm("Bond_Distortion_10.0%/job") if_present_rm("job") - # test "--all" option + # test "all" behaviour os.chdir("..") shutil.copytree("vac_1_Ti_0", "vac_1_Ti_1") shutil.copyfile( "v_Ge_s16_0/Bond_Distortion_-60.0%/OUTCAR", "v_Ge_s16_0/Bond_Distortion_-60.0%/prev_otcr", ) - proc = subprocess.Popen( - ["snb-run", "-v", "-a"], stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) + proc = subprocess.Popen(["snb-run", "-v"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) out = str(proc.communicate()[0]) self.assertIn("Looping through distortion folders for v_Ca_s0_0", out) self.assertIn( @@ -2128,27 +2008,15 @@ def test_run(self): self.assertIn("Looping through distortion folders for v_Ge_s16_0", out) # Check it filters distortions stuck in high energy basins for v_Ge (50%, while -60% is handled # separately as the energy doesn't change for the last 50 steps) - self.assertIn( - "More than 150 ionic steps present for Bond_Distortion_50.0%.", out - ) - self.assertIn( - "Bond_Distortion_40.0% not (fully) relaxed, saving files and rerunning", out - ) - self.assertFalse( - os.path.exists("v_Ge_s16_0/Bond_Distortion_-60.0%_High_Energy") - ) + self.assertIn("More than 150 ionic steps present for Bond_Distortion_50.0%.", out) + self.assertIn("Bond_Distortion_40.0% not (fully) relaxed, saving files and rerunning", out) + self.assertFalse(os.path.exists("v_Ge_s16_0/Bond_Distortion_-60.0%_High_Energy")) self.assertTrue(os.path.exists("v_Ge_s16_0/Bond_Distortion_50.0%_High_Energy")) self.assertFalse(os.path.exists("v_Ge_s16_0/Bond_Distortion_50.0%")) # Check number of OUTCARs in Bond_Distortion_50.0%_High_Energy - outcars = [ - f - for f in os.listdir("v_Ge_s16_0/Bond_Distortion_50.0%_High_Energy") - if "OUTCAR" in f - ] + outcars = [f for f in os.listdir("v_Ge_s16_0/Bond_Distortion_50.0%_High_Energy") if "OUTCAR" in f] assert len(outcars) == 6 # last OUTCAR not saved cause high energy distortion - outcars = [ - f for f in os.listdir("v_Ge_s16_0/Bond_Distortion_40.0%") if "OUTCAR" in f - ] + outcars = [f for f in os.listdir("v_Ge_s16_0/Bond_Distortion_40.0%") if "OUTCAR" in f] assert len(outcars) == 3 # last OUTCAR saved # Clean up; rename high energy directories os.rename( @@ -2170,19 +2038,10 @@ def test_run(self): ) os.chdir("..") - proc = subprocess.Popen( - ["snb-run", "-v"], stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - out = str(proc.communicate()[0]) - self.assertIn("No distortion folders found in current directory", out) - - proc = subprocess.Popen( - ["snb-run", "-v", "-a"], stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) + proc = subprocess.Popen(["snb-run", "-v"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) out = str(proc.communicate()[0]) self.assertIn( - "No defect folders (with names ending in a number (charge state)) found in " - "current directory", + "No defect folders (with names ending in a number (charge state)) found in current directory", out, ) @@ -2262,9 +2121,7 @@ def test_run(self): with open("Bond_Distortion_10.0%/OUTCAR", "w") as fp: fp.write(positive_energies_outcar_string) car_files = [ - file - for file in os.listdir("Bond_Distortion_10.0%") - if "on" in file and "CAR_" in file + file for file in os.listdir("Bond_Distortion_10.0%") if "on" in file and "CAR_" in file ] for i in car_files: # remove os.remove(f"Bond_Distortion_10.0%/{i}") @@ -2286,13 +2143,9 @@ def test_run(self): ) self.assertFalse(os.path.exists("Bond_Distortion_10.0%_High_Energy")) self.assertTrue(os.path.exists("Bond_Distortion_10.0%")) - self.assertIn( - "Bond_Distortion_10.0% not (fully) relaxed, saving files and rerunning", out - ) + self.assertIn("Bond_Distortion_10.0% not (fully) relaxed, saving files and rerunning", out) saved_files = [ - file - for file in os.listdir("Bond_Distortion_10.0%") - if "on" in file and "CAR_" in file + file for file in os.listdir("Bond_Distortion_10.0%") if "on" in file and "CAR_" in file ] self.assertEqual(len(saved_files), 2) # CONTCAR, OUTCAR self.assertEqual(len([i for i in saved_files if "CONTCAR" in i]), 1) @@ -2326,9 +2179,7 @@ def _test_OUTCAR_error(error_string): self.assertIn("Unperturbed fully relaxed", out) self.assertNotIn("Bond_Distortion_10.0% fully relaxed", out) # also present self.assertNotIn("Running job for Bond_Distortion_10.0%", out) - self.assertNotIn( - "this vac_1_Ti_0_10.0% job_file", out - ) # job submit command + self.assertNotIn("this vac_1_Ti_0_10.0% job_file", out) # job submit command self.assertIn( "Positive energies or forces error encountered for Bond_Distortion_10.0%, " "ignoring and renaming to Bond_Distortion_10.0%_High_Energy", @@ -2370,9 +2221,7 @@ def _test_OUTCAR_error(error_string): self.assertIn("Bond_Distortion_-40.0% fully relaxed", out) self.assertIn("Unperturbed fully relaxed", out) self.assertNotIn("Bond_Distortion_10.0% fully relaxed", out) - self.assertNotIn( - "Running job for Bond_Distortion_10.0%", out - ) # not running job though! + self.assertNotIn("Running job for Bond_Distortion_10.0%", out) # not running job though! self.assertNotIn("this vac_1_Ti_0_10.0% job_file", out) # job submit command self.assertNotIn( "Positive energies or forces error encountered for Bond_Distortion_10.0%, " @@ -2386,9 +2235,7 @@ def _test_OUTCAR_error(error_string): ) self.assertFalse(os.path.exists("Bond_Distortion_10.0%_High_Energy")) self.assertTrue(os.path.exists("Bond_Distortion_10.0%")) - self.assertNotIn( - "Bond_Distortion_10.0% not (fully) relaxed, saving files and rerunning", out - ) + self.assertNotIn("Bond_Distortion_10.0% not (fully) relaxed, saving files and rerunning", out) files = os.listdir("Bond_Distortion_10.0%") saved_files = [file for file in files if "on" in file and "CAR_" in file] self.assertEqual(len(saved_files), 0) @@ -2423,9 +2270,7 @@ def _test_OUTCAR_error(error_string): self.assertIn("Bond_Distortion_-40.0% fully relaxed", out) self.assertIn("Unperturbed fully relaxed", out) self.assertNotIn("Bond_Distortion_10.0% fully relaxed", out) - self.assertNotIn( - "Running job for Bond_Distortion_10.0%", out - ) # not running job though! + self.assertNotIn("Running job for Bond_Distortion_10.0%", out) # not running job though! self.assertNotIn("this vac_1_Ti_0_10.0% job_file", out) # job submit command self.assertNotIn( "Positive energies or forces error encountered for Bond_Distortion_10.0%, " @@ -2439,9 +2284,7 @@ def _test_OUTCAR_error(error_string): ) self.assertFalse(os.path.exists("Bond_Distortion_10.0%_High_Energy")) self.assertTrue(os.path.exists("Bond_Distortion_10.0%")) - self.assertNotIn( - "Bond_Distortion_10.0% not (fully) relaxed, saving files and rerunning", out - ) + self.assertNotIn("Bond_Distortion_10.0% not (fully) relaxed, saving files and rerunning", out) files = os.listdir("Bond_Distortion_10.0%") saved_files = [file for file in files if "on" in file and "CAR_" in file] self.assertEqual(len(saved_files), 0) @@ -2513,9 +2356,7 @@ def _test_OUTCAR_error(error_string): self.assertNotIn("Unperturbed fully relaxed", out) self.assertNotIn("Running job for Unperturbed", out) self.assertIn("this vac_1_Ti_0_10.0% job_file", out) # job submit command - self.assertIn( - "Positive energies or forces error encountered for Unperturbed.", out - ) + self.assertIn("Positive energies or forces error encountered for Unperturbed.", out) self.assertIn( "This typically indicates the initial defect structure supplied to " "ShakeNBreak is highly unstable, often with bond lengths smaller than the " @@ -2562,9 +2403,7 @@ def _test_OUTCAR_error(error_string): ) self.assertIn("Running job for Unperturbed", out) self.assertIn("this vac_1_Ti_0_10.0% job_file", out) # job submit command - self.assertNotIn( - "Positive energies or forces error encountered for Unperturbed.", out - ) + self.assertNotIn("Positive energies or forces error encountered for Unperturbed.", out) self.assertTrue(os.path.exists("Unperturbed")) # not renamed shutil.move("Unperturbed/OUTCAR_backup", "Unperturbed/OUTCAR") if_present_rm("Unperturbed/job_file") @@ -2598,9 +2437,7 @@ def _test_OUTCAR_error(error_string): self.assertNotIn("Previous run for Unperturbed", out) self.assertIn("Running job for Unperturbed", out) self.assertIn("this vac_1_Ti_0_10.0% job_file", out) # job submit command - self.assertNotIn( - "Positive energies or forces error encountered for Unperturbed.", out - ) + self.assertNotIn("Positive energies or forces error encountered for Unperturbed.", out) self.assertTrue(os.path.exists("Unperturbed")) # not renamed shutil.move("Unperturbed/OUTCAR_backup", "Unperturbed/OUTCAR") if_present_rm("Unperturbed/job_file") @@ -2622,9 +2459,7 @@ def _test_OUTCAR_error(error_string): fp.write("ALGO = Fast") car_files = [ - file - for file in os.listdir("Bond_Distortion_10.0%") - if "CAR_" in file and "on" in file + file for file in os.listdir("Bond_Distortion_10.0%") if "CAR_" in file and "on" in file ] for file in car_files: os.remove(f"Bond_Distortion_10.0%/{file}") @@ -2643,13 +2478,10 @@ def _test_OUTCAR_error(error_string): self.assertFalse(os.path.exists("Bond_Distortion_10.0%_High_Energy")) self.assertTrue(os.path.exists("Bond_Distortion_10.0%")) self.assertIn( - "Bond_Distortion_10.0% is showing poor electronic convergence, changing ALGO " - "to All.", + "Bond_Distortion_10.0% is showing poor electronic convergence, changing ALGO " "to All.", out, ) - self.assertIn( - "Bond_Distortion_10.0% not (fully) relaxed, saving files and rerunning", out - ) + self.assertIn("Bond_Distortion_10.0% not (fully) relaxed, saving files and rerunning", out) self.assertIn("ALGO = All", open("Bond_Distortion_10.0%/INCAR").read()) files = os.listdir("Bond_Distortion_10.0%") saved_files = [file for file in files if "on" in file and "CAR_" in file] @@ -2663,10 +2495,7 @@ def _test_OUTCAR_error(error_string): # test changing no message with poor electronic convergence when ALGO already = All with open("Bond_Distortion_10.0%/OUTCAR", "w") as fp: - fp.write( - poor_electronic_convergence_outcar_string - + "\nIALGO = 58 # i.e. ALGO = All" - ) + fp.write(poor_electronic_convergence_outcar_string + "\nIALGO = 58 # i.e. ALGO = All") proc = subprocess.Popen( ["snb-run", "-v", "-s echo", "-n this", "-j job_file"], @@ -2698,14 +2527,10 @@ def _test_OUTCAR_error(error_string): with open("Unperturbed/OUTCAR", "r") as f: outcar_string = f.read() final_mag_string = outcar_string[-5870:] - edited_final_mag_string = re.sub( - "[0-9]\.\d+", "0.000", final_mag_string - ) # zero magnetisation + edited_final_mag_string = re.sub("[0-9]\.\d+", "0.000", final_mag_string) # zero magnetisation with open("Unperturbed/OUTCAR", "w") as f: - f.write( - outcar_string[:-5870] + edited_final_mag_string - ) # zero magnetisation + f.write(outcar_string[:-5870] + edited_final_mag_string) # zero magnetisation proc = subprocess.Popen( ["snb-run", "-v", "-s echo", "-n this", "-j job_file"], @@ -2717,9 +2542,7 @@ def _test_OUTCAR_error(error_string): self.assertIn("Unperturbed fully relaxed", out) with open(f"Bond_Distortion_-40.0%/INCAR", "r") as fp: incar_string = fp.read() - self.assertIn( - "ISPIN = 2", incar_string - ) # no change in INCAR as run was fully converged + self.assertIn("ISPIN = 2", incar_string) # no change in INCAR as run was fully converged self.assertEqual(len(os.listdir(f"Bond_Distortion_-40.0%")), 3) # no new files with open(f"Unperturbed/INCAR", "r") as fp: @@ -2749,15 +2572,11 @@ def _test_OUTCAR_error(error_string): self.assertNotIn("Unperturbed fully relaxed", out) self.assertIn("Running job for Unperturbed", out) self.assertIn("this vac_1_Ti_0_Unperturbed job_file", out) # job submit command - self.assertIn( - "Unperturbed not (fully) relaxed, saving files and rerunning", out - ) + self.assertIn("Unperturbed not (fully) relaxed, saving files and rerunning", out) with open(f"Bond_Distortion_-40.0%/INCAR", "r") as fp: incar_string = fp.read() - self.assertIn( - "ISPIN = 2", incar_string - ) # no change in INCAR as run was fully converged + self.assertIn("ISPIN = 2", incar_string) # no change in INCAR as run was fully converged self.assertEqual(len(os.listdir("Bond_Distortion_-40.0%")), 3) # no new files with open(f"Unperturbed/INCAR", "r") as fp: @@ -2775,9 +2594,7 @@ def _test_OUTCAR_error(error_string): old_incar = [i for i in saved_files if "INCAR" in i][0] with open(f"Unperturbed/{old_incar}", "r") as fp: old_incar_string = fp.read() - self.assertIn( - "ISPIN = 2", old_incar_string - ) # Old INCAR unchanged from ISPIN = 2 + self.assertIn("ISPIN = 2", old_incar_string) # Old INCAR unchanged from ISPIN = 2 self.assertEqual(len([i for i in saved_files if "CONTCAR" in i]), 1) self.assertEqual(len([i for i in saved_files if "OUTCAR" in i]), 1) for i in saved_files: @@ -2804,9 +2621,7 @@ def _test_OUTCAR_error(error_string): self.assertNotIn("Unperturbed fully relaxed", out) with open(f"Bond_Distortion_-40.0%/INCAR", "r") as fp: incar_string = fp.read() - self.assertIn( - "ISPIN = 2", incar_string - ) # no change in INCAR as run was fully converged + self.assertIn("ISPIN = 2", incar_string) # no change in INCAR as run was fully converged self.assertEqual(len(os.listdir(f"Bond_Distortion_-40.0%")), 3) # no new files with open(f"Unperturbed/INCAR", "r") as fp: @@ -2829,9 +2644,7 @@ def test_parse(self): def _parse_v_Ti_and_check_output(verbose=False): with open(f"{self.EXAMPLE_RESULTS}/{defect}/{defect}.yaml", "w") as f: - f.write( - "" - ) # write empty energies file, otherwise no verbose print because detects + f.write("") # write empty energies file, otherwise no verbose print because detects # that it already exists with the same energies args = ["parse", "-d", defect, "-p", self.EXAMPLE_RESULTS] if verbose: @@ -2898,7 +2711,7 @@ def _parse_v_Ti_and_check_output(verbose=False): self.assertEqual(test_energies, energies) os.remove(f"{self.VASP_DIR}/{defect}/{defect}.yaml") - # Test --all option + # Test "all" behaviour self.tearDown() os.mkdir(f"{self.EXAMPLE_RESULTS}/pesky_defects") defect = "v_Ti" @@ -2914,22 +2727,15 @@ def _parse_v_Ti_and_check_output(verbose=False): snb, [ "parse", - "--all", "-p", f"{self.EXAMPLE_RESULTS}/pesky_defects", ], catch_exceptions=False, ) self.assertTrue( - os.path.exists( - f"{self.EXAMPLE_RESULTS}/pesky_defects/{defect}_-1/{defect}_-1.yaml" - ) - ) - self.assertTrue( - os.path.exists( - f"{self.EXAMPLE_RESULTS}/pesky_defects/{defect}_0/{defect}_0.yaml" - ) + os.path.exists(f"{self.EXAMPLE_RESULTS}/pesky_defects/{defect}_-1/{defect}_-1.yaml") ) + self.assertTrue(os.path.exists(f"{self.EXAMPLE_RESULTS}/pesky_defects/{defect}_0/{defect}_0.yaml")) # Test parsing from inside the defect folder defect = "v_Ti" @@ -2943,9 +2749,7 @@ def _parse_v_Ti_and_check_output(verbose=False): catch_exceptions=False, ) self.assertTrue( - os.path.exists( - f"{self.EXAMPLE_RESULTS}/pesky_defects/{defect}_-1/{defect}_-1.yaml" - ) + os.path.exists(f"{self.EXAMPLE_RESULTS}/pesky_defects/{defect}_-1/{defect}_-1.yaml") ) os.chdir(file_path) @@ -2964,21 +2768,19 @@ def _parse_v_Ti_and_check_output(verbose=False): ], catch_exceptions=False, ) + print([str(warning.message) for warning in w]) # for debugging self.assertTrue(any([warning.category == UserWarning for warning in w])) self.assertTrue( any( [ - str(warning.message) - == "`--path` option ignored when running from within defect folder (i.e. " - "when `--defect` is not specified." + "`--path` option ignored when running from within defect folder" + in str(warning.message) for warning in w ] ) ) self.assertTrue( - os.path.exists( - f"{self.EXAMPLE_RESULTS}/pesky_defects/{defect}_-1/{defect}_-1.yaml" - ) + os.path.exists(f"{self.EXAMPLE_RESULTS}/pesky_defects/{defect}_-1/{defect}_-1.yaml") ) os.chdir(file_path) shutil.rmtree(f"{self.EXAMPLE_RESULTS}/pesky_defects/") @@ -3003,17 +2805,14 @@ def _parse_v_Ti_and_check_output(verbose=False): ) self.assertTrue( os.path.exists( - f"{self.EXAMPLE_RESULTS}/{defect_name}_defect_folder/{defect_name}" - f"/{defect_name}.yaml" + f"{self.EXAMPLE_RESULTS}/{defect_name}_defect_folder/{defect_name}" f"/{defect_name}.yaml" ) ) # test warning when nothing parsed because defect folder not recognised os.chdir(self.EXAMPLE_RESULTS) with warnings.catch_warnings(record=True) as w: - result = runner.invoke( - snb, ["parse", "-d", "defect"], catch_exceptions=True - ) + result = runner.invoke(snb, ["parse", "-d", "defect"], catch_exceptions=True) self.assertTrue(any([warning.category == UserWarning for warning in w])) self.assertTrue( any( @@ -3022,31 +2821,17 @@ def _parse_v_Ti_and_check_output(verbose=False): for warning in w ) ) - self.assertFalse( - any(os.path.exists(i) for i in os.listdir() if i.endswith(".yaml")) - ) + self.assertFalse(any(os.path.exists(i) for i in os.listdir() if i.endswith(".yaml"))) os.chdir(file_path) # Test warning when run with no arguments in top-level folder os.chdir(self.EXAMPLE_RESULTS) with warnings.catch_warnings(record=True) as w: result = runner.invoke(snb, ["parse"], catch_exceptions=True) - self.assertTrue(any([warning.category == UserWarning for warning in w])) - self.assertTrue( - any( - [ - str(warning.message) - == "Energies could not be parsed for defect 'example_results' in" - f" '{self.DATA_DIR}'. If these directories are correct, " - "check calculations have converged, and that distortion subfolders match " - "ShakeNBreak naming (e.g. Bond_Distortion_xxx, Rattled, Unperturbed)" - for warning in w - ] - ) - ) - self.assertFalse( - any(os.path.exists(i) for i in os.listdir() if i.endswith(".yaml")) - ) + print([str(warning.message) for warning in w]) # for debugging + self.assertFalse(any([warning.category == UserWarning for warning in w])) + self.assertFalse(any(os.path.exists(i) for i in os.listdir() if i.endswith(".yaml"))) + self.assertTrue(os.path.exists("v_Ti_0/v_Ti_0.yaml")) # parsed successfully os.chdir(file_path) # test ignoring "*High_Energy*" folder(s) @@ -3073,9 +2858,7 @@ def _parse_v_Ti_and_check_output(verbose=False): for file in os.listdir(f"{self.EXAMPLE_RESULTS}/{defect}") if os.path.isfile(f"{self.EXAMPLE_RESULTS}/{defect}/{file}") ] - shutil.rmtree( - f"{self.EXAMPLE_RESULTS}/{defect}/Bond_Distortion_-20.0%_High_Energy" - ) + shutil.rmtree(f"{self.EXAMPLE_RESULTS}/{defect}/Bond_Distortion_-20.0%_High_Energy") # test parsing energies of calculations that still haven't converged defect = "v_Ti_0" @@ -3108,9 +2891,7 @@ def _parse_v_Ti_and_check_output(verbose=False): catch_exceptions=False, ) energies = loadfn(f"{self.EXAMPLE_RESULTS}/{defect}/{defect}.yaml") - self.assertNotEqual( - test_energies, energies - ) # Bond_Distortion_-20.0%_not_converged now included + self.assertNotEqual(test_energies, energies) # Bond_Distortion_-20.0%_not_converged now included not_converged_energies = copy.deepcopy(test_energies) not_converged_energies["distortions"].update( {"Bond_Distortion_-20.0%_not_converged": -1110.37833497} @@ -3128,9 +2909,7 @@ def _parse_v_Ti_and_check_output(verbose=False): for file in os.listdir(f"{self.EXAMPLE_RESULTS}/{defect}") if os.path.isfile(f"{self.EXAMPLE_RESULTS}/{defect}/{file}") ] - shutil.rmtree( - f"{self.EXAMPLE_RESULTS}/{defect}/Bond_Distortion_-20.0%_not_converged" - ) + shutil.rmtree(f"{self.EXAMPLE_RESULTS}/{defect}/Bond_Distortion_-20.0%_not_converged") # test parsing energies of residual-forces calculations defect = "v_Ti_0" @@ -3166,9 +2945,7 @@ def _parse_v_Ti_and_check_output(verbose=False): catch_exceptions=False, ) energies = loadfn(f"{self.EXAMPLE_RESULTS}/{defect}/{defect}.yaml") - self.assertNotEqual( - test_energies, energies - ) # Bond_Distortion_-20.0%_residual_forces now included + self.assertNotEqual(test_energies, energies) # Bond_Distortion_-20.0%_residual_forces now included residual_forces_energies = copy.deepcopy(test_energies) residual_forces_energies["distortions"].update( {"Bond_Distortion_-20.0%_residual_forces": -675.21988502} @@ -3183,9 +2960,7 @@ def _parse_v_Ti_and_check_output(verbose=False): for file in os.listdir(f"{self.EXAMPLE_RESULTS}/{defect}") if os.path.isfile(f"{self.EXAMPLE_RESULTS}/{defect}/{file}") ] - shutil.rmtree( - f"{self.EXAMPLE_RESULTS}/{defect}/Bond_Distortion_-20.0%_residual_forces" - ) + shutil.rmtree(f"{self.EXAMPLE_RESULTS}/{defect}/Bond_Distortion_-20.0%_residual_forces") # test warning when all parsed distortions are >0.1 eV higher energy than unperturbed defect = "v_Ti_+3" @@ -3219,13 +2994,9 @@ def _parse_v_Ti_and_check_output(verbose=False): "distortions": {-0.4: -675.21988502}, "Unperturbed": -1173.02056574, } # energies still parsed despite being high and not converged (like myself) - self.assertEqual( - high_energies_dict, energies - ) # energies still parsed, but all high energy + self.assertEqual(high_energies_dict, energies) # energies still parsed, but all high energy # test print statement about not being fully relaxed - self.assertIn( - "Bond_Distortion_-40.0% for v_Ti_+3 is not fully relaxed", result.output - ) + self.assertIn("Bond_Distortion_-40.0% for v_Ti_+3 is not fully relaxed", result.output) self.assertTrue(len([i for i in w if i.category == UserWarning]) == 1) self.assertTrue( any( @@ -3282,12 +3053,8 @@ def _parse_v_Ti_and_check_output(verbose=False): } # energies still parsed despite being odd self.assertEqual(low_energies_dict, energies) # energies still parsed # test print statement about not being fully relaxed - self.assertIn( - "Bond_Distortion_-40.0% for v_Ti_+3 is not fully relaxed", result.output - ) - self.assertTrue( - len([i for i in w if i.category == UserWarning]) == 0 - ) # no warning when + self.assertIn("Bond_Distortion_-40.0% for v_Ti_+3 is not fully relaxed", result.output) + self.assertTrue(len([i for i in w if i.category == UserWarning]) == 0) # no warning when # <2 parsed distortions shutil.copytree( @@ -3318,9 +3085,7 @@ def _parse_v_Ti_and_check_output(verbose=False): } # energies still parsed despite being odd self.assertEqual(low_energies_dict, energies) # energies still parsed # test print statement about not being fully relaxed - self.assertIn( - "Bond_Distortion_-40.0% for v_Ti_+3 is not fully relaxed", result.output - ) + self.assertIn("Bond_Distortion_-40.0% for v_Ti_+3 is not fully relaxed", result.output) self.assertTrue( any( [ @@ -3329,8 +3094,7 @@ def _parse_v_Ti_and_check_output(verbose=False): f"it can indicate that a bulk phase transformation is occurring within your " f"defect supercell. If so, see " f"https://shakenbreak.readthedocs.io/en/latest/Tips.html#bulk-phase" - f"-transformations for advice on dealing with this phenomenon." - == str(i.message) + f"-transformations for advice on dealing with this phenomenon." == str(i.message) for i in w if i.category == UserWarning ] @@ -3407,12 +3171,10 @@ def test_parse_codes(self): ], catch_exceptions=False, ) - self.assertTrue( - os.path.exists(f"{self.DATA_DIR}/{code}/{defect}/{defect}.yaml") - ) - with open( - f"{self.DATA_DIR}/{code}/{defect}/test_{defect}.yaml", "r" - ) as test, open(f"{self.DATA_DIR}/{code}/{defect}/{defect}.yaml", "r") as new: + self.assertTrue(os.path.exists(f"{self.DATA_DIR}/{code}/{defect}/{defect}.yaml")) + with open(f"{self.DATA_DIR}/{code}/{defect}/test_{defect}.yaml", "r") as test, open( + f"{self.DATA_DIR}/{code}/{defect}/{defect}.yaml", "r" + ) as new: test_yaml = yaml.safe_load(test) new_yaml = yaml.safe_load(new) self.assertDictEqual(test_yaml, new_yaml) @@ -3433,12 +3195,10 @@ def test_parse_codes(self): ], catch_exceptions=False, ) - self.assertTrue( - os.path.exists(f"{self.DATA_DIR}/{code}/{defect}/{defect}.yaml") - ) - with open( - f"{self.DATA_DIR}/{code}/{defect}/test_{defect}.yaml", "r" - ) as test, open(f"{self.DATA_DIR}/{code}/{defect}/{defect}.yaml", "r") as new: + self.assertTrue(os.path.exists(f"{self.DATA_DIR}/{code}/{defect}/{defect}.yaml")) + with open(f"{self.DATA_DIR}/{code}/{defect}/test_{defect}.yaml", "r") as test, open( + f"{self.DATA_DIR}/{code}/{defect}/{defect}.yaml", "r" + ) as new: test_yaml = yaml.safe_load(test) new_yaml = yaml.safe_load(new) self.assertDictEqual(test_yaml, new_yaml) @@ -3459,12 +3219,10 @@ def test_parse_codes(self): ], catch_exceptions=False, ) - self.assertTrue( - os.path.exists(f"{self.DATA_DIR}/{code}/{defect}/{defect}.yaml") - ) - with open( - f"{self.DATA_DIR}/{code}/{defect}/test_{defect}.yaml", "r" - ) as test, open(f"{self.DATA_DIR}/{code}/{defect}/{defect}.yaml", "r") as new: + self.assertTrue(os.path.exists(f"{self.DATA_DIR}/{code}/{defect}/{defect}.yaml")) + with open(f"{self.DATA_DIR}/{code}/{defect}/test_{defect}.yaml", "r") as test, open( + f"{self.DATA_DIR}/{code}/{defect}/{defect}.yaml", "r" + ) as new: test_yaml = yaml.safe_load(test) new_yaml = yaml.safe_load(new) self.assertDictEqual(test_yaml, new_yaml) @@ -3485,12 +3243,10 @@ def test_parse_codes(self): ], catch_exceptions=False, ) - self.assertTrue( - os.path.exists(f"{self.DATA_DIR}/{code}/{defect}/{defect}.yaml") - ) - with open( - f"{self.DATA_DIR}/{code}/{defect}/test_{defect}.yaml", "r" - ) as test, open(f"{self.DATA_DIR}/{code}/{defect}/{defect}.yaml", "r") as new: + self.assertTrue(os.path.exists(f"{self.DATA_DIR}/{code}/{defect}/{defect}.yaml")) + with open(f"{self.DATA_DIR}/{code}/{defect}/test_{defect}.yaml", "r") as test, open( + f"{self.DATA_DIR}/{code}/{defect}/{defect}.yaml", "r" + ) as new: test_yaml = yaml.safe_load(test) new_yaml = yaml.safe_load(new) self.assertDictEqual(test_yaml, new_yaml) @@ -3533,7 +3289,7 @@ def test_analyse(self): if os.path.isfile(f"{self.EXAMPLE_RESULTS}/{defect}/{file}") ] - # Test --all flag + # Test 'all' behaviour os.mkdir(f"{self.EXAMPLE_RESULTS}/pesky_defects") defect_name = "v_Ti_-1" shutil.copytree( @@ -3548,20 +3304,15 @@ def test_analyse(self): snb, [ "analyse", - "--all", "-p", f"{self.EXAMPLE_RESULTS}/pesky_defects", ], catch_exceptions=False, ) self.assertTrue( - os.path.exists( - f"{self.EXAMPLE_RESULTS}/pesky_defects/{defect_name}/{defect_name}.csv" - ) - ) - self.assertTrue( - os.path.exists(f"{self.EXAMPLE_RESULTS}/pesky_defects/v_Ti_0/v_Ti_0.csv") + os.path.exists(f"{self.EXAMPLE_RESULTS}/pesky_defects/{defect_name}/{defect_name}.csv") ) + self.assertTrue(os.path.exists(f"{self.EXAMPLE_RESULTS}/pesky_defects/v_Ti_0/v_Ti_0.csv")) shutil.rmtree(f"{self.EXAMPLE_RESULTS}/pesky_defects/") # Test non-existent defect name = "v_Ti_-2" @@ -3579,8 +3330,8 @@ def test_analyse(self): self.assertIn( f"Could not analyse defect '{name}' in directory '{self.EXAMPLE_RESULTS}'. Please " "either specify a defect to analyse (with option --defect), run from within a single " - "defect directory (without setting --defect) or use the --all flag to analyse all " - "defects in the specified/current directory.", + "defect directory (without setting --defect) or run from the top-level directory to " + f"analyse all defects in the specified/current directory.", str(result.exception), ) @@ -3592,9 +3343,7 @@ def test_analyse(self): f"{self.EXAMPLE_RESULTS}/{defect}", f"{self.EXAMPLE_RESULTS}/{defect}_defect_folder/{defect}", ) - with open( - f"{self.EXAMPLE_RESULTS}/{defect}_defect_folder/{defect}/{defect}.yaml", "w" - ) as f: + with open(f"{self.EXAMPLE_RESULTS}/{defect}_defect_folder/{defect}/{defect}.yaml", "w") as f: f.write("") runner = CliRunner() result = runner.invoke( @@ -3613,9 +3362,7 @@ def test_analyse(self): f"Saved results to {self.EXAMPLE_RESULTS}/{defect}_defect_folder/{defect}/{defect}.csv", result.output, ) - with open( - f"{self.EXAMPLE_RESULTS}/{defect}_defect_folder/{defect}/{defect}.csv" - ) as f: + with open(f"{self.EXAMPLE_RESULTS}/{defect}_defect_folder/{defect}/{defect}.csv") as f: file = f.read() csv_content = ( ",Bond Distortion,Σ{Displacements} (Å),Max Distance (Å),Δ Energy (eV)\n" @@ -3642,8 +3389,7 @@ def test_analyse(self): self.assertTrue( any( [ - str(warning.message) == "No output file in Bond_Distortion_10.0% " - "directory" + str(warning.message) == "No output file in Bond_Distortion_10.0% " "directory" for warning in w ] ) @@ -3671,13 +3417,13 @@ def test_analyse(self): ], catch_exceptions=True, ) + print([str(warning.message) for warning in w]) # for debugging self.assertTrue(any([warning.category == UserWarning for warning in w])) self.assertTrue( any( [ - str(warning.message) - == "`--path` option ignored when running from within defect folder (i.e. " - "when `--defect` is not specified." + "`--path` option ignored when running from within defect folder" + in str(warning.message) for warning in w ] ) @@ -3693,24 +3439,13 @@ def test_analyse(self): os.remove(f"{defect_name}.yaml") os.chdir(file_path) - # Test exception when run with no arguments in top-level folder + # Test no exception when run with no arguments in top-level folder os.chdir(self.EXAMPLE_RESULTS) - result = runner.invoke(snb, ["analyse"], catch_exceptions=True) - self.assertIn( - f"Could not analyse defect 'example_results' in directory '{self.DATA_DIR}'. Please " - "either specify a defect to analyse (with option --defect), run from within a single " - "defect directory (without setting --defect) or use the --all flag to analyse all " - "defects in the specified/current directory.", - str(result.exception), - ) - self.assertNotIn("Saved results to", result.output) + result = runner.invoke(snb, ["analyse"]) self.assertFalse( - any( - os.path.exists(i) - for i in os.listdir() - if (i.endswith(".csv") or i.endswith(".yaml")) - ) + any(os.path.exists(i) for i in os.listdir() if (i.endswith(".csv") or i.endswith(".yaml"))) ) + self.assertTrue(os.path.exists("v_Ti_0/v_Ti_0.csv")) # parsed successfully os.chdir(file_path) def test_plot(self): @@ -3746,9 +3481,7 @@ def test_plot(self): "and unperturbed: -3.26 eV.", result.output, ) # verbose output - self.assertIn( - f"Plot saved to {defect_name}_0/{defect_name}_0.png", result.output - ) + self.assertIn(f"Plot saved to {defect_name}_0/{defect_name}_0.png", result.output) self.assertEqual(w[0].category, UserWarning) self.assertEqual( f"Path {self.EXAMPLE_RESULTS}/distortion_metadata.json or {self.EXAMPLE_RESULTS}/" @@ -3756,9 +3489,7 @@ def test_plot(self): "its contents (to specify which neighbour atoms were distorted in plot text).", str(w[0].message), ) - self.assertTrue( - os.path.exists(os.path.join(self.EXAMPLE_RESULTS, defect, defect + ".png")) - ) + self.assertTrue(os.path.exists(os.path.join(self.EXAMPLE_RESULTS, defect, defect + ".png"))) # Figures are compared in the local test since on Github Actions images are saved # with a different size (raising error when comparing). self.tearDown() @@ -3770,9 +3501,7 @@ def test_plot(self): # test with new doped naming defect = "Te_i_Td_Te2.83_+2" - shutil.copytree( - f"{self.EXAMPLE_RESULTS}/v_Ti_0", f"{self.EXAMPLE_RESULTS}/{defect}" - ) + shutil.copytree(f"{self.EXAMPLE_RESULTS}/v_Ti_0", f"{self.EXAMPLE_RESULTS}/{defect}") with warnings.catch_warnings(record=True) as w: result = runner.invoke( snb, @@ -3798,12 +3527,10 @@ def test_plot(self): "its contents (to specify which neighbour atoms were distorted in plot text).", str(w[0].message), ) - self.assertTrue( - os.path.exists(os.path.join(self.EXAMPLE_RESULTS, defect, defect + ".png")) - ) + self.assertTrue(os.path.exists(os.path.join(self.EXAMPLE_RESULTS, defect, defect + ".png"))) self.tearDown() - # Test --all option, with the distortion_metadata.json file present to parse number of + # Test 'all' behaviour, with the distortion_metadata.json file present to parse number of # distorted neighbours and their identities defect = "v_Ti_0" defect_name = "v_Ti" @@ -3840,7 +3567,6 @@ def test_plot(self): snb, [ "plot", - "--all", "-p", self.EXAMPLE_RESULTS, "-f", @@ -3849,18 +3575,10 @@ def test_plot(self): catch_exceptions=False, ) self.assertTrue( - os.path.exists( - os.path.join( - self.EXAMPLE_RESULTS, f"{defect_name}_0/{defect_name}_0.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")) + os.path.exists(os.path.join(self.EXAMPLE_RESULTS, f"{defect_name}_0/{defect_name}_0.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"))) if w: [ self.assertNotIn( # no distortion_metadata.json warning @@ -3893,9 +3611,7 @@ def test_plot(self): "and unperturbed: -3.26 eV.", result.output, ) # non-verbose output - self.assertNotIn( - "Plot saved to v_Ti_0/v_Ti_0.png", result.output - ) # non-verbose + self.assertNotIn("Plot saved to v_Ti_0/v_Ti_0.png", result.output) # non-verbose self.assertTrue( len([warning for warning in w if warning.category == UserWarning]) == 0 ) # non-verbose @@ -3941,9 +3657,7 @@ def test_plot(self): result.output, ) # verbose output self.assertIn("Plot saved to v_Ti_0/v_Ti_0.png", result.output) # verbose - self.assertTrue( - len([warning for warning in w if warning.category == UserWarning]) == 1 - ) # verbose + self.assertTrue(len([warning for warning in w if warning.category == UserWarning]) == 1) # verbose self.assertTrue( any( [ # verbose @@ -3959,15 +3673,12 @@ def test_plot(self): ) self.assertTrue( os.path.exists( - f"{self.EXAMPLE_RESULTS}/{defect_name}_defect_folder" - f"/{defect_name}/v_Ti_0.png" + f"{self.EXAMPLE_RESULTS}/{defect_name}_defect_folder" f"/{defect_name}/v_Ti_0.png" ) ) self.assertTrue( os.path.exists( - f"{self.EXAMPLE_RESULTS}/" - f"{defect_name}_defect_folder/" - f"{defect_name}/v_Ti_0.yaml" + f"{self.EXAMPLE_RESULTS}/" f"{defect_name}_defect_folder/" f"{defect_name}/v_Ti_0.yaml" ) ) @@ -3996,9 +3707,7 @@ def test_plot(self): len([warning for warning in w if warning.category == UserWarning]) == 0 ) # verbose, but no distortion_metadata warning self.assertTrue( - os.path.exists( - f"{self.EXAMPLE_RESULTS}/{defect_name}_defect_folder/{defect_name}/v_Ti_0.png" - ) + os.path.exists(f"{self.EXAMPLE_RESULTS}/{defect_name}_defect_folder/{defect_name}/v_Ti_0.png") ) self.tearDown() @@ -4020,9 +3729,8 @@ def test_plot(self): self.assertTrue( any( [ - str(warning.message) - == "`--path` option ignored when running from within defect folder (i.e. " - "when `--defect` is not specified." + "`--path` option ignored when running from within defect folder" + in str(warning.message) for warning in w ] ) @@ -4050,28 +3758,18 @@ def test_plot(self): if_present_rm(os.getcwd() + "/v_Ti_0.yaml") self.tearDown() - # Test exception when run with no arguments in top-level folder + # Test no exception when run with no arguments in top-level folder os.chdir(self.EXAMPLE_RESULTS) - result = runner.invoke(snb, ["plot"], catch_exceptions=True) - self.assertIn( - f"Could not analyse & plot defect 'example_results' in directory '{self.DATA_DIR}'. " - "Please either specify a defect to analyse (with option --defect), run from within a " - "single defect directory (without setting --defect) or use the --all flag to analyse " - "all defects in the specified/current directory.", - str(result.exception), - ) - self.assertNotIn(f"Plot saved to v_Ti_0/v_Ti_0.png", result.output) - self.assertFalse( - any(os.path.exists(i) for i in os.listdir() if i.endswith(".yaml")) - ) + result = runner.invoke(snb, ["plot", "-v"]) + self.assertIn("Plot saved to v_Ti_0/v_Ti_0.png", result.output) + self.assertTrue(os.path.exists("v_Ti_0/v_Ti_0.png")) # parsed successfully self.tearDown() - # Test --all option, with --min_energy option + # Test 'all' behaviour, with --min_energy option result = runner.invoke( snb, [ "plot", - "--all", "-min", "1", "-p", @@ -4112,17 +3810,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) - ] + non_ignored_warnings = [warning for warning in w if "Subfolders with" not in str(warning.message)] self.assertEqual( - len( - [ - warning - for warning in non_ignored_warnings - if warning.category == UserWarning - ] - ), + len([warning for warning in non_ignored_warnings if warning.category == UserWarning]), 0, ) @@ -4181,14 +3871,10 @@ def test_regenerate(self): # test "*High_Energy*" ignored and doesn't cause errors if not os.path.exists( - os.path.join( - self.EXAMPLE_RESULTS, f"{defect}_0/Bond_Distortion_-48.0%_High_Energy" - ) + os.path.join(self.EXAMPLE_RESULTS, f"{defect}_0/Bond_Distortion_-48.0%_High_Energy") ): shutil.copytree( - os.path.join( - self.EXAMPLE_RESULTS, f"{defect}_0/Bond_Distortion_-60.0%" - ), + os.path.join(self.EXAMPLE_RESULTS, f"{defect}_0/Bond_Distortion_-60.0%"), os.path.join( self.EXAMPLE_RESULTS, f"{defect}_0/Bond_Distortion_-48.0%_High_Energy", @@ -4205,17 +3891,9 @@ def test_regenerate(self): ], catch_exceptions=False, ) - non_ignored_warnings = [ - warning for warning in w if "Subfolders with" not in str(warning.message) - ] + non_ignored_warnings = [warning for warning in w if "Subfolders with" not in str(warning.message)] self.assertEqual( - len( - [ - warning - for warning in non_ignored_warnings - if warning.category == UserWarning - ] - ), + len([warning for warning in non_ignored_warnings if warning.category == UserWarning]), 0, ) assert any( @@ -4274,17 +3952,13 @@ def test_groundstate(self): ], catch_exceptions=False, ) - self.assertTrue( - os.path.exists(f"{self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate/POSCAR") - ) + self.assertTrue(os.path.exists(f"{self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate/POSCAR")) self.assertIn( f"{defect}: Ground state structure (found with -0.55 distortion) saved to" f" {self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate/POSCAR", result.output, ) - gs_structure = Structure.from_file( - f"{self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate/POSCAR" - ) + gs_structure = Structure.from_file(f"{self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate/POSCAR") self.assertEqual(gs_structure, self.V_Cd_minus0pt55_CONTCAR_struc) if_present_rm(f"{self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate") @@ -4304,9 +3978,7 @@ def test_groundstate(self): catch_exceptions=False, ) self.assertTrue( - os.path.exists( - f"{self.VASP_CDTE_DATA_DIR}/{defect}/My_Groundstate/Groundstate_CONTCAR" - ) + os.path.exists(f"{self.VASP_CDTE_DATA_DIR}/{defect}/My_Groundstate/Groundstate_CONTCAR") ) self.assertIn( f"{defect}: Ground state structure (found with -0.55 distortion) saved to" @@ -4332,9 +4004,7 @@ def test_groundstate(self): ], catch_exceptions=True, ) - self.assertFalse( - os.path.exists(f"{self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate") - ) + self.assertFalse(os.path.exists(f"{self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate")) self.assertIsInstance(result.exception, FileNotFoundError) self.assertIn( "The structure file Fake_structure is not present in the directory" @@ -4351,9 +4021,7 @@ def test_groundstate(self): f" {self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate/POSCAR", result.output, ) - gs_structure = Structure.from_file( - f"{self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate/POSCAR" - ) + gs_structure = Structure.from_file(f"{self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate/POSCAR") self.assertEqual(gs_structure, self.V_Cd_minus0pt55_CONTCAR_struc) if_present_rm(f"{self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate") @@ -4385,9 +4053,7 @@ def test_groundstate(self): f" {self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate/POSCAR", result.output, ) - gs_structure = Structure.from_file( - f"{self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate/POSCAR" - ) + gs_structure = Structure.from_file(f"{self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate/POSCAR") self.assertEqual(gs_structure, self.V_Cd_minus0pt55_CONTCAR_struc) if_present_rm(f"{self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate") @@ -4409,9 +4075,7 @@ def test_groundstate(self): # test "*High_Energy*" ignored and doesn't cause errors defect = "vac_1_Cd_0" # in self.VASP_CDTE_DATA_DIR if not os.path.exists( - os.path.join( - self.EXAMPLE_RESULTS, f"v_Cd_0/Bond_Distortion_-48.0%_High_Energy" - ) + os.path.join(self.EXAMPLE_RESULTS, f"v_Cd_0/Bond_Distortion_-48.0%_High_Energy") ): shutil.copytree( os.path.join(self.EXAMPLE_RESULTS, f"v_Cd_0/Bond_Distortion_-60.0%"), @@ -4429,12 +4093,8 @@ def test_groundstate(self): ], catch_exceptions=False, ) - self.assertTrue( - os.path.exists(f"{self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate/POSCAR") - ) - gs_structure = Structure.from_file( - f"{self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate/POSCAR" - ) + self.assertTrue(os.path.exists(f"{self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate/POSCAR")) + gs_structure = Structure.from_file(f"{self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate/POSCAR") self.assertEqual(gs_structure, self.V_Cd_minus0pt55_CONTCAR_struc) if_present_rm(f"{self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate") self.assertFalse("High_Energy" in result.output) @@ -4449,17 +4109,13 @@ def test_groundstate(self): ], catch_exceptions=False, ) - self.assertTrue( - os.path.exists(f"{self.EXAMPLE_RESULTS}/{defect}/Groundstate/POSCAR") - ) + self.assertTrue(os.path.exists(f"{self.EXAMPLE_RESULTS}/{defect}/Groundstate/POSCAR")) self.assertIn( f"{defect}: Ground state structure (found with -0.4 distortion) saved to" f" {self.EXAMPLE_RESULTS}/{defect}/Groundstate/POSCAR", result.output, ) - gs_structure = Structure.from_file( - f"{self.EXAMPLE_RESULTS}/{defect}/Groundstate/POSCAR" - ) + gs_structure = Structure.from_file(f"{self.EXAMPLE_RESULTS}/{defect}/Groundstate/POSCAR") V_Ti_minus0pt4_structure = Structure.from_file( f"{self.EXAMPLE_RESULTS}/{defect}/Bond_Distortion_-40.0%/CONTCAR" ) @@ -4479,16 +4135,10 @@ def test_groundstate(self): ], catch_exceptions=False, ) - self.assertTrue( - os.path.exists(f"{self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate/POSCAR") - ) - self.assertFalse( - result.output - ) # no output (No "Parsing..." or "Groundstate structure + self.assertTrue(os.path.exists(f"{self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate/POSCAR")) + self.assertFalse(result.output) # no output (No "Parsing..." or "Groundstate structure # saved to...") - gs_structure = Structure.from_file( - f"{self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate/POSCAR" - ) + gs_structure = Structure.from_file(f"{self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate/POSCAR") self.assertEqual(gs_structure, self.V_Cd_minus0pt55_CONTCAR_struc) if_present_rm(f"{self.VASP_CDTE_DATA_DIR}/{defect}/Groundstate") @@ -4512,9 +4162,7 @@ def test_groundstate(self): result.output, ) gs_structure = Structure.from_file(f"{defect}/Groundstate/POSCAR") - V_Ti_minus0pt4_structure = Structure.from_file( - f"{defect}/Bond_Distortion_-40.0%/CONTCAR" - ) + V_Ti_minus0pt4_structure = Structure.from_file(f"{defect}/Bond_Distortion_-40.0%/CONTCAR") self.assertEqual(gs_structure, V_Ti_minus0pt4_structure) # test groundstate with only Unperturbed not high energy (should still write groundstate fine) @@ -4526,9 +4174,7 @@ def test_groundstate(self): os.chdir("v_Ti_0") os.remove("Bond_Distortion_-40.0%/OUTCAR") os.remove("Unperturbed/OUTCAR") - dumpfn( - {"distortions": {}, "Unperturbed": -822.66563022}, "v_Ti_0.yaml" - ) # only Unperturbed + dumpfn({"distortions": {}, "Unperturbed": -822.66563022}, "v_Ti_0.yaml") # only Unperturbed # parsed as all high energy result = runner.invoke(