3
3
"""
4
4
5
5
import builtins
6
+ import itertools
7
+ import os
6
8
import unittest
7
9
import warnings
8
10
17
19
from pymatgen .electronic_structure .core import Spin
18
20
from pymatgen .electronic_structure .dos import FermiDos
19
21
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
+ )
21
28
22
29
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" )
23
32
24
33
25
34
class TestGetPyScFermiDosFromFermiDos (unittest .TestCase ):
26
35
"""
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.
28
37
"""
29
38
30
39
@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):
35
44
# Create a mock FermiDos object
36
45
mock_fermi_dos = MagicMock (spec = FermiDos )
37
46
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 ]),
40
49
}
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 )
45
69
46
70
# 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 ())
48
73
49
74
# 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
54
100
55
101
@unittest .skipIf (not py_sc_fermi_available , "py_sc_fermi is not available" )
56
102
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):
66
112
mock_fermi_dos .get_gap .return_value = 0.5
67
113
68
114
# 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
+ )
70
118
71
119
# 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
77
124
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?
78
167
class TestFermiSolverWithLoadedData (unittest .TestCase ):
79
168
"""
80
169
Tests for FermiSolver initialization with loaded data.
0 commit comments