def __init__(self, band_structure: BandStructure, num_electrons: int, interpolation_factor: float = 20, soc: bool = False, magmom: Optional[np.ndarray] = None, mommat: Optional[np.ndarray] = None, interpolate_projections: bool = False): self._band_structure = band_structure self._num_electrons = num_electrons self._soc = soc self._spins = self._band_structure.bands.keys() self._interpolate_projections = interpolate_projections self.interpolation_factor = interpolation_factor self._lattice_matrix = (band_structure.structure.lattice.matrix * units.Angstrom) self._coefficients = {} self._projection_coefficients = defaultdict(dict) kpoints = np.array([k.frac_coords for k in band_structure.kpoints]) atoms = AseAtomsAdaptor.get_atoms(band_structure.structure) logger.info("Getting band interpolation coefficients") t0 = time.perf_counter() self._equivalences = sphere.get_equivalences( atoms=atoms, nkpt=kpoints.shape[0] * interpolation_factor, magmom=magmom) # get the interpolation mesh used by BoltzTraP2 self.interpolation_mesh = 2 * np.max( np.abs(np.vstack(self._equivalences)), axis=0) + 1 for spin in self._spins: energies = band_structure.bands[spin] * units.eV data = DFTData(kpoints, energies, self._lattice_matrix, mommat=mommat) self._coefficients[spin] = fite.fitde3D(data, self._equivalences) log_time_taken(t0) if self._interpolate_projections: logger.info("Getting projection interpolation coefficients") if not band_structure.projections: raise ValueError( "interpolate_projections is True but band structure has no " "projections") for spin in self._spins: for label, projection in _get_projections( band_structure.projections[spin]): data = DFTData(kpoints, projection, self._lattice_matrix, mommat=mommat) self._projection_coefficients[spin][label] = fite.fitde3D( data, self._equivalences) log_time_taken(t0)
def _interpolate_zero_rates(rates, kpoints, masks: Optional = None): # loop over all scattering types, doping, temps, and bands and interpolate # zero scattering rates based on the nearest k-point logger.info("Interpolating missing scattering rates") n_rates = sum([np.product(rates[spin].shape[:-1]) for spin in rates]) pbar = tqdm(total=n_rates, ncols=output_width, desc=" ├── progress", bar_format='{l_bar}{bar}| {elapsed}<{remaining}{postfix}', file=sys.stdout) n_zero_rates = 0 n_lots = 0 t0 = time.perf_counter() k_idx = np.arange(len(kpoints)) for spin in rates: for s, d, t, b in np.ndindex(rates[spin].shape[:-1]): if masks is not None: mask = np.invert(masks[spin][s, d, t, b]) else: mask = [True] * len(rates[spin][s, d, t, b]) non_zero_rates = rates[spin][s, d, t, b, mask] > 1e7 # non_zero_rates = rates[spin][s, d, t, b, mask] != 0 zero_rate_idx = k_idx[mask][~non_zero_rates] non_zero_rate_idx = k_idx[mask][non_zero_rates] if not np.any(non_zero_rates): # all scattering rates are zero so cannot interpolate # generally this means the scattering prefactor is zero. E.g. # for POP when studying non polar materials rates[spin][s, d, t, b, mask] += small_val elif np.sum(non_zero_rates) != np.sum(mask): n_lots += 1 # interpolation seems to work best when all the kpoints are +ve # therefore add 0.5 # Todo: Use cartesian coordinates (will be more robust to # oddly shaped cells) rates[spin][s, d, t, b, zero_rate_idx] = griddata( points=kpoints[non_zero_rate_idx] + 0.5, values=rates[spin][s, d, t, b, non_zero_rate_idx], xi=kpoints[zero_rate_idx] + 0.5, method='nearest') pbar.update() pbar.close() log_time_taken(t0) if n_zero_rates > 0: logger.warning("WARNING: N zero rates: {:.0f}".format(n_zero_rates / n_lots)) return rates
def compute_volumes(self): logger.info("Calculating k-point Voronoi diagram:") logger.debug(" ├── num k-points near extra points: {}".format( len(self._voronoi_points))) t0 = time.perf_counter() # after some testing it seems like sorting the points before calculating # the Voronoi diagram can speed things up by > 1000x when there are many # points sorted_idx = np.argsort(self._voronoi_points, axis=0)[:, 1] # voro = Voronoi(self._voronoi_points[sorted_idx], qhull_options="Qbb Qc Qz") voro = Voronoi(self._voronoi_points[sorted_idx], qhull_options="Qbb Qc Qz") # need to unsort regions to get correct points inv_sorted_idx = np.argsort(sorted_idx) regions = voro.point_region[inv_sorted_idx][self._volume_points_idx] indices = np.array(voro.regions)[regions] vertices = [voro.vertices[i] for i in indices] log_time_taken(t0) volumes = self._final_volumes.copy() # divide volumes by reciprocal lattice volume to get the fractional volume volumes[self._volume_in_final_idx] = self._get_voronoi_volumes( indices, vertices)[self._expand_ir] / self._volume zero_vols = volumes == 0 if zero_vols.any(): logger.warning("{} volumes are zero".format(np.sum(zero_vols))) inf_vols: np.ndarray = volumes == np.inf if inf_vols.any(): logger.warning("{} volumes are infinite".format(inf_vols.sum())) sum_volumes = volumes.sum() vol_diff = sum_volumes - 1 if abs(vol_diff) > 1e-7: logger.warning("Sum of weights does not equal 1 (diff = {:.3f} " "%)... renormalising weights".format(vol_diff * 100)) volumes /= sum_volumes return volumes
def _get_voronoi_volumes(self, indices, vertices) -> np.ndarray: logger.info("Calculating k-point weights:") voronoi_info = tqdm( list(zip(indices, vertices)), total=len(indices), ncols=output_width, desc=" ├── progress", file=sys.stdout, bar_format='{l_bar}{bar}| {elapsed}<{remaining}{postfix}') t0 = time.perf_counter() volumes = Parallel(n_jobs=self._nworkers, prefer="processes")(delayed(_get_volume)(idx, verts) for idx, verts in voronoi_info) log_time_taken(t0) return np.array(volumes)
def compute_volumes(self): logger.info("Calculating k-point Voronoi diagram:") logger.debug(" ├── num k-points near extra points: {}".format( len(self._voronoi_points))) t0 = time.perf_counter() # after some testing it seems like sorting the points before calculating # the Voronoi diagram can speed things up by > 1000x when there are many # points sorted_idx = np.argsort(self._voronoi_points, axis=0)[:, 1] # add the QJ option to qhull, necessary to slightly jiggle the points voro = Voronoi(self._voronoi_points[sorted_idx], qhull_options="Qbb Qc Qz QJ") # need to unsort regions to get correct points inv_sorted_idx = np.argsort(sorted_idx) regions = voro.point_region[inv_sorted_idx][self._volume_points_idx] indices = np.array(voro.regions)[regions] vertices = [voro.vertices[i] for i in indices] log_time_taken(t0) volumes = self._final_volumes.copy() volumes[self._volume_in_final_idx] = self._get_voronoi_volumes( indices, vertices) zero_vols = volumes == 0 if any(zero_vols): logger.warning("{} volumes are zero".format(np.sum(zero_vols))) inf_vols: np.ndarray = volumes == np.inf if any(inf_vols): logger.warning("{} volumes are infinite".format(np.sum(inf_vols))) sum_volumes = volumes.sum() vol_diff = abs(sum_volumes - 1) if vol_diff > 0.01: logger.warning( "Sum of weights does not equal 1 (diff = {:.1f})... " "renormalising weights".format(vol_diff * 100)) volumes = volumes / sum_volumes return volumes
def solve_bte(self, amset_data: AmsetData): if not all([amset_data.doping is not None, amset_data.temperatures is not None, amset_data.scattering_rates is not None]): raise ValueError("Electronic structure must contain dopings " "temperatures and scattering rates") logger.info("Calculating conductivity, Seebeck, and electronic thermal " "conductivity tensors.") t0 = time.perf_counter() sigma, seebeck, kappa = _calculate_transport_properties(amset_data) log_time_taken(t0) if not self.calculate_mobility: return sigma, seebeck, kappa, None if amset_data.is_metal: logger.info("System is metallic, refusing to calculate carrier " "mobility") return sigma, seebeck, kappa, None logger.info("Calculating overall mobility") t0 = time.perf_counter() mobility = {"overall": _calculate_mobility( amset_data, list(range(len(amset_data.scattering_labels))))} log_time_taken(t0) if self.separate_scattering_mobilities: logger.info("Calculating individual scattering rate mobilities") t0 = time.perf_counter() for rate_idx, name in enumerate(amset_data.scattering_labels): mobility[name] = _calculate_mobility(amset_data, rate_idx) log_time_taken(t0) return sigma, seebeck, kappa, mobility
def _interpolate_zero_rates(rates, kpoints): # loop over all scattering types, doping, temps, and bands and interpolate # zero scattering rates based on the nearest k-point logger.info("Interpolating missing scattering rates") n_rates = sum([np.product(rates[spin].shape[:-1]) for spin in rates]) pbar = tqdm(total=n_rates, ncols=output_width, desc=" ├── progress", bar_format='{l_bar}{bar}| {elapsed}<{remaining}{postfix}', file=sys.stdout) n_zero_rates = 0 n_lots = 0 t0 = time.perf_counter() for spin in rates: for s, d, t, b in np.ndindex(rates[spin].shape[:-1]): non_zero_rates = rates[spin][s, d, t, b] != 0 if not any(non_zero_rates): # all scattering rates are zero so cannot interpolate # generally this means the scattering prefactor is zero. E.g. # for POP when studying non polar materials rates[spin][s, d, t, b] += small_val elif np.sum(non_zero_rates) != len(rates[spin][s, d, t, b]): n_zero_rates += sum(non_zero_rates == False) n_lots += 1 rates[spin][s, d, t, b, ~non_zero_rates] = griddata( points=kpoints[non_zero_rates], values=rates[spin][s, d, t, b, non_zero_rates], xi=kpoints[~non_zero_rates], method='nearest') pbar.update() pbar.close() log_time_taken(t0) if n_zero_rates > 0: print("WARNING: N zero rates: {:.0f}".format(n_zero_rates/n_lots)) return rates
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)
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, )
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_effective_mass: bool = False, return_projections: bool = False, return_vel_outer_prod: bool = True, coords_are_cartesian: bool = False, atomic_units: bool = False, skip_coefficients: Optional[float] = None, ) -> 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_effective_mass: Whether to return the band effective masses. 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 effective masses in units of electron rest mass, m0. Returns: The band energies as dictionary of:: {spin: energies} If ``return_velocity``, ``return_effective_mass`` or ``return_projections`` a tuple is returned, formatted as:: (energies, Optional[velocities], Optional[effective_masses], 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 if energy_cutoff and self._band_structure.is_metal(): min_e = self._band_structure.efermi - energy_cutoff max_e = self._band_structure.efermi + energy_cutoff elif energy_cutoff: min_e = self._band_structure.get_vbm()['energy'] - energy_cutoff max_e = self._band_structure.get_cbm()['energy'] + energy_cutoff else: min_e = min([self._band_structure.bands[spin].min() for spin in self._spins]) max_e = max([self._band_structure.bands[spin].max() for spin in self._spins]) if coords_are_cartesian: kpoints = self._band_structure.structure.lattice. \ reciprocal_lattice.get_fractional_coords(kpoints) kpoints = np.asarray(kpoints) energies = {} velocities = {} effective_masses = {} projections = defaultdict(dict) for spin in self._spins: ibands = np.any((self._band_structure.bands[spin] > min_e) & (self._band_structure.bands[spin] < 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_effective_mass) log_time_taken(t0) energies[spin] = fitted[0] velocities[spin] = fitted[1] 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 = sum(ibands[: vb_idx + 1]) - 1 energies[spin] = _shift_energies( energies[spin], new_vb_idx, scissor=scissor, bandgap=bandgap) 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, len(kpoints))) 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_effective_mass: effective_masses[spin] = fitted[2] if not atomic_units: energies[spin] = energies[spin] / units.eV velocities[spin] = _convert_velocities( velocities[spin], self._band_structure.structure.lattice.matrix) if return_effective_mass: effective_masses[spin] = _convert_effective_masses( effective_masses[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] log_time_taken(t0) if not (return_velocity or return_effective_mass or return_projections): return energies to_return = [energies] if return_velocity: to_return.append(velocities) if return_effective_mass: to_return.append(effective_masses) if return_projections: to_return.append(projections) return tuple(to_return)