Example #1
0
File: run.py Project: gmp007/amset
    def run(self,
            directory: Union[str, Path] = '.',
            prefix: Optional[str] = None,
            return_usage_stats: bool = False):
        mem_usage, (amset_data,
                    usage_stats) = memory_usage(partial(self._run_wrapper,
                                                        directory=directory,
                                                        prefix=prefix),
                                                max_usage=True,
                                                retval=True,
                                                interval=.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[0])])

        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 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.")

        self.doping = doping
        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)))

        logger.info("Calculated Fermi levels:")

        fermi_level_info = []
        for n, t in np.ndindex(self.fermi_levels.shape):
            # do minus -c as FermiDos treats negative concentrations as electron
            # doping and +ve as hole doping (the opposite to amset).
            self.fermi_levels[n, t], self.electron_conc[n, t], \
                self.hole_conc[n, t] = self.dos.get_fermi(
                    -doping[n], temperatures[t], rtol=1e-4, precision=10,
                    return_electron_hole_conc=True)

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

        log_list(fermi_level_info)
        self._calculate_fermi_functions()
Example #3
0
File: run.py Project: gmp007/amset
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)

    log_list([
        "energy: {:.3f} eV".format(edge_data['energy']),
        "k-point: {}".format(kpoint_str), "band indices: {}".format(b_indices)
    ])
Example #4
0
    def calculate_scattering_rates(self):
        spins = self.amset_data.spins
        full_kpoints = self.amset_data.full_kpoints

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

        if self.use_symmetry:
            nkpoints = len(self.amset_data.ir_kpoints_idx)
        else:
            nkpoints = len(full_kpoints)

        if len(self.amset_data.full_kpoints) > 5e5:
            batch_size = 20
        else:
            batch_size = 80

        nsplits = math.ceil(nkpoints / batch_size)
        logger.info("Scattering information:")
        log_list([
            "energy tolerance: {} eV".format(self.gauss_width),
            "# k-points: {}".format(nkpoints),
            "batch size: {}".format(batch_size)
        ])

        for spin in spins:
            for b_idx in range(len(self.amset_data.energies[spin])):
                logger.info("Calculating rates for {} band {}".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, nsplits)

                log_list([
                    "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)
                ])

        # 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, full_kpoints, masks)

        return rates
Example #5
0
    def densify(
        self,
        target_de: float = idefaults["fine_mesh_de"],
        symprec: float = pdefaults["symprec"],
    ):
        densify_info = ["fine mesh de: {} eV".format(target_de)]

        if self._minimum_dim is not None:
            dim_str = _dim_str.format(*self._minimum_dim)
            densify_info.append("minimum dim for IMP: {}".format(dim_str))

        logger.info("Densifying band structure around Fermi integrals")
        log_list(densify_info)

        additional_kpoints, _ = self.get_fine_mesh(
            target_de=target_de, minimum_dim=self._minimum_dim)

        # have to use amset_data scissor and not the user specified band gap,
        # as there is no guarantee that the extra k-points will go through the
        # CBM & VBM
        energies, vvelocities, curvature, projections, mapping_info = self._interpolater.get_energies(
            additional_kpoints,
            energy_cutoff=self._energy_cutoff,
            scissor=self._amset_data.scissor / units.eV,
            return_velocity=True,
            return_curvature=True,
            return_projections=True,
            atomic_units=True,
            return_vel_outer_prod=True,
            return_kpoint_mapping=True,
            symprec=symprec,
        )

        old_mapping = self._amset_data.ir_to_full_kpoint_mapping
        new_mapping = mapping_info["ir_to_full_idx"] + old_mapping.max() + 1
        ir_to_full_kpoint_mapping = np.concatenate((old_mapping, new_mapping))

        voronoi = PeriodicVoronoi(
            self._amset_data.structure.lattice.reciprocal_lattice,
            self._amset_data.full_kpoints,
            self._amset_data.kpoint_mesh,
            additional_kpoints,
            ir_to_full_idx=ir_to_full_kpoint_mapping,
            extra_ir_points_idx=mapping_info["ir_kpoints_idx"],
        )
        kpoint_weights = voronoi.compute_volumes()

        # note k-point weights is for all k-points, whereas the other properties
        # are just for the additional k-points
        return (
            additional_kpoints,
            energies,
            vvelocities,
            projections,
            kpoint_weights,
            mapping_info["ir_kpoints_idx"],
            mapping_info["ir_to_full_idx"],
            curvature,
        )
Example #6
0
def _log_settings(runner: AmsetRunner):
    log_banner("SETTINGS")

    logger.info("Run parameters:")
    run_params = [
        "doping: {}".format(", ".join(map("{:g}".format, runner.doping))),
        "temperatures: {}".format(", ".join(map(str, runner.temperatures))),
        "interpolation_factor: {}".format(runner.interpolation_factor),
        "scattering_type: {}".format(runner.scattering_type),
        "soc: {}".format(runner.soc)
    ]

    if runner.user_bandgap:
        run_params.append("bandgap: {}".format(runner.user_bandgap))

    if runner.scissor:
        run_params.append("scissor: {}".format(runner.scissor))

    log_list(run_params)

    logger.info("Performance parameters:")
    log_list([
        "{}: {}".format(k, v)
        for k, v in runner.performance_parameters.items()
    ])

    logger.info("Output parameters:")
    log_list(
        ["{}: {}".format(k, v) for k, v in runner.output_parameters.items()])

    logger.info("Material properties:")
    log_list([
        "{}: {}".format(k, v) for k, v in runner.material_properties.items()
        if v is not None
    ])
Example #7
0
File: run.py Project: gmp007/amset
def _log_band_structure_information(band_structure: BandStructure):
    log_banner("BAND STRUCTURE")

    logger.info("Input band structure information:")
    log_list([
        "# 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())
    ])

    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']
        direct_kpoint.append(
            _kpt_str.format(
                k=band_structure.kpoints[direct_kindex].frac_coords))

    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 #8
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

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

        n_po = n_po[None, :, None, None]

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

        # 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}

        unit_conversion = 1e9 / e
        self._prefactor = unit_conversion * (
                e ** 2 * self.pop_frequency / (8 * np.pi ** 2) *
                (1 / self.properties["high_frequency_dielectric"] -
                 1 / self.properties["static_dielectric"]) / epsilon_0)
Example #9
0
    def calculate_dos(self,
                      dos_estep: float = defaults["performance"]["dos_estep"],
                      dos_width: float = defaults["performance"]["dos_width"]):
        """
        Args:
            dos_estep: The DOS energy step, where smaller numbers give more
                accuracy but are more expensive.
            dos_width: The DOS gaussian smearing width in eV.
        """

        all_energies = np.vstack([self.energies[spin] for spin in self.spins])
        all_energies /= units.eV  # convert from Hartree to eV for DOS

        # add a few multiples of dos_width to emin and emax to account for tails
        pad = dos_width if dos_width else 0
        dos_emin = np.min(all_energies) - pad * 5
        dos_emax = np.max(all_energies) + pad * 5
        npts = int(round((dos_emax - dos_emin) / dos_estep))

        logger.debug("DOS parameters:")
        log_list(["emin: {:.2f} eV".format(dos_emin),
                  "emax: {:.2f} eV".format(dos_emax),
                  "broadening width: {} eV".format(dos_width)])

        emesh, densities = DOS(all_energies.T, erange=(dos_emin, dos_emax),
                               npts=npts)

        if dos_width:
            densities = gaussian_filter1d(densities, dos_width /
                                          (emesh[1] - emesh[0]))

        # integrate up to Fermi level to get number of electrons
        efermi = self._efermi / units.eV
        energy_mask = emesh <= efermi + pad
        nelect = scipy.trapz(densities[energy_mask], emesh[energy_mask])

        logger.debug("Intrinsic DOS Fermi level: {:.4f}".format(efermi))
        logger.debug("DOS contains {:.3f} electrons".format(nelect))

        dos = Dos(efermi, emesh, {Spin.up: densities})
        self.dos_weight = 1 if self._soc or len(self.spins) == 2 else 2
        self.dos = FermiDos(dos, structure=self.structure,
                            dos_weight=self.dos_weight)
Example #10
0
    def __init__(self, materials_properties: Dict[str, Any],
                 amset_data: AmsetData):
        super().__init__(materials_properties, amset_data)
        logger.debug("Initializing IMP scattering")

        self.beta_sq = np.zeros(amset_data.fermi_levels.shape)
        self.impurity_concentration = np.zeros(amset_data.fermi_levels.shape)

        tdos = amset_data.dos.tdos
        energies = amset_data.dos.energies
        fermi_levels = amset_data.fermi_levels
        vol = amset_data.structure.volume

        imp_info = []
        for n, t in np.ndindex(self.beta_sq.shape):
            ef = fermi_levels[n, t]
            temp = amset_data.temperatures[t]
            f = f0(energies, ef, temp)
            integral = trapz(tdos * f * (1 - f), x=energies)
            self.beta_sq[n, t] = (e**2 * integral * 1e12 /
                                  (self.properties["static_dielectric"] *
                                   epsilon_0 * k_B * temp * e * vol))

            n_conc = np.abs(amset_data.electron_conc[n, t])
            p_conc = np.abs(amset_data.hole_conc[n, t])

            self.impurity_concentration[n, t] = (
                n_conc * self.properties["donor_charge"]**2 +
                p_conc * self.properties["acceptor_charge"]**2)
            imp_info.append(
                "{:.2g} cm⁻³ & {} K: β² = {:.4g}, Nᵢᵢ = {:.4g}".format(
                    amset_data.doping[n], temp, self.beta_sq[n, t],
                    self.impurity_concentration[n, t]))

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

        self._prefactor = ((1e-3 /
                            (e**2)) * e**4 * self.impurity_concentration /
                           (4.0 * np.pi**2 * epsilon_0**2 * hbar *
                            self.properties["static_dielectric"]**2))
Example #11
0
File: run.py Project: gmp007/amset
def _log_structure_information(structure: Structure, symprec):
    log_banner("STRUCTURE")
    logger.info("Structure information:")

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

    if not symprec:
        symprec = 0.01

    sga = SpacegroupAnalyzer(structure, symprec=symprec)
    log_list([
        "formula: {}".format(unicodeify(formula)),
        "# sites: {}".format(structure.num_sites), "space group: {}".format(
            unicodeify_spacegroup(sga.get_space_group_symbol()))
    ])

    logger.info("Lattice:")
    log_list([
        "a, b, c [Å]: {:.2f}, {:.2f}, {:.2f}".format(*structure.lattice.abc),
        "α, β, γ [°]: {:.0f}, {:.0f}, {:.0f}".format(*structure.lattice.angles)
    ])
Example #12
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_projections: bool = False,
        return_vel_outer_prod: bool = False,
        coords_are_cartesian: bool = False,
        atomic_units: bool = False,
        skip_coefficients: Optional[float] = None,
        symprec: Optional[float] = None,
        return_kpoint_mapping: bool = False,
        return_efermi: bool = False,
        return_vb_idx: bool = False,
        return_scissor: 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_projections: Whether to return the interpolated projections.
            return_vel_outer_prod: Whether to return the outer product of
                velocity, as used by BoltzTraP2 to calculate transport
                properties.
            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_kpoint_mapping: If `True`, the kpoint symmetry mapping information
                will be returned. If ``symprec`` is None then all sites will be
                considered symmetry inequivalent.
            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}``.
            return_scissor: Whether to return the determined scissor value, given in
                Hartree.

        Returns:
            The band energies as dictionary of::

                {spin: energies}

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

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

            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"))

        if not self._interpolate_projections and return_projections:
            raise ValueError(
                "Band structure projections needed to obtain full "
                "electronic structure. Reinitialise the "
                "interpolater with interpolate_projections=True")

        n_equivalences = len(self._equivalences)
        if not skip_coefficients or skip_coefficients > 1:
            skip = n_equivalences
        else:
            skip = int(skip_coefficients * n_equivalences)

        # only calculate the energies for the bands within the energy cutoff
        min_e, max_e = _get_energy_cutoffs(energy_cutoff, self._band_structure)
        lattice = self._band_structure.structure.lattice

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

        nkpoints = len(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,
            )
            similarity_matrix = np.array([
                similarity_transformation(lattice.reciprocal_lattice.matrix, r)
                for r in rot_mapping
            ])
            # similarity_matrix = rot_mapping

            inv_similarity_matrix = np.array(
                [np.linalg.inv(s) for s in similarity_matrix])

            log_list([
                "# original k-points: {}".format(nkpoints),
                "# reduced k-points {}".format(len(kpoints)),
            ])
        else:
            kpoints = np.asarray(kpoints)
            nkpoints = kpoints.shape[0]
            ir_kpoints_idx = np.arange(nkpoints)
            ir_to_full_idx = np.arange(nkpoints)
            weights = np.full(nkpoints, 1 / nkpoints)

        energies = {}
        velocities = {}
        curvature = {}
        projections = defaultdict(dict)
        new_vb_idx = {}
        for spin in self._spins:
            bands = self._band_structure.bands[spin]
            ibands = np.any((bands > min_e) & (bands < max_e), axis=1)

            logger.info("Interpolating {} bands {}-{}".format(
                spin_name[spin],
                np.where(ibands)[0].min() + 1,
                np.where(ibands)[0].max() + 1,
            ))

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

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

            if symprec:
                energies[spin] = energies[spin][:, ir_to_full_idx]

                # apply rotation matrices to the velocities at the symmetry
                # reduced k-points, to get the velocities for the full
                # original mesh (this is just the dot product of the velocity
                # and appropriate rotation matrix. The weird ordering of the
                # indices is because the velocities has the shape
                # (3, nbands, nkpoints)
                # velocities[spin] = np.einsum(
                #     "kij,jkl->lij",
                #     velocities[spin][:, :, ir_to_full_idx],
                #     similarity_matrix,
                # )
                velocities[spin] = np.einsum(
                    "jkl,kij->lij",
                    similarity_matrix,
                    velocities[spin][:, :, ir_to_full_idx],
                )

            if not self._band_structure.is_metal():
                vb_idx = max(
                    self._band_structure.get_vbm()["band_index"][spin])

                # need to know the index of the valence band after discounting
                # bands during the interpolation. As ibands is just a list of
                # True/False, we can count the number of Trues included up to
                # and including the VBM to get the new number of valence bands
                new_vb_idx[spin] = sum(ibands[:vb_idx + 1]) - 1

            if return_vel_outer_prod:
                # calculate the outer produce of velocities with itself
                # this code is adapted from BoltzTraP2.fite
                iu0 = np.triu_indices(3)
                il1 = np.tril_indices(3, -1)
                iu1 = np.triu_indices(3, 1)

                velocities[spin] = velocities[spin].transpose((1, 0, 2))
                vvband = np.zeros((len(velocities[spin]), 3, 3, nkpoints))
                vvband[:, iu0[0], iu0[1]] = (velocities[spin][:, iu0[0]] *
                                             velocities[spin][:, iu0[1]])
                vvband[:, il1[0], il1[1]] = vvband[:, iu1[0], iu1[1]]
                velocities[spin] = vvband

            if return_curvature:
                curvature[spin] = fitted[2]

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

                if symprec:
                    curvature[spin] = curvature[spin][:, ir_to_full_idx, ...]
                    new_curvature = np.empty(curvature[spin].shape)
                    for b_idx, k_idx in np.ndindex(curvature[spin].shape[:2]):
                        new_curvature[b_idx, k_idx] = np.dot(
                            inv_similarity_matrix[k_idx],
                            np.dot(curvature[spin][b_idx, k_idx],
                                   similarity_matrix[k_idx]),
                        )
                    curvature[spin] = new_curvature

            if not atomic_units:
                energies[spin] = energies[spin] / units.eV
                velocities[spin] = _convert_velocities(velocities[spin],
                                                       lattice.matrix)

                if return_curvature:
                    curvature[spin] = _convert_curvature(curvature[spin])

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

                t0 = time.perf_counter()
                for label, proj_coeffs in self._projection_coefficients[
                        spin].items():
                    projections[spin][label] = fite.getBands(
                        kpoints,
                        self._equivalences[:skip],
                        self._lattice_matrix,
                        proj_coeffs[ibands, :skip],
                        curvature=False,
                    )[0]

                    if symprec:
                        projections[spin][label] = projections[spin][
                            label][:, ir_to_full_idx]

                log_time_taken(t0)

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

        if not (return_velocity or return_curvature or return_projections
                or return_kpoint_mapping or return_efermi or return_vb_idx
                or return_scissor):
            return energies

        to_return = [energies]

        if return_velocity:
            to_return.append(velocities)

        if return_curvature:
            to_return.append(curvature)

        if return_projections:
            to_return.append(projections)

        if symprec and return_kpoint_mapping:
            to_return.append({
                "weights": weights,
                "ir_kpoints_idx": ir_kpoints_idx,
                "ir_to_full_idx": ir_to_full_idx,
            })

        if return_efermi:
            if self._band_structure.is_metal():
                efermi = self._band_structure.efermi
                if atomic_units:
                    efermi *= units.eV
            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 return_scissor:
            to_return.append(scissor)

        return tuple(to_return)
Example #13
0
    def get_amset_data_from_kpoints(
        self,
        kpoints: Union[np.ndarray, List[int], float, int],
        energy_cutoff: Optional[float] = None,
        scissor: float = None,
        bandgap: float = None,
        symprec: float = 0.01,
    ) -> 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:
            kpoints: The k-points, either provided as a list of k-points (either with
                the shape (nkpoints, 3) or (nkpoints, 4) where the 4th column is the
                k-point weights). Alternatively, the k-points can be specified as a
                1x3 mesh, e.g.,``[6, 6, 6]`` from which the full Gamma centered mesh
                will be computed. Alternatively, if a single value is provided this will
                be treated as a real-space length cutoff and the k-point mesh dimensions
                generated automatically.
            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.

        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()

        mesh_info = []
        if isinstance(kpoints, numeric_types) or isinstance(
                kpoints[0], int_types):
            # k-points is given as a cut-off or mesh
            if isinstance(kpoints, numeric_types):
                kpoints = get_kpoint_mesh(self._band_structure.structure,
                                          kpoints)

            interpolation_mesh = np.asarray(kpoints)
            str_kmesh = "x".join(map(str, kpoints))
            mesh_info.append("k-point mesh: {}".format(str_kmesh))

            _, _, kpoints, _, _ = get_kpoints(
                kpoints,
                self._band_structure.structure,
                symprec=symprec,
                return_full_kpoints=True,
                boltztrap_ordering=False,
            )
            weights = np.full(len(kpoints), 1 / len(kpoints))

        else:
            # the full list of k-points has been specified
            interpolation_mesh = None
            kpoints = np.asarray(kpoints)
            nkpoints = kpoints.shape[0]
            if kpoints.shape[-1] == 4:
                # kpoints have been provided with weights
                weights = kpoints[:, 3]
                kpoints = kpoints[:, :3]
            else:
                logger.warning(
                    "User supplied k-points have no weights... assuming uniform mesh"
                )
                weights = np.full(nkpoints, 1 / nkpoints)
            mesh_info.append(["# user supplied k-points: {}".format(nkpoints)])

        mesh_info.append("energy cutoff: {} eV".format(energy_cutoff))
        logger.info("Interpolation parameters:")
        log_list(mesh_info)

        energies, vvelocities, curvature, projections, mapping_info, efermi, vb_idx, scissor = self.get_energies(
            kpoints,
            energy_cutoff=energy_cutoff,
            scissor=scissor,
            bandgap=bandgap,
            return_velocity=True,
            return_curvature=True,
            return_projections=True,
            atomic_units=True,
            return_vel_outer_prod=True,
            return_kpoint_mapping=True,
            return_efermi=True,
            symprec=symprec,
            return_vb_idx=True,
            return_scissor=True,
        )

        ir_kpoints_idx = mapping_info["ir_kpoints_idx"]
        ir_to_full_idx = mapping_info["ir_to_full_idx"]
        ir_kpoints = kpoints[ir_kpoints_idx]

        return AmsetData(
            self._band_structure.structure,
            energies,
            vvelocities,
            projections,
            interpolation_mesh,
            kpoints,
            ir_kpoints,
            ir_kpoints_idx,
            ir_to_full_idx,
            efermi,
            is_metal,
            self._soc,
            vb_idx=vb_idx,
            scissor=scissor,
            kpoint_weights=weights,
            curvature=curvature,
        )
Example #14
0
    def get_amset_data(
        self,
        energy_cutoff: Optional[float] = None,
        scissor: float = None,
        bandgap: float = None,
        symprec: float = 0.01,
        nworkers: int = -1,
    ) -> 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"))

        if not self._interpolate_projections:
            raise ValueError(
                "Band structure projections needed to obtain full "
                "electronic structure. Reinitialise the "
                "interpolater with interpolate_projections=True")

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

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

        # only calculate the energies for the bands within the energy cutoff
        min_e, max_e = _get_energy_cutoffs(energy_cutoff, self._band_structure)

        energies = {}
        vvelocities = {}
        curvature = {}
        projections = defaultdict(dict)
        new_vb_idx = {}
        for spin in self._spins:
            bands = self._band_structure.bands[spin]
            ibands = np.any((bands > min_e) & (bands < max_e), axis=1)

            logger.info("Interpolating {} bands {}-{}".format(
                spin_name[spin],
                np.where(ibands)[0].min() + 1,
                np.where(ibands)[0].max() + 1,
            ))

            t0 = time.perf_counter()
            energies[spin], vvelocities[spin], curvature[
                spin] = fite.getBTPbands(
                    self._equivalences,
                    self._coefficients[spin][ibands],
                    self._lattice_matrix,
                    curvature=True,
                    nworkers=nworkers,
                )
            log_time_taken(t0)

            if not is_metal:
                # Need the largest VB index. Sometimes the index of the band
                # containing the VBM is not the largest index of all VBs, so
                # find all bands with energies less than the VBM and count that
                # number
                vbm_energy = self._band_structure.get_vbm()["energy"]
                vb_idx = np.any(bands <= vbm_energy, axis=1).sum() - 1

                # need to know the index of the valence band after discounting
                # bands during the interpolation (i.e., if energy_cutoff is
                # set). As ibands is just a list of True/False, we can count the
                # number of Trues up to and including the VBM to get the new
                # number of valence bands
                new_vb_idx[spin] = sum(ibands[:vb_idx + 1]) - 1

            logger.info("Interpolating {} projections".format(spin_name[spin]))
            t0 = time.perf_counter()

            for label, proj_coeffs in self._projection_coefficients[
                    spin].items():
                projections[spin][label] = fite.getBTPbands(
                    self._equivalences,
                    proj_coeffs[ibands],
                    self._lattice_matrix,
                    nworkers=nworkers,
                )[0]

            log_time_taken(t0)

        if is_metal:
            efermi = self._band_structure.efermi * units.eV
            scissor = 0.0
        else:
            energies, scissor = _shift_energies(
                energies,
                new_vb_idx,
                scissor=scissor,
                bandgap=bandgap,
                return_scissor=True,
            )

            # 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 = get_kpoints(
            self.interpolation_mesh,
            self._band_structure.structure,
            symprec=symprec,
            return_full_kpoints=True,
        )

        return AmsetData(
            self._band_structure.structure,
            energies,
            vvelocities,
            projections,
            self.interpolation_mesh,
            full_kpts,
            ir_kpts,
            ir_kpts_idx,
            ir_to_full_idx,
            efermi,
            is_metal,
            self._soc,
            vb_idx=new_vb_idx,
            scissor=scissor,
            # curvature=curvature,
        )
Example #15
0
    def get_fine_mesh(
        self,
        target_de: float = idefaults["fine_mesh_de"],
        minimum_dim: Optional[np.ndarray] = None,
    ):
        mesh_de = np.full(self._grid_kpoints.shape, -1)
        fd_cutoffs = self._amset_data.fd_cutoffs

        if minimum_dim is not None:
            # convert minimum dim into an energy difference for ease of use
            minimum_de = (target_de * units.eV) * minimum_dim
        else:
            minimum_de = None

        for band_energies in self._grid_energies:
            # effectively make a supercell of the energies on the regular grid
            # containing one extra plane of energies per dimension, on either
            # face of the 3D energy mesh
            pad_energies = np.pad(band_energies, 1, "wrap")

            x_diffs = np.abs(np.diff(pad_energies, axis=2))
            y_diffs = np.abs(np.diff(pad_energies, axis=1))
            z_diffs = np.abs(np.diff(pad_energies, axis=0))

            # remove the diffs related to the extra padding
            x_diffs = x_diffs[1:-1, 1:-1, :].astype(float)
            y_diffs = y_diffs[1:-1, :, 1:-1].astype(float)
            z_diffs = z_diffs[:, 1:-1, 1:-1].astype(float)

            #  calculate moving maxes
            x_diff_averages = maximum_filter1d(x_diffs, 2, axis=2)[:, :, 1:]
            y_diff_averages = maximum_filter1d(y_diffs, 2, axis=1)[:, 1:, :]
            z_diff_averages = maximum_filter1d(z_diffs, 2, axis=0)[1:, :, :]

            # stack the averages to get the formatted energy different array
            band_de = np.stack(
                (z_diff_averages, y_diff_averages, x_diff_averages), axis=-1)

            if minimum_de is not None:
                band_de = np.maximum(band_de, minimum_de)

            if fd_cutoffs:
                # if the energies do not lie within the Fermi Dirac cutoffs
                # set the dims to 0 as there is no point of interpolating
                # around these k-points
                mask = (band_energies > fd_cutoffs[0]) & (band_energies <
                                                          fd_cutoffs[1])

                # expand the mask as the Fermi surface defined by the cutoffs
                # may also fall in the volume defined by an adjacent k-point
                # even if the k-point itself is not included in the Fermi
                # surface.
                mask = maximum_filter(mask,
                                      footprint=np.ones((3, 3, 3)),
                                      mode="wrap")

                # set points outside FD cutoffs to -1 so we can filter them later
                band_de[~mask] = np.array([-1, -1, -1])

            # take the dimensions if they are greater than the current dimensions
            mesh_de = np.maximum(mesh_de, band_de)

        # TODO: add test for all zero fine mesh points
        fine_mesh_dims = np.ceil(mesh_de / (target_de * units.eV)).astype(int)
        skip_points = (fine_mesh_dims <= 1).all(axis=3)

        fine_mesh_dims[skip_points] = 1
        fine_mesh_dims[fine_mesh_dims == 0] = 1

        additional_kpoints = []
        interpolated_idxs = []
        kpoint_log = []

        for i, j, k in np.ndindex(self._mesh):
            dim = fine_mesh_dims[i, j, k]

            if (dim == 1).all():
                continue

            grid_point = self._grid_kpoints[i, j, k]
            kpt_str = _kpt_str.format(*grid_point)
            dim_str = _dim_str.format(*dim)
            kpoint_log.append("kpt: {}, mesh: {}".format(kpt_str, dim_str))
            kpoints = get_dense_kpoint_mesh_spglib(dim, spg_order=True)

            # remove [0, 0, 0] as other wise this will overlap with the existing mesh
            kpoints = kpoints[1:]
            kpoints /= self._mesh
            kpoints += grid_point
            additional_kpoints.append(kpoints)
            interpolated_idxs.append(self._idxs[i, j, k])

        if additional_kpoints:
            additional_kpoints = np.concatenate(additional_kpoints)

        interpolated_idxs = np.array(interpolated_idxs)
        n_additional_kpoints = len(additional_kpoints)

        logger.info("Densified {} kpoints with {} extra points".format(
            len(interpolated_idxs), n_additional_kpoints))
        log_list(kpoint_log, level=logging.DEBUG)
        additional_kpoints = kpoints_to_first_bz(additional_kpoints)

        logger.info("Symmetrizing dense k-point mesh")
        # symmetrize the interpolated k-point mesh. This is to make sure we make
        # maximum use of symmetry as any new k-points will effective be free
        # This step is also necessary if fd_cutoffs is true, to avoid the effects
        # of aliasing from the maximum_filter
        additional_kpoints = symmetrize_kpoints(self._amset_data.structure,
                                                additional_kpoints)
        n_sym = len(additional_kpoints) - n_additional_kpoints
        log_list(["{} k-points added in symmetrization".format(n_sym)])

        # we may have added k-points around previously un-densified points, use
        # the symmetry mapping to ensure that interpolated_idxs covers all
        # symmetry equivalent points
        ir_interpolated_idxs = np.unique(
            self._amset_data.ir_to_full_kpoint_mapping[interpolated_idxs])
        interpolated_idxs = np.concatenate(
            self._amset_data.grouped_ir_to_full[ir_interpolated_idxs])

        return kpoints_to_first_bz(additional_kpoints), interpolated_idxs
Example #16
0
    def densify(self,
                num_extra_kpoints: float = gdefaults["num_extra_kpoints"]):
        logger.info("Densifying band structure around Fermi integrals")

        # add additional k-points around the k-points in the existing mesh
        # the number of additional k-points is proportional to the
        # densification weight for that k-point
        factor = num_extra_kpoints / self._sum_weights
        extra_kpoint_counts = {
            s: np.ceil(self._densification_weights[s] * factor).astype(int)
            for s in self._densification_weights}

        # get the number of extra points for each kpoint. Shape of
        # n_points_per_kpoint is (nkpoints, )
        n_points_per_kpoint = np.sum(np.concatenate([
            extra_kpoint_counts[s] for s in extra_kpoint_counts]), axis=0)

        # recalculate the number of points to take into account rounding
        # generally means the number of additional k-points is larger than
        # specified by the user
        total_points = np.sum(n_points_per_kpoint)

        # max distance is just under half the distance to the nearest k-point.
        # Note, the averaging means that this only works well if the original
        # k-point density is isotropic, which will be the case if using
        # BoltzTraP interpolation.
        max_dist = np.average(1 / self._amset_data.kpoint_mesh) / 2.1

        # get the kpoints which will be densified
        k_mask = n_points_per_kpoint > 0
        k_coords = self._amset_data.full_kpoints[k_mask]
        n_points_per_kpoint = n_points_per_kpoint[k_mask]

        # add additional points in concenctric spheres around the k-points
        #
        extra_kpoints = np.concatenate(
            [_generate_points(n_extra, max_dist) + kpoint
             for kpoint, n_extra in zip(k_coords, n_points_per_kpoint)])
        log_list(["# extra kpoints: {}".format(total_points),
                  "max frac k-distance: {:.5f}".format(max_dist)])

        # from monty.serialization import dumpfn
        # dumpfn(extra_kpoints, "extra_kpoints.json")

        extra_kpoints = kpoints_to_first_bz(extra_kpoints)

        skip = 5 / self._interpolater.interpolation_factor
        energies, vvelocities, projections = self._interpolater.get_energies(
            extra_kpoints, energy_cutoff=self._energy_cutoff,
            bandgap=self._bandgap, scissor=self._scissor,
            return_velocity=True, return_effective_mass=False,
            return_projections=True, atomic_units=True,
            return_vel_outer_prod=True, skip_coefficients=skip)

        voronoi = PeriodicVoronoi(
            self._amset_data.structure.lattice.reciprocal_lattice,
            self._amset_data.full_kpoints,
            self._amset_data.kpoint_mesh,
            extra_kpoints)
        kpoint_weights = voronoi.compute_volumes()

        # note k-point weights is for all k-points, whereas the other properties
        # are just for the additional k-points
        return (extra_kpoints, energies, vvelocities, projections,
                kpoint_weights)
Example #17
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(BK) 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(
                "{:.2g} cm⁻³ & {} K: β² = {:.4g} nm⁻², Nᵢᵢ = {:.4g}".format(
                    amset_data.doping[n],
                    amset_data.temperatures[t],
                    inverse_screening_length_sq[n, t],
                    impurity_concentration[n, t],
                )
            )

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

        inv_cm_to_bohr = 100 * physical_constants["Bohr radius"][0]
        inv_nm_to_bohr = 1e9 * physical_constants["Bohr radius"][0]

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

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

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

        prefactor = (
            impurity_concentration
            * inv_cm_to_bohr ** 3
            * 4
            * np.pi
            / self.properties["static_dielectric"] ** 2
        )

        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

            v = np.diagonal(
                np.sqrt(amset_data.velocities_product[spin]), 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],
                (len(self.doping), len(self.temperatures), 1, 1),
            )

            D = (
                1
                + (2 * screening * c ** 2 / k_sq)
                + (3 * screening ** 2 * c ** 4 / (4 * k_sq ** 2))
            )
            B = (
                (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 = (8 * masses * energies) / screening

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