Example #1
0
    def run(
        self,
        directory: Union[str, Path] = ".",
        return_usage_stats: bool = False,
        prefix: Optional[str] = None,
    ):
        mem_usage, (amset_data, usage_stats) = memory_usage(
            partial(self._run_wrapper, directory=directory, prefix=prefix),
            max_usage=True,
            retval=True,
            interval=0.1,
            include_children=False,
            multiprocess=True,
        )

        log_banner("END")

        logger.info("Timing and memory usage:")
        timing_info = [
            "{} time: {:.4f} s".format(name, t)
            for name, t in usage_stats.items()
        ]
        log_list(timing_info + ["max memory: {:.1f} MB".format(mem_usage)])

        now = datetime.datetime.now()
        logger.info("amset exiting on {} at {}".format(
            now.strftime("%d %b %Y"), now.strftime("%H:%M")))

        if return_usage_stats:
            usage_stats["max memory"] = mem_usage[0]
            return amset_data, usage_stats

        else:
            return amset_data
Example #2
0
def _log_structure_information(structure: Structure, symprec):
    log_banner("STRUCTURE")
    logger.info("Structure information:")

    comp = structure.composition
    lattice = structure.lattice
    formula = comp.get_reduced_formula_and_factor(iupac_ordering=True)[0]

    if not symprec:
        symprec = 1e-32

    sga = SpacegroupAnalyzer(structure, symprec=symprec)
    spg = unicodeify_spacegroup(sga.get_space_group_symbol())

    comp_info = [
        "formula: {}".format(unicodeify(formula)),
        "# sites: {}".format(structure.num_sites),
        "space group: {}".format(spg),
    ]
    log_list(comp_info)

    logger.info("Lattice:")
    lattice_info = [
        "a, b, c [Å]: {:.2f}, {:.2f}, {:.2f}".format(*lattice.abc),
        "α, β, γ [°]: {:.0f}, {:.0f}, {:.0f}".format(*lattice.angles),
    ]
    log_list(lattice_info)
Example #3
0
def _log_band_edge_information(band_structure, edge_data):
    """Log data about the valence band maximum or conduction band minimum.

    Args:
        band_structure: A band structure.
        edge_data (dict): The :obj:`dict` from ``bs.get_vbm()`` or
            ``bs.get_cbm()``
    """
    if band_structure.is_spin_polarized:
        spins = edge_data["band_index"].keys()
        b_indices = [
            ", ".join([str(i + 1) for i in edge_data["band_index"][spin]]) +
            "({})".format(spin.name.capitalize()) for spin in spins
        ]
        b_indices = ", ".join(b_indices)
    else:
        b_indices = ", ".join(
            [str(i + 1) for i in edge_data["band_index"][Spin.up]])

    kpoint = edge_data["kpoint"]
    kpoint_str = _kpt_str.format(k=kpoint.frac_coords)

    info = [
        "energy: {:.3f} eV".format(edge_data["energy"]),
        "k-point: {}".format(kpoint_str),
        "band indices: {}".format(b_indices),
    ]
    log_list(info)
Example #4
0
    def run(
        self,
        directory: Union[str, Path] = ".",
        return_usage_stats: bool = False,
        prefix: Optional[str] = None,
    ):
        mem_usage, (amset_data, usage_stats) = memory_usage(
            partial(self._run_wrapper, directory=directory, prefix=prefix),
            max_usage=True,
            retval=True,
            interval=0.1,
            include_children=False,
            multiprocess=True,
        )
        log_banner("END")

        logger.info("Timing and memory usage:")
        timing_info = [f"{n} time: {t:.4f} s" for n, t in usage_stats.items()]
        log_list(timing_info + [f"max memory: {mem_usage:.1f} MB"])

        this_date = datetime.datetime.now().strftime("%d %b %Y")
        this_time = datetime.datetime.now().strftime("%H:%M")
        logger.info(f"amset exiting on {this_date} at {this_time}")

        if return_usage_stats:
            usage_stats["max_memory"] = mem_usage
            return amset_data, usage_stats
        else:
            return amset_data
Example #5
0
    def set_doping_and_temperatures(self, doping: np.ndarray,
                                    temperatures: np.ndarray):
        if not self.dos:
            raise RuntimeError(
                "The DOS should be calculated (AmsetData.calculate_dos) before "
                "setting doping levels.")

        if doping is None:
            # Generally this is for metallic systems; here we use the intrinsic Fermi
            # level
            self.doping = [0]
            print("doping is none")
        else:
            self.doping = doping * (1 / cm_to_bohr)**3

        self.temperatures = temperatures

        self.fermi_levels = np.zeros((len(doping), len(temperatures)))
        self.electron_conc = np.zeros((len(doping), len(temperatures)))
        self.hole_conc = np.zeros((len(doping), len(temperatures)))

        fermi_level_info = []
        tols = np.logspace(-5, 0, 6)
        for n, t in np.ndindex(self.fermi_levels.shape):
            for i, tol in enumerate(tols):
                # Finding the Fermi level is quite fickle. Enumerate multiple
                # tolerances and use the first one that works!
                try:
                    if self.doping[n] == 0:
                        self.fermi_levels[
                            n, t] = self.dos.get_fermi_from_num_electrons(
                                self.num_electrons,
                                temperatures[t],
                                tol=tol / 1000,
                                precision=10,
                            )
                    else:
                        self.fermi_levels[n, t], self.electron_conc[
                            n, t], self.hole_conc[n, t] = self.dos.get_fermi(
                                self.doping[n],
                                temperatures[t],
                                tol=tol,
                                precision=10,
                                return_electron_hole_conc=True,
                            )
                    break
                except ValueError:
                    if i == len(tols) - 1:
                        raise ValueError(
                            "Could not calculate Fermi level position."
                            "Try a denser k-point mesh.")
                    else:
                        pass

            fermi_level_info.append("{:.2g} cm⁻³ & {} K: {:.4f} eV".format(
                doping[n], temperatures[t],
                self.fermi_levels[n, t] / units.eV))

        logger.info("Calculated Fermi levels:")
        log_list(fermi_level_info)
Example #6
0
    def from_amset_data(cls, materials_properties: Dict[str, Any],
                        amset_data: AmsetData):
        logger.info("Initializing POP scattering")

        # convert from THz to angular frequency in Hz
        pop_frequency = (materials_properties["pop_frequency"] * 1e12 * 2 *
                         np.pi / s_to_au)

        if materials_properties["free_carrier_screening"]:
            # use high-frequency diel for screening length
            avg_diel = np.linalg.eigvalsh(
                materials_properties["high_frequency_dielectric"]).mean()
            inverse_screening_length_sq = calculate_inverse_screening_length_sq(
                amset_data, avg_diel)
        else:
            inverse_screening_length_sq = np.zeros_like(
                amset_data.fermi_levels)

        # n_po (phonon concentration) has shape (ntemps, )
        n_po = 1 / (np.exp(pop_frequency /
                           (boltzmann_au * amset_data.temperatures)) - 1)

        n_po = n_po[None, :]

        log_list([
            "average N_po: {:.4f}".format(np.mean(n_po)),
            "ω_po: {:.4g} 2π THz".format(
                materials_properties["pop_frequency"] * 2 * np.pi),
            "ħω: {:.4f} eV".format(pop_frequency * hbar * s_to_au),
        ])

        # want to store two intermediate properties for:
        #             emission      and        absorption
        # (1-f)(N_po + 1) + f(N_po) and (1-f)N_po + f(N_po + 1)
        # note that these are defined for the scattering rate S(k', k).
        # For the rate S(k, k') the definitions are reversed.

        # self.emission_f_out = {
        #     s: n_po + 1 - amset_data.f[s]
        #     for s in amset_data.spins}
        # self.absorption_f_out = {
        #     s: n_po + amset_data.f[s]
        #     for s in amset_data.spins}
        #
        # self.emission_f_in = {
        #     s: n_po + amset_data.f[s]
        #     for s in amset_data.spins}
        # self.absorption_f_in = {
        #     s: n_po + 1 - amset_data.f[s]
        #     for s in amset_data.spins}
        return cls(
            cls.get_properties(materials_properties),
            amset_data.doping,
            amset_data.temperatures,
            cls.get_nbands(amset_data),
            pop_frequency,
            n_po,
            inverse_screening_length_sq,
        )
Example #7
0
def _log_settings(runner: AmsetRunner):
    log_banner("SETTINGS")
    logger.info("Run parameters:")
    p = [
        "{}: {}".format(k, v) for k, v in runner.settings.items()
        if v is not None
    ]
    log_list(p)
Example #8
0
    def calculate_scattering_rates(self):
        spins = self.amset_data.spins
        kpoints = self.amset_data.kpoints
        f_shape = self.amset_data.fermi_levels.shape
        scattering_shape = (len(self.scatterer_labels), ) + f_shape

        # rates has shape (spin, nscatterers, ndoping, ntemp, nbands, nkpoints)
        rates = {
            s: np.zeros(scattering_shape + self.amset_data.energies[s].shape)
            for s in spins
        }
        masks = {
            s: np.full(scattering_shape + self.amset_data.energies[s].shape,
                       True)
            for s in spins
        }

        nkpoints = len(self.amset_data.ir_kpoints_idx)

        logger.info("Scattering information:")
        log_list(["# ir k-points: {}".format(nkpoints)])

        for spin in spins:
            for b_idx in range(len(self.amset_data.energies[spin])):
                str_b = "Calculating rates for {} band {}"
                logger.info(str_b.format(spin_name[spin], b_idx + 1))

                t0 = time.perf_counter()
                (
                    rates[spin][..., b_idx, :],
                    masks[spin][..., b_idx, :],
                ) = self.calculate_band_rates(spin, b_idx)

                info = [
                    "max rate: {:.4g}".format(rates[spin][...,
                                                          b_idx, :].max()),
                    "min rate: {:.4g}".format(rates[spin][...,
                                                          b_idx, :].min()),
                    "time: {:.4f} s".format(time.perf_counter() - t0),
                ]
                log_list(info)

            # fill in k-points outside Fermi-Dirac cutoffs with a default value
            rates[spin][masks[spin]] = 1e14

        # if the k-point density is low, some k-points may not have other k-points
        # within the energy tolerance leading to zero rates
        rates = _interpolate_zero_rates(rates,
                                        kpoints,
                                        masks,
                                        progress_bar=self.progress_bar)

        return rates
Example #9
0
    def __init__(self, materials_properties: Dict[str, Any],
                 amset_data: AmsetData):
        super().__init__(materials_properties, amset_data)
        logger.debug("Initializing POP scattering")

        # convert from THz to angular frequency in Hz
        self.pop_frequency = (self.properties["pop_frequency"] * 1e12 * 2 *
                              np.pi / Second)

        # n_po (phonon concentration) has shape (ntemps, )
        n_po = 1 / (np.exp(self.pop_frequency /
                           (BOLTZMANN * amset_data.temperatures)) - 1)

        self.n_po = n_po[None, :]

        log_list([
            "average N_po: {:.4f}".format(np.mean(n_po)),
            "ω_po: {:.4g} 2π THz".format(self.properties["pop_frequency"] * 2 *
                                         np.pi),
            "ħω: {:.4f} eV".format(self.pop_frequency * hbar * Second),
        ])

        # want to store two intermediate properties for:
        #             emission      and        absorption
        # (1-f)(N_po + 1) + f(N_po) and (1-f)N_po + f(N_po + 1)
        # note that these are defined for the scattering rate S(k', k).
        # For the rate S(k, k') the definitions are reversed.

        # self.emission_f_out = {
        #     s: n_po + 1 - amset_data.f[s]
        #     for s in amset_data.spins}
        # self.absorption_f_out = {
        #     s: n_po + amset_data.f[s]
        #     for s in amset_data.spins}
        #
        # self.emission_f_in = {
        #     s: n_po + amset_data.f[s]
        #     for s in amset_data.spins}
        # self.absorption_f_in = {
        #     s: n_po + 1 - amset_data.f[s]
        #     for s in amset_data.spins}
        dielectric_term = (4 * np.pi *
                           (1 / self.properties["high_frequency_dielectric"] -
                            1 / self.properties["static_dielectric"]))

        # don't need Second conversion as we pop_frequency is not in atomic units anyway
        self._prefactor = (Second * self.pop_frequency * dielectric_term /
                           (8 * np.pi**2))
Example #10
0
def _log_band_structure_information(band_structure: BandStructure):
    log_banner("BAND STRUCTURE")

    info = [
        "# bands: {}".format(band_structure.nb_bands),
        "# k-points: {}".format(len(band_structure.kpoints)),
        "Fermi level: {:.3f} eV".format(band_structure.efermi),
        "spin polarized: {}".format(band_structure.is_spin_polarized),
        "metallic: {}".format(band_structure.is_metal()),
    ]
    logger.info("Input band structure information:")
    log_list(info)

    if band_structure.is_metal():
        return

    logger.info("Band gap:")
    band_gap_info = []

    bg_data = band_structure.get_band_gap()
    if not bg_data["direct"]:
        band_gap_info.append("indirect band gap: {:.3f} eV".format(
            bg_data["energy"]))

    direct_data = band_structure.get_direct_band_gap_dict()
    direct_bg = min((spin_data["value"] for spin_data in direct_data.values()))
    band_gap_info.append("direct band gap: {:.3f} eV".format(direct_bg))

    direct_kpoint = []
    for spin, spin_data in direct_data.items():
        direct_kindex = spin_data["kpoint_index"]
        kpt_str = _kpt_str.format(
            k=band_structure.kpoints[direct_kindex].frac_coords)
        direct_kpoint.append(kpt_str)

    band_gap_info.append("direct k-point: {}".format(", ".join(direct_kpoint)))
    log_list(band_gap_info)

    vbm_data = band_structure.get_vbm()
    cbm_data = band_structure.get_cbm()

    logger.info("Valence band maximum:")
    _log_band_edge_information(band_structure, vbm_data)

    logger.info("Conduction band minimum:")
    _log_band_edge_information(band_structure, cbm_data)
Example #11
0
    def calculate_dos(
        self,
        estep: float = defaults["dos_estep"],
        progress_bar: bool = defaults["print_log"]
    ):
        """
        Args:
            estep: The DOS energy step in eV, where smaller numbers give more
                accuracy but are more expensive.
            progress_bar: Show a progress bar for DOS calculation.
        """
        emin = np.min([np.min(spin_eners) for spin_eners in self.energies.values()])
        emax = np.max([np.max(spin_eners) for spin_eners in self.energies.values()])
        epoints = int(round((emax - emin) / (estep * units.eV)))
        energies = np.linspace(emin, emax, epoints)
        dos_weight = 1 if self._soc or len(self.spins) == 2 else 2

        logger.debug("DOS parameters:")
        log_list(
            [
                "emin: {:.2f} eV".format(emin / units.eV),
                "emax: {:.2f} eV".format(emax / units.eV),
                "dos weight: {}".format(dos_weight),
                "n points: {}".format(epoints),
            ]
        )

        logger.debug("Generating tetrahedral DOS:")
        t0 = time.perf_counter()
        emesh, dos = self.tetrahedral_band_structure.get_density_of_states(
            energies=energies, progress_bar=progress_bar
        )
        log_time_taken(t0)

        num_electrons = self.num_electrons if self.is_metal else None

        self.dos = FermiDos(
            self.intrinsic_fermi_level,
            emesh,
            dos,
            self.structure,
            atomic_units=True,
            dos_weight=dos_weight,
            num_electrons=num_electrons,
        )
Example #12
0
    def __init__(self, materials_properties: Dict[str, Any], amset_data: AmsetData):
        super().__init__(materials_properties, amset_data)
        from amset.log import log_list
        from amset.constants import bohr_to_cm

        self._rlat = amset_data.structure.lattice.reciprocal_lattice.matrix

        logger.debug("Initializing IMP scattering")

        self.inverse_screening_length_sq = calculate_inverse_screening_length_sq(
            amset_data, self.properties["static_dielectric"]
        )
        impurity_concentration = np.zeros(amset_data.fermi_levels.shape)

        imp_info = []
        for n, t in np.ndindex(self.inverse_screening_length_sq.shape):
            n_conc = np.abs(amset_data.electron_conc[n, t])
            p_conc = np.abs(amset_data.hole_conc[n, t])

            impurity_concentration[n, t] = (
                n_conc * self.properties["donor_charge"] ** 2
                + p_conc * self.properties["acceptor_charge"] ** 2
            )
            imp_info.append(
                "{:3.2e} cm⁻³ & {} K: β² = {:4.3e} a₀⁻², Nᵢᵢ = {:4.3e} cm⁻³".format(
                    amset_data.doping[n] * (1 / bohr_to_cm) ** 3,
                    amset_data.temperatures[t],
                    self.inverse_screening_length_sq[n, t],
                    impurity_concentration[n, t] * (1 / bohr_to_cm) ** 3,
                )
            )

        logger.debug(
            "Inverse screening length (β) and impurity concentration " "(Nᵢᵢ):"
        )
        log_list(imp_info, level=logging.DEBUG)

        self._prefactor = (
            impurity_concentration
            * (4 * np.pi) ** 2
            * units.Second
            / self.properties["static_dielectric"] ** 2
        )
Example #13
0
def _log_settings(runner: Runner):
    from pymatgen.core.tensors import Tensor

    def ff(prop):
        # format tensor properties
        if isinstance(prop, np.ndarray):
            if prop.shape == (3, 3):
                return _tensor_str.format(*prop.ravel())
            elif prop.shape == (3, 3, 3):
                return _piezo_tensor_str.format(*Tensor(prop).voigt.ravel())
            elif prop.shape == (3, 3, 3, 3):
                return _elastic_tensor_str.format(*Tensor(prop).voigt.ravel())

        return prop

    log_banner("SETTINGS")
    logger.info("Run parameters:")
    p = ["{}: {}".format(k, ff(v)) for k, v in runner.settings.items() if v is not None]
    log_list(p)
Example #14
0
    def __init__(self, materials_properties: Dict[str, Any],
                 amset_data: AmsetData):
        super().__init__(materials_properties, amset_data)
        logger.info("Initializing POP scattering")

        # convert from THz to angular frequency in Hz
        self.pop_frequency = (self.properties["pop_frequency"] * 1e12 * 2 *
                              np.pi / s_to_au)

        # n_po (phonon concentration) has shape (ntemps, )
        n_po = 1 / (np.exp(self.pop_frequency /
                           (boltzmann_au * amset_data.temperatures)) - 1)

        self.n_po = n_po[None, :]

        log_list([
            "average N_po: {:.4f}".format(np.mean(n_po)),
            "ω_po: {:.4g} 2π THz".format(self.properties["pop_frequency"] * 2 *
                                         np.pi),
            "ħω: {:.4f} eV".format(self.pop_frequency * hbar * s_to_au),
        ])

        # want to store two intermediate properties for:
        #             emission      and        absorption
        # (1-f)(N_po + 1) + f(N_po) and (1-f)N_po + f(N_po + 1)
        # note that these are defined for the scattering rate S(k', k).
        # For the rate S(k, k') the definitions are reversed.

        # self.emission_f_out = {
        #     s: n_po + 1 - amset_data.f[s]
        #     for s in amset_data.spins}
        # self.absorption_f_out = {
        #     s: n_po + amset_data.f[s]
        #     for s in amset_data.spins}
        #
        # self.emission_f_in = {
        #     s: n_po + amset_data.f[s]
        #     for s in amset_data.spins}
        # self.absorption_f_in = {
        #     s: n_po + 1 - amset_data.f[s]
        #     for s in amset_data.spins}
        self._prefactor = s_to_au * self.pop_frequency / 2
Example #15
0
    def get_amset_data(
        self,
        energy_cutoff: Optional[float] = None,
        scissor: float = None,
        bandgap: float = None,
        symprec: float = defaults["symprec"],
        nworkers: int = defaults["nworkers"],
    ) -> AmsetData:
        """Gets an AmsetData object using the interpolated bands.

        Note, the interpolation mesh is determined using by
        ``interpolate_factor`` option in the ``Inteprolater`` constructor.

        This method is much faster than the ``get_energies`` function but
        doesn't provide as much flexibility.

        The degree of parallelization is controlled by the ``nworkers`` option.

        Args:
            energy_cutoff: The energy cut-off to determine which bands are
                included in the interpolation. If the energy of a band falls
                within the cut-off at any k-point it will be included. For
                metals the range is defined as the Fermi level ± energy_cutoff.
                For gapped materials, the energy range is from the VBM -
                energy_cutoff to the CBM + energy_cutoff.
            scissor: The amount by which the band gap is scissored. Cannot
                be used in conjunction with the ``bandgap`` option. Has no
                effect for metallic systems.
            bandgap: Automatically adjust the band gap to this value. Cannot
                be used in conjunction with the ``scissor`` option. Has no
                effect for metallic systems.
            symprec: The symmetry tolerance used when determining the symmetry
                inequivalent k-points on which to interpolate.
            nworkers: The number of processors used to perform the
                interpolation. If set to ``-1``, the number of workers will
                be set to the number of CPU cores.

        Returns:
            The electronic structure (including energies, velocities, density of
            states and k-point information) as an AmsetData object.
        """
        is_metal = self._band_structure.is_metal()

        if is_metal and (bandgap or scissor):
            raise ValueError("{} option set but system is metallic".format(
                "bandgap" if bandgap else "scissor"))

        nworkers = multiprocessing.cpu_count() if nworkers == -1 else nworkers

        logger.info("Interpolation parameters:")
        iinfo = [
            "k-point mesh: {}".format("x".join(
                map(str, self.interpolation_mesh))),
            "energy cutoff: {} eV".format(energy_cutoff),
        ]
        log_list(iinfo)

        ibands = get_ibands(energy_cutoff, self._band_structure)
        new_vb_idx = get_vb_idx(energy_cutoff, self._band_structure)

        energies = {}
        vvelocities = {}
        velocities = {}
        forgotten_electrons = 0
        for spin in self._spins:
            spin_ibands = ibands[spin]
            min_b = spin_ibands.min() + 1
            max_b = spin_ibands.max() + 1
            info = "Interpolating {} bands {}-{}".format(
                spin_name[spin], min_b, max_b)
            logger.info(info)

            # these are bands beneath the Fermi level that are dropped
            forgotten_electrons += min_b - 1

            t0 = time.perf_counter()
            energies[spin], vvelocities[spin], _, velocities[
                spin] = get_bands_fft(
                    self._equivalences,
                    self._coefficients[spin][spin_ibands],
                    self._lattice_matrix,
                    return_effective_mass=False,
                    nworkers=nworkers,
                )
            log_time_taken(t0)

        if not self._soc and len(self._spins) == 1:
            forgotten_electrons *= 2
        nelectrons = self._num_electrons - forgotten_electrons

        if is_metal:
            efermi = self._band_structure.efermi * ev_to_hartree

        else:
            energies = _shift_energies(energies,
                                       new_vb_idx,
                                       scissor=scissor,
                                       bandgap=bandgap)

            # if material is semiconducting, set Fermi level to middle of gap
            efermi = _get_efermi(energies, new_vb_idx)

        # get the actual k-points used in the BoltzTraP2 interpolation
        # unfortunately, BoltzTraP2 doesn't expose this information so we
        # have to get it ourselves
        (
            ir_kpts,
            _,
            full_kpts,
            ir_kpts_idx,
            ir_to_full_idx,
            tetrahedra,
            *ir_tetrahedra_info,
        ) = get_kpoints_tetrahedral(
            self.interpolation_mesh,
            self._band_structure.structure,
            symprec=symprec,
            time_reversal_symmetry=not self._soc,
        )

        energies, vvelocities, velocities = sort_amset_results(
            full_kpts, energies, vvelocities, velocities)
        atomic_structure = get_atomic_structure(self._band_structure.structure)

        return AmsetData(
            atomic_structure,
            energies,
            vvelocities,
            velocities,
            self.interpolation_mesh,
            full_kpts,
            ir_kpts,
            ir_kpts_idx,
            ir_to_full_idx,
            tetrahedra,
            ir_tetrahedra_info,
            efermi,
            nelectrons,
            is_metal,
            self._soc,
            vb_idx=new_vb_idx,
        )
Example #16
0
File: data.py Project: obaica/amset
    def calculate_fd_cutoffs(
        self, fd_tolerance: Optional[float] = 0.01, cutoff_pad: float = 0.0
    ):
        energies = self.dos.energies

        # three fermi integrals govern transport properties:
        #   1. df/de controls conductivity and mobility
        #   2. (e-u) * df/de controls Seebeck
        #   3. (e-u)^2 df/de controls electronic thermal conductivity
        # take the absolute sum of the integrals across all doping and
        # temperatures. this gives us the energies that are important for
        # transport

        weights = np.zeros(energies.shape)
        for n, t in np.ndindex(self.fermi_levels.shape):
            ef = self.fermi_levels[n, t]
            temp = self.temperatures[t]

            dfde = -dFDde(energies, ef, temp * units.BOLTZMANN)
            sigma_int = np.abs(dfde)
            seeb_int = np.abs((energies - ef) * dfde)
            ke_int = np.abs((energies - ef) ** 2 * dfde)

            # normalize the transport integrals and sum
            nt_weights = sigma_int / sigma_int.max()
            nt_weights += seeb_int / seeb_int.max()
            nt_weights += ke_int / ke_int.max()
            weights = np.maximum(weights, nt_weights)

        if not self.is_metal:
            # weights should be zero in the band gap as there will be no density
            vb_bands = [
                np.max(self.energies[s][: self.vb_idx[s] + 1]) for s in self.spins
            ]
            cb_bands = [
                np.min(self.energies[s][self.vb_idx[s] + 1 :]) for s in self.spins
            ]
            vbm_e = np.max(vb_bands)
            cbm_e = np.min(cb_bands)
            weights[(energies > vbm_e) & (energies < cbm_e)] = 0

        weights /= np.max(weights)
        cumsum = np.cumsum(weights)
        cumsum /= np.max(cumsum)

        if fd_tolerance:
            min_cutoff = energies[cumsum < fd_tolerance / 2].max()
            max_cutoff = energies[cumsum > 1 - fd_tolerance / 2].min()
        else:
            min_cutoff = energies.min()
            max_cutoff = energies.max()

        min_cutoff -= cutoff_pad
        max_cutoff += cutoff_pad

        logger.info("Calculated Fermi–Dirac cut-offs:")
        log_list(
            [
                "min: {:.3f} eV".format(min_cutoff / units.eV),
                "max: {:.3f} eV".format(max_cutoff / units.eV),
            ]
        )
        self.fd_cutoffs = (min_cutoff, max_cutoff)
Example #17
0
    def calculate_fd_cutoffs(
        self,
        fd_tolerance: Optional[float] = 0.01,
        cutoff_pad: float = 0.0,
        max_moment: int = 2,
        mobility_rates_only: bool = False,
    ):
        energies = self.dos.energies
        vv = {
            s: v.transpose((0, 3, 1, 2))
            for s, v in self.velocities_product.items()
        }
        _, vvdos = self.tetrahedral_band_structure.get_density_of_states(
            energies, integrand=vv, sum_spins=True, use_cached_weights=True)
        vvdos = tensor_average(vvdos)
        # vvdos = np.array(self.dos.get_densities())

        # three fermi integrals govern transport properties:
        #   1. df/de controls conductivity and mobility
        #   2. (e-u) * df/de controls Seebeck
        #   3. (e-u)^2 df/de controls electronic thermal conductivity
        # take the absolute sum of the integrals across all doping and
        # temperatures. this gives us the energies that are important for
        # transport
        if fd_tolerance:

            def get_min_max_cutoff(cumsum):
                min_idx = np.where(cumsum < fd_tolerance / 2)[0].max()
                max_idx = np.where(cumsum > (1 - fd_tolerance / 2))[0].min()
                return energies[min_idx], energies[max_idx]

            min_cutoff = np.inf
            max_cutoff = -np.inf
            for n, t in np.ndindex(self.fermi_levels.shape):
                ef = self.fermi_levels[n, t]
                temp = self.temperatures[t]
                dfde = -dfdde(energies, ef, temp * boltzmann_au)

                for moment in range(max_moment + 1):
                    weight = np.abs((energies - ef)**moment * dfde)
                    weight_dos = weight * vvdos
                    weight_cumsum = np.cumsum(weight_dos)
                    weight_cumsum /= np.max(weight_cumsum)

                    cmin, cmax = get_min_max_cutoff(weight_cumsum)
                    min_cutoff = min(cmin, min_cutoff)
                    max_cutoff = max(cmax, max_cutoff)

                    # import matplotlib.pyplot as plt
                    # ax = plt.gca()
                    # plt.plot(energies / units.eV, weight / weight.max())
                    # plt.plot(energies / units.eV, vvdos / vvdos.max())
                    # plt.plot(energies / units.eV, weight_dos / weight_dos.max())
                    # plt.plot(energies / units.eV, weight_cumsum / weight_cumsum.max())
                    # ax.set(xlim=(4, 7.5))
                    # plt.show()

        else:
            min_cutoff = energies.min()
            max_cutoff = energies.max()

        if mobility_rates_only:
            vbm = get_vbm_energy(self.energies, self.vb_idx)
            cbm = get_cbm_energy(self.energies, self.vb_idx)
            mid_gap = (cbm + vbm) / 2
            if np.all(self.doping < 0):
                # only electron mobility so don't calculate valence band rates
                min_cutoff = max(min_cutoff, mid_gap)
            elif np.all(self.doping < 0):
                # only hole mobility so don't calculate conudction band rates
                max_cutoff = min(max_cutoff, mid_gap)

        min_cutoff -= cutoff_pad
        max_cutoff += cutoff_pad

        logger.info("Calculated Fermi–Dirac cut-offs:")
        log_list([
            "min: {:.3f} eV".format(min_cutoff * hartree_to_ev),
            "max: {:.3f} eV".format(max_cutoff * hartree_to_ev),
        ])
        self.fd_cutoffs = (min_cutoff, max_cutoff)
Example #18
0
def expand_kpoints(
    structure,
    kpoints,
    symprec=defaults["symprec"],
    return_mapping=False,
    time_reversal=True,
    verbose=True,
):
    if verbose:
        logger.info("Desymmetrizing k-point mesh")

    kpoints = np.array(kpoints).round(8)

    # due to limited input precision of the k-points, the mesh is returned as a float
    mesh, is_shifted = get_mesh_from_kpoint_diff(kpoints)
    status_info = [
        "Found initial mesh: {:.3f} x {:.3f} x {:.3f}".format(*mesh)
    ]

    if is_shifted:
        shift = np.array([1, 1, 1])
    else:
        shift = np.array([0, 0, 0])

    # to avoid issues to limited input precision, recalculate the input k-points
    # so that the mesh is integer and the k-points are not truncated
    # to a small precision
    addresses = np.rint((kpoints + shift / (mesh * 2)) * mesh)
    mesh = np.rint(mesh)
    kpoints = addresses / mesh - shift / (mesh * 2)

    status_info.append("Integer mesh: {} x {} x {}".format(*map(int, mesh)))

    rotations, translations, is_tr = get_reciprocal_point_group_operations(
        structure, symprec=symprec, time_reversal=time_reversal)
    n_ops = len(rotations)
    if verbose:
        status_info.append("Using {} symmetry operations".format(n_ops))
        log_list(status_info)

    # rotate all-kpoints
    all_rotated_kpoints = []
    for r in rotations:
        all_rotated_kpoints.append(np.dot(r, kpoints.T).T)
    all_rotated_kpoints = np.concatenate(all_rotated_kpoints)

    # map to first BZ
    all_rotated_kpoints -= np.rint(all_rotated_kpoints)
    all_rotated_kpoints = all_rotated_kpoints.round(8)

    # zone boundary consistent with VASP not with spglib
    all_rotated_kpoints[all_rotated_kpoints == -0.5] = 0.5

    # Find unique points
    unique_rotated_kpoints, unique_idxs = np.unique(all_rotated_kpoints,
                                                    return_index=True,
                                                    axis=0)

    # find integer addresses
    unique_addresses = (unique_rotated_kpoints + shift / (mesh * 2)) * mesh
    unique_addresses -= np.rint(unique_addresses)
    in_uniform_mesh = (np.abs(unique_addresses) < 1e-5).all(axis=1)

    n_mapped = int(np.sum(in_uniform_mesh))
    n_expected = int(np.product(mesh))
    if n_mapped != n_expected:
        raise ValueError("Expected {} points but found {}".format(
            n_expected, n_mapped))

    full_kpoints = unique_rotated_kpoints[in_uniform_mesh]
    full_idxs = unique_idxs[in_uniform_mesh]

    if not return_mapping:
        return full_kpoints

    op_mapping = np.floor(full_idxs / len(kpoints)).astype(int)
    kp_mapping = (full_idxs % len(kpoints)).astype(int)

    return full_kpoints, rotations, translations, is_tr, op_mapping, kp_mapping
Example #19
0
    def get_energies(
        self,
        kpoints: Union[np.ndarray, List],
        energy_cutoff: Optional[float] = None,
        scissor: float = None,
        bandgap: float = None,
        return_velocity: bool = False,
        return_curvature: bool = False,
        return_other_properties: bool = False,
        coords_are_cartesian: bool = False,
        atomic_units: bool = False,
        symprec: Optional[float] = defaults["symprec"],
        return_efermi: bool = False,
        return_vb_idx: bool = False,
    ) -> Union[Dict[Spin, np.ndarray], Tuple[Dict[Spin, np.ndarray], ...]]:
        """Gets the interpolated energies for multiple k-points in a band.

        Note, the accuracy of the interpolation is dependant on the
        ``interpolate_factor`` used to initialize the Interpolater.

        Args:
            kpoints: The k-point coordinates.
            energy_cutoff: The energy cut-off to determine which bands are
                included in the interpolation. If the energy of a band falls
                within the cut-off at any k-point it will be included. For
                metals the range is defined as the Fermi level ± energy_cutoff.
                For gapped materials, the energy range is from the VBM -
                energy_cutoff to the CBM + energy_cutoff.
            scissor: The amount by which the band gap is scissored. Cannot
                be used in conjunction with the ``bandgap`` option. Has no
                effect for metallic systems.
            bandgap: Automatically adjust the band gap to this value. Cannot
                be used in conjunction with the ``scissor`` option. Has no
                effect for metallic systems.
            return_velocity: Whether to return the band velocities.
            return_curvature: Whether to return the band curvature (inverse effective
                mass).
            return_other_properties: Whether to return the interpolated results
                for the ``other_properties`` data.
            coords_are_cartesian: Whether the kpoints are in cartesian or
                fractional coordinates.
            atomic_units: Return the energies, velocities, and effective_massses
                in atomic units. If False, energies will be in eV, velocities in
                cm/s, and curvature in units of 1 / electron rest mass (1/m0).
            symprec: Symmetry precision. If set, symmetry will be used to
                reduce the nummber of calculated k-points and velocities.
            return_efermi: Whether to return the Fermi level with the unit
                determined by ``atomic_units``. If the system is semiconducting
                the Fermi level will be given in the middle of the band gap.
            return_vb_idx: Whether to return the index of the highest valence band
                in the interpolated bands. Will be returned as a dictionary of
                ``{spin: vb_idx}``.

        Returns:
            The band energies as dictionary of::

                {spin: energies}

            If ``return_velocity``, ``curvature`` or ``return_other_properties`` a tuple
            is returned, formatted as::

                (energies, Optional[velocities], Optional[curvature],
                 Optional[other_properties])

            The velocities and effective masses are given as the 1x3 trace and
            full 3x3 tensor, respectively (along cartesian directions). The
            projections are summed for each orbital type (s, p, d) across all
            atoms, and are given as::

                {spin: {orbital: projections}}
        """
        if self._band_structure.is_metal() and (bandgap or scissor):
            raise ValueError("{} option set but system is metallic".format(
                "bandgap" if bandgap else "scissor"))

        # only calculate the energies for the bands within the energy cutoff
        lattice = self._band_structure.structure.lattice
        kpoints = np.asarray(kpoints)
        nkpoints = len(kpoints)

        if coords_are_cartesian:
            kpoints = lattice.reciprocal_lattice.get_fractional_coords(kpoints)

        if symprec:
            logger.info("Reducing # k-points using symmetry")
            (
                kpoints,
                weights,
                ir_kpoints_idx,
                ir_to_full_idx,
                _,
                rot_mapping,
            ) = get_symmetry_equivalent_kpoints(
                self._band_structure.structure,
                kpoints,
                symprec=symprec,
                return_inverse=True,
                time_reversal_symmetry=not self._soc,
            )
            k_info = [
                "# original k-points: {}".format(nkpoints),
                "# reduced k-points {}".format(len(kpoints)),
            ]
            log_list(k_info)

        ibands = get_ibands(energy_cutoff, self._band_structure)
        new_vb_idx = get_vb_idx(energy_cutoff, self._band_structure)

        energies = {}
        velocities = {}
        curvature = {}
        other_properties = defaultdict(dict)
        for spin in self._spins:
            spin_ibands = ibands[spin]
            min_b = spin_ibands.min() + 1
            max_b = spin_ibands.max() + 1
            info = "Interpolating {} bands {}-{}".format(
                spin_name[spin], min_b, max_b)
            logger.info(info)

            t0 = time.perf_counter()
            fitted = fite.getBands(
                kpoints,
                self._equivalences,
                self._lattice_matrix,
                self._coefficients[spin][spin_ibands],
                curvature=return_curvature,
            )
            log_time_taken(t0)

            energies[spin] = fitted[0]
            velocities[spin] = fitted[1]

            if return_curvature:
                # make curvature have the shape ((nbands, nkpoints, 3, 3)
                curvature[spin] = fitted[2]
                curvature[spin] = curvature[spin].transpose((2, 3, 0, 1))

            if return_other_properties:
                logger.info("Interpolating {} properties".format(
                    spin_name[spin]))

                t0 = time.perf_counter()
                for label, coeffs in self._other_coefficients[spin].items():
                    other_properties[spin][label], _ = fite.getBands(
                        kpoints,
                        self._equivalences,
                        self._lattice_matrix,
                        coeffs[spin_ibands],
                        curvature=False,
                    )

                log_time_taken(t0)

            if not atomic_units:
                energies[spin] = energies[spin] * hartree_to_ev
                velocities[spin] = _convert_velocities(velocities[spin],
                                                       lattice.matrix)

        if symprec:
            energies, velocities, curvature, other_properties = symmetrize_results(
                energies,
                velocities,
                curvature,
                other_properties,
                ir_to_full_idx,
                rot_mapping,
                self._band_structure.structure.lattice.reciprocal_lattice.
                matrix,
            )

        if not self._band_structure.is_metal():
            energies = _shift_energies(energies,
                                       new_vb_idx,
                                       scissor=scissor,
                                       bandgap=bandgap)

        to_return = [energies]

        if return_velocity:
            to_return.append(velocities)

        if return_curvature:
            to_return.append(curvature)

        if return_other_properties:
            to_return.append(other_properties)

        if return_efermi:
            if self._band_structure.is_metal():
                efermi = self._band_structure.efermi
                if atomic_units:
                    efermi *= ev_to_hartree
            else:
                # if semiconducting, set Fermi level to middle of gap
                efermi = _get_efermi(energies, new_vb_idx)

            to_return.append(efermi)

        if return_vb_idx:
            to_return.append(new_vb_idx)

        if len(to_return) == 1:
            return to_return[0]
        else:
            return tuple(to_return)
Example #20
0
    def __init__(self, materials_properties: Dict[str, Any],
                 amset_data: AmsetData):
        # this is similar to the full IMP scattering, except for the prefactor
        # which follows the simplified BH formula
        super().__init__(materials_properties, amset_data)
        logger.debug("Initializing IMP (Brooks–Herring) scattering")

        inverse_screening_length_sq = calculate_inverse_screening_length_sq(
            amset_data, self.properties["static_dielectric"])
        impurity_concentration = np.zeros(amset_data.fermi_levels.shape)

        imp_info = []
        for n, t in np.ndindex(inverse_screening_length_sq.shape):
            n_conc = np.abs(amset_data.electron_conc[n, t])
            p_conc = np.abs(amset_data.hole_conc[n, t])

            impurity_concentration[n, t] = (
                n_conc * self.properties["donor_charge"]**2 +
                p_conc * self.properties["acceptor_charge"]**2)
            imp_info.append(
                "{:3.2e} cm⁻³ & {} K: β² = {:4.3e} a₀⁻², Nᵢᵢ = {:4.3e} cm⁻³".
                format(
                    amset_data.doping[n] * (1 / bohr_to_cm)**3,
                    amset_data.temperatures[t],
                    inverse_screening_length_sq[n, t],
                    impurity_concentration[n, t] * (1 / bohr_to_cm)**3,
                ))

        logger.debug("Inverse screening length (β) and impurity concentration "
                     "(Nᵢᵢ):")
        log_list(imp_info, level=logging.DEBUG)

        # normalized energies has shape (nspins, ndoping, ntemps, nbands, n_ir_kpoints)
        normalized_energies = get_normalized_energies(amset_data)

        # dos effective masses has shape (nspins, nbands, n_ir_kpoints)
        dos_effective_masses = get_dos_effective_masses(amset_data)

        # screening has shape (ndoping, nbands, 1, 1)
        screening = inverse_screening_length_sq[..., None, None]

        prefactor = (impurity_concentration * 8 * np.pi * Second /
                     self.properties["static_dielectric"]**2)

        ir_kpoints_idx = amset_data.ir_kpoints_idx
        ir_to_full_idx = amset_data.ir_to_full_kpoint_mapping
        self._rates = {}
        for spin in self.spins:
            masses = np.tile(
                dos_effective_masses[spin],
                (len(self.doping), len(self.temperatures), 1, 1),
            )
            energies = normalized_energies[spin]

            k_sq = 2 * masses * energies
            vvelocities = amset_data.velocities_product[spin][...,
                                                              ir_kpoints_idx]
            v = np.sqrt(np.diagonal(vvelocities, axis1=1, axis2=2))
            v = np.linalg.norm(v, axis=2) / np.sqrt(3)
            v[v < 0.005] = 0.005

            velocities = np.tile(
                v, (len(self.doping), len(self.temperatures), 1, 1))
            c = np.tile(
                amset_data.c_factor[spin][:, ir_kpoints_idx],
                (len(self.doping), len(self.temperatures), 1, 1),
            )

            d_factor = (1 + (2 * screening * c**2 / k_sq) +
                        (3 * screening**2 * c**4 / (4 * k_sq**2)))
            b_factor = (
                (4 * k_sq / screening) / (1 + 4 * k_sq / screening) +
                (8 * c**2 * (screening + 2 * k_sq) / (screening + 4 * k_sq)) +
                (c**4 *
                 (3 * screening**2 + 6 * screening * k_sq - 8 * k_sq**2) /
                 ((screening + 4 * k_sq) * k_sq)))

            b = (4 * k_sq) / screening

            self._rates[spin] = (prefactor[:, :, None, None] *
                                 (d_factor * np.log(1 + b) - b_factor) /
                                 (velocities * k_sq))

            # these will be interpolated
            self._rates[spin][np.isnan(self.rates[spin])] = 0
            self.rates[spin][energies < 1e-8] = 0
            self.rates[spin] = self.rates[spin][..., ir_to_full_idx]