diff --git a/src/pymatgen/analysis/xas/spectrum.py b/src/pymatgen/analysis/xas/spectrum.py index 3fd22671448..a1dac4b2224 100644 --- a/src/pymatgen/analysis/xas/spectrum.py +++ b/src/pymatgen/analysis/xas/spectrum.py @@ -10,12 +10,16 @@ from scipy.interpolate import interp1d from pymatgen.analysis.structure_matcher import StructureMatcher +from pymatgen.core import Element from pymatgen.core.spectrum import Spectrum from pymatgen.symmetry.analyzer import SpacegroupAnalyzer if TYPE_CHECKING: + from collections.abc import Sequence from typing import Literal + from pymatgen.core import Structure + __author__ = "Chen Zheng, Yiming Chen" __copyright__ = "Copyright 2012, The Materials Project" __version__ = "3.0" @@ -42,10 +46,11 @@ class XAS(Spectrum): Attributes: x (Sequence[float]): The sequence of energies. y (Sequence[float]): The sequence of mu(E). - absorbing_element (str): The absorbing element of the spectrum. + absorbing_element (str or .Element): The absorbing element of the spectrum. edge (str): The edge of the spectrum. spectrum_type (str): The type of the spectrum (XANES or EXAFS). absorbing_index (int): The absorbing index of the spectrum. + zero_negative_intensity (bool) : Whether to set unphysical negative intensities to zero """ XLABEL = "Energy" @@ -53,18 +58,19 @@ class XAS(Spectrum): def __init__( self, - x, - y, - structure, - absorbing_element, - edge="K", - spectrum_type="XANES", - absorbing_index=None, + x: Sequence, + y: Sequence, + structure: Structure, + absorbing_element: str | Element, + edge: str = "K", + spectrum_type: str = "XANES", + absorbing_index: int | None = None, + zero_negative_intensity: bool = False, ): """Initialize a spectrum object.""" super().__init__(x, y, structure, absorbing_element, edge) self.structure = structure - self.absorbing_element = absorbing_element + self.absorbing_element = Element(absorbing_element) self.edge = edge self.spectrum_type = spectrum_type self.e0 = self.x[np.argmax(np.gradient(self.y) / np.gradient(self.x))] @@ -75,8 +81,16 @@ def __init__( ] self.absorbing_index = absorbing_index # check for empty spectra and negative intensities - if sum(1 for i in self.y if i <= 0) / len(self.y) > 0.05: - raise ValueError("Double check the intensities. Most of them are non-positive.") + neg_intens_mask = self.y < 0.0 + if len(self.y[neg_intens_mask]) / len(self.y) > 0.05: + warnings.warn( + "Double check the intensities. More than 5% of them are negative.", + UserWarning, + stacklevel=2, + ) + self.zero_negative_intensity = zero_negative_intensity + if self.zero_negative_intensity: + self.y[neg_intens_mask] = 0.0 def __str__(self): return ( diff --git a/src/pymatgen/io/lobster/outputs.py b/src/pymatgen/io/lobster/outputs.py index bc5f8e17800..300e2f68c90 100644 --- a/src/pymatgen/io/lobster/outputs.py +++ b/src/pymatgen/io/lobster/outputs.py @@ -1710,29 +1710,17 @@ def has_good_quality_check_occupied_bands( Returns: bool: True if the quality of the projection is good. """ - for matrix in self.band_overlaps_dict[Spin.up]["matrices"]: - for iband1, band1 in enumerate(matrix): - for iband2, band2 in enumerate(band1): - if iband1 < number_occ_bands_spin_up and iband2 < number_occ_bands_spin_up: - if iband1 == iband2: - if abs(band2 - 1.0).all() > limit_deviation: - return False - elif band2.all() > limit_deviation: - return False - - if spin_polarized: - for matrix in self.band_overlaps_dict[Spin.down]["matrices"]: - for iband1, band1 in enumerate(matrix): - for iband2, band2 in enumerate(band1): - if number_occ_bands_spin_down is None: - raise ValueError("number_occ_bands_spin_down has to be specified") - - if iband1 < number_occ_bands_spin_down and iband2 < number_occ_bands_spin_down: - if iband1 == iband2: - if abs(band2 - 1.0).all() > limit_deviation: - return False - elif band2.all() > limit_deviation: - return False + if spin_polarized and number_occ_bands_spin_down is None: + raise ValueError("number_occ_bands_spin_down has to be specified") + + for spin in (Spin.up, Spin.down) if spin_polarized else (Spin.up,): + num_occ_bands = number_occ_bands_spin_up if spin is Spin.up else number_occ_bands_spin_down + + for overlap_matrix in self.band_overlaps_dict[spin]["matrices"]: + sub_array = np.asarray(overlap_matrix)[:num_occ_bands, :num_occ_bands] + + if not np.allclose(sub_array, np.identity(num_occ_bands), atol=limit_deviation, rtol=0): + return False return True diff --git a/src/pymatgen/io/vasp/inputs.py b/src/pymatgen/io/vasp/inputs.py index 07bff38a14a..6fa0f690df2 100644 --- a/src/pymatgen/io/vasp/inputs.py +++ b/src/pymatgen/io/vasp/inputs.py @@ -970,6 +970,7 @@ def proc_val(key: str, val: str) -> list | bool | float | int | str: "PARAM1", "PARAM2", "ENCUT", + "NUPDOWN", ) int_keys = ( "NSW", @@ -987,7 +988,6 @@ def proc_val(key: str, val: str) -> list | bool | float | int | str: "LMAXMIX", "NSIM", "NKRED", - "NUPDOWN", "ISPIND", "LDAUTYPE", "IVDW", diff --git a/tests/analysis/xas/test_spectrum.py b/tests/analysis/xas/test_spectrum.py index eee2483bb17..92a04879dc3 100644 --- a/tests/analysis/xas/test_spectrum.py +++ b/tests/analysis/xas/test_spectrum.py @@ -67,10 +67,10 @@ def test_str(self): assert str(self.k_xanes) == "Co K Edge XANES for LiCoO2: , >" def test_validate(self): - y_zeros = np.zeros(len(self.k_xanes.x)) - with pytest.raises( - ValueError, - match="Double check the intensities. Most of them are non-positive", + y_zeros = -np.ones(len(self.k_xanes.x)) + with pytest.warns( + UserWarning, + match="Double check the intensities. More than 5% of them are negative.", ): XAS( self.k_xanes.x, @@ -79,6 +79,17 @@ def test_validate(self): self.k_xanes.absorbing_element, ) + def test_zero_negative_intensity(self): + y_w_neg_intens = [(-1) ** i * v for i, v in enumerate(self.k_xanes.y)] + spectrum = XAS( + self.k_xanes.x, + y_w_neg_intens, + self.k_xanes.structure, + self.k_xanes.absorbing_element, + zero_negative_intensity=True, + ) + assert all(v == 0.0 for i, v in enumerate(spectrum.y) if i % 2 == 1) + def test_stitch_xafs(self): with pytest.raises(ValueError, match="Invalid mode. Only XAFS and L23 are supported"): XAS.stitch(self.k_xanes, self.k_exafs, mode="invalid") diff --git a/tests/io/lobster/test_outputs.py b/tests/io/lobster/test_outputs.py index 30ea62e687f..4dac8c4a01b 100644 --- a/tests/io/lobster/test_outputs.py +++ b/tests/io/lobster/test_outputs.py @@ -1,5 +1,6 @@ from __future__ import annotations +import copy import json import os from unittest import TestCase @@ -1481,7 +1482,7 @@ def test_get_bandstructure(self): class TestBandoverlaps(TestCase): def setUp(self): - # test spin-polarized calc and non spinpolarized calc + # test spin-polarized calc and non spin-polarized calc self.band_overlaps1 = Bandoverlaps(f"{TEST_DIR}/bandOverlaps.lobster.1") self.band_overlaps2 = Bandoverlaps(f"{TEST_DIR}/bandOverlaps.lobster.2") @@ -1515,9 +1516,18 @@ def test_attributes(self): assert self.band_overlaps2.max_deviation[-1] == approx(1.48451e-05) assert self.band_overlaps2_new.max_deviation[-1] == approx(0.45154) - def test_has_good_quality(self): + def test_has_good_quality_maxDeviation(self): assert not self.band_overlaps1.has_good_quality_maxDeviation(limit_maxDeviation=0.1) assert not self.band_overlaps1_new.has_good_quality_maxDeviation(limit_maxDeviation=0.1) + + assert self.band_overlaps1.has_good_quality_maxDeviation(limit_maxDeviation=100) + assert self.band_overlaps1_new.has_good_quality_maxDeviation(limit_maxDeviation=100) + assert self.band_overlaps2.has_good_quality_maxDeviation() + assert not self.band_overlaps2_new.has_good_quality_maxDeviation() + assert not self.band_overlaps2.has_good_quality_maxDeviation(limit_maxDeviation=0.0000001) + assert not self.band_overlaps2_new.has_good_quality_maxDeviation(limit_maxDeviation=0.0000001) + + def test_has_good_quality_check_occupied_bands(self): assert not self.band_overlaps1.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=9, number_occ_bands_spin_down=5, @@ -1545,65 +1555,58 @@ def test_has_good_quality(self): assert not self.band_overlaps1.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=1, number_occ_bands_spin_down=1, - limit_deviation=0.000001, + limit_deviation=1e-6, spin_polarized=True, ) assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=1, number_occ_bands_spin_down=1, - limit_deviation=0.000001, + limit_deviation=1e-6, spin_polarized=True, ) assert not self.band_overlaps1.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=1, number_occ_bands_spin_down=0, - limit_deviation=0.000001, + limit_deviation=1e-6, spin_polarized=True, ) assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=1, number_occ_bands_spin_down=0, - limit_deviation=0.000001, + limit_deviation=1e-6, spin_polarized=True, ) assert not self.band_overlaps1.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=0, number_occ_bands_spin_down=1, - limit_deviation=0.000001, + limit_deviation=1e-6, spin_polarized=True, ) assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=0, number_occ_bands_spin_down=1, - limit_deviation=0.000001, + limit_deviation=1e-6, spin_polarized=True, ) assert not self.band_overlaps1.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=4, number_occ_bands_spin_down=4, - limit_deviation=0.001, + limit_deviation=1e-3, spin_polarized=True, ) assert not self.band_overlaps1_new.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=4, number_occ_bands_spin_down=4, - limit_deviation=0.001, + limit_deviation=1e-3, spin_polarized=True, ) - - assert self.band_overlaps1.has_good_quality_maxDeviation(limit_maxDeviation=100) - assert self.band_overlaps1_new.has_good_quality_maxDeviation(limit_maxDeviation=100) - assert self.band_overlaps2.has_good_quality_maxDeviation() - assert not self.band_overlaps2_new.has_good_quality_maxDeviation() - assert not self.band_overlaps2.has_good_quality_maxDeviation(limit_maxDeviation=0.0000001) - assert not self.band_overlaps2_new.has_good_quality_maxDeviation(limit_maxDeviation=0.0000001) assert not self.band_overlaps2.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=10, limit_deviation=0.0000001 + number_occ_bands_spin_up=10, limit_deviation=1e-7 ) assert not self.band_overlaps2_new.has_good_quality_check_occupied_bands( - number_occ_bands_spin_up=10, limit_deviation=0.0000001 + number_occ_bands_spin_up=10, limit_deviation=1e-7 ) - assert not self.band_overlaps2.has_good_quality_check_occupied_bands( + assert self.band_overlaps2.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=1, limit_deviation=0.1 ) @@ -1614,7 +1617,7 @@ def test_has_good_quality(self): number_occ_bands_spin_up=1, limit_deviation=1e-8 ) assert self.band_overlaps2.has_good_quality_check_occupied_bands(number_occ_bands_spin_up=10, limit_deviation=1) - assert not self.band_overlaps2_new.has_good_quality_check_occupied_bands( + assert self.band_overlaps2_new.has_good_quality_check_occupied_bands( number_occ_bands_spin_up=2, limit_deviation=0.1 ) assert self.band_overlaps2.has_good_quality_check_occupied_bands(number_occ_bands_spin_up=1, limit_deviation=1) @@ -1622,6 +1625,78 @@ def test_has_good_quality(self): number_occ_bands_spin_up=1, limit_deviation=2 ) + def test_has_good_quality_check_occupied_bands_patched(self): + """Test with patched data.""" + + limit_deviation = 0.1 + + rng = np.random.default_rng(42) # set seed for reproducibility + + band_overlaps = copy.deepcopy(self.band_overlaps1_new) + + number_occ_bands_spin_up_all = list(range(band_overlaps.band_overlaps_dict[Spin.up]["matrices"][0].shape[0])) + number_occ_bands_spin_down_all = list( + range(band_overlaps.band_overlaps_dict[Spin.down]["matrices"][0].shape[0]) + ) + + for actual_deviation in [0.05, 0.1, 0.2, 0.5, 1.0]: + for spin in (Spin.up, Spin.down): + for number_occ_bands_spin_up, number_occ_bands_spin_down in zip( + number_occ_bands_spin_up_all, number_occ_bands_spin_down_all, strict=False + ): + for i_arr, array in enumerate(band_overlaps.band_overlaps_dict[spin]["matrices"]): + number_occ_bands = number_occ_bands_spin_up if spin is Spin.up else number_occ_bands_spin_down + + shape = array.shape + assert np.all(np.array(shape) >= number_occ_bands) + assert len(shape) == 2 + assert shape[0] == shape[1] + + # Generate a noisy background array + patch_array = rng.uniform(0, 10, shape) + + # Patch the top-left sub-array (the part that would be checked) + patch_array[:number_occ_bands, :number_occ_bands] = np.identity(number_occ_bands) + rng.uniform( + 0, actual_deviation, (number_occ_bands, number_occ_bands) + ) + + band_overlaps.band_overlaps_dict[spin]["matrices"][i_arr] = patch_array + + result = band_overlaps.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=number_occ_bands_spin_up, + number_occ_bands_spin_down=number_occ_bands_spin_down, + spin_polarized=True, + limit_deviation=limit_deviation, + ) + # Assert for expected results + if ( + actual_deviation == 0.05 + and number_occ_bands_spin_up <= 7 + and number_occ_bands_spin_down <= 7 + and spin is Spin.up + or actual_deviation == 0.05 + and spin is Spin.down + or actual_deviation == 0.1 + or actual_deviation in [0.2, 0.5, 1.0] + and number_occ_bands_spin_up == 0 + and number_occ_bands_spin_down == 0 + ): + assert result + else: + assert not result + + def test_exceptions(self): + with pytest.raises(ValueError, match="number_occ_bands_spin_down has to be specified"): + self.band_overlaps1.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=4, + spin_polarized=True, + ) + with pytest.raises(ValueError, match="number_occ_bands_spin_down has to be specified"): + self.band_overlaps1_new.has_good_quality_check_occupied_bands( + number_occ_bands_spin_up=4, + spin_polarized=True, + ) + def test_msonable(self): dict_data = self.band_overlaps2_new.as_dict() bandoverlaps_from_dict = Bandoverlaps.from_dict(dict_data)