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
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)
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)
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
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)
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, )
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)
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
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))
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)
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, )
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 )
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)
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
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, )
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)
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)
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
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)
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]