Skip to content

Commit

Permalink
Merge branch 'master' of github.com:materialsproject/pymatgen
Browse files Browse the repository at this point in the history
  • Loading branch information
shyuep committed Nov 18, 2024
2 parents 729fce8 + f8f2d21 commit deee1fa
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 60 deletions.
36 changes: 25 additions & 11 deletions src/pymatgen/analysis/xas/spectrum.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -42,29 +46,31 @@ 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"
YLABEL = "Intensity"

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))]
Expand All @@ -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 (
Expand Down
34 changes: 11 additions & 23 deletions src/pymatgen/io/lobster/outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/pymatgen/io/vasp/inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,7 @@ def proc_val(key: str, val: str) -> list | bool | float | int | str:
"PARAM1",
"PARAM2",
"ENCUT",
"NUPDOWN",
)
int_keys = (
"NSW",
Expand All @@ -987,7 +988,6 @@ def proc_val(key: str, val: str) -> list | bool | float | int | str:
"LMAXMIX",
"NSIM",
"NKRED",
"NUPDOWN",
"ISPIND",
"LDAUTYPE",
"IVDW",
Expand Down
19 changes: 15 additions & 4 deletions tests/analysis/xas/test_spectrum.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ def test_str(self):
assert str(self.k_xanes) == "Co K Edge XANES for LiCoO2: <super: <class 'XAS'>, <XAS object>>"

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,
Expand All @@ -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")
Expand Down
117 changes: 96 additions & 21 deletions tests/io/lobster/test_outputs.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import copy
import json
import os
from unittest import TestCase
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
)

Expand All @@ -1614,14 +1617,86 @@ 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)
assert self.band_overlaps2_new.has_good_quality_check_occupied_bands(
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)
Expand Down

0 comments on commit deee1fa

Please sign in to comment.