def __init__( self, structure: Structure, energies: Dict[Spin, np.ndarray], vvelocities_product: Dict[Spin, np.ndarray], effective_mass: Dict[Spin, np.ndarray], projections: Dict[Spin, Dict[str, np.ndarray]], kpoint_mesh: np.ndarray, kpoints: np.ndarray, ir_kpoints: np.ndarray, ir_kpoints_idx: np.ndarray, ir_to_full_kpoint_mapping: np.ndarray, tetrahedra: np.ndarray, ir_tetrahedra_info: np.ndarray, efermi: float, num_electrons: float, is_metal: bool, soc: bool, vb_idx: Optional[Dict[Spin, int]] = None, ): self.structure = structure self.energies = energies self.velocities_product = vvelocities_product self.effective_mass = effective_mass self.kpoint_mesh = kpoint_mesh self.kpoints = kpoints self.ir_kpoints = ir_kpoints self.ir_kpoints_idx = ir_kpoints_idx self.ir_to_full_kpoint_mapping = ir_to_full_kpoint_mapping self.intrinsic_fermi_level = efermi self._soc = soc self.num_electrons = num_electrons self.is_metal = is_metal self.vb_idx = vb_idx self.spins = self.energies.keys() self.a_factor, self.c_factor = _calculate_orbital_factors(projections) self.dos = None self.scattering_rates = None self.scattering_labels = None self.doping = None self.temperatures = None self.fermi_levels = None self.electron_conc = None self.hole_conc = None self.conductivity = None self.seebeck = None self.electronic_thermal_conductivity = None self.mobility = None self.overlap_calculator = None self.fd_cutoffs = None self.grouped_ir_to_full = groupby(np.arange(len(kpoints)), ir_to_full_kpoint_mapping) self.tetrahedral_band_structure = TetrahedralBandStructure( energies, kpoints, tetrahedra, structure, ir_kpoints_idx, ir_to_full_kpoint_mapping, *ir_tetrahedra_info)
def __init__( self, structure: Structure, energies: Dict[Spin, np.ndarray], vvelocities_product: Dict[Spin, np.ndarray], velocities: Dict[Spin, np.ndarray], kpoint_mesh: np.ndarray, kpoints: np.ndarray, ir_kpoints_idx: np.ndarray, ir_to_full_kpoint_mapping: np.ndarray, tetrahedra: np.ndarray, ir_tetrahedra_info: np.ndarray, efermi: float, num_electrons: float, is_metal: bool, soc: bool, vb_idx: Optional[Dict[Spin, int]] = None, ): self.structure = structure self.velocities_product = vvelocities_product self.kpoint_mesh = kpoint_mesh self.intrinsic_fermi_level = efermi self.ir_to_full_kpoint_mapping = ir_to_full_kpoint_mapping self._soc = soc self.num_electrons = num_electrons self.is_metal = is_metal self.vb_idx = vb_idx self.spins = list(energies.keys()) self.velocities = { s: v.transpose((0, 2, 1)) for s, v in velocities.items() } self.dos = None self.scattering_rates = None self.scattering_labels = None self.doping = None self.temperatures = None self.fermi_levels = None self.electron_conc = None self.hole_conc = None self.conductivity = None self.seebeck = None self.electronic_thermal_conductivity = None self.mobility = None self.overlap_calculator = None self.mrta_calculator = None self.fd_cutoffs = None self.grouped_ir_to_full = groupby(np.arange(len(kpoints)), ir_to_full_kpoint_mapping) self.tetrahedral_band_structure = TetrahedralBandStructure.from_data( energies, kpoints, tetrahedra, structure, ir_kpoints_idx, ir_to_full_kpoint_mapping, *ir_tetrahedra_info) logger.info("Initializing momentum relaxation time factor calculator") self.mrta_calculator = MRTACalculator.from_data( kpoints, self.velocities)
def get_dos( self, kpoint_mesh: Union[float, int, List[int]], energy_cutoff: Optional[float] = None, scissor: Optional[float] = None, bandgap: Optional[float] = None, estep: float = defaults["dos_estep"], symprec: float = defaults["symprec"], atomic_units: bool = False, ) -> Union[Dos, FermiDos]: """Calculates the density of states using the interpolated bands. Args: kpoint_mesh: The k-point mesh as a 1x3 array. E.g.,``[6, 6, 6]``. Alternatively, if a single value is provided this will be treated as a reciprocal density 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. estep: The energy step, where smaller numbers give more accuracy but are more expensive. symprec: The symmetry tolerance used when determining the symmetry inequivalent k-points on which to interpolate. atomic_units: Whether to return the DOS in atomic units. If False, the unit of energy will be eV. Returns: The density of states. """ if isinstance(kpoint_mesh, numeric_types): logger.info("DOS k-point length cutoff: {}".format(kpoint_mesh)) else: str_mesh = "x".join(map(str, kpoint_mesh)) logger.info("DOS k-point mesh: {}".format(str_mesh)) structure = self._band_structure.structure tri = not self._soc ( ir_kpts, _, full_kpts, ir_kpts_idx, ir_to_full_idx, tetrahedra, *ir_tetrahedra_info, ) = get_kpoints_tetrahedral(kpoint_mesh, structure, symprec=symprec, time_reversal_symmetry=tri) energies, efermi, vb_idx = self.get_energies( ir_kpts, scissor=scissor, bandgap=bandgap, energy_cutoff=energy_cutoff, atomic_units=atomic_units, return_efermi=True, return_vb_idx=True, ) if not self._band_structure.is_metal(): # if not a metal, set the Fermi level to the top of the valence band. efermi = np.max( [np.max(e[vb_idx[spin]]) for spin, e in energies.items()]) full_energies = {s: e[:, ir_to_full_idx] for s, e in energies.items()} tetrahedral_band_structure = TetrahedralBandStructure( full_energies, full_kpts, tetrahedra, structure, ir_kpts_idx, ir_to_full_idx, *ir_tetrahedra_info) emin = np.min([np.min(spin_eners) for spin_eners in energies.values()]) emax = np.max([np.max(spin_eners) for spin_eners in energies.values()]) epoints = int(round((emax - emin) / estep)) energies = np.linspace(emin, emax, epoints) _, dos = tetrahedral_band_structure.get_density_of_states(energies) return FermiDos(efermi, energies, dos, structure, atomic_units=atomic_units)
class AmsetData(MSONable): def __init__( self, structure: Structure, energies: Dict[Spin, np.ndarray], vvelocities_product: Dict[Spin, np.ndarray], velocities: Dict[Spin, np.ndarray], kpoint_mesh: np.ndarray, kpoints: np.ndarray, ir_kpoints: np.ndarray, ir_kpoints_idx: np.ndarray, ir_to_full_kpoint_mapping: np.ndarray, tetrahedra: np.ndarray, ir_tetrahedra_info: np.ndarray, efermi: float, num_electrons: float, is_metal: bool, soc: bool, vb_idx: Optional[Dict[Spin, int]] = None, ): self.structure = structure self.energies = energies self.velocities_product = vvelocities_product self.kpoint_mesh = kpoint_mesh self.kpoints = kpoints self.ir_kpoints = ir_kpoints self.ir_kpoints_idx = ir_kpoints_idx self.ir_to_full_kpoint_mapping = ir_to_full_kpoint_mapping self.intrinsic_fermi_level = efermi self._soc = soc self.num_electrons = num_electrons self.is_metal = is_metal self.vb_idx = vb_idx self.spins = self.energies.keys() self.dos = None self.scattering_rates = None self.scattering_labels = None self.doping = None self.temperatures = None self.fermi_levels = None self.electron_conc = None self.hole_conc = None self.conductivity = None self.seebeck = None self.electronic_thermal_conductivity = None self.mobility = None self.overlap_calculator = None self.mrta_calculator = None self.fd_cutoffs = None self.velocities = {s: v.transpose((0, 2, 1)) for s, v in velocities.items()} self.grouped_ir_to_full = groupby( np.arange(len(kpoints)), ir_to_full_kpoint_mapping ) self.tetrahedral_band_structure = TetrahedralBandStructure( energies, kpoints, tetrahedra, structure, ir_kpoints_idx, ir_to_full_kpoint_mapping, *ir_tetrahedra_info ) self.mrta_calculator = MRTACalculator( self.kpoints, self.kpoint_mesh, self.velocities ) def set_overlap_calculator(self, overlap_calculator): nbands_equal = [ self.energies[s].shape[0] == overlap_calculator.nbands[s] for s in self.spins ] if not all(nbands_equal): raise RuntimeError( "Overlap calculator does not have the correct number of bands\n" "If using wavefunction coefficients, ensure they were generated using" "the same energy_cutoff (not encut)" ) self.overlap_calculator = overlap_calculator def calculate_dos(self, estep: float = defaults["dos_estep"]): """ Args: estep: The DOS energy step in eV, where smaller numbers give more accuracy but are more expensive. """ 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=True ) 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 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 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 set_scattering_rates( self, scattering_rates: Dict[Spin, np.ndarray], scattering_labels: List[str] ): for spin in self.spins: s = (len(self.doping), len(self.temperatures)) + self.energies[spin].shape if scattering_rates[spin].shape[1:] != s: raise ValueError( "Shape of scattering_type rates array does not match the " "number of dopings, temperatures, bands, or kpoints" ) if scattering_rates[spin].shape[0] != len(scattering_labels): raise ValueError( "Number of scattering_type rates does not match number of " "scattering_type labels" ) self.scattering_rates = scattering_rates self.scattering_labels = scattering_labels def set_transport_properties( self, conductivity: np.ndarray, seebeck: np.ndarray, electronic_thermal_conductivity: np.ndarray, mobility: Optional[np.ndarray] = None, ): self.conductivity = conductivity self.seebeck = seebeck self.electronic_thermal_conductivity = electronic_thermal_conductivity self.mobility = mobility def to_dict(self, include_mesh=defaults["write_mesh"]): data = { "doping": self.doping, "temperatures": self.temperatures, "fermi_levels": self.fermi_levels, "conductivity": self.conductivity, "seebeck": self.seebeck, "electronic_thermal_conductivity": self.electronic_thermal_conductivity, "mobility": self.mobility, } if include_mesh: rates = self.scattering_rates energies = self.energies vv = self.velocities_product ir_rates = {s: r[..., self.ir_kpoints_idx] for s, r in rates.items()} ir_energies = {s: e[:, self.ir_kpoints_idx] for s, e in energies.items()} ir_vv = {s: v[..., self.ir_kpoints_idx] for s, v in vv.items()} mesh_data = { "energies": cast_dict_list(ir_energies), "kpoints": self.kpoints, "ir_kpoints": self.ir_kpoints, "ir_to_full_kpoint_mapping": self.ir_to_full_kpoint_mapping, "efermi": self.intrinsic_fermi_level, "vb_idx": cast_dict_list(self.vb_idx), "dos": self.dos, "velocities_product": cast_dict_list(ir_vv), "scattering_rates": cast_dict_list(ir_rates), "scattering_labels": self.scattering_labels, "is_metal": self.is_metal, "fd_cutoffs": self.fd_cutoffs, "structure": self.structure, "soc": self._soc, } data.update(mesh_data) return data def to_data(self): data = [] triu = np.triu_indices(3) for n, t in np.ndindex(len(self.doping), len(self.temperatures)): row = [self.doping[n], self.temperatures[t], self.fermi_levels[n, t]] row.extend(self.conductivity[n, t][triu]) row.extend(self.seebeck[n, t][triu]) row.extend(self.electronic_thermal_conductivity[n, t][triu]) if self.mobility is not None: for mob in self.mobility.values(): row.extend(mob[n, t][triu]) data.append(row) headers = ["doping[cm^-3]", "temperature[K]", "Fermi_level[eV]"] ds = ("xx", "xy", "xz", "yy", "yz", "zz") # TODO: confirm unit of kappa for prop, unit in [("cond", "S/m"), ("seebeck", "µV/K"), ("kappa", "?")]: headers.extend(["{}_{}[{}]".format(prop, d, unit) for d in ds]) if self.mobility is not None: for name in self.mobility.keys(): headers.extend(["{}_mobility_{}[cm^2/V.s]".format(name, d) for d in ds]) return data, headers def to_file( self, directory: str = ".", prefix: Optional[str] = None, write_mesh: bool = defaults["write_mesh"], file_format: str = defaults["file_format"], suffix_mesh: bool = True, ): if self.conductivity is None: raise ValueError("Can't write AmsetData, transport properties not set") if not prefix: prefix = "" else: prefix += "_" if suffix_mesh: suffix = "_{}".format("x".join(map(str, self.kpoint_mesh))) else: suffix = "" if file_format in ["json", "yaml"]: data = self.to_dict(include_mesh=write_mesh) filename = joinpath( directory, "{}amset_data{}.{}".format(prefix, suffix, file_format) ) dumpfn(data, filename) elif file_format in ["csv", "txt"]: # don't write the data as JSON, instead write raw text files data, headers = self.to_data() filename = joinpath( directory, "{}amset_transport{}.{}".format(prefix, suffix, file_format) ) np.savetxt(filename, data, header=" ".join(headers)) if write_mesh: logger.warning("Writing mesh data as txt or csv not supported") else: raise ValueError("Unrecognised output format: {}".format(file_format)) return filename
def calculate_rate( tbs: TetrahedralBandStructure, overlap_calculator, mrta_calculator, elastic_scatterers, inelastic_scatterers, amset_data_min: _AmsetDataMin, coeffs, coeffs_mapping, spin, b_idx, k_idx, energy_diff=None, ): rlat = amset_data_min.structure.lattice.reciprocal_lattice.matrix velocity = amset_data_min.velocities[spin][b_idx, k_idx] energy = tbs.energies[spin][b_idx, k_idx] if energy_diff: energy += energy_diff ( tet_dos, tet_mask, cs_weights, tet_contributions, ) = tbs.get_tetrahedra_density_of_states( spin, energy, return_contributions=True, symmetry_reduce=False, # band_idx=b_idx, # turn this on to disable interband scattering ) if len(tet_dos) == 0: return 0 # next, get k-point indices and band_indices property_mask, band_kpoint_mask, band_mask, kpoint_mask = tbs.get_masks( spin, tet_mask) k = tbs.kpoints[k_idx] k_primes = tbs.kpoints[kpoint_mask] if coeffs is not None: # use cached coefficients to calculate the overlap on the fine mesh # tetrahedron vertices spin_coeffs = coeffs[spin] spin_coeffs_mapping = coeffs_mapping[spin] if len(spin_coeffs.shape) == 3: # ncl overlap = _get_overlap_ncl(spin_coeffs, spin_coeffs_mapping, b_idx, k_idx, band_mask, kpoint_mask) else: overlap = _get_overlap(spin_coeffs, spin_coeffs_mapping, b_idx, k_idx, band_mask, kpoint_mask) else: overlap = overlap_calculator.get_overlap(spin, b_idx, k, band_mask, k_primes) # put overlap back in array with shape (nbands, nkpoints) all_overlap = np.zeros(tbs.energies[spin].shape) all_overlap[band_kpoint_mask] = overlap # now select the properties at the tetrahedron vertices vert_overlap = all_overlap[property_mask] # get interpolated overlap at centre of tetrahedra cross sections tet_overlap = get_cross_section_values(vert_overlap, *tet_contributions) tetrahedra = tbs.tetrahedra[spin][tet_mask] # have to deal with the case where the tetrahedron cross section crosses the # zone boundary. This is a slight inaccuracy but we just treat the # cross section as if it is on one side of the boundary tet_kpoints = tbs.kpoints[tetrahedra] base_kpoints = tet_kpoints[:, 0][:, None, :] k_diff = pbc_diff(tet_kpoints, base_kpoints) + pbc_diff(base_kpoints, k) # project the tetrahedron cross sections onto 2D surfaces in either a triangle # or quadrilateral k_diff = np.dot(k_diff, rlat) intersections = get_cross_section_values(k_diff, *tet_contributions, average=False) projected_intersections, basis = get_projected_intersections(intersections) k_spacing = np.linalg.norm(np.dot(rlat, 1 / amset_data_min.kpoint_mesh)) qpoints, weights, mapping = get_fine_mesh_qpoints( projected_intersections, basis, *tet_contributions[0:3], high_tol=k_spacing * 0.5, med_tol=k_spacing * 2, cross_section_weights=cs_weights, ) qpoint_norm_sq = np.sum(qpoints**2, axis=-1) k_primes = np.dot(qpoints, np.linalg.inv(rlat)) + k k_primes = kpoints_to_first_bz(k_primes) # unit q in reciprocal cartesian coordinates unit_q = qpoints / np.sqrt(qpoint_norm_sq)[:, None] if energy_diff: e_fd = _get_fd(energy, amset_data_min.fermi_levels, amset_data_min.temperatures) emission = energy_diff <= 0 rates = [ s.factor(unit_q, qpoint_norm_sq, emission, e_fd) for s in inelastic_scatterers ] mrta_factor = 1 else: mrta_factor = mrta_calculator.get_mrta_factor(spin, b_idx, k, tet_mask[0][mapping], k_primes) rates = [ s.factor(unit_q, qpoint_norm_sq, spin, b_idx, k, velocity) for s in elastic_scatterers ] rates = np.array(rates) rates /= amset_data_min.structure.lattice.reciprocal_lattice.volume rates *= tet_overlap[mapping] * weights * mrta_factor # this is too expensive vs tetrahedron integration and doesn't add much more # accuracy; could offer this as an option # overlap = self.amset_data.overlap_calculator.get_overlap( # spin, b_idx, k, tet_mask[0][mapping], k_primes # ) # rates *= overlap * weights * mrta_factor # sometimes the projected intersections can be nan when the density of states # contribution is infinitesimally small; this catches those errors rates[np.isnan(rates)] = 0 return np.sum(rates, axis=-1)
def scattering_worker( tbs_reference, overlap_type, overlap_calculator_reference, mrta_calculator_reference, elastic_scatterers, inelastic_scatterers, amset_data_min_reference, coeffs_buffer, coeffs_mapping_buffer, in_queue, out_queue, ): try: tbs = TetrahedralBandStructure.from_reference(*tbs_reference) mrta_calculator = MRTACalculator.from_reference( *mrta_calculator_reference) amset_data_min = _AmsetDataMin.from_reference( *amset_data_min_reference) if coeffs_buffer is None: coeffs = None coeffs_mapping = None else: coeffs = dict_array_from_buffer(coeffs_buffer) coeffs_mapping = dict_array_from_buffer(coeffs_mapping_buffer) if overlap_type == "wavefunction": overlap_calculator = WavefunctionOverlapCalculator.from_reference( *overlap_calculator_reference) elif overlap_type == "projection": overlap_calculator = ProjectionOverlapCalculator.from_reference( *overlap_calculator_reference) else: raise ValueError( "Unrecognised overlap type: {}".format(overlap_type)) elastic_scatterers = [ AcousticDeformationPotentialScattering.from_reference( *s) if isinstance(s, tuple) else s for s in elastic_scatterers ] with np.errstate(all="ignore"): while True: job = in_queue.get() if job is None: break spin, b_idx, k_idx, energy_diff, ir_k_idx = job rate = calculate_rate( tbs, overlap_calculator, mrta_calculator, elastic_scatterers, inelastic_scatterers, amset_data_min, coeffs, coeffs_mapping, spin, b_idx, k_idx, energy_diff=energy_diff, ) out_queue.put((ir_k_idx, rate)) except BaseException as e: error_msg = traceback.format_exc() out_queue.put((e, error_msg))
class AmsetData(MSONable): def __init__( self, structure: Structure, energies: Dict[Spin, np.ndarray], vvelocities_product: Dict[Spin, np.ndarray], velocities: Dict[Spin, np.ndarray], kpoint_mesh: np.ndarray, kpoints: np.ndarray, ir_kpoints: np.ndarray, ir_kpoints_idx: np.ndarray, ir_to_full_kpoint_mapping: np.ndarray, tetrahedra: np.ndarray, ir_tetrahedra_info: np.ndarray, efermi: float, num_electrons: float, is_metal: bool, soc: bool, vb_idx: Optional[Dict[Spin, int]] = None, ): self.structure = structure self.energies = energies self.velocities_product = vvelocities_product self.kpoint_mesh = kpoint_mesh self.kpoints = kpoints self.ir_kpoints = ir_kpoints self.ir_kpoints_idx = ir_kpoints_idx self.ir_to_full_kpoint_mapping = ir_to_full_kpoint_mapping self.intrinsic_fermi_level = efermi self._soc = soc self.num_electrons = num_electrons self.is_metal = is_metal self.vb_idx = vb_idx self.spins = list(self.energies.keys()) self.velocities = { s: v.transpose((0, 2, 1)) for s, v in velocities.items() } self.dos = None self.scattering_rates = None self.scattering_labels = None self.doping = None self.temperatures = None self.fermi_levels = None self.electron_conc = None self.hole_conc = None self.conductivity = None self.seebeck = None self.electronic_thermal_conductivity = None self.mobility = None self.overlap_calculator = None self.mrta_calculator = None self.fd_cutoffs = None self.grouped_ir_to_full = groupby(np.arange(len(kpoints)), ir_to_full_kpoint_mapping) self.tetrahedral_band_structure = TetrahedralBandStructure( energies, kpoints, tetrahedra, structure, ir_kpoints_idx, ir_to_full_kpoint_mapping, *ir_tetrahedra_info) logger.info("Initializing momentum relaxation time factor calculator") self.mrta_calculator = MRTACalculator(self.kpoints, self.velocities) def set_overlap_calculator(self, overlap_calculator): equal = check_nbands_equal(overlap_calculator, self) if not equal: raise RuntimeError( "Overlap calculator does not have the correct number of bands\n" "If using wavefunction coefficients, ensure they were generated using" "the same energy_cutoff (not encut)") self.overlap_calculator = overlap_calculator 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 * ev_to_hartree))) energies = np.linspace(emin, emax, epoints) dos_weight = 1 if self._soc or len(self.spins) == 2 else 2 logger.info("DOS parameters:") log_list([ "emin: {:.2f} eV".format(emin * hartree_to_ev), "emax: {:.2f} eV".format(emax * hartree_to_ev), "dos weight: {}".format(dos_weight), "n points: {}".format(epoints), ]) logger.info("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 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((doping[n], temperatures[t], self.fermi_levels[n, t] * hartree_to_ev)) table = tabulate( fermi_level_info, headers=("conc [cm⁻³]", "temp [K]", "E_fermi [eV]"), numalign="right", stralign="center", floatfmt=(".2e", ".1f", ".4f"), ) logger.info("Calculated Fermi levels:") logger.info(table) 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 = max( [self.energies[s][self.vb_idx[s]].max() for s in self.spins]) cbm = min([ self.energies[s][self.vb_idx[s] + 1].min() for s in self.spins ]) 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 set_scattering_rates(self, scattering_rates: Dict[Spin, np.ndarray], scattering_labels: List[str]): for spin in self.spins: s = (len(self.doping), len( self.temperatures)) + self.energies[spin].shape if scattering_rates[spin].shape[1:] != s: raise ValueError( "Shape of scattering_type rates array does not match the " "number of dopings, temperatures, bands, or kpoints") if scattering_rates[spin].shape[0] != len(scattering_labels): raise ValueError( "Number of scattering_type rates does not match number of " "scattering_type labels") self.scattering_rates = scattering_rates self.scattering_labels = scattering_labels def fill_rates_outside_cutoffs(self, fill_value=None): if self.scattering_rates is None: raise ValueError( "Scattering rates must be set before being filled") min_fd, max_fd = self.fd_cutoffs snt_fill = fill_value for spin, spin_energies in self.energies.items(): mask = (spin_energies < min_fd) | (spin_energies > max_fd) rate_info = defaultdict(list) for s, n, t in np.ndindex(self.scattering_rates[spin].shape[:3]): if fill_value is None: # get average log rate inside cutoffs snt_fill = np.log(self.scattering_rates[spin][s, n, t, ~mask]) snt_fill = np.exp(snt_fill.mean()) rate_info[self.scattering_labels[s]].append(snt_fill) self.scattering_rates[spin][s, n, t, mask] = snt_fill if len(self.spins) == 1: logger.info( "Filling scattering rates [s⁻¹] outside FD cutoffs with:") else: logger.info( "Filling {} scattering rates [s⁻¹] outside FD cutoffs " "with:".format(spin_name[spin])) headers = ["conc [cm⁻³]", "temp [K]"] headers += ["{}".format(s) for s in self.scattering_labels] rate_table = [] for i, (n, t) in enumerate(np.ndindex(self.fermi_levels.shape)): col = [ self.doping[n] * (1 / bohr_to_cm)**3, self.temperatures[t] ] col += [rate_info[s][i] for s in self.scattering_labels] rate_table.append(col) table = tabulate( rate_table, headers=headers, numalign="right", stralign="center", floatfmt=[".2e", ".1f"] + [".2e"] * len(self.scattering_labels), ) logger.info(table) def set_transport_properties( self, conductivity: np.ndarray, seebeck: np.ndarray, electronic_thermal_conductivity: np.ndarray, mobility: Optional[np.ndarray] = None, ): self.conductivity = conductivity self.seebeck = seebeck self.electronic_thermal_conductivity = electronic_thermal_conductivity self.mobility = mobility def to_dict(self, include_mesh=defaults["write_mesh"]): data = { "doping": (self.doping * cm_to_bohr**3).round(), "temperatures": self.temperatures, "fermi_levels": self.fermi_levels * hartree_to_ev, "conductivity": self.conductivity, "seebeck": self.seebeck, "electronic_thermal_conductivity": self.electronic_thermal_conductivity, "mobility": self.mobility, } if include_mesh: rates = self.scattering_rates energies = self.energies vel = self.velocities ir_rates = { s: r[..., self.ir_kpoints_idx] for s, r in rates.items() } ir_energies = { s: e[:, self.ir_kpoints_idx] * hartree_to_ev for s, e in energies.items() } ir_vel = {s: v[:, self.ir_kpoints_idx] for s, v in vel.items()} mesh_data = { "energies": ir_energies, "kpoints": self.kpoints, "ir_kpoints": self.ir_kpoints, "ir_to_full_kpoint_mapping": self.ir_to_full_kpoint_mapping, "efermi": self.intrinsic_fermi_level * hartree_to_ev, "vb_idx": self.vb_idx, "num_electrons": self.num_electrons, # "dos": self.dos, # TODO: Convert dos to eV "velocities": ir_vel, # TODO: convert units "scattering_rates": ir_rates, "scattering_labels": self.scattering_labels, "is_metal": self.is_metal, "fd_cutoffs": ( self.fd_cutoffs[0] * hartree_to_ev, self.fd_cutoffs[1] * hartree_to_ev, ), "structure": get_angstrom_structure(self.structure), "soc": self._soc, "doping": data["doping"], "temperatures": data["temperatures"], "fermi_levels": data["fermi_levels"], } data["mesh"] = mesh_data return data def to_data(self): data = [] triu = np.triu_indices(3) for n, t in np.ndindex(len(self.doping), len(self.temperatures)): row = [ self.doping[n] * cm_to_bohr**3, self.temperatures[t], self.fermi_levels[n, t] * hartree_to_ev, ] row.extend(self.conductivity[n, t][triu]) row.extend(self.seebeck[n, t][triu]) row.extend(self.electronic_thermal_conductivity[n, t][triu]) if self.mobility is not None: for mob in self.mobility.values(): row.extend(mob[n, t][triu]) data.append(row) headers = ["doping[cm^-3]", "temperature[K]", "Fermi_level[eV]"] ds = ("xx", "xy", "xz", "yy", "yz", "zz") # TODO: confirm unit of kappa for prop, unit in [("cond", "S/m"), ("seebeck", "µV/K"), ("kappa", "?")]: headers.extend(["{}_{}[{}]".format(prop, d, unit) for d in ds]) if self.mobility is not None: for name in self.mobility.keys(): headers.extend( ["{}_mobility_{}[cm^2/V.s]".format(name, d) for d in ds]) return data, headers def to_file( self, directory: str = ".", prefix: Optional[str] = None, write_mesh_file: bool = defaults["write_mesh"], file_format: str = defaults["file_format"], suffix_mesh: bool = True, ): if self.conductivity is None: raise ValueError( "Can't write AmsetData, transport properties not set") if not prefix: prefix = "" else: prefix += "_" if suffix_mesh: suffix = "_{}".format("x".join(map(str, self.kpoint_mesh))) else: suffix = "" if file_format in ["json", "yaml"]: data = self.to_dict() data = cast_dict_list(data) filename = joinpath( directory, "{}transport{}.{}".format(prefix, suffix, file_format)) dumpfn(data, filename, indent=4) elif file_format in ["csv", "txt"]: # don't write the data as JSON, instead write raw text files data, headers = self.to_data() filename = joinpath( directory, "{}transport{}.{}".format(prefix, suffix, file_format)) np.savetxt(filename, data, header=" ".join(headers)) else: raise ValueError( "Unrecognised output format: {}".format(file_format)) if write_mesh_file: mesh_data = self.to_dict(include_mesh=True)["mesh"] mesh_filename = joinpath(directory, "{}mesh{}.h5".format(prefix, suffix)) write_mesh(mesh_data, filename=mesh_filename) return filename, mesh_filename else: return filename