diff --git a/docs/source/tutorials/python/phonons.ipynb b/docs/source/tutorials/python/phonons.ipynb index dd9bdd28..96e7a49c 100644 --- a/docs/source/tutorials/python/phonons.ipynb +++ b/docs/source/tutorials/python/phonons.ipynb @@ -140,7 +140,7 @@ " temp_min=0.0,\n", " temp_max=1000.0,\n", " minimize=False,\n", - " force_consts_to_hdf5=True,\n", + " hdf5=True,\n", " plot_to_file=True,\n", " symmetrize=False,\n", " write_full=True,\n", @@ -249,7 +249,7 @@ " temp_min=0.0,\n", " temp_max=1000.0,\n", " minimize=True,\n", - " force_consts_to_hdf5=True,\n", + " hdf5=True,\n", " plot_to_file=True,\n", " symmetrize=False,\n", " write_full=True,\n", @@ -317,7 +317,7 @@ " temp_min=0.0,\n", " temp_max=1000.0,\n", " minimize=True,\n", - " force_consts_to_hdf5=True,\n", + " hdf5=True,\n", " plot_to_file=True,\n", " symmetrize=False,\n", " write_full=True,\n", @@ -399,7 +399,7 @@ " temp_min=0.0,\n", " temp_max=1000.0,\n", " minimize=True,\n", - " force_consts_to_hdf5=True,\n", + " hdf5=True,\n", " plot_to_file=True,\n", " symmetrize=False,\n", " write_full=True,\n", @@ -440,7 +440,7 @@ " temp_min=0.0,\n", " temp_max=1000.0,\n", " minimize=True,\n", - " force_consts_to_hdf5=True,\n", + " hdf5=True,\n", " plot_to_file=True,\n", " symmetrize=False,\n", " write_full=True,\n", diff --git a/docs/source/user_guide/command_line.rst b/docs/source/user_guide/command_line.rst index 21641135..2644d166 100644 --- a/docs/source/user_guide/command_line.rst +++ b/docs/source/user_guide/command_line.rst @@ -494,7 +494,7 @@ This will save the Phonopy parameters, including displacements and force constan in addition to generating a log file, ``NaCl-phonons-log.yml``, and summary of inputs, ``NaCl-phonons-summary.yml``. Additionally, the ``--bands`` option can be added to calculate the band structure -and save the results to a compressed yaml file, ``NaCl-auto_bands.yml.xz``: +and save the results to a compressed yaml file, ``NaCl-auto_bands.hdf5``: .. code-block:: bash @@ -533,7 +533,7 @@ but band paths can also be specified explicitly using the ``--paths`` option to janus phonons --struct tests/data/NaCl.cif --arch mace_mp --bands --plot-to-file --paths tests/data/paths.yml -This will save the results in a compressed yaml file, ``NaCl-bands.yml.xz``, as well as the generated plot, ``NaCl-bands.svg``. +This will save the results in a compressed yaml file, ``NaCl-bands.hdf5``, as well as the generated plot, ``NaCl-bands.svg``. The ``--paths`` file must include: diff --git a/janus_core/calculations/phonons.py b/janus_core/calculations/phonons.py index ae7e079c..5c4c3369 100644 --- a/janus_core/calculations/phonons.py +++ b/janus_core/calculations/phonons.py @@ -4,6 +4,7 @@ from collections.abc import Sequence from typing import Any, get_args +from warnings import warn from ase import Atoms from numpy import ndarray @@ -108,7 +109,9 @@ class Phonons(BaseCalculation): temp_step Temperature step for thermal properties calculations, in K. Default is 50.0. force_consts_to_hdf5 - Whether to write force constants in hdf format or not. Default is True. + Deprecated. Please use `hdf5`. + hdf5 + Whether to write force constants and bands in hdf5 or not. Default is True. plot_to_file Whether to plot various graphs as band stuctures, dos/pdos in svg. Default is False. @@ -159,7 +162,8 @@ def __init__( temp_min: float = 0.0, temp_max: float = 1000.0, temp_step: float = 50.0, - force_consts_to_hdf5: bool = True, + force_consts_to_hdf5: bool | None = None, + hdf5: bool = True, plot_to_file: bool = False, write_results: bool = True, write_full: bool = True, @@ -238,7 +242,9 @@ def __init__( temp_step Temperature step for thermal calculations, in K. Default is 50.0. force_consts_to_hdf5 - Whether to write force constants in hdf format or not. Default is True. + Deprecated. Please use `hdf5`. + hdf5 + Whether to write force constants and bands in hdf5 or not. Default is True. plot_to_file Whether to plot various graphs as band stuctures, dos/pdos in svg. Default is False. @@ -277,12 +283,27 @@ def __init__( self.temp_min = temp_min self.temp_max = temp_max self.temp_step = temp_step - self.force_consts_to_hdf5 = force_consts_to_hdf5 + self.hdf5 = hdf5 self.plot_to_file = plot_to_file self.write_results = write_results self.write_full = write_full self.enable_progress_bar = enable_progress_bar + # Handle deprecation + if force_consts_to_hdf5 is not None: + if hdf5 is False: + raise ValueError( + """`force_consts_to_hdf5`: has replaced `hdf5`. + Please only use `hdf5`""" + ) + self.hdf5 = force_consts_to_hdf5 + warn( + """`force_consts_to_hdf5` has been deprecated. + Please use `hdf5`.""", + FutureWarning, + stacklevel=2, + ) + # Ensure supercell is a valid list self.supercell = [supercell] * 3 if isinstance(supercell, int) else supercell if len(self.supercell) not in [3, 9]: @@ -340,10 +361,11 @@ def __init__( # Output files self.phonopy_file = self._build_filename("phonopy.yml") self.force_consts_file = self._build_filename("force_constants.hdf5") - if self.qpoint_file: - self.bands_file = self._build_filename("bands.yml.xz") - else: - self.bands_file = self._build_filename("auto_bands.yml.xz") + + filename = "bands" + (".hdf5" if hdf5 else ".yml") + if not self.qpoint_file: + filename = f"auto_{filename}" + self.bands_file = self._build_filename(filename) self.bands_plot_file = self._build_filename("bands.svg") self.dos_file = self._build_filename("dos.dat") self.dos_plot_file = self._build_filename("dos.svg") @@ -430,9 +452,7 @@ def output_files(self) -> None: return { "log": self.log_kwargs["filename"] if self.logger else None, "params": self.phonopy_file if self.write_results else None, - "force_constants": ( - self.force_consts_file if self.force_consts_to_hdf5 else None - ), + "force_constants": (self.force_consts_file if self.hdf5 else None), "bands": ( self.bands_file if self.write_results and "bands" in self.calcs @@ -552,7 +572,7 @@ def write_force_constants( self, *, phonopy_file: PathLike | None = None, - force_consts_to_hdf5: bool | None = None, + hdf5: bool | None = None, force_consts_file: PathLike | None = None, ) -> None: """ @@ -563,11 +583,11 @@ def write_force_constants( phonopy_file Name of yaml file to save params of phonopy and optionally force constants. Default is inferred from `file_prefix`. - force_consts_to_hdf5 + hdf5 Whether to save the force constants separately to an hdf5 file. Default is - self.force_consts_to_hdf5. + self.hdf5. force_consts_file - Name of hdf5 file to save force constants. Unused if `force_consts_to_hdf5` + Name of hdf5 file to save force constants. Unused if `hdf5` is False. Default is inferred from `file_prefix`. """ if "phonon" not in self.results: @@ -576,8 +596,8 @@ def write_force_constants( "Please run `calc_force_constants` first" ) - if force_consts_to_hdf5 is None: - force_consts_to_hdf5 = self.force_consts_to_hdf5 + if hdf5 is None: + hdf5 = self.hdf5 if phonopy_file: self.phonopy_file = phonopy_file @@ -586,11 +606,11 @@ def write_force_constants( phonon = self.results["phonon"] - save_force_consts = not force_consts_to_hdf5 + save_force_consts = not hdf5 build_file_dir(self.phonopy_file) phonon.save(self.phonopy_file, settings={"force_constants": save_force_consts}) - if force_consts_to_hdf5: + if hdf5: build_file_dir(self.force_consts_file) write_force_constants_to_hdf5( phonon.force_constants, filename=self.force_consts_file @@ -621,6 +641,7 @@ def calc_bands(self, write_bands: bool | None = None, **kwargs) -> None: def write_bands( self, *, + hdf5: bool | None = None, bands_file: PathLike | None = None, save_plots: bool | None = None, plot_file: PathLike | None = None, @@ -630,6 +651,9 @@ def write_bands( Parameters ---------- + hdf5 + Whether to save the bands in an hdf5 file. Default is + self.hdf5. bands_file Name of yaml file to save band structure. Default is inferred from `file_prefix`. @@ -645,11 +669,15 @@ def write_bands( "Please run `calc_force_constants` first" ) + if hdf5 is None: + hdf5 = self.hdf5 + if save_plots is None: save_plots = self.plot_to_file if bands_file: self.bands_file = bands_file + if plot_file: self.bands_plot_file = plot_file @@ -682,10 +710,14 @@ def write_bands( ) build_file_dir(self.bands_file) - self.results["phonon"].write_yaml_band_structure( - filename=self.bands_file, - compression="lzma", - ) + if hdf5: + self.results["phonon"].write_hdf5_band_structure( + filename=self.bands_file, + ) + else: + self.results["phonon"].write_yaml_band_structure( + filename=self.bands_file, + ) bplt = self.results["phonon"].plot_band_structure() if save_plots: diff --git a/janus_core/cli/phonons.py b/janus_core/cli/phonons.py index 24786521..504f8c2f 100644 --- a/janus_core/cli/phonons.py +++ b/janus_core/cli/phonons.py @@ -26,7 +26,7 @@ Summary, Tracker, ) -from janus_core.cli.utils import yaml_converter_callback +from janus_core.cli.utils import deprecated_option, yaml_converter_callback app = Typer() @@ -110,11 +110,20 @@ def phonons( ), ] = 0.1, minimize_kwargs: MinimizeKwargs = None, - hdf5: Annotated[ + force_consts_to_hdf5: Annotated[ bool, Option( help="Whether to save force constants in hdf5.", rich_help_panel="Calculation", + callback=deprecated_option, + hidden=True, + ), + ] = None, + hdf5: Annotated[ + bool, + Option( + help="Whether to save force constants and bands in hdf5.", + rich_help_panel="Calculation", ), ] = True, plot_to_file: Annotated[ @@ -228,8 +237,11 @@ def phonons( Default is 0.1. minimize_kwargs Other keyword arguments to pass to geometry optimizer. Default is {}. + force_consts_to_hdf5 + Deprecated. Please use `hdf5`. hdf5 - Whether to save force constants in hdf5 format. Default is True. + Whether to save force constants and bands in hdf5 format. + Default is True. plot_to_file Whether to plot. Default is False. write_full @@ -391,7 +403,7 @@ def phonons( "temp_min": temp_min, "temp_max": temp_max, "temp_step": temp_step, - "force_consts_to_hdf5": hdf5, + "hdf5": hdf5, "plot_to_file": plot_to_file, "write_results": True, "write_full": write_full, diff --git a/pyproject.toml b/pyproject.toml index 15a41715..7f2a2940 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ "click<9,>=8.2.1", "codecarbon<4.0.0,>=3.0.4", "numpy<3.0.0,>=1.26.4", - "phonopy<3.0.0,>=2.23.1", + "phonopy<3.0.0,>=2.39.0", "pymatgen>=2025.1.24", "pyyaml<7.0.0,>=6.0.1", "rich<14.0.0,>=13.9.1", diff --git a/tests/test_phonons.py b/tests/test_phonons.py index 4706d3b8..f4e8df28 100644 --- a/tests/test_phonons.py +++ b/tests/test_phonons.py @@ -40,6 +40,22 @@ def test_calc_phonons(): assert "phonon" in phonons.results +def test_force_consts_to_hdf5_deprecation(): + """Test deprecation of force-consts-to-hdf5.""" + struct = read(DATA_PATH / "NaCl.cif") + struct.calc = choose_calculator(arch="mace_mp", model=MODEL_PATH) + with pytest.warns( + FutureWarning, match="`force_consts_to_hdf5` has been deprecated." + ): + phonons = Phonons( + struct=struct, + force_consts_to_hdf5=True, + ) + + phonons.calc_force_constants(write_force_consts=True) + assert "phonon" in phonons.results + + def test_optimize(tmp_path): """Test optimizing structure before calculation.""" log_file = tmp_path / "phonons.log" diff --git a/tests/test_phonons_cli.py b/tests/test_phonons_cli.py index 19d03acf..7ee9b85b 100644 --- a/tests/test_phonons_cli.py +++ b/tests/test_phonons_cli.py @@ -2,10 +2,10 @@ from __future__ import annotations -import lzma from pathlib import Path from ase.io import read +from h5py import File as HDF5Open import pytest from typer.testing import CliRunner import yaml @@ -37,7 +37,7 @@ def test_phonons(tmp_path): with chdir(tmp_path): results_dir = Path("janus_results") phonopy_path = results_dir / "NaCl-phonopy.yml" - bands_path = results_dir / "NaCl-auto_bands.yml.xz" + bands_path = results_dir / "NaCl-auto_bands.yml" log_path = results_dir / "NaCl-phonons-log.yml" summary_path = results_dir / "NaCl-phonons-summary.yml" @@ -66,14 +66,12 @@ def test_phonons(tmp_path): has_eigenvectors = False has_velocity = False - with lzma.open(bands_path, mode="rt") as file: - for line in file: - if "eigenvector" in line: - has_eigenvectors = True - if "group_velocity" in line: - has_velocity = True - if has_eigenvectors and has_velocity: - break + with open(bands_path, encoding="utf8") as file: + bands = yaml.safe_load(file) + if "eigenvector" in bands["phonon"][0]["band"][0]: + has_eigenvectors = True + if "group_velocity" in bands["phonon"][0]["band"][0]: + has_velocity = True assert has_eigenvectors and has_velocity assert "command" in phonon_summary @@ -109,7 +107,7 @@ def test_phonons(tmp_path): def test_bands_simple(tmp_path): """Test calculating force constants and reduced bands information.""" file_prefix = tmp_path / "NaCl" - autoband_results = tmp_path / "NaCl-auto_bands.yml.xz" + autoband_results = tmp_path / "NaCl-auto_bands.yml" summary_path = tmp_path / "NaCl-phonons-summary.yml" result = runner.invoke( @@ -132,7 +130,7 @@ def test_bands_simple(tmp_path): assert result.exit_code == 0 assert autoband_results.exists() - with lzma.open(autoband_results, mode="rb") as file: + with open(autoband_results, encoding="utf-8") as file: bands = yaml.safe_load(file) assert "eigenvector" not in bands["phonon"][0]["band"][0] assert bands["nqpoint"] == 126 @@ -149,10 +147,11 @@ def test_bands_simple(tmp_path): def test_hdf5(tmp_path): - """Test saving force constants to HDF5 in new directory.""" + """Test saving force constants and bands to HDF5 in new directory.""" file_prefix = tmp_path / "test" / "NaCl" phonon_results = tmp_path / "test" / "NaCl-phonopy.yml" hdf5_results = tmp_path / "test" / "NaCl-force_constants.hdf5" + bands_results = tmp_path / "test" / "NaCl-auto_bands.hdf5" summary_path = tmp_path / "test" / "NaCl-phonons-summary.yml" log_path = tmp_path / "test" / "NaCl-phonons-log.yml" @@ -164,6 +163,7 @@ def test_hdf5(tmp_path): DATA_PATH / "NaCl.cif", "--arch", "mace_mp", + "--bands", "--file-prefix", file_prefix, "--hdf5", @@ -172,6 +172,69 @@ def test_hdf5(tmp_path): assert result.exit_code == 0 assert phonon_results.exists() assert hdf5_results.exists() + assert bands_results.exists() + + # Read phonons summary file + with open(summary_path, encoding="utf8") as file: + phonon_summary = yaml.safe_load(file) + + output_files = { + "params": phonon_results, + "force_constants": hdf5_results, + "bands": bands_results, + "bands_plot": None, + "dos": None, + "dos_plot": None, + "band_dos_plot": None, + "pdos": None, + "pdos_plot": None, + "thermal": None, + "minimized_initial_structure": None, + "log": log_path, + "summary": summary_path, + } + check_output_files(summary=phonon_summary, output_files=output_files) + has_eigenvectors = False + has_velocity = False + nqpoints = 0 + with HDF5Open(bands_results, "r") as bands: + if "eigenvector" in bands: + has_eigenvectors = True + if "group_velocity" in bands: + has_velocity = True + nqpoints = bands["nqpoint"][0] + assert has_eigenvectors and has_velocity + assert nqpoints == 306 + + +def test_force_consts_to_hdf5_deprecated(tmp_path): + """Test saving force constants to HDF5 deprecated.""" + file_prefix = tmp_path / "test" / "NaCl" + phonon_results = tmp_path / "test" / "NaCl-phonopy.yml" + hdf5_results = tmp_path / "test" / "NaCl-force_constants.hdf5" + bands_results = tmp_path / "test" / "NaCl-auto_bands.hdf5" + summary_path = tmp_path / "test" / "NaCl-phonons-summary.yml" + log_path = tmp_path / "test" / "NaCl-phonons-log.yml" + + result = runner.invoke( + app, + [ + "phonons", + "--struct", + DATA_PATH / "NaCl.cif", + "--arch", + "mace_mp", + "--bands", + "--file-prefix", + file_prefix, + "--force-consts-to-hdf5", + ], + ) + + assert result.exit_code == 0 + assert phonon_results.exists() + assert hdf5_results.exists() + assert bands_results.exists() # Read phonons summary file with open(summary_path, encoding="utf8") as file: @@ -180,7 +243,7 @@ def test_hdf5(tmp_path): output_files = { "params": phonon_results, "force_constants": hdf5_results, - "bands": None, + "bands": bands_results, "bands_plot": None, "dos": None, "dos_plot": None, @@ -289,10 +352,10 @@ def test_plot(tmp_path): """Test for plotting routines.""" file_prefix = tmp_path / "NaCl" phonon_results = tmp_path / "NaCl-phonopy.yml" - bands_path = tmp_path / "NaCl-auto_bands.yml.xz" + bands_path = tmp_path / "NaCl-auto_bands.yml" pdos_results = tmp_path / "NaCl-pdos.dat" dos_results = tmp_path / "NaCl-dos.dat" - autoband_results = tmp_path / "NaCl-auto_bands.yml.xz" + autoband_results = tmp_path / "NaCl-auto_bands.yml" summary_path = tmp_path / "NaCl-phonons-summary.yml" log_path = tmp_path / "NaCl-phonons-log.yml" svgs = [ @@ -630,7 +693,7 @@ def test_paths(tmp_path): """Test passing qpoint file.""" file_prefix = tmp_path / "NaCl" qpoint_file = DATA_PATH / "paths.yml" - band_results = tmp_path / "NaCl-bands.yml.xz" + band_results = tmp_path / "NaCl-bands.yml" result = runner.invoke( app, @@ -651,7 +714,7 @@ def test_paths(tmp_path): assert result.exit_code == 0 assert band_results.exists() - with lzma.open(band_results, mode="rb") as file: + with open(band_results, encoding="utf-8") as file: bands = yaml.safe_load(file) assert bands["nqpoint"] == 11 assert bands["npath"] == 1