• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

materialsproject / pymatgen / 4075885785

pending completion
4075885785

push

github

Shyue Ping Ong
Merge branch 'master' of github.com:materialsproject/pymatgen

96 of 96 new or added lines in 27 files covered. (100.0%)

81013 of 102710 relevant lines covered (78.88%)

0.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.52
/pymatgen/electronic_structure/dos.py
1
# Copyright (c) Pymatgen Development Team.
2
# Distributed under the terms of the MIT License.
3

4
"""
1✔
5
This module defines classes to represent the density of states, etc.
6
"""
7

8
from __future__ import annotations
1✔
9

10
import functools
1✔
11
import warnings
1✔
12
from collections import namedtuple
1✔
13
from typing import Mapping, NamedTuple
1✔
14

15
import numpy as np
1✔
16
from monty.json import MSONable
1✔
17
from scipy.constants import value as _cd
1✔
18
from scipy.signal import hilbert
1✔
19

20
from pymatgen.core.periodic_table import get_el_sp
1✔
21
from pymatgen.core.sites import PeriodicSite
1✔
22
from pymatgen.core.spectrum import Spectrum
1✔
23
from pymatgen.core.structure import Structure
1✔
24
from pymatgen.electronic_structure.core import Orbital, OrbitalType, Spin
1✔
25
from pymatgen.util.coord import get_linear_interpolated_value
1✔
26
from pymatgen.util.typing import ArrayLike, SpeciesLike
1✔
27

28

29
class DOS(Spectrum):
1✔
30
    """
31
    Replacement basic DOS object. All other DOS objects are extended versions
32
    of this object. Work in progress.
33

34
    .. attribute: energies
35

36
        The sequence of energies
37

38
    .. attribute: densities
39

40
        A dict of spin densities, e.g., {Spin.up: [...], Spin.down: [...]}
41

42
    .. attribute: efermi
43

44
        Fermi level
45
    """
46

47
    XLABEL = "Energy"
1✔
48
    YLABEL = "Density"
1✔
49

50
    def __init__(self, energies: ArrayLike, densities: ArrayLike, efermi: float):
1✔
51
        """
52
        Args:
53
            energies: A sequence of energies
54
            densities (ndarray): Either a Nx1 or a Nx2 array. If former, it is
55
                interpreted as a Spin.up only density. Otherwise, the first column
56
                is interpreted as Spin.up and the other is Spin.down.
57
            efermi: Fermi level energy.
58
        """
59
        super().__init__(energies, densities, efermi)
1✔
60
        self.efermi = efermi
1✔
61

62
    def get_interpolated_gap(self, tol: float = 0.001, abs_tol: bool = False, spin: Spin | None = None):
1✔
63
        """
64
        Expects a DOS object and finds the gap
65

66
        Args:
67
            tol: tolerance in occupations for determining the gap
68
            abs_tol: Set to True for an absolute tolerance and False for a
69
                relative one.
70
            spin: Possible values are None - finds the gap in the summed
71
                densities, Up - finds the gap in the up spin channel,
72
                Down - finds the gap in the down spin channel.
73

74
        Returns:
75
            (gap, cbm, vbm):
76
                Tuple of floats in eV corresponding to the gap, cbm and vbm.
77
        """
78
        if spin is None:
1✔
79
            tdos = self.y if len(self.ydim) == 1 else np.sum(self.y, axis=1)
1✔
80
        elif spin == Spin.up:
×
81
            tdos = self.y[:, 0]
×
82
        else:
83
            tdos = self.y[:, 1]
×
84

85
        if not abs_tol:
1✔
86
            tol = tol * tdos.sum() / tdos.shape[0]
1✔
87
        energies = self.x
1✔
88
        below_fermi = [i for i in range(len(energies)) if energies[i] < self.efermi and tdos[i] > tol]
1✔
89
        above_fermi = [i for i in range(len(energies)) if energies[i] > self.efermi and tdos[i] > tol]
1✔
90
        vbm_start = max(below_fermi)
1✔
91
        cbm_start = min(above_fermi)
1✔
92
        if vbm_start == cbm_start:
1✔
93
            return 0.0, self.efermi, self.efermi
×
94
        # Interpolate between adjacent values
95
        terminal_dens = tdos[vbm_start : vbm_start + 2][::-1]
1✔
96
        terminal_energies = energies[vbm_start : vbm_start + 2][::-1]
1✔
97
        start = get_linear_interpolated_value(terminal_dens, terminal_energies, tol)
1✔
98
        terminal_dens = tdos[cbm_start - 1 : cbm_start + 1]
1✔
99
        terminal_energies = energies[cbm_start - 1 : cbm_start + 1]
1✔
100
        end = get_linear_interpolated_value(terminal_dens, terminal_energies, tol)
1✔
101
        return end - start, end, start
1✔
102

103
    def get_cbm_vbm(self, tol: float = 0.001, abs_tol: bool = False, spin=None):
1✔
104
        """
105
        Expects a DOS object and finds the cbm and vbm.
106

107
        Args:
108
            tol: tolerance in occupations for determining the gap
109
            abs_tol: An absolute tolerance (True) and a relative one (False)
110
            spin: Possible values are None - finds the gap in the summed
111
                densities, Up - finds the gap in the up spin channel,
112
                Down - finds the gap in the down spin channel.
113

114
        Returns:
115
            (cbm, vbm): float in eV corresponding to the gap
116
        """
117
        # determine tolerance
118
        if spin is None:
1✔
119
            tdos = self.y if len(self.ydim) == 1 else np.sum(self.y, axis=1)
1✔
120
        elif spin == Spin.up:
1✔
121
            tdos = self.y[:, 0]
1✔
122
        else:
123
            tdos = self.y[:, 1]
1✔
124

125
        if not abs_tol:
1✔
126
            tol = tol * tdos.sum() / tdos.shape[0]
1✔
127

128
        # find index of fermi energy
129
        i_fermi = 0
1✔
130
        while self.x[i_fermi] <= self.efermi:
1✔
131
            i_fermi += 1
1✔
132

133
        # work backwards until tolerance is reached
134
        i_gap_start = i_fermi
1✔
135
        while i_gap_start - 1 >= 0 and tdos[i_gap_start - 1] <= tol:
1✔
136
            i_gap_start -= 1
1✔
137

138
        # work forwards until tolerance is reached
139
        i_gap_end = i_gap_start
1✔
140
        while i_gap_end < tdos.shape[0] and tdos[i_gap_end] <= tol:
1✔
141
            i_gap_end += 1
1✔
142
        i_gap_end -= 1
1✔
143
        return self.x[i_gap_end], self.x[i_gap_start]
1✔
144

145
    def get_gap(self, tol: float = 0.001, abs_tol: bool = False, spin: Spin | None = None):
1✔
146
        """
147
        Expects a DOS object and finds the gap.
148

149
        Args:
150
            tol: tolerance in occupations for determining the gap
151
            abs_tol: An absolute tolerance (True) and a relative one (False)
152
            spin: Possible values are None - finds the gap in the summed
153
                densities, Up - finds the gap in the up spin channel,
154
                Down - finds the gap in the down spin channel.
155

156
        Returns:
157
            gap in eV
158
        """
159
        (cbm, vbm) = self.get_cbm_vbm(tol, abs_tol, spin)
1✔
160
        return max(cbm - vbm, 0.0)
1✔
161

162
    def __str__(self):
1✔
163
        """
164
        Returns a string which can be easily plotted (using gnuplot).
165
        """
166
        if Spin.down in self.densities:
×
167
            stringarray = [f"#{'Energy':30s} {'DensityUp':30s} {'DensityDown':30s}"]
×
168
            for i, energy in enumerate(self.energies):
×
169
                stringarray.append(f"{energy:.5f} {self.densities[Spin.up][i]:.5f} {self.densities[Spin.down][i]:.5f}")
×
170
        else:
171
            stringarray = [f"#{'Energy':30s} {'DensityUp':30s}"]
×
172
            for i, energy in enumerate(self.energies):
×
173
                stringarray.append(f"{energy:.5f} {self.densities[Spin.up][i]:.5f}")
×
174
        return "\n".join(stringarray)
×
175

176

177
class Dos(MSONable):
1✔
178
    """
179
    Basic DOS object. All other DOS objects are extended versions of this
180
    object.
181

182
    .. attribute: energies
183

184
        The sequence of energies
185

186
    .. attribute: densities
187

188
        A dict of spin densities, e.g., {Spin.up: [...], Spin.down: [...]}
189

190
    .. attribute: efermi
191

192
        Fermi level
193
    """
194

195
    def __init__(
1✔
196
        self, efermi: float, energies: ArrayLike, densities: Mapping[Spin, ArrayLike], norm_vol: float | None = None
197
    ) -> None:
198
        """
199
        Args:
200
            efermi: Fermi level energy
201
            energies: A sequences of energies
202
            densities (dict[Spin: np.array]): representing the density of states for each Spin.
203
            norm_vol: The volume used to normalize the densities. Defaults to 1 if None which will not perform any
204
                normalization. If not None, the resulting density will have units of states/eV/Angstrom^3, otherwise
205
                the density will be in states/eV.
206
        """
207
        self.efermi = efermi
1✔
208
        self.energies = np.array(energies)
1✔
209
        self.norm_vol = norm_vol
1✔
210
        vol = norm_vol or 1
1✔
211
        self.densities = {k: np.array(d) / vol for k, d in densities.items()}
1✔
212

213
    def get_densities(self, spin: Spin | None = None):
1✔
214
        """
215
        Returns the density of states for a particular spin.
216

217
        Args:
218
            spin: Spin
219

220
        Returns:
221
            Returns the density of states for a particular spin. If Spin is
222
            None, the sum of all spins is returned.
223
        """
224
        if self.densities is None:
1✔
225
            result = None
×
226
        elif spin is None:
1✔
227
            if Spin.down in self.densities:
1✔
228
                result = self.densities[Spin.up] + self.densities[Spin.down]
1✔
229
            else:
230
                result = self.densities[Spin.up]
1✔
231
        else:
232
            result = self.densities[spin]
1✔
233
        return result
1✔
234

235
    def get_smeared_densities(self, sigma: float):
1✔
236
        """
237
        Returns the Dict representation of the densities, {Spin: densities},
238
        but with a Gaussian smearing of std dev sigma
239

240
        Args:
241
            sigma: Std dev of Gaussian smearing function.
242

243
        Returns:
244
            Dict of Gaussian-smeared densities.
245
        """
246
        from scipy.ndimage import gaussian_filter1d
1✔
247

248
        smeared_dens = {}
1✔
249
        diff = [self.energies[i + 1] - self.energies[i] for i in range(len(self.energies) - 1)]
1✔
250
        avgdiff = sum(diff) / len(diff)
1✔
251
        for spin, dens in self.densities.items():
1✔
252
            smeared_dens[spin] = gaussian_filter1d(dens, sigma / avgdiff)
1✔
253
        return smeared_dens
1✔
254

255
    def __add__(self, other):
1✔
256
        """
257
        Adds two DOS together. Checks that energy scales are the same.
258
        Otherwise, a ValueError is thrown.
259

260
        Args:
261
            other: Another DOS object.
262

263
        Returns:
264
            Sum of the two DOSs.
265
        """
266
        if not all(np.equal(self.energies, other.energies)):
1✔
267
            raise ValueError("Energies of both DOS are not compatible!")
×
268
        densities = {spin: self.densities[spin] + other.densities[spin] for spin in self.densities}
1✔
269
        return Dos(self.efermi, self.energies, densities)
1✔
270

271
    def get_interpolated_value(self, energy: float):
1✔
272
        """
273
        Returns interpolated density for a particular energy.
274

275
        Args:
276
            energy: Energy to return the density for.
277
        """
278
        f = {}
1✔
279
        for spin in self.densities:
1✔
280
            f[spin] = get_linear_interpolated_value(self.energies, self.densities[spin], energy)
1✔
281
        return f
1✔
282

283
    def get_interpolated_gap(self, tol: float = 0.001, abs_tol: bool = False, spin: Spin | None = None):
1✔
284
        """
285
        Expects a DOS object and finds the gap
286

287
        Args:
288
            tol: tolerance in occupations for determining the gap
289
            abs_tol: Set to True for an absolute tolerance and False for a
290
                relative one.
291
            spin: Possible values are None - finds the gap in the summed
292
                densities, Up - finds the gap in the up spin channel,
293
                Down - finds the gap in the down spin channel.
294

295
        Returns:
296
            (gap, cbm, vbm):
297
                Tuple of floats in eV corresponding to the gap, cbm and vbm.
298
        """
299
        tdos = self.get_densities(spin)
1✔
300
        if not abs_tol:
1✔
301
            tol = tol * tdos.sum() / tdos.shape[0]
1✔
302
        energies = self.energies
1✔
303
        below_fermi = [i for i in range(len(energies)) if energies[i] < self.efermi and tdos[i] > tol]
1✔
304
        above_fermi = [i for i in range(len(energies)) if energies[i] > self.efermi and tdos[i] > tol]
1✔
305
        vbm_start = max(below_fermi)
1✔
306
        cbm_start = min(above_fermi)
1✔
307
        if vbm_start == cbm_start:
1✔
308
            return 0.0, self.efermi, self.efermi
×
309

310
        # Interpolate between adjacent values
311
        terminal_dens = tdos[vbm_start : vbm_start + 2][::-1]
1✔
312
        terminal_energies = energies[vbm_start : vbm_start + 2][::-1]
1✔
313
        start = get_linear_interpolated_value(terminal_dens, terminal_energies, tol)
1✔
314
        terminal_dens = tdos[cbm_start - 1 : cbm_start + 1]
1✔
315
        terminal_energies = energies[cbm_start - 1 : cbm_start + 1]
1✔
316
        end = get_linear_interpolated_value(terminal_dens, terminal_energies, tol)
1✔
317
        return end - start, end, start
1✔
318

319
    def get_cbm_vbm(self, tol: float = 0.001, abs_tol: bool = False, spin: Spin | None = None):
1✔
320
        """
321
        Expects a DOS object and finds the cbm and vbm.
322

323
        Args:
324
            tol: tolerance in occupations for determining the gap
325
            abs_tol: An absolute tolerance (True) and a relative one (False)
326
            spin: Possible values are None - finds the gap in the summed
327
                densities, Up - finds the gap in the up spin channel,
328
                Down - finds the gap in the down spin channel.
329

330
        Returns:
331
            (cbm, vbm): float in eV corresponding to the gap
332
        """
333
        # determine tolerance
334
        tdos = self.get_densities(spin)
1✔
335
        if not abs_tol:
1✔
336
            tol = tol * tdos.sum() / tdos.shape[0]
1✔
337

338
        # find index of fermi energy
339
        i_fermi = 0
1✔
340
        while self.energies[i_fermi] <= self.efermi:
1✔
341
            i_fermi += 1
1✔
342

343
        # work backwards until tolerance is reached
344
        i_gap_start = i_fermi
1✔
345
        while i_gap_start - 1 >= 0 and tdos[i_gap_start - 1] <= tol:
1✔
346
            i_gap_start -= 1
1✔
347

348
        # work forwards until tolerance is reached
349
        i_gap_end = i_gap_start
1✔
350
        while i_gap_end < tdos.shape[0] and tdos[i_gap_end] <= tol:
1✔
351
            i_gap_end += 1
1✔
352
        i_gap_end -= 1
1✔
353
        return self.energies[i_gap_end], self.energies[i_gap_start]
1✔
354

355
    def get_gap(self, tol: float = 0.001, abs_tol: bool = False, spin: Spin | None = None):
1✔
356
        """
357
        Expects a DOS object and finds the gap.
358

359
        Args:
360
            tol: tolerance in occupations for determining the gap
361
            abs_tol: An absolute tolerance (True) and a relative one (False)
362
            spin: Possible values are None - finds the gap in the summed
363
                densities, Up - finds the gap in the up spin channel,
364
                Down - finds the gap in the down spin channel.
365

366
        Returns:
367
            gap in eV
368
        """
369
        (cbm, vbm) = self.get_cbm_vbm(tol, abs_tol, spin)
1✔
370
        return max(cbm - vbm, 0.0)
1✔
371

372
    def __str__(self):
1✔
373
        """
374
        Returns a string which can be easily plotted (using gnuplot).
375
        """
376
        if Spin.down in self.densities:
×
377
            stringarray = [f"#{'Energy':30s} {'DensityUp':30s} {'DensityDown':30s}"]
×
378
            for i, energy in enumerate(self.energies):
×
379
                stringarray.append(f"{energy:.5f} {self.densities[Spin.up][i]:.5f} {self.densities[Spin.down][i]:.5f}")
×
380
        else:
381
            stringarray = [f"#{'Energy':30s} {'DensityUp':30s}"]
×
382
            for i, energy in enumerate(self.energies):
×
383
                stringarray.append(f"{energy:.5f} {self.densities[Spin.up][i]:.5f}")
×
384
        return "\n".join(stringarray)
×
385

386
    @classmethod
1✔
387
    def from_dict(cls, d) -> Dos:
1✔
388
        """
389
        Returns Dos object from dict representation of Dos.
390
        """
391
        return Dos(
1✔
392
            d["efermi"],
393
            d["energies"],
394
            {Spin(int(k)): v for k, v in d["densities"].items()},
395
        )
396

397
    def as_dict(self) -> dict:
1✔
398
        """
399
        JSON-serializable dict representation of Dos.
400
        """
401
        return {
1✔
402
            "@module": type(self).__module__,
403
            "@class": type(self).__name__,
404
            "efermi": self.efermi,
405
            "energies": self.energies.tolist(),
406
            "densities": {str(spin): dens.tolist() for spin, dens in self.densities.items()},
407
        }
408

409

410
class FermiDos(Dos, MSONable):
1✔
411
    """
412
    This wrapper class helps relate the density of states, doping levels
413
    (i.e. carrier concentrations) and corresponding fermi levels. A negative
414
    doping concentration indicates the majority carriers are electrons
415
    (n-type doping); a positive doping concentration indicates holes are the
416
    majority carriers (p-type doping).
417
    """
418

419
    def __init__(
1✔
420
        self,
421
        dos: Dos,
422
        structure: Structure | None = None,
423
        nelecs: float | None = None,
424
        bandgap: float | None = None,
425
    ):
426
        """
427
        Args:
428
            dos: Pymatgen Dos object.
429
            structure: A structure. If not provided, the structure
430
                of the dos object will be used. If the dos does not have an
431
                associated structure object, an error will be thrown.
432
            nelecs: The number of electrons included in the energy range of
433
                dos. It is used for normalizing the densities. Default is the total
434
                number of electrons in the structure.
435
            bandgap: If set, the energy values are scissored so that the electronic
436
                band gap matches this value.
437
        """
438
        super().__init__(
1✔
439
            dos.efermi,
440
            energies=dos.energies,
441
            densities={k: np.array(d) for k, d in dos.densities.items()},
442
        )
443

444
        if structure is None:
1✔
445
            if hasattr(dos, "structure"):
1✔
446
                structure = dos.structure
1✔
447
            else:
448
                raise ValueError("Structure object is not provided and not present in dos")
×
449

450
        self.structure = structure
1✔
451
        self.nelecs = nelecs or self.structure.composition.total_electrons
1✔
452

453
        self.volume = self.structure.volume
1✔
454
        self.energies = np.array(dos.energies)
1✔
455
        self.de = np.hstack((self.energies[1:], self.energies[-1])) - self.energies
1✔
456

457
        # normalize total density of states based on integral at 0K
458
        tdos = np.array(self.get_densities())
1✔
459
        self.tdos = tdos * self.nelecs / (tdos * self.de)[self.energies <= self.efermi].sum()
1✔
460

461
        ecbm, evbm = self.get_cbm_vbm()
1✔
462
        self.idx_vbm = int(np.argmin(abs(self.energies - evbm)))
1✔
463
        self.idx_cbm = int(np.argmin(abs(self.energies - ecbm)))
1✔
464
        self.A_to_cm = 1e-8
1✔
465

466
        if bandgap:
1✔
467
            if evbm < self.efermi < ecbm:
1✔
468
                eref = self.efermi
1✔
469
            else:
470
                eref = (evbm + ecbm) / 2.0
×
471

472
            idx_fermi = int(np.argmin(abs(self.energies - eref)))
1✔
473

474
            if idx_fermi == self.idx_vbm:
1✔
475
                # Fermi level and vbm should be different indices
476
                idx_fermi += 1
1✔
477

478
            self.energies[:idx_fermi] -= (bandgap - (ecbm - evbm)) / 2.0
1✔
479
            self.energies[idx_fermi:] += (bandgap - (ecbm - evbm)) / 2.0
1✔
480

481
    def get_doping(self, fermi_level: float, temperature: float) -> float:
1✔
482
        """
483
        Calculate the doping (majority carrier concentration) at a given
484
        fermi level  and temperature. A simple Left Riemann sum is used for
485
        integrating the density of states over energy & equilibrium Fermi-Dirac
486
        distribution.
487

488
        Args:
489
            fermi_level: The fermi_level level in eV.
490
            temperature: The temperature in Kelvin.
491

492
        Returns:
493
            The doping concentration in units of 1/cm^3. Negative values
494
            indicate that the majority carriers are electrons (n-type doping)
495
            whereas positive values indicates the majority carriers are holes
496
            (p-type doping).
497
        """
498
        cb_integral = np.sum(
1✔
499
            self.tdos[self.idx_cbm :]
500
            * f0(self.energies[self.idx_cbm :], fermi_level, temperature)
501
            * self.de[self.idx_cbm :],
502
            axis=0,
503
        )
504
        vb_integral = np.sum(
1✔
505
            self.tdos[: self.idx_vbm + 1]
506
            * f0(-self.energies[: self.idx_vbm + 1], -fermi_level, temperature)
507
            * self.de[: self.idx_vbm + 1],
508
            axis=0,
509
        )
510
        return (vb_integral - cb_integral) / (self.volume * self.A_to_cm**3)
1✔
511

512
    def get_fermi_interextrapolated(
1✔
513
        self, concentration: float, temperature: float, warn: bool = True, c_ref: float = 1e10, **kwargs
514
    ) -> float:
515
        """
516
        Similar to get_fermi except that when get_fermi fails to converge,
517
        an interpolated or extrapolated fermi is returned with the assumption
518
        that the fermi level changes linearly with log(abs(concentration)).
519

520
        Args:
521
            concentration: The doping concentration in 1/cm^3. Negative values
522
                represent n-type doping and positive values represent p-type
523
                doping.
524
            temperature: The temperature in Kelvin.
525
            warn: Whether to give a warning the first time the fermi cannot be
526
                found.
527
            c_ref: A doping concentration where get_fermi returns a
528
                value without error for both c_ref and -c_ref.
529
            **kwargs: Keyword arguments passed to the get_fermi function.
530

531
        Returns:
532
            The Fermi level. Note, the value is possibly interpolated or
533
            extrapolated and must be used with caution.
534
        """
535
        try:
1✔
536
            return self.get_fermi(concentration, temperature, **kwargs)
1✔
537
        except ValueError as e:
1✔
538
            if warn:
1✔
539
                warnings.warn(str(e))
1✔
540

541
            if abs(concentration) < c_ref:
1✔
542
                if abs(concentration) < 1e-10:
1✔
543
                    concentration = 1e-10
1✔
544

545
                # max(10, ) is to avoid log(0<x<1) and log(1+x) both of which
546
                # are slow
547
                f2 = self.get_fermi_interextrapolated(
1✔
548
                    max(10, abs(concentration) * 10.0), temperature, warn=False, **kwargs
549
                )
550
                f1 = self.get_fermi_interextrapolated(
1✔
551
                    -max(10, abs(concentration) * 10.0), temperature, warn=False, **kwargs
552
                )
553
                c2 = np.log(abs(1 + self.get_doping(f2, temperature)))
1✔
554
                c1 = -np.log(abs(1 + self.get_doping(f1, temperature)))
1✔
555
                slope = (f2 - f1) / (c2 - c1)
1✔
556
                return f2 + slope * (np.sign(concentration) * np.log(abs(1 + concentration)) - c2)
1✔
557

558
            f_ref = self.get_fermi_interextrapolated(np.sign(concentration) * c_ref, temperature, warn=False, **kwargs)
1✔
559
            f_new = self.get_fermi_interextrapolated(concentration / 10.0, temperature, warn=False, **kwargs)
1✔
560
            clog = np.sign(concentration) * np.log(abs(concentration))
1✔
561
            c_newlog = np.sign(concentration) * np.log(abs(self.get_doping(f_new, temperature)))
1✔
562
            slope = (f_new - f_ref) / (c_newlog - np.sign(concentration) * 10.0)
1✔
563
            return f_new + slope * (clog - c_newlog)
1✔
564

565
    def get_fermi(
1✔
566
        self,
567
        concentration: float,
568
        temperature: float,
569
        rtol: float = 0.01,
570
        nstep: int = 50,
571
        step: float = 0.1,
572
        precision: int = 8,
573
    ) -> float:
574
        """
575
        Finds the fermi level at which the doping concentration at the given
576
        temperature (T) is equal to concentration. A greedy algorithm is used
577
        where the relative error is minimized by calculating the doping at a
578
        grid which continually becomes finer.
579

580
        Args:
581
            concentration: The doping concentration in 1/cm^3. Negative values
582
                represent n-type doping and positive values represent p-type
583
                doping.
584
            temperature: The temperature in Kelvin.
585
            rtol: The maximum acceptable relative error.
586
            nstep: THe number of steps checked around a given fermi level.
587
            step: Initial step in energy when searching for the Fermi level.
588
            precision: Essentially the decimal places of calculated Fermi level.
589

590
        Raises:
591
            ValueError: If the Fermi level cannot be found.
592

593
        Returns:
594
            The fermi level in eV. Note that this is different from the default
595
            dos.efermi.
596
        """
597
        fermi = self.efermi  # initialize target fermi
1✔
598
        relative_error = [float("inf")]
1✔
599
        for _ in range(precision):
1✔
600
            frange = np.arange(-nstep, nstep + 1) * step + fermi
1✔
601
            calc_doping = np.array([self.get_doping(f, temperature) for f in frange])
1✔
602
            relative_error = np.abs(calc_doping / concentration - 1.0)  # type: ignore
1✔
603
            fermi = frange[np.argmin(relative_error)]
1✔
604
            step /= 10.0
1✔
605

606
        if min(relative_error) > rtol:
1✔
607
            raise ValueError(f"Could not find fermi within {rtol:.1%} of {concentration=}")
1✔
608
        return fermi
1✔
609

610
    @classmethod
1✔
611
    def from_dict(cls, d) -> FermiDos:
1✔
612
        """
613
        Returns Dos object from dict representation of Dos.
614
        """
615
        dos = Dos(
×
616
            d["efermi"],
617
            d["energies"],
618
            {Spin(int(k)): v for k, v in d["densities"].items()},
619
        )
620
        return FermiDos(dos, structure=Structure.from_dict(d["structure"]), nelecs=d["nelecs"])
×
621

622
    def as_dict(self) -> dict:
1✔
623
        """
624
        JSON-serializable dict representation of Dos.
625
        """
626
        return {
1✔
627
            "@module": type(self).__module__,
628
            "@class": type(self).__name__,
629
            "efermi": self.efermi,
630
            "energies": self.energies.tolist(),
631
            "densities": {str(spin): dens.tolist() for spin, dens in self.densities.items()},
632
            "structure": self.structure,
633
            "nelecs": self.nelecs,
634
        }
635

636

637
class CompleteDos(Dos):
1✔
638
    """
639
    This wrapper class defines a total dos, and also provides a list of PDos.
640
    Mainly used by pymatgen.io.vasp.Vasprun to create a complete Dos from
641
    a vasprun.xml file. You are unlikely to try to generate this object
642
    manually.
643

644
    .. attribute:: structure
645

646
        Structure associated with the CompleteDos.
647

648
    .. attribute:: pdos
649

650
        Dict of partial densities of the form {Site:{Orbital:{Spin:Densities}}}
651
    """
652

653
    def __init__(
1✔
654
        self,
655
        structure: Structure,
656
        total_dos: Dos,
657
        pdoss: Mapping[PeriodicSite, Mapping[Orbital, Mapping[Spin, ArrayLike]]],
658
        normalize: bool = False,
659
    ) -> None:
660
        """
661
        Args:
662
            structure: Structure associated with this particular DOS.
663
            total_dos: total Dos for structure
664
            pdoss: The pdoss are supplied as an {Site: {Orbital: {Spin:Densities}}}
665
            normalize: Whether to normalize the densities by the volume of the structure.
666
                If True, the units of the densities are states/eV/Angstrom^3. Otherwise,
667
                the units are states/eV.
668
        """
669
        vol = structure.volume if normalize else None
1✔
670
        super().__init__(
1✔
671
            total_dos.efermi,
672
            energies=total_dos.energies,
673
            densities={k: np.array(d) for k, d in total_dos.densities.items()},
674
            norm_vol=vol,
675
        )
676
        self.pdos = pdoss
1✔
677
        self.structure = structure
1✔
678

679
    def get_normalized(self) -> CompleteDos:
1✔
680
        """
681
        Returns a normalized version of the CompleteDos.
682
        """
683
        if self.norm_vol is not None:
1✔
684
            return self
1✔
685
        return CompleteDos(
1✔
686
            structure=self.structure,
687
            total_dos=self,
688
            pdoss=self.pdos,
689
            normalize=True,
690
        )
691

692
    def get_site_orbital_dos(self, site: PeriodicSite, orbital: Orbital) -> Dos:
1✔
693
        """
694
        Get the Dos for a particular orbital of a particular site.
695

696
        Args:
697
            site: Site in Structure associated with CompleteDos.
698
            orbital: Orbital in the site.
699

700
        Returns:
701
            Dos containing densities for orbital of site.
702
        """
703
        return Dos(self.efermi, self.energies, self.pdos[site][orbital])
1✔
704

705
    def get_site_dos(self, site: PeriodicSite) -> Dos:
1✔
706
        """
707
        Get the total Dos for a site (all orbitals).
708

709
        Args:
710
            site: Site in Structure associated with CompleteDos.
711

712
        Returns:
713
            Dos containing summed orbital densities for site.
714
        """
715
        site_dos = functools.reduce(add_densities, self.pdos[site].values())
1✔
716
        return Dos(self.efermi, self.energies, site_dos)
1✔
717

718
    def get_site_spd_dos(self, site: PeriodicSite) -> dict[OrbitalType, Dos]:
1✔
719
        """
720
        Get orbital projected Dos of a particular site
721

722
        Args:
723
            site: Site in Structure associated with CompleteDos.
724

725
        Returns:
726
            dict of {OrbitalType: Dos}, e.g. {OrbitalType.s: Dos object, ...}
727
        """
728
        spd_dos: dict[OrbitalType, dict[Spin, np.ndarray]] = {}
1✔
729
        for orb, pdos in self.pdos[site].items():
1✔
730
            orbital_type = _get_orb_type(orb)
1✔
731
            if orbital_type in spd_dos:
1✔
732
                spd_dos[orbital_type] = add_densities(spd_dos[orbital_type], pdos)
1✔
733
            else:
734
                spd_dos[orbital_type] = pdos  # type: ignore[assignment]
1✔
735
        return {orb: Dos(self.efermi, self.energies, densities) for orb, densities in spd_dos.items()}
1✔
736

737
    def get_site_t2g_eg_resolved_dos(self, site: PeriodicSite) -> dict[str, Dos]:
1✔
738
        """
739
        Get the t2g, eg projected DOS for a particular site.
740

741
        Args:
742
            site: Site in Structure associated with CompleteDos.
743

744
        Returns:
745
            dict[str, Dos]: A dict {"e_g": Dos, "t2g": Dos} containing summed e_g and t2g DOS for the site.
746
        """
747
        t2g_dos = []
1✔
748
        eg_dos = []
1✔
749
        for s, atom_dos in self.pdos.items():
1✔
750
            if s == site:
1✔
751
                for orb, pdos in atom_dos.items():
1✔
752
                    if orb in (Orbital.dxy, Orbital.dxz, Orbital.dyz):
1✔
753
                        t2g_dos.append(pdos)
1✔
754
                    elif orb in (Orbital.dx2, Orbital.dz2):
1✔
755
                        eg_dos.append(pdos)
1✔
756
        return {
1✔
757
            "t2g": Dos(self.efermi, self.energies, functools.reduce(add_densities, t2g_dos)),
758
            "e_g": Dos(self.efermi, self.energies, functools.reduce(add_densities, eg_dos)),
759
        }
760

761
    def get_spd_dos(self) -> dict[OrbitalType, Dos]:
1✔
762
        """
763
        Get orbital projected Dos.
764

765
        Returns:
766
            dict of {OrbitalType: Dos}, e.g. {OrbitalType.s: Dos object, ...}
767
        """
768
        spd_dos = {}
1✔
769
        for atom_dos in self.pdos.values():
1✔
770
            for orb, pdos in atom_dos.items():
1✔
771
                orbital_type = _get_orb_type(orb)
1✔
772
                if orbital_type not in spd_dos:
1✔
773
                    spd_dos[orbital_type] = pdos
1✔
774
                else:
775
                    spd_dos[orbital_type] = add_densities(spd_dos[orbital_type], pdos)
1✔
776
        return {orb: Dos(self.efermi, self.energies, densities) for orb, densities in spd_dos.items()}
1✔
777

778
    def get_element_dos(self) -> dict[SpeciesLike, Dos]:
1✔
779
        """
780
        Get element projected Dos.
781

782
        Returns:
783
            dict of {Element: Dos}
784
        """
785
        el_dos = {}
1✔
786
        for site, atom_dos in self.pdos.items():
1✔
787
            el = site.specie
1✔
788
            for pdos in atom_dos.values():
1✔
789
                if el not in el_dos:
1✔
790
                    el_dos[el] = pdos
1✔
791
                else:
792
                    el_dos[el] = add_densities(el_dos[el], pdos)
1✔
793
        return {el: Dos(self.efermi, self.energies, densities) for el, densities in el_dos.items()}
1✔
794

795
    def get_element_spd_dos(self, el: SpeciesLike) -> dict[OrbitalType, Dos]:
1✔
796
        """
797
        Get element and spd projected Dos
798

799
        Args:
800
            el: Element in Structure.composition associated with CompleteDos
801

802
        Returns:
803
            dict of {OrbitalType: Dos}, e.g. {OrbitalType.s: Dos object, ...}
804
        """
805
        el = get_el_sp(el)
1✔
806
        el_dos = {}
1✔
807
        for site, atom_dos in self.pdos.items():
1✔
808
            if site.specie == el:
1✔
809
                for orb, pdos in atom_dos.items():
1✔
810
                    orbital_type = _get_orb_type(orb)
1✔
811
                    if orbital_type not in el_dos:
1✔
812
                        el_dos[orbital_type] = pdos
1✔
813
                    else:
814
                        el_dos[orbital_type] = add_densities(el_dos[orbital_type], pdos)
1✔
815

816
        return {orb: Dos(self.efermi, self.energies, densities) for orb, densities in el_dos.items()}
1✔
817

818
    @property
1✔
819
    def spin_polarization(self) -> float | None:
1✔
820
        """
821
        Calculates spin polarization at Fermi level. If the
822
        calculation is not spin-polarized, None will be
823
        returned.
824

825
        See Sanvito et al., doi: 10.1126/sciadv.1602241 for
826
        an example usage.
827

828
        :return (float): spin polarization in range [0, 1],
829
        will also return NaN if spin polarization ill-defined
830
        (e.g. for insulator)
831
        """
832
        n_F = self.get_interpolated_value(self.efermi)
1✔
833

834
        n_F_up = n_F[Spin.up]
1✔
835
        if Spin.down not in n_F:
1✔
836
            return None
1✔
837
        n_F_down = n_F[Spin.down]
1✔
838

839
        if (n_F_up + n_F_down) == 0:
1✔
840
            # only well defined for metals or half-metals
841
            return float("NaN")
×
842

843
        spin_polarization = (n_F_up - n_F_down) / (n_F_up + n_F_down)
1✔
844

845
        return abs(spin_polarization)
1✔
846

847
    def get_band_filling(
1✔
848
        self,
849
        band: OrbitalType = OrbitalType.d,
850
        elements: list[SpeciesLike] | None = None,
851
        sites: list[PeriodicSite] | None = None,
852
        spin: Spin | None = None,
853
    ) -> float:
854
        """
855
        Computes the orbital-projected band filling, defined as the zeroth moment
856
        up to the Fermi level
857

858
        Args:
859
            band: Orbital type to get the band center of (default is d-band)
860
            elements: Elements to get the band center of (cannot be used in conjunction with site)
861
            sites: Sites to get the band center of (cannot be used in conjunction with el)
862
            spin: Spin channel to use. By default, the spin channels will be combined.
863

864
        Returns:
865
            band filling in eV, often denoted f_d for the d-band
866
        """
867
        # Get the projected DOS
868
        if elements and sites:
1✔
869
            raise ValueError("Both element and site cannot be specified.")
×
870

871
        if elements:
1✔
872
            for i, el in enumerate(elements):
×
873
                spd_dos = self.get_element_spd_dos(el)[band]
×
874
                if i == 0:
×
875
                    densities = spd_dos.densities
×
876
                else:
877
                    densities = add_densities(densities, spd_dos.densities)
×
878
            dos = Dos(self.efermi, self.energies, densities)
×
879
        elif sites:
1✔
880
            for i, site in enumerate(sites):
×
881
                spd_dos = self.get_site_spd_dos(site)[band]
×
882
                if i == 0:
×
883
                    densities = spd_dos.densities
×
884
                else:
885
                    densities = add_densities(densities, spd_dos.densities)
×
886
            dos = Dos(self.efermi, self.energies, densities)
×
887
        else:
888
            dos = self.get_spd_dos()[band]
1✔
889

890
        energies = dos.energies - dos.efermi
1✔
891
        dos_densities = dos.get_densities(spin=spin)
1✔
892

893
        # Only consider up to Fermi level in numerator
894
        energies = dos.energies - dos.efermi
1✔
895
        band_filling = np.trapz(dos_densities[energies < 0], x=energies[energies < 0]) / np.trapz(
1✔
896
            dos_densities, x=energies
897
        )
898

899
        return band_filling
1✔
900

901
    def get_band_center(
1✔
902
        self,
903
        band: OrbitalType = OrbitalType.d,
904
        elements: list[SpeciesLike] | None = None,
905
        sites: list[PeriodicSite] | None = None,
906
        spin: Spin | None = None,
907
        erange: list[float] | None = None,
908
    ) -> float:
909
        """
910
        Computes the orbital-projected band center, defined as the first moment
911
        relative to the Fermi level
912
            int_{-inf}^{+inf} rho(E)*E dE/int_{-inf}^{+inf} rho(E) dE
913
        based on the work of Hammer and Norskov, Surf. Sci., 343 (1995) where the
914
        limits of the integration can be modified by erange and E is the set
915
        of energies taken with respect to the Fermi level. Note that the band center
916
        is often highly sensitive to the selected erange.
917

918
        Args:
919
            band: Orbital type to get the band center of (default is d-band)
920
            elements: Elements to get the band center of (cannot be used in conjunction with site)
921
            sites: Sites to get the band center of (cannot be used in conjunction with el)
922
            spin: Spin channel to use. By default, the spin channels will be combined.
923
            erange: [min, max] energy range to consider, with respect to the Fermi level.
924
                Default is None, which means all energies are considered.
925

926
        Returns:
927
            band center in eV, often denoted epsilon_d for the d-band center
928
        """
929
        band_center = self.get_n_moment(
1✔
930
            1, elements=elements, sites=sites, band=band, spin=spin, erange=erange, center=False
931
        )
932

933
        return band_center
1✔
934

935
    def get_band_width(
1✔
936
        self,
937
        band: OrbitalType = OrbitalType.d,
938
        elements: list[SpeciesLike] | None = None,
939
        sites: list[PeriodicSite] | None = None,
940
        spin: Spin | None = None,
941
        erange: list[float] | None = None,
942
    ) -> float:
943
        """
944
        Get the orbital-projected band width, defined as the square root of the second moment
945
            sqrt(int_{-inf}^{+inf} rho(E)*(E-E_center)^2 dE/int_{-inf}^{+inf} rho(E) dE)
946
        where E_center is the orbital-projected band center, the limits of the integration can be
947
        modified by erange, and E is the set of energies taken with respect to the Fermi level.
948
        Note that the band width is often highly sensitive to the selected erange.
949

950
        Args:
951
            band: Orbital type to get the band center of (default is d-band)
952
            elements: Elements to get the band center of (cannot be used in conjunction with site)
953
            sites: Sites to get the band center of (cannot be used in conjunction with el)
954
            spin: Spin channel to use. By default, the spin channels will be combined.
955
            erange: [min, max] energy range to consider, with respect to the Fermi level.
956
                Default is None, which means all energies are considered.
957

958
        Returns:
959
            Orbital-projected band width in eV
960
        """
961
        band_width = np.sqrt(self.get_n_moment(2, elements=elements, sites=sites, band=band, spin=spin, erange=erange))
1✔
962

963
        return band_width
1✔
964

965
    def get_band_skewness(
1✔
966
        self,
967
        band: OrbitalType = OrbitalType.d,
968
        elements: list[SpeciesLike] | None = None,
969
        sites: list[PeriodicSite] | None = None,
970
        spin: Spin | None = None,
971
        erange: list[float] | None = None,
972
    ) -> float:
973
        """
974
        Get the orbital-projected skewness, defined as the third standardized moment
975
            int_{-inf}^{+inf} rho(E)*(E-E_center)^3 dE/int_{-inf}^{+inf} rho(E) dE)
976
            /
977
            (int_{-inf}^{+inf} rho(E)*(E-E_center)^2 dE/int_{-inf}^{+inf} rho(E) dE))^(3/2)
978
        where E_center is the orbital-projected band center, the limits of the integration can be
979
        modified by erange, and E is the set of energies taken with respect to the Fermi level.
980
        Note that the skewness is often highly sensitive to the selected erange.
981

982
        Args:
983
            band: Orbitals to get the band center of (default is d-band)
984
            elements: Elements to get the band center of (cannot be used in conjunction with site)
985
            sites: Sites to get the band center of (cannot be used in conjunction with el)
986
            spin: Spin channel to use. By default, the spin channels will be combined.
987
            erange: [min, max] energy range to consider, with respect to the Fermi level.
988
                Default is None, which means all energies are considered.
989

990
        Returns:
991
            Orbital-projected skewness in eV
992
        """
993
        skewness = self.get_n_moment(
1✔
994
            3, elements=elements, sites=sites, band=band, spin=spin, erange=erange
995
        ) / self.get_n_moment(2, elements=elements, sites=sites, band=band, spin=spin, erange=erange) ** (3 / 2)
996

997
        return skewness
1✔
998

999
    def get_band_kurtosis(
1✔
1000
        self,
1001
        band: OrbitalType = OrbitalType.d,
1002
        elements: list[SpeciesLike] | None = None,
1003
        sites: list[PeriodicSite] | None = None,
1004
        spin: Spin | None = None,
1005
        erange: list[float] | None = None,
1006
    ) -> float:
1007
        """
1008
        Get the orbital-projected kurtosis, defined as the fourth standardized moment
1009
            int_{-inf}^{+inf} rho(E)*(E-E_center)^4 dE/int_{-inf}^{+inf} rho(E) dE)
1010
            /
1011
            (int_{-inf}^{+inf} rho(E)*(E-E_center)^2 dE/int_{-inf}^{+inf} rho(E) dE))^2
1012
        where E_center is the orbital-projected band center, the limits of the integration can be
1013
        modified by erange, and E is the set of energies taken with respect to the Fermi level.
1014
        Note that the skewness is often highly sensitive to the selected erange.
1015

1016
        Args:
1017
            band: Orbital type to get the band center of (default is d-band)
1018
            elements: Elements to get the band center of (cannot be used in conjunction with site)
1019
            sites: Sites to get the band center of (cannot be used in conjunction with el)
1020
            spin: Spin channel to use. By default, the spin channels will be combined.
1021
            erange: [min, max] energy range to consider, with respect to the Fermi level.
1022
                Default is None, which means all energies are considered.
1023

1024
        Returns:
1025
            Orbital-projected kurtosis in eV
1026
        """
1027
        kurtosis = (
1✔
1028
            self.get_n_moment(4, elements=elements, sites=sites, band=band, spin=spin, erange=erange)
1029
            / self.get_n_moment(2, elements=elements, sites=sites, band=band, spin=spin, erange=erange) ** 2
1030
        )
1031

1032
        return kurtosis
1✔
1033

1034
    def get_n_moment(
1✔
1035
        self,
1036
        n: int,
1037
        band: OrbitalType = OrbitalType.d,
1038
        elements: list[SpeciesLike] | None = None,
1039
        sites: list[PeriodicSite] | None = None,
1040
        spin: Spin | None = None,
1041
        erange: list[float] | None = None,
1042
        center: bool = True,
1043
    ) -> float:
1044
        """
1045
        Get the nth moment of the DOS centered around the orbital-projected band center, defined as
1046
            int_{-inf}^{+inf} rho(E)*(E-E_center)^n dE/int_{-inf}^{+inf} rho(E) dE
1047
        where n is the order, E_center is the orbital-projected band center, the limits of the integration can be
1048
        modified by erange, and E is the set of energies taken with respect to the Fermi level. If center is False,
1049
        then the E_center reference is not used.
1050

1051
        Args:
1052
            n: The order for the moment
1053
            band: Orbital type to get the band center of (default is d-band)
1054
            elements: Elements to get the band center of (cannot be used in conjunction with site)
1055
            sites: Sites to get the band center of (cannot be used in conjunction with el)
1056
            spin: Spin channel to use. By default, the spin channels will be combined.
1057
            erange: [min, max] energy range to consider, with respect to the Fermi level.
1058
                Default is None, which means all energies are considered.
1059
            center: Take moments with respect to the band center
1060

1061
        Returns:
1062
            Orbital-projected nth moment in eV
1063
        """
1064
        # Get the projected DOS
1065
        if elements and sites:
1✔
1066
            raise ValueError("Both element and site cannot be specified.")
×
1067

1068
        if elements:
1✔
1069
            for i, el in enumerate(elements):
1✔
1070
                spd_dos = self.get_element_spd_dos(el)[band]
1✔
1071
                if i == 0:
1✔
1072
                    densities = spd_dos.densities
1✔
1073
                else:
1074
                    densities = add_densities(densities, spd_dos.densities)
1✔
1075
            dos = Dos(self.efermi, self.energies, densities)
1✔
1076
        elif sites:
1✔
1077
            for i, site in enumerate(sites):
1✔
1078
                spd_dos = self.get_site_spd_dos(site)[band]
1✔
1079
                if i == 0:
1✔
1080
                    densities = spd_dos.densities
1✔
1081
                else:
1082
                    densities = add_densities(densities, spd_dos.densities)
1✔
1083
            dos = Dos(self.efermi, self.energies, densities)
1✔
1084
        else:
1085
            dos = self.get_spd_dos()[band]
1✔
1086

1087
        energies = dos.energies - dos.efermi
1✔
1088
        dos_densities = dos.get_densities(spin=spin)
1✔
1089

1090
        # Only consider a given erange, if desired
1091
        if erange:
1✔
1092
            dos_densities = dos_densities[(energies >= erange[0]) & (energies <= erange[1])]
1✔
1093
            energies = energies[(energies >= erange[0]) & (energies <= erange[1])]
1✔
1094

1095
        # Center the energies about the band center if requested
1096
        if center:
1✔
1097
            band_center = self.get_band_center(elements=elements, sites=sites, band=band, spin=spin, erange=erange)
1✔
1098
            p = energies - band_center
1✔
1099
        else:
1100
            p = energies
1✔
1101

1102
        # Take the nth moment
1103
        nth_moment = np.trapz(p**n * dos_densities, x=energies) / np.trapz(dos_densities, x=energies)
1✔
1104

1105
        return nth_moment
1✔
1106

1107
    def get_hilbert_transform(
1✔
1108
        self,
1109
        band: OrbitalType = OrbitalType.d,
1110
        elements: list[SpeciesLike] | None = None,
1111
        sites: list[PeriodicSite] | None = None,
1112
    ) -> Dos:
1113
        """
1114
        Returns the Hilbert transform of the orbital-projected density of states,
1115
        often plotted for a Newns-Anderson analysis.
1116

1117
        Args:
1118
            elements: Elements to get the band center of (cannot be used in conjunction with site)
1119
            sites: Sites to get the band center of (cannot be used in conjunction with el)
1120
            band: Orbitals to get the band center of (default is d-band)
1121

1122
        Returns:
1123
            Hilbert transformation of the projected DOS.
1124
        """
1125
        # Get the projected DOS
1126
        if elements and sites:
1✔
1127
            raise ValueError("Both element and site cannot be specified.")
×
1128

1129
        if elements:
1✔
1130
            densities: Mapping[Spin, ArrayLike]
1131
            for i, el in enumerate(elements):
1✔
1132
                spd_dos = self.get_element_spd_dos(el)[band]
1✔
1133
                if i == 0:
1✔
1134
                    densities = spd_dos.densities
1✔
1135
                else:
1136
                    densities = add_densities(densities, spd_dos.densities)
×
1137
            dos = Dos(self.efermi, self.energies, densities)
1✔
1138
        elif sites:
1✔
1139
            for i, site in enumerate(sites):
1✔
1140
                spd_dos = self.get_site_spd_dos(site)[band]
1✔
1141
                if i == 0:
1✔
1142
                    densities = spd_dos.densities
1✔
1143
                else:
1144
                    densities = add_densities(densities, spd_dos.densities)
×
1145
            dos = Dos(self.efermi, self.energies, densities)
1✔
1146
        else:
1147
            dos = self.get_spd_dos()[band]
1✔
1148

1149
        # Get Hilbert-transformed densities
1150
        densities_transformed = {Spin.up: np.imag(hilbert(dos.get_densities(spin=Spin.up)))}
1✔
1151
        if Spin.down in self.densities:
1✔
1152
            densities_transformed[Spin.down] = np.imag(hilbert(dos.get_densities(spin=Spin.down)))
×
1153

1154
        return Dos(self.efermi, self.energies, densities_transformed)
1✔
1155

1156
    def get_upper_band_edge(
1✔
1157
        self,
1158
        band: OrbitalType = OrbitalType.d,
1159
        elements: list[SpeciesLike] | None = None,
1160
        sites: list[PeriodicSite] | None = None,
1161
        spin: Spin | None = None,
1162
        erange: list[float] | None = None,
1163
    ) -> float:
1164
        """
1165
        Get the orbital-projected upper band edge. The definition by Xin et al.
1166
        Phys. Rev. B, 89, 115114 (2014) is used, which is the highest peak position of the
1167
        Hilbert transform of the orbital-projected DOS.
1168

1169
        Args:
1170
            band: Orbital type to get the band center of (default is d-band)
1171
            elements: Elements to get the band center of (cannot be used in conjunction with site)
1172
            sites: Sites to get the band center of (cannot be used in conjunction with el)
1173
            spin: Spin channel to use. By default, the spin channels will be combined.
1174
            erange: [min, max] energy range to consider, with respect to the Fermi level.
1175
                Default is None, which means all energies are considered.
1176

1177
        Returns:
1178
            Upper band edge in eV, often denoted epsilon_u
1179
        """
1180
        # Get the Hilbert-transformed DOS
1181
        transformed_dos = self.get_hilbert_transform(elements=elements, sites=sites, band=band)
1✔
1182

1183
        energies = transformed_dos.energies - transformed_dos.efermi
1✔
1184
        densities = transformed_dos.get_densities(spin=spin)
1✔
1185

1186
        # Only consider a given erange, if specified
1187
        if erange:
1✔
1188
            densities = densities[(energies >= erange[0]) & (energies <= erange[1])]
1✔
1189
            energies = energies[(energies >= erange[0]) & (energies <= erange[1])]
1✔
1190

1191
        # Calculate the upper band edge
1192
        upper_band_edge = energies[np.argmax(densities)]
1✔
1193
        return upper_band_edge
1✔
1194

1195
    def get_dos_fp(
1✔
1196
        self,
1197
        type: str = "summed_pdos",
1198
        binning: bool = True,
1199
        min_e: float | None = None,
1200
        max_e: float | None = None,
1201
        n_bins: int = 256,
1202
        normalize: bool = True,
1203
    ) -> NamedTuple:
1204
        """
1205
        Generates the DOS fingerprint based on work of
1206
        F. Knoop, T. A. r Purcell, M. Scheffler, C. Carbogno, J. Open Source Softw. 2020, 5, 2671.
1207
        Source - https://gitlab.com/vibes-developers/vibes/-/tree/master/vibes/materials_fp
1208
        Copyright (c) 2020 Florian Knoop, Thomas A.R.Purcell, Matthias Scheffler, Christian Carbogno
1209

1210

1211
        Args:
1212
            type (str): Specify fingerprint type needed can accept '{s/p/d/f/}summed_{pdos/tdos}'
1213
            (default is summed_pdos)
1214
            min_e (float): The minimum mode energy to include in the fingerprint (default is None)
1215
            max_e (float): The maximum mode energy to include in the fingerprint (default is None)
1216
            n_bins (int): Number of bins to be used in the fingerprint (default is 256)
1217
            normalize (bool): If true, normalizes the area under fp to equal to 1 (default is True)
1218

1219
        Raises:
1220
            ValueError: If type is not one of the accepted values {s/p/d/f/}summed_{pdos/tdos}.
1221

1222
        Returns:
1223
            Fingerprint(namedtuple) : The electronic density of states fingerprint
1224
            of format (energies, densities, type, n_bins)
1225
        """
1226
        fingerprint = namedtuple("fingerprint", "energies densities type n_bins bin_width")
1✔
1227
        energies = self.energies - self.efermi
1✔
1228

1229
        if max_e is None:
1✔
1230
            max_e = np.max(energies)
1✔
1231

1232
        if min_e is None:
1✔
1233
            min_e = np.min(energies)
1✔
1234

1235
        pdos_obj = self.get_spd_dos()
1✔
1236

1237
        pdos = {}
1✔
1238
        for key in pdos_obj:
1✔
1239
            dens = pdos_obj[key].get_densities()
1✔
1240

1241
            pdos[key.name] = dens
1✔
1242

1243
        pdos["summed_pdos"] = np.sum(list(pdos.values()), axis=0)
1✔
1244
        pdos["tdos"] = self.get_densities()
1✔
1245

1246
        try:
1✔
1247
            densities = pdos[type]
1✔
1248
            if len(energies) < n_bins:
1✔
1249
                inds = np.where((energies >= min_e) & (energies <= max_e))
×
1250
                return fingerprint(energies[inds], densities[inds], type, len(energies), np.diff(energies)[0])
×
1251

1252
            if binning:
1✔
1253
                ener_bounds = np.linspace(min_e, max_e, n_bins + 1)
1✔
1254
                ener = ener_bounds[:-1] + (ener_bounds[1] - ener_bounds[0]) / 2.0
1✔
1255
                bin_width = np.diff(ener)[0]
1✔
1256
            else:
1257
                ener_bounds = np.array(energies)
1✔
1258
                ener = np.append(energies, [energies[-1] + np.abs(energies[-1]) / 10])
1✔
1259
                n_bins = len(energies)
1✔
1260
                bin_width = np.diff(energies)[0]
1✔
1261

1262
            dos_rebin = np.zeros(ener.shape)
1✔
1263

1264
            for ii, e1, e2 in zip(range(len(ener)), ener_bounds[0:-1], ener_bounds[1:]):
1✔
1265
                inds = np.where((energies >= e1) & (energies < e2))
1✔
1266
                dos_rebin[ii] = np.sum(densities[inds])
1✔
1267
            if normalize:  # scale DOS bins to make area under histogram equal 1
1✔
1268
                area = np.sum(dos_rebin * bin_width)
1✔
1269
                dos_rebin_sc = dos_rebin / area
1✔
1270
            else:
1271
                dos_rebin_sc = dos_rebin
1✔
1272

1273
            return fingerprint(np.array([ener]), dos_rebin_sc, type, n_bins, bin_width)
1✔
1274

1275
        except KeyError:
1✔
1276
            raise ValueError(
1✔
1277
                "Please recheck type requested, either the orbital projections unavailable in input DOS or "
1278
                "there's a typo in type."
1279
            )
1280

1281
    @staticmethod
1✔
1282
    def fp_to_dict(fp: NamedTuple) -> dict:
1✔
1283
        """Converts a fingerprint into a dictionary
1284

1285
        Args:
1286
            fp: The DOS fingerprint to be converted into a dictionary
1287

1288
        Returns:
1289
            dict: A dict of the fingerprint Keys=type, Values=np.ndarray(energies, densities)
1290
        """
1291
        fp_dict = {}
1✔
1292
        fp_dict[fp[2]] = np.array([fp[0], fp[1]], dtype="object").T
1✔
1293

1294
        return fp_dict
1✔
1295

1296
    @staticmethod
1✔
1297
    def get_dos_fp_similarity(
1✔
1298
        fp1: NamedTuple,
1299
        fp2: NamedTuple,
1300
        col: int = 1,
1301
        pt: int | str = "All",
1302
        normalize: bool = False,
1303
        tanimoto: bool = False,
1304
    ) -> float:
1305
        """Calculates the similarity index (dot product) of two fingerprints
1306

1307
        Args:
1308
            fp1 (NamedTuple): The 1st dos fingerprint object
1309
            fp2 (NamedTuple): The 2nd dos fingerprint object
1310
            col (int): The item in the fingerprints (0:energies,1: densities) to take the dot product of (default is 1)
1311
            pt (int or str) : The index of the point that the dot product is to be taken (default is All)
1312
            normalize (bool): If True normalize the scalar product to 1 (default is False)
1313
            tanimoto (bool): If True will compute Tanimoto index (default is False)
1314

1315
        Raises:
1316
            ValueError: If both tanimoto and normalize are set to True.
1317

1318
        Returns:
1319
        Similarity index (float): The value of dot product
1320
        """
1321
        if not isinstance(fp1, dict):
1✔
1322
            fp1_dict = CompleteDos.fp_to_dict(fp1)
1✔
1323
        else:
1324
            fp1_dict = fp1
×
1325

1326
        if not isinstance(fp2, dict):
1✔
1327
            fp2_dict = CompleteDos.fp_to_dict(fp2)
1✔
1328
        else:
1329
            fp2_dict = fp2
×
1330

1331
        if pt == "All":
1✔
1332
            vec1 = np.array([pt[col] for pt in fp1_dict.values()]).flatten()
1✔
1333
            vec2 = np.array([pt[col] for pt in fp2_dict.values()]).flatten()
1✔
1334
        else:
1335
            vec1 = fp1_dict[fp1[2][pt]][col]
×
1336
            vec2 = fp2_dict[fp2[2][pt]][col]
×
1337

1338
        if not normalize and tanimoto:
1✔
1339
            rescale = np.linalg.norm(vec1) ** 2 + np.linalg.norm(vec2) ** 2 - np.dot(vec1, vec2)
1✔
1340
            return np.dot(vec1, vec2) / rescale
1✔
1341

1342
        elif not tanimoto and normalize:
1✔
1343
            rescale = np.linalg.norm(vec1) * np.linalg.norm(vec2)
×
1344
            return np.dot(vec1, vec2) / rescale
×
1345

1346
        elif not tanimoto and not normalize:
1✔
1347
            rescale = 1.0
×
1348
            return np.dot(vec1, vec2) / rescale
×
1349

1350
        else:
1351
            raise ValueError(
1✔
1352
                "Cannot compute similarity index. Please set either normalize=True or tanimoto=True or both to False."
1353
            )
1354

1355
    @classmethod
1✔
1356
    def from_dict(cls, d) -> CompleteDos:
1✔
1357
        """
1358
        Returns CompleteDos object from dict representation.
1359
        """
1360
        tdos = Dos.from_dict(d)
1✔
1361
        struct = Structure.from_dict(d["structure"])
1✔
1362
        pdoss = {}
1✔
1363
        for i in range(len(d["pdos"])):
1✔
1364
            at = struct[i]
1✔
1365
            orb_dos = {}
1✔
1366
            for orb_str, odos in d["pdos"][i].items():
1✔
1367
                orb = Orbital[orb_str]
1✔
1368
                orb_dos[orb] = {Spin(int(k)): v for k, v in odos["densities"].items()}
1✔
1369
            pdoss[at] = orb_dos
1✔
1370
        return CompleteDos(struct, tdos, pdoss)
1✔
1371

1372
    def as_dict(self) -> dict:
1✔
1373
        """
1374
        JSON-serializable dict representation of CompleteDos.
1375
        """
1376
        d = {
1✔
1377
            "@module": type(self).__module__,
1378
            "@class": type(self).__name__,
1379
            "efermi": self.efermi,
1380
            "structure": self.structure.as_dict(),
1381
            "energies": self.energies.tolist(),
1382
            "densities": {str(spin): dens.tolist() for spin, dens in self.densities.items()},
1383
            "pdos": [],
1384
        }
1385
        if len(self.pdos) > 0:
1✔
1386
            for at in self.structure:
1✔
1387
                dd = {}
1✔
1388
                for orb, pdos in self.pdos[at].items():
1✔
1389
                    dd[str(orb)] = {
1✔
1390
                        "densities": {str(int(spin)): list(dens) for spin, dens in pdos.items()}  # type: ignore
1391
                    }
1392
                d["pdos"].append(dd)
1✔
1393
            d["atom_dos"] = {str(at): dos.as_dict() for at, dos in self.get_element_dos().items()}
1✔
1394
            d["spd_dos"] = {str(orb): dos.as_dict() for orb, dos in self.get_spd_dos().items()}
1✔
1395
        return d
1✔
1396

1397
    def __str__(self):
1✔
1398
        return "Complete DOS for " + str(self.structure)
1✔
1399

1400

1401
class LobsterCompleteDos(CompleteDos):
1✔
1402
    """
1403
    Extended CompleteDOS for Lobster
1404
    """
1405

1406
    def get_site_orbital_dos(self, site: PeriodicSite, orbital: str) -> Dos:  # type: ignore
1✔
1407
        """
1408
        Get the Dos for a particular orbital of a particular site.
1409

1410
        Args:
1411
            site: Site in Structure associated with CompleteDos.
1412
            orbital: principal quantum number and orbital in string format, e.g. "4s".
1413
                    possible orbitals are: "s", "p_y", "p_z", "p_x", "d_xy", "d_yz", "d_z^2",
1414
                    "d_xz", "d_x^2-y^2", "f_y(3x^2-y^2)", "f_xyz",
1415
                    "f_yz^2", "f_z^3", "f_xz^2", "f_z(x^2-y^2)", "f_x(x^2-3y^2)"
1416
                    In contrast to the Cohpcar and the Cohplist objects, the strings from the Lobster files are used
1417

1418
        Returns:
1419
            Dos containing densities of an orbital of a specific site.
1420
        """
1421
        if orbital[1:] not in [
1✔
1422
            "s",
1423
            "p_y",
1424
            "p_z",
1425
            "p_x",
1426
            "d_xy",
1427
            "d_yz",
1428
            "d_z^2",
1429
            "d_xz",
1430
            "d_x^2-y^2",
1431
            "f_y(3x^2-y^2)",
1432
            "f_xyz",
1433
            "f_yz^2",
1434
            "f_z^3",
1435
            "f_xz^2",
1436
            "f_z(x^2-y^2)",
1437
            "f_x(x^2-3y^2)",
1438
        ]:
1439
            raise ValueError("orbital is not correct")
×
1440
        return Dos(self.efermi, self.energies, self.pdos[site][orbital])  # type: ignore
1✔
1441

1442
    def get_site_t2g_eg_resolved_dos(self, site: PeriodicSite) -> dict[str, Dos]:
1✔
1443
        """
1444
        Get the t2g, eg projected DOS for a particular site.
1445
        Args:
1446
            site: Site in Structure associated with CompleteDos.
1447

1448
        Returns:
1449
            A dict {"e_g": Dos, "t2g": Dos} containing summed e_g and t2g DOS
1450
            for the site.
1451
        """
1452
        warnings.warn("Are the orbitals correctly oriented? Are you sure?")
1✔
1453
        t2g_dos = []
1✔
1454
        eg_dos = []
1✔
1455
        for s, atom_dos in self.pdos.items():
1✔
1456
            if s == site:
1✔
1457
                for orb, pdos in atom_dos.items():
1✔
1458
                    if _get_orb_lobster(orb) in (Orbital.dxy, Orbital.dxz, Orbital.dyz):
1✔
1459
                        t2g_dos.append(pdos)
1✔
1460
                    elif _get_orb_lobster(orb) in (Orbital.dx2, Orbital.dz2):
1✔
1461
                        eg_dos.append(pdos)
1✔
1462
        return {
1✔
1463
            "t2g": Dos(self.efermi, self.energies, functools.reduce(add_densities, t2g_dos)),
1464
            "e_g": Dos(self.efermi, self.energies, functools.reduce(add_densities, eg_dos)),
1465
        }
1466

1467
    def get_spd_dos(self) -> dict[str, Dos]:  # type: ignore
1✔
1468
        """
1469
        Get orbital projected Dos.
1470
        For example, if 3s and 4s are included in the basis of some element, they will be both summed in the orbital
1471
        projected DOS
1472

1473
        Returns:
1474
            dict of {orbital: Dos}, e.g. {"s": Dos object, ...}
1475
        """
1476
        spd_dos = {}
1✔
1477
        for atom_dos in self.pdos.values():
1✔
1478
            for orb, pdos in atom_dos.items():
1✔
1479
                orbital_type = _get_orb_type_lobster(orb)
1✔
1480
                if orbital_type not in spd_dos:
1✔
1481
                    spd_dos[orbital_type] = pdos
1✔
1482
                else:
1483
                    spd_dos[orbital_type] = add_densities(spd_dos[orbital_type], pdos)
1✔
1484

1485
        return {orb: Dos(self.efermi, self.energies, densities) for orb, densities in spd_dos.items()}
1✔
1486

1487
    def get_element_spd_dos(self, el: SpeciesLike) -> dict[str, Dos]:  # type: ignore
1✔
1488
        """
1489
        Get element and spd projected Dos
1490

1491

1492
        Args:
1493
            el: Element in Structure.composition associated with LobsterCompleteDos
1494

1495
        Returns:
1496
            dict of {OrbitalType.s: densities, OrbitalType.p: densities, OrbitalType.d: densities}
1497
        """
1498
        el = get_el_sp(el)
1✔
1499
        el_dos = {}
1✔
1500
        for site, atom_dos in self.pdos.items():
1✔
1501
            if site.specie == el:
1✔
1502
                for orb, pdos in atom_dos.items():
1✔
1503
                    orbital_type = _get_orb_type_lobster(orb)
1✔
1504
                    if orbital_type not in el_dos:
1✔
1505
                        el_dos[orbital_type] = pdos
1✔
1506
                    else:
1507
                        el_dos[orbital_type] = add_densities(el_dos[orbital_type], pdos)
1✔
1508

1509
        return {orb: Dos(self.efermi, self.energies, densities) for orb, densities in el_dos.items()}
1✔
1510

1511
    @classmethod
1✔
1512
    def from_dict(cls, d) -> LobsterCompleteDos:
1✔
1513
        """
1514
        Returns: CompleteDos object from dict representation.
1515
        """
1516
        tdos = Dos.from_dict(d)
1✔
1517
        struct = Structure.from_dict(d["structure"])
1✔
1518
        pdoss = {}
1✔
1519
        for i in range(len(d["pdos"])):
1✔
1520
            at = struct[i]
1✔
1521
            orb_dos = {}
1✔
1522
            for orb_str, odos in d["pdos"][i].items():
1✔
1523
                orb = orb_str
1✔
1524
                orb_dos[orb] = {Spin(int(k)): v for k, v in odos["densities"].items()}
1✔
1525
            pdoss[at] = orb_dos
1✔
1526
        return LobsterCompleteDos(struct, tdos, pdoss)
1✔
1527

1528

1529
def add_densities(density1: Mapping[Spin, ArrayLike], density2: Mapping[Spin, ArrayLike]) -> dict[Spin, np.ndarray]:
1✔
1530
    """
1531
    Method to sum two densities.
1532

1533
    Args:
1534
        density1: First density.
1535
        density2: Second density.
1536

1537
    Returns:
1538
        dict[Spin, np.ndarray]
1539
    """
1540
    return {spin: np.array(density1[spin]) + np.array(density2[spin]) for spin in density1}
1✔
1541

1542

1543
def _get_orb_type(orb):
1✔
1544
    try:
1✔
1545
        return orb.orbital_type
1✔
1546
    except AttributeError:
×
1547
        return orb
×
1548

1549

1550
def f0(E, fermi, T) -> float:
1✔
1551
    """Returns the equilibrium fermi-dirac.
1552

1553
    Args:
1554
        E (float): energy in eV
1555
        fermi (float): the fermi level in eV
1556
        T (float): the temperature in kelvin
1557

1558
    Returns:
1559
        float
1560
    """
1561
    return 1.0 / (1.0 + np.exp((E - fermi) / (_cd("Boltzmann constant in eV/K") * T)))
1✔
1562

1563

1564
def _get_orb_type_lobster(orb):
1✔
1565
    """
1566
    Args:
1567
        orb: string representation of orbital
1568
    Returns:
1569
        OrbitalType
1570
    """
1571
    orb_labs = [
1✔
1572
        "s",
1573
        "p_y",
1574
        "p_z",
1575
        "p_x",
1576
        "d_xy",
1577
        "d_yz",
1578
        "d_z^2",
1579
        "d_xz",
1580
        "d_x^2-y^2",
1581
        "f_y(3x^2-y^2)",
1582
        "f_xyz",
1583
        "f_yz^2",
1584
        "f_z^3",
1585
        "f_xz^2",
1586
        "f_z(x^2-y^2)",
1587
        "f_x(x^2-3y^2)",
1588
    ]
1589

1590
    try:
1✔
1591
        orbital = Orbital(orb_labs.index(orb[1:]))
1✔
1592
        return orbital.orbital_type
1✔
1593
    except AttributeError:
×
1594
        print("Orb not in list")
×
1595
    return None
×
1596

1597

1598
def _get_orb_lobster(orb):
1✔
1599
    """
1600
    Args:
1601
        orb: string representation of orbital
1602
    Returns:
1603
         Orbital
1604
    """
1605
    orb_labs = [
1✔
1606
        "s",
1607
        "p_y",
1608
        "p_z",
1609
        "p_x",
1610
        "d_xy",
1611
        "d_yz",
1612
        "d_z^2",
1613
        "d_xz",
1614
        "d_x^2-y^2",
1615
        "f_y(3x^2-y^2)",
1616
        "f_xyz",
1617
        "f_yz^2",
1618
        "f_z^3",
1619
        "f_xz^2",
1620
        "f_z(x^2-y^2)",
1621
        "f_x(x^2-3y^2)",
1622
    ]
1623

1624
    try:
1✔
1625
        orbital = Orbital(orb_labs.index(orb[1:]))
1✔
1626
        return orbital
1✔
1627
    except AttributeError:
×
1628
        print("Orb not in list")
×
1629
    return None
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc