Skip to content

Commit d3dac17

Browse files
committed
Add carrier concentration tests to TestGetPyScFermiDosFromFermiDos (most important property)
1 parent 8c58508 commit d3dac17

File tree

1 file changed

+108
-19
lines changed

1 file changed

+108
-19
lines changed

tests/test_fermisolver.py

Lines changed: 108 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
"""
44

55
import builtins
6+
import itertools
7+
import os
68
import unittest
79
import warnings
810

@@ -17,14 +19,21 @@
1719
from pymatgen.electronic_structure.core import Spin
1820
from pymatgen.electronic_structure.dos import FermiDos
1921

20-
from doped.thermodynamics import FermiSolver, _get_py_sc_fermi_dos_from_fermi_dos
22+
from doped.thermodynamics import (
23+
FermiSolver,
24+
_get_py_sc_fermi_dos_from_fermi_dos,
25+
get_e_h_concs,
26+
get_fermi_dos,
27+
)
2128

2229
py_sc_fermi_available = bool(find_spec("py_sc_fermi"))
30+
module_path = os.path.dirname(os.path.abspath(__file__))
31+
EXAMPLE_DIR = os.path.join(module_path, "../examples")
2332

2433

2534
class TestGetPyScFermiDosFromFermiDos(unittest.TestCase):
2635
"""
27-
Tests for the _get_py_sc_fermi_dos_from_fermi_dos function.
36+
Tests for the ``_get_py_sc_fermi_dos_from_fermi_dos`` function.
2837
"""
2938

3039
@unittest.skipIf(not py_sc_fermi_available, "py_sc_fermi is not available")
@@ -35,22 +44,59 @@ def test_get_py_sc_fermi_dos(self):
3544
# Create a mock FermiDos object
3645
mock_fermi_dos = MagicMock(spec=FermiDos)
3746
mock_fermi_dos.densities = {
38-
Spin.up: np.array([1.0, 2.0, 3.0]),
39-
Spin.down: np.array([0.5, 1.0, 1.5]),
47+
Spin.up: np.array([1.0, 2.0, 3.0, 4.0]),
48+
Spin.down: np.array([0.5, 1.0, 1.5, 2.0]),
4049
}
41-
mock_fermi_dos.energies = np.array([0.0, 0.5, 1.0])
42-
mock_fermi_dos.get_cbm_vbm.return_value = (None, 0.0) # VBM = 0.0
43-
mock_fermi_dos.nelecs = 10 # Mock number of electrons
44-
mock_fermi_dos.get_gap.return_value = 0.5 # Mock bandgap
50+
mock_fermi_dos.energies = np.array([0.0, 0.5, 1.0, 1.5])
51+
mock_fermi_dos.get_cbm_vbm.return_value = (1.0, 0.5) # VBM = 0.5
52+
mock_fermi_dos.nelecs = 7.5 # Mock number of electrons
53+
mock_fermi_dos.get_gap.return_value = 0.4999 # Mock bandgap
54+
mock_fermi_dos.volume = 1
55+
mock_fermi_dos.idx_vbm = 1
56+
mock_fermi_dos.idx_cbm = 2
57+
mock_fermi_dos.de = (
58+
np.hstack((mock_fermi_dos.energies[1:], mock_fermi_dos.energies[-1])) - mock_fermi_dos.energies
59+
)
60+
e_vbm = mock_fermi_dos.get_cbm_vbm(tol=1e-4, abs_tol=True)[1]
61+
tdos = sum(mock_fermi_dos.densities.values())
62+
mock_fermi_dos.tdos = (
63+
tdos
64+
* mock_fermi_dos.nelecs
65+
/ (tdos * mock_fermi_dos.de)[mock_fermi_dos.energies <= e_vbm].sum()
66+
)
67+
mock_fermi_dos.A_to_cm = 1e-8
68+
gap = mock_fermi_dos.get_gap(tol=1e-4, abs_tol=True)
4569

4670
# Test with default values
47-
result = _get_py_sc_fermi_dos_from_fermi_dos(mock_fermi_dos)
71+
pyscfermi_dos = _get_py_sc_fermi_dos_from_fermi_dos(mock_fermi_dos)
72+
print(pyscfermi_dos._n0_index())
4873

4974
# Assertions
50-
assert result.nelect == 10
51-
assert result.bandgap == 0.5
52-
np.testing.assert_array_equal(result.edos, np.array([0.0, 0.5, 1.0]))
53-
assert result.spin_polarised
75+
assert pyscfermi_dos.nelect == mock_fermi_dos.nelecs
76+
assert pyscfermi_dos.bandgap == gap
77+
assert pyscfermi_dos.spin_polarised
78+
np.testing.assert_array_equal(pyscfermi_dos.edos, mock_fermi_dos.energies - e_vbm)
79+
80+
# test carrier concentrations (indirectly tests DOS densities, this is the relevant property
81+
# from the DOS objects):
82+
pyscfermi_scale = 1e24 / mock_fermi_dos.volume
83+
for e_fermi, temperature in itertools.product(
84+
np.linspace(-0.25, gap + 0.25, 10), np.linspace(300, 1000.0, 10)
85+
):
86+
pyscfermi_h_e = pyscfermi_dos.carrier_concentrations(e_fermi, temperature) # rel to VBM
87+
doped_e_h = get_e_h_concs(mock_fermi_dos, e_fermi + e_vbm, temperature) # raw Fermi eigenvalue
88+
assert np.allclose(
89+
(pyscfermi_h_e[1] * pyscfermi_scale, pyscfermi_h_e[0] * pyscfermi_scale),
90+
doped_e_h,
91+
rtol=0.25,
92+
atol=1e4,
93+
), f"e_fermi={e_fermi}, temperature={temperature}"
94+
# tests: absolute(a - b) <= (atol + rtol * absolute(b)), so rtol of 15% but with a base atol
95+
# of 1e4 to allow larger relative mismatches for very small densities (more sensitive to
96+
# differences in integration schemes) -- main difference seems to be hard chopping of
97+
# integrals in py-sc-fermi at the expected VBM/CBM indices (but ``doped`` is agnostic to
98+
# these to improve robustness), makes more difference at low temperatures so only T >= 300K
99+
# tested here
54100

55101
@unittest.skipIf(not py_sc_fermi_available, "py_sc_fermi is not available")
56102
def test_get_py_sc_fermi_dos_with_custom_parameters(self):
@@ -66,15 +112,58 @@ def test_get_py_sc_fermi_dos_with_custom_parameters(self):
66112
mock_fermi_dos.get_gap.return_value = 0.5
67113

68114
# Test with custom parameters
69-
result = _get_py_sc_fermi_dos_from_fermi_dos(mock_fermi_dos, vbm=0.1, nelect=12, bandgap=0.5)
115+
pyscfermi_dos = _get_py_sc_fermi_dos_from_fermi_dos(
116+
mock_fermi_dos, vbm=0.1, nelect=12, bandgap=0.5
117+
)
70118

71119
# Assertions
72-
assert result.nelect == 12
73-
assert result.bandgap == 0.5
74-
np.testing.assert_array_equal(result.edos, np.array([-0.1, 0.4, 0.9]))
75-
assert not result.spin_polarised
76-
120+
assert pyscfermi_dos.nelect == 12
121+
assert pyscfermi_dos.bandgap == 0.5
122+
np.testing.assert_array_equal(pyscfermi_dos.edos, np.array([-0.1, 0.4, 0.9]))
123+
assert not pyscfermi_dos.spin_polarised
77124

125+
@unittest.skipIf(not py_sc_fermi_available, "py_sc_fermi is not available")
126+
def test_get_py_sc_fermi_dos_from_CdTe_dos(self):
127+
"""
128+
Test conversion of FermiDos to py_sc_fermi DOS with default parameters.
129+
"""
130+
fermi_dos = get_fermi_dos(
131+
os.path.join(EXAMPLE_DIR, "CdTe/CdTe_prim_k181818_NKRED_2_vasprun.xml.gz")
132+
)
133+
pyscfermi_dos = _get_py_sc_fermi_dos_from_fermi_dos(fermi_dos)
134+
assert pyscfermi_dos.nelect == fermi_dos.nelecs
135+
assert pyscfermi_dos.nelect == 18
136+
assert np.isclose(pyscfermi_dos.bandgap, fermi_dos.get_gap(tol=1e-4, abs_tol=True))
137+
assert np.isclose(pyscfermi_dos.bandgap, 1.526, atol=1e-3)
138+
assert not pyscfermi_dos.spin_polarised # SOC DOS
139+
140+
e_vbm = fermi_dos.get_cbm_vbm(tol=1e-4, abs_tol=True)[1]
141+
gap = fermi_dos.get_gap(tol=1e-4, abs_tol=True)
142+
np.testing.assert_array_equal(pyscfermi_dos.edos, fermi_dos.energies - e_vbm)
143+
144+
# test carrier concentrations (indirectly tests DOS densities, this is the relevant property
145+
# from the DOS objects):
146+
pyscfermi_scale = 1e24 / fermi_dos.volume
147+
for e_fermi, temperature in itertools.product(
148+
np.linspace(-0.5, gap + 0.5, 10), np.linspace(300, 2000.0, 10)
149+
):
150+
pyscfermi_h_e = pyscfermi_dos.carrier_concentrations(e_fermi, temperature) # rel to VBM
151+
doped_e_h = get_e_h_concs(fermi_dos, e_fermi + e_vbm, temperature) # raw Fermi eigenvalue
152+
assert np.allclose(
153+
(pyscfermi_h_e[1] * pyscfermi_scale, pyscfermi_h_e[0] * pyscfermi_scale),
154+
doped_e_h,
155+
rtol=0.15,
156+
atol=1e4,
157+
), f"e_fermi={e_fermi}, temperature={temperature}"
158+
# tests: absolute(a - b) <= (atol + rtol * absolute(b)), so rtol of 15% but with a base atol
159+
# of 1e4 to allow larger relative mismatches for very small densities (more sensitive to
160+
# differences in integration schemes) -- main difference seems to be hard chopping of
161+
# integrals in py-sc-fermi at the expected VBM/CBM indices (but ``doped`` is agnostic to
162+
# these to improve robustness), makes more difference at low temperatures so only T >= 300K
163+
# tested here
164+
165+
166+
# TODO: Use pytest fixtures to reduce code redundancy here?
78167
class TestFermiSolverWithLoadedData(unittest.TestCase):
79168
"""
80169
Tests for FermiSolver initialization with loaded data.

0 commit comments

Comments
 (0)