diff --git a/Framework/PythonInterface/plugins/algorithms/WorkflowAlgorithms/SimulatedDensityOfStates.py b/Framework/PythonInterface/plugins/algorithms/WorkflowAlgorithms/SimulatedDensityOfStates.py index 7054b1d1c580..9d6a3bf02ff7 100644 --- a/Framework/PythonInterface/plugins/algorithms/WorkflowAlgorithms/SimulatedDensityOfStates.py +++ b/Framework/PythonInterface/plugins/algorithms/WorkflowAlgorithms/SimulatedDensityOfStates.py @@ -18,7 +18,7 @@ import mantid.simpleapi as s_api from dos.load_castep import parse_castep_file -from dos.load_euphonic import euphonic_available, get_data_with_euphonic +from dos.load_euphonic import get_data_with_euphonic from dos.load_phonon import parse_phonon_file @@ -135,14 +135,6 @@ def validateInputs(self): ions = self.getProperty("Ions").value calc_partial = len(ions) > 0 - if euphonic_filename and not euphonic_available(): - issues["ForceConstantsFile"] = ( - "Cannot import the Euphonic library for force constants import. " - "This will be included in a future version of Mantid. " - "Until then, it can be installed using users/AdamJackson/install_euphonic.py " - "from the Script Repository." - ) - if spec_type == "IonTable" and not pdos_available: issues["SpectrumType"] = "Cannot produce ion table when only .castep file is provided" @@ -271,13 +263,9 @@ def _read_file(self): elif castep_filename: return self._read_data_from_file(castep_filename) elif euphonic_filename: - if euphonic_available(): - file_data, self._element_isotope = get_data_with_euphonic( - euphonic_filename, cutoff=float(self.getPropertyValue("ForceConstantsSampling")), acoustic_sum_rule=None - ) - else: - raise ValueError("Could not load file using Euphonic: you may need to install this library.") - + file_data, self._element_isotope = get_data_with_euphonic( + euphonic_filename, cutoff=float(self.getPropertyValue("ForceConstantsSampling")), acoustic_sum_rule=None + ) self._num_ions = file_data["num_ions"] return file_data diff --git a/Testing/SystemTests/tests/framework/SimulatedDensityOfStatesEuphonicPackaging.py b/Testing/SystemTests/tests/framework/SimulatedDensityOfStatesEuphonicPackaging.py deleted file mode 100644 index c0d11ecd471c..000000000000 --- a/Testing/SystemTests/tests/framework/SimulatedDensityOfStatesEuphonicPackaging.py +++ /dev/null @@ -1,132 +0,0 @@ -import importlib -import os -import pathlib -import site -import subprocess -import sys -import tempfile - -import scipy - -from abins.test_helpers import find_file -from dos.load_euphonic import euphonic_available -from mantid.simpleapi import SimulatedDensityOfStates -from systemtesting import MantidSystemTest - - -class SimulatedDensityOfStatesTest(MantidSystemTest): - """Make sure normal case will run regardless of Euphonic status""" - - def runTest(self): - SimulatedDensityOfStates( - CASTEPFile=find_file("Na2SiF6_CASTEP.phonon"), Function="Gaussian", SpectrumType="DOS", OutputWorkspace="Na2SiF6_DOS" - ) - - def validate(self): - return ("Na2SiF6_DOS", "Na2SiF6_DOS.nxs") - - -class SimulatedDensityOfStatesEuphonicTest(MantidSystemTest): - """ "Install Euphonic library to temporary prefix and check results""" - - def skipTests(self): - return sys.platform.startswith("darwin") - - @staticmethod - def _add_libs_from_prefix(prefix_path): - package_dirs = [] - for lib_dir in ("lib", "lib64"): - if (prefix_path / lib_dir).is_dir(): - site_packages = next((prefix_path / lib_dir).iterdir()) / "site-packages" - if site_packages.is_dir(): - site.addsitedir(site_packages) - package_dirs.append(site_packages) - - if package_dirs: - return package_dirs - else: - if not prefix_path.is_dir(): - raise FileNotFoundError(f"Install prefix {prefix_path} does not exist.") - else: - directory_contents = list(prefix_path.iterdir()) - raise FileNotFoundError( - ("Could not find site-packages for temporary dir. " "Here are the directory contents: ") + "; ".join(directory_contents) - ) - - @classmethod - def _install_euphonic_to_tmp_prefix(cls, tmp_prefix, verbose=False): - """Install Euphonic library to temporary prefix - - Up-to-date versions of Pip and Packaging are used to support this - - If necessary, additional dependencies are installed to scipy_prefix - """ - # First check if we are using debian-style Pip, where --system - # is needed to prevent default --user option from breaking --prefix. - - if "--system" in subprocess.check_output([sys.executable, "-m", "pip", "install", "-h"]).decode(): - compatibility_args = ["--system"] - else: - compatibility_args = [] - - process = subprocess.run( - [sys.executable, "-m", "pip", "install", "--prefix", tmp_prefix] + compatibility_args + ["pip", "packaging"], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) - - prefix_path = pathlib.Path(tmp_prefix) - - # Add to path if anything was installed so that subsequent calls to - # Pip can see those dependencies - env = os.environ.copy() - if list(prefix_path.iterdir()): - tmp_site_packages = cls._add_libs_from_prefix(prefix_path) - - process_pythonpath = ":".join([str(dir) for dir in tmp_site_packages] + [os.environ["PYTHONPATH"]]) - env["PYTHONPATH"] = process_pythonpath - - # Install minimum Scipy version for Euphonic if necessary - from packaging import version - - if version.parse(scipy.version.version) < version.parse("1.0.1"): - process = subprocess.run( - [sys.executable, "-m", "pip", "install", "--ignore-installed", "--no-deps", "--prefix", tmp_prefix, "scipy==1.0", "pytest"], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - env=env, - ) - if verbose: - print(process.stdout.decode("utf-8")) - - # Add prefix again, in case nothing was installed before - tmp_site_packages = cls._add_libs_from_prefix(prefix_path) - importlib.reload(scipy) - - process = subprocess.run( - [sys.executable, "-m", "pip", "install", "--prefix", tmp_prefix, "euphonic[phonopy_reader]==0.6.5"], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - env=env, - ) - if verbose: - print(process.stdout.decode("utf-8")) - - # Update path again, in case a new lib/lib64 diretory was created - cls._add_libs_from_prefix(pathlib.Path(tmp_prefix)) - - def runTest(self): - with tempfile.TemporaryDirectory() as tmp_prefix: - - if not euphonic_available(): - self._install_euphonic_to_tmp_prefix(tmp_prefix) - - import pint # noqa: F401 - import euphonic # noqa: F401 - - SimulatedDensityOfStates( - ForceConstantsFile=find_file("phonopy-Al.yaml"), Function="Gaussian", SpectrumType="DOS", OutputWorkspace="phonopy-Al_DOS" - ) - - def validate(self): - return ("phonopy-Al_DOS", "phonopy-Al_DOS.nxs") diff --git a/Testing/SystemTests/tests/framework/SimulatedDensityOfStatesTest.py b/Testing/SystemTests/tests/framework/SimulatedDensityOfStatesTest.py new file mode 100644 index 000000000000..57a8f3fa4dd9 --- /dev/null +++ b/Testing/SystemTests/tests/framework/SimulatedDensityOfStatesTest.py @@ -0,0 +1,27 @@ +from abins.test_helpers import find_file +from mantid.simpleapi import SimulatedDensityOfStates +from systemtesting import MantidSystemTest + + +class SimulatedDensityOfStatesTest(MantidSystemTest): + """Make sure normal case will run regardless of Euphonic status""" + + def runTest(self): + SimulatedDensityOfStates( + CASTEPFile=find_file("Na2SiF6_CASTEP.phonon"), Function="Gaussian", SpectrumType="DOS", OutputWorkspace="Na2SiF6_DOS" + ) + + def validate(self): + return ("Na2SiF6_DOS", "Na2SiF6_DOS.nxs") + + +class SimulatedDensityOfStatesEuphonicTest(MantidSystemTest): + """ "Install Euphonic library to temporary prefix and check results""" + + def runTest(self): + SimulatedDensityOfStates( + ForceConstantsFile=find_file("phonopy-Al.yaml"), Function="Gaussian", SpectrumType="DOS", OutputWorkspace="phonopy-Al_DOS" + ) + + def validate(self): + return ("phonopy-Al_DOS", "phonopy-Al_DOS.nxs") diff --git a/scripts/Inelastic/dos/load_euphonic.py b/scripts/Inelastic/dos/load_euphonic.py index 2caf0075fed3..424804011b46 100644 --- a/scripts/Inelastic/dos/load_euphonic.py +++ b/scripts/Inelastic/dos/load_euphonic.py @@ -4,52 +4,61 @@ # NScD Oak Ridge National Laboratory, European Spallation Source, # Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS # SPDX - License - Identifier: GPL - 3.0 + -from typing import Optional + +from os import PathLike +from typing import Optional, Union + +from euphonic import ForceConstants, QpointPhononModes +from euphonic.cli.utils import load_data_from_file import numpy as np from mantid.kernel import logger -def euphonic_available(): - """Find out if Euphonic modules can be imported, without raising an error - - This allows the Simulation Interface to query the availability of these - formats without complex error-handling. +def force_constants_from_file(filename: Union[str, PathLike]) -> ForceConstants: """ - try: - from euphonic.cli.utils import force_constants_from_file # noqa: F401 - from euphonic.util import mp_grid # noqa: F401 - except ImportError: - return False - return True + Read force constants file with Euphonic + Imitates a function from Euphonic versions < 1.0 for compatibility purposes -def euphonic_calculate_modes(filename: str, cutoff: float = 20.0, gamma: bool = True, acoustic_sum_rule: Optional[str] = "reciprocal"): + Args: + filename: Force constant data file (phonopy.yaml, .castep_bin or + Euphonic JSON) """ - Read force constants file with Euphonic and sample frequencies/modes + data = load_data_from_file(filename) + if not isinstance(data, ForceConstants): + raise ValueError(f"File {filename} does not contain force constants") + return data - :param filename: Input data - :param cutoff: - Sampling density of Brillouin-zone. Specified as real-space length - cutoff in Angstrom. - :param gamma: - Shift sampling grid to include the Gamma-point. - :param acoustic_sum_rule: - Apply acoustic sum rule correction to force constants: options are - 'realspace' and 'reciprocal', specifying different implementations of - the correction. If None, no correction is applied. This option is - referred to as "asr" in the Euphonic python API and command-line tools. - :returns: euphonic.QpointPhononModes +def euphonic_calculate_modes( + filename: str, cutoff: float = 20.0, gamma: bool = True, acoustic_sum_rule: Optional[str] = "reciprocal" +) -> QpointPhononModes: + """ + Read force constants file with Euphonic and sample frequencies/modes + Args: + filename: + Input data + cutoff: + Sampling density of Brillouin-zone. Specified as real-space length + cutoff in Angstrom. + gamma: + Shift sampling grid to include the Gamma-point. + acoustic_sum_rule: + Apply acoustic sum rule correction to force constants: options are + 'realspace' and 'reciprocal', specifying different implementations + of the correction. If None, no correction is applied. This option + is referred to as "asr" in the Euphonic python API and command-line + tools. """ from math import ceil - from euphonic.cli.utils import force_constants_from_file from euphonic.util import mp_grid fc = force_constants_from_file(filename) + recip_lattice_lengths = np.linalg.norm(fc.crystal.reciprocal_cell().to("1/angstrom").magnitude, axis=1) mp_sampling = [ceil(x) for x in (cutoff * recip_lattice_lengths / (2 * np.pi))] qpts = mp_grid(mp_sampling) diff --git a/scripts/abins/abinsalgorithm.py b/scripts/abins/abinsalgorithm.py index fa4e35c23c12..686247c1ddfb 100644 --- a/scripts/abins/abinsalgorithm.py +++ b/scripts/abins/abinsalgorithm.py @@ -782,27 +782,18 @@ def _validate_castep_input_file(cls, filename_full_path: str) -> dict: @classmethod def _validate_euphonic_input_file(cls, filename_full_path: str) -> dict: logger.information("Validate force constants file for interpolation.") - from dos.load_euphonic import euphonic_available - if euphonic_available(): - try: - from euphonic.cli.utils import force_constants_from_file + from dos.load_euphonic import force_constants_from_file - force_constants_from_file(filename_full_path) - return dict(Invalid=False, Comment="") - except Exception as error: - if hasattr(error, "message"): - message = error.message - else: - message = str(error) - return dict(Invalid=True, Comment=f"Problem opening force constants file with Euphonic.: {message}") - else: - return dict( - Invalid=True, - Comment=( - "Could not import Euphonic module. " "Try running user/AdamJackson/install_euphonic.py from the Script Repository." - ), - ) + try: + force_constants_from_file(filename_full_path) + return dict(Invalid=False, Comment="") + except Exception as error: + if hasattr(error, "message"): + message = error.message + else: + message = str(error) + return dict(Invalid=True, Comment=f"Problem opening force constants file with Euphonic.: {message}") @classmethod def _validate_vasp_input_file(cls, filename_full_path: str) -> dict: diff --git a/scripts/abins/input/euphonicloader.py b/scripts/abins/input/euphonicloader.py index b29a903fc177..a7098a1b97ec 100644 --- a/scripts/abins/input/euphonicloader.py +++ b/scripts/abins/input/euphonicloader.py @@ -10,7 +10,7 @@ from .abinitioloader import AbInitioLoader from abins.parameters import sampling as sampling_parameters -from dos.load_euphonic import euphonic_available, euphonic_calculate_modes +from dos.load_euphonic import euphonic_calculate_modes class EuphonicLoader(AbInitioLoader): @@ -39,11 +39,6 @@ def read_vibrational_or_phonon_data(self): gamma-point-only sampling. """ - if not euphonic_available(): - raise ImportError( - "Could not import Euphonic library; this is " "required to import force constants from Phonopy or .castep_bin." - ) - cutoff = sampling_parameters["force_constants"]["qpt_cutoff"] modes = euphonic_calculate_modes(filename=self._clerk.get_input_filename(), cutoff=cutoff) diff --git a/scripts/test/Abins/AbinsLoadPhonopyTest.py b/scripts/test/Abins/AbinsLoadPhonopyTest.py index f366977dd924..25a6ff26fc77 100644 --- a/scripts/test/Abins/AbinsLoadPhonopyTest.py +++ b/scripts/test/Abins/AbinsLoadPhonopyTest.py @@ -9,7 +9,6 @@ import abins.input import abins.test_helpers from abins.input import EuphonicLoader -from dos.load_euphonic import euphonic_available class AbinsLoadPhonopyTest(unittest.TestCase, abins.input.Tester): @@ -22,7 +21,6 @@ def tearDown(self): abins.test_helpers.remove_output_files(list_of_names=["_LoadPhonopy"]) abins.parameters.sampling["force_constants"]["qpt_cutoff"] = self.default_cutoff - @unittest.skipUnless(euphonic_available(), "Optional dependency (euphonic) not available") def test_non_existing_file(self): with self.assertRaises(IOError): bad_phonopy_reader = EuphonicLoader(input_ab_initio_filename="NonExistingFile.yaml")