def parse_interpolate(args): """Parse command-line arguments for the interpolate subcommand.""" # Check if the output file is writable at this point. Note that this # does not guarantee that it remains so. It is just a "courtesy" check # to avoid a lengthy calculation ending in failure to save. args.directory = os.path.abspath(args.directory) args.output = os.path.abspath(args.output) if not os.path.isdir(args.directory): lexit("the specified directory does not exist") if not is_writable(args.output): lexit("the output file is not writable") # Try and read the input. data = BoltzTraP2.dft.DFTData(args.directory, args.derivatives) # Perform some sanity checks on the energy window specification if args.emin >= args.emax: lexit("zero-width energy window") emin = args.emin + (0. if args.absolute else data.fermi) emax = args.emax + (0. if args.absolute else data.fermi) if emin >= data.fermi or emax <= data.fermi: lexit("the energy window must bracked the Fermi level") info("Nvalence (before BANDANA) =", data.nelect) # Drop bands outside the chosen energy range nemin = data.bandana(emin, emax)[0] info("Nvalence (after BANDANA) =", data.nelect) # Refuse to interpolate to fewer k points than there are in the input nkinput = data.kpoints.shape[0] logging.info( "there are {} irreducible k points in the input".format(nkinput)) if args.multiplier is not None: nktarget = args.multiplier * nkinput else: nktarget = args.kpoints logging.info( "{} irreducible k points have been requested".format(nktarget)) equivalences = BoltzTraP2.sphere.get_equivalences(data.atoms, nktarget) nkoutput = len(equivalences) logging.info( "{} irreducible k points have been generated".format(nkoutput)) if nkoutput < nkinput: lexit("refusing to interpolate to a sparser grid") # Perform the interpolation with TimerContext() as timer: coeffs = BoltzTraP2.fite.fitde3D(data, equivalences, args.nworkers) deltat = timer.get_deltat() info("the interpolation took {:.3g} s".format(deltat)) # Gather the metadata metadata = BoltzTraP2.serialization.gen_bt2_metadata(data, args.derivatives) # Save the result info("about to save the results to", args.output) with TimerContext() as timer: BoltzTraP2.serialization.save_calculation( args.output, data, equivalences, coeffs, metadata) deltat = timer.get_deltat() info("saving the results took {:.3g} s".format(deltat))
def parse_plot(args): """Parse command-line arguments for the plotbands subcommand.""" # Try and load the data from the integration step (data, fermi, Tr, mu0, mur, N, sdos, cv, cond, seebeck, kappa, hall, metadata) = BoltzTraP2.serialization.load_results(args.btj_file) info("sucessfully loaded " + args.btj_file) nformulas = data.get_formula_count() # Perform some sanity checks tensors = ("sigma", "S", "kappae", "L", "PF", "RH") components = args.components if args.quantity in tensors: if args.components is None: lexit("{} is a tensor but no components have been specified" .format(args.quantity)) if args.quantity not in tensors: if components: warning(("{} is not a tensor. " "The --components options will have no effect" ).format(args.quantity)) components = [()] elif args.quantity == "RH": if not all(i is None or len(i) == 3 for i in args.components): lexit("component specifications for the Hall tensor" " need three indices") else: if not all(i is None or len(i) == 2 for i in args.components): lexit(("component specifications for {}" " need two indices").format(args.quantity)) # Prepare the abscissas, the third variable and the axis labels if args.abscissa == "T": x = Tr xlabel = r"$T\;\left[\mathrm{K}\right]$" z = mur[::args.subsample] zlabel0 = r"$\mu - \varepsilon_F = {:5g}\;\mathrm{{Ry}}$" else: x = mur - fermi xlabel = r"$\mu - \varepsilon_F\;\left[\mathrm{Ha}\right]$" z = Tr[::args.subsample] zlabel0 = r"$T = {:5g}\;\mathrm{{K}}$" ylabel = dict( cv=r"$c_v\;\left[\mathrm{J\,mol^{-1}\,K^{-1}}\right]$", n=r"$n\;\left[\mathrm{\left|e\right|\,uc^{-1}}\right]$", DOS=r"$\mathrm{DOS}\;\left[\mathrm{uc^{-1}}\right]$", sigma=r"$\sigma^{{\left({}\right)}}/\tau_0\;" r"\left[\mathrm{{ohm^{{-1}}\,m^{{-1}}\," r"s^{{-1}}}}\right]$", S=r"$S^{{\left({}\right)}}\;" + r"\left[\mathrm{{V\,K^{{-1}}}}\right]$", kappae=r"$\kappa_e^{{\left({}\right)}}/\tau_0\;" r"\left[\mathrm{{W\,m^{{-1}}\,K^{{-1}}\,s^{{-1}}}}\right]$", L=r"$L^{{\left({}\right)}}\;" r"\left[\mathrm{{W\,\Omega\,K^{{-2}}}}\right]$", PF=r"$\left(S^2\sigma\right)^{{\left({}\right)}}/ \tau_0" r"\;\left[\mathrm{{W\,m^{{-1}}\,K^{{-2}}\,s^{{-1}}}}\right]$", RH=r"$R_H^{{\left({}\right)}}\;" + r"\left[\mathrm{{m^3\,C^{{-1}}}}\right]$") ylabel0 = ylabel[args.quantity] # Prepare the ordinate if args.quantity == "cv": y = cv * AVOGADRO / nformulas elif args.quantity == "n": y = N + data.nelect elif args.quantity == "DOS": y = sdos elif args.quantity == "sigma": y = cond elif args.quantity == "S": y = seebeck elif args.quantity == "kappae": y = kappa elif args.quantity == "L": L0 = 2.44e-8 y = np.empty_like(cond) for iT in range(y.shape[0]): for imu in range(y.shape[1]): y[iT, imu] = la.solve(cond[iT, imu].T, kappa[iT, imu].T).T / Tr[iT] elif args.quantity == "PF": y = np.empty_like(cond) for iT in range(y.shape[0]): for imu in range(y.shape[1]): y[iT, imu] = (cond[iT, imu] @ seebeck[iT, imu] @ seebeck[iT, imu]) elif args.quantity == "RH": y = hall if args.abscissa == "T": y = y[:, ::args.subsample] else: y = y[::args.subsample, :] # Create the plots for c in components: if args.quantity in tensors: desc = "scalar" if c is None else "".join("xyz" [i] for i in c) ylabel = ylabel0.format(desc) else: ylabel = ylabel0 plt.figure() for iz, zv in enumerate(z): zlabel = zlabel0.format(zv) if args.abscissa == "T": indices = [slice(None, None, None), iz] else: indices = [iz, slice(None, None, None)] if c is None: thisy = np.zeros_like(x) if args.quantity == "RH": complements = ((0, 1, 2), (1, 2, 0), (2, 0, 1)) else: complements = [[i, i] for i in range(3)] for i in complements: tindices = tuple(indices + list(i)) thisy += y[tindices] thisy /= float(len(complements)) else: indices += c indices = tuple(indices) thisy = y[indices] plt.plot(x, thisy, label=zlabel) if args.abscissa == "mu": plt.axvline(x=0., color=PSEUDO_BLACK, lw=2) if args.quantity == "L": plt.axhline(y=L0, color=PSEUDO_BLACK, lw=2) plt.xlabel(xlabel) plt.ylabel(ylabel) plt.legend(loc="best").draggable(True) plt.tight_layout() plt.show()
def parse_plotbands(args): """Parse command-line arguments for the plotbands subcommand.""" # Try and load the data from the interpolation step data, equivalences, coeffs, metadata = ( BoltzTraP2.serialization.load_calculation(args.bt2_file)) lattvec = data.get_lattvec() info("sucessfully loaded " + args.bt2_file) # The second position alargument is first interpreted as a Python literal, # and after parsing it is cast to a NumPy array, which must have the right # dimensions. The special value None directs the parser to split the path # in several parts. try: kpaths = ast.literal_eval(args.kpath) except ValueError: lexit("'{}' cannot be parsed as a Python literal".format(kpaths)) if not isinstance(kpaths, list): lexit("'{}' cannot be parsed as a Python list".format(kpaths)) kpaths = [ list(group) for k, group in itertools.groupby( kpaths, key=lambda x: x is not None) if k ] try: kpaths = [np.array(i, dtype=np.float64) for i in kpaths] for i in kpaths: if i.shape[0] < 2 or i.shape[1] != 3: raise ValueError except ValueError: lexit("the path cannot be interpreted as a set of N x 3" " arrays (with N >= 2") plt.figure() ax = plt.gca() ticks = [] dividers = [] offset = 0. for ikpath, kpath in enumerate(kpaths): ax.set_prop_cycle( color=matplotlib.rcParams["axes.prop_cycle"].by_key()["color"]) info("k path #{}".format(i + 1)) # Generate the explicit point list. kp, dkp, dcl = asekp.bandpath(kpath, data.atoms.cell, args.nkpoints) dkp += offset dcl += offset # Compute the band energies. with TimerContext() as timer: egrid = BoltzTraP2.fite.getBands(kp, equivalences, data.get_lattvec(), coeffs)[0] deltat = timer.get_deltat() info("rebuilding the bands took {:.3g} s".format(deltat)) egrid -= data.fermi # Create the plot nbands = egrid.shape[0] for i in range(nbands): plt.plot(dkp, egrid[i, :], lw=2.) ticks += dcl.tolist() dividers += [dcl[0], dcl[-1]] offset = dkp[-1] ax.set_xticks(ticks) ax.set_xticklabels([]) for d in ticks: plt.axvline(x=d, color=PSEUDO_BLACK, ls="--", lw=.5) for d in dividers: plt.axvline(x=d, color=PSEUDO_BLACK, ls="-", lw=2.) plt.axhline(y=0., color=PSEUDO_BLACK, lw=1.) plt.ylabel(r"$\varepsilon - \varepsilon_F\;\left[\mathrm{Ha}\right]$") plt.tight_layout() plt.show()
def parse_integrate(args): """Parse command-line arguments for the integrate subcommand.""" # If the number of bins is not set by hand, let the code handle it # automatically. try: args.bins except AttributeError: args.bins = None # Try and load the data from the interpolation step data, equivalences, coeffs, metadata = ( BoltzTraP2.serialization.load_calculation(args.bt2_file)) lattvec = data.get_lattvec() info("sucessfully loaded " + args.bt2_file) # Rebuild the bands with TimerContext() as timer: eband, vvband, cband = BoltzTraP2.fite.getBTPbands( equivalences, coeffs, lattvec, True, args.nworkers) deltat = timer.get_deltat() info("rebuilding the bands took {:.3g} s".format(deltat)) # Compute the DOS histogram with TimerContext() as timer: epsilon, dos, vvdos, cdos = BoltzTraP2.bandlib.BTPDOS( eband, vvband, cband, npts=args.bins, scattering_model=args.scattering_model) deltat = timer.get_deltat() info("computing the DOS took {:.3g} s".format(deltat)) info("Number of DOS bins:", epsilon.size) # Refine the estimate of the intrinsic chemical potential at # each temperature. Tr = args.temperature mu0 = np.empty_like(Tr) for iT, T in enumerate(Tr): mu0[iT] = BoltzTraP2.bandlib.refine_mu0(epsilon, dos, data.nelect, T, data.dosweight) # And at 0 K fermi = BoltzTraP2.bandlib.refine_mu0(epsilon, dos, data.nelect, 0., data.dosweight) # Compute the moments of the FD distribution at each point. # Determine the chemical potentials to explore by taking the values of the # energy bins and discarding those that are too close to the edge to give # meaningful results. # 9 * kB * T gives a reduction in fFD of about 1e-4, a relatively # safe margin (although still far from the hard cutoff in bandlib). margin = 9. * BOLTZMANN * Tr.max() mur_indices = np.logical_and(epsilon > epsilon.min() + margin, epsilon < epsilon.max() - margin) mur = epsilon[mur_indices] if mur.size == 0: lexit("the energy window is too narrow") # Get the smooth DOS at each temperature with TimerContext() as timer: sdos = np.empty((Tr.size, epsilon.size)) for iT, T in enumerate(Tr): sdos[iT, :] = BoltzTraP2.bandlib.smoothen_DOS(epsilon, dos, T) sdos = sdos[:, mur_indices] deltat = timer.get_deltat() info("smoothing the DOS took {:.3g} s".format(deltat)) info("Temperatures:", Tr) info("Fermi level from DFT:", data.fermi) info("Refined Fermi level:", fermi) info("Intrinsic chemical potential for each T:", mu0) info("Chemical potentials:", mur) with TimerContext() as timer: cv = BoltzTraP2.bandlib.calc_cv( epsilon, dos, mur, Tr, dosweight=data.dosweight) N, L0, L1, L2, Lm11 = BoltzTraP2.bandlib.fermiintegrals( epsilon, dos, vvdos, mur=mur, Tr=Tr, dosweight=data.dosweight, cdos=cdos) deltat = timer.get_deltat() info("computing the FD moments took {:.3g} s".format(deltat)) # Rescale and combine the moments to get the Onsager transport coefficients vuc = data.atoms.get_volume() * Angstrom**3 L11, seebeck, kappa, hall = BoltzTraP2.bandlib.calc_Onsager_coefficients( L0, L1, L2, mur, Tr, vuc, Lm11) # Save the results to BoltzTraP-style files basefn = os.path.splitext(args.bt2_file)[0] tracefn = basefn + ".trace" info("trace output file:", tracefn) BoltzTraP2.io.save_trace(tracefn, data, Tr, mur, N, sdos, cv, L11, seebeck, kappa, hall) condtensfn = basefn + ".condtens" info("conductivity/seebeck output file:", condtensfn) BoltzTraP2.io.save_condtens(condtensfn, Tr, mur, N, L11, seebeck, kappa) halltensfn = basefn + ".halltens" info("Hall coefficient output file:", halltensfn) BoltzTraP2.io.save_halltens(halltensfn, Tr, mur, N, hall) # Save the results to a single compressed JSON file metadata2 = BoltzTraP2.serialization.gen_btj_metadata( metadata, args.scattering_model) jsonfn = basefn + ".btj" info("JSON output file:", jsonfn) BoltzTraP2.serialization.save_results(jsonfn, data, fermi, Tr, mu0, mur, N, sdos, cv, L11, seebeck, kappa, hall, metadata2)
def parse_dope(args): """Parse command-line arguments for the dope subcommand. This is based on parse_integrate, but it selects the values of the chemical potential to explore based on the requested doping levels. """ try: args.bins except AttributeError: args.bins = None Tr = args.temperature if Tr.size == 0: lexit("empty temperature specification") elif Tr.min() <= 0.: lexit("all temperatures must be positive") data, equivalences, coeffs, metadata = ( BoltzTraP2.serialization.load_calculation(args.bt2_file)) lattvec = data.get_lattvec() info("sucessfully loaded " + args.bt2_file) with TimerContext() as timer: eband, vvband, cband = BoltzTraP2.fite.getBTPbands( equivalences, coeffs, lattvec, True, args.nworkers) deltat = timer.get_deltat() info("rebuilding the bands took {:.3g} s".format(deltat)) # Calculate the DOS. with TimerContext() as timer: epsilon, dos, vvdos, cdos = BoltzTraP2.bandlib.BTPDOS( eband, vvband, cband, npts=args.bins, scattering_model=args.scattering_model, Tmin=Tr.min()) deltat = timer.get_deltat() info("computing the DOS took {:.3g} s".format(deltat)) # Change the band gap if required. if args.scissor is not None: args.scissor *= eV info("Band gap value of {:.3g} Ha specified.".format(args.scissor) + " Trying to shift the gap to that value.") eband = BoltzTraP2.bandlib.apply_scissor(epsilon, dos, data.nelect, eband, args.scissor, data.dosweight) with TimerContext() as timer: epsilon, dos, vvdos, cdos = BoltzTraP2.bandlib.BTPDOS( eband, vvband, cband, npts=args.bins, scattering_model=args.scattering_model, Tmin=Tr.min()) deltat = timer.get_deltat() info("recomputing the DOS took {:.3g} s".format(deltat)) info("Number of DOS bins:", epsilon.size) nT = len(Tr) mu0 = np.empty_like(Tr) for iT, T in enumerate(Tr): mu0[iT] = BoltzTraP2.bandlib.solve_for_mu(epsilon, dos, data.nelect, T, data.dosweight, refine=True, try_center=True) fermi = BoltzTraP2.bandlib.solve_for_mu(epsilon, dos, data.nelect, 0., data.dosweight, refine=True, try_center=True) margin = 9. * BOLTZMANN * Tr.max() # The function diverges from parse_integrate from this point on. info("Temperatures:", Tr) info("Fermi level from DFT:", data.fermi) info("Refined Fermi level:", fermi) info("Intrinsic chemical potential for each T:", mu0) # Minimum and maximum reasonable values of mu mumin = epsilon.min() + margin mumax = epsilon.max() - margin if mumin >= mumax: lexit("the energy window is too narrow") # Convert the doping levels to values of mu at each temperature vuc = data.atoms.get_volume() * Angstrom**3 vuccm3 = data.atoms.get_volume() * 1e-24 # in cm^3 dopingr = args.doping_level ndoping = len(dopingr) mur = np.empty((nT, ndoping)) for iT, T in enumerate(Tr): dopingmin = BoltzTraP2.bandlib.calc_N(epsilon, dos, mumax, T, data.dosweight) + data.nelect dopingmin /= vuccm3 dopingmax = BoltzTraP2.bandlib.calc_N(epsilon, dos, mumin, T, data.dosweight) + data.nelect dopingmax /= vuccm3 # Check that the requested doping levels are in the acceptable range if dopingr.min() <= dopingmin or dopingr.max() >= dopingmax: lexit("minimum and maximum possible concentrations" " at T = {:.2f} K: {:.2g}, {:.2g}".format( T, dopingmin, dopingmax)) for idoping, doping in enumerate(dopingr): # Invert N(mu) to obtain an estimate of mu for each case. # Refine the estimate by not constraining it to one of the energies # in the histogram. N = data.nelect - doping * vuccm3 mur[iT, idoping] = BoltzTraP2.bandlib.solve_for_mu( epsilon, dos, N, T, data.dosweight, refine=True, try_center=False) info("Chemical potentials for T = {:.2f} K:".format(T), mur[iT, :]) # The smooth DOS is obtained using a direct KDE in energy space, instead # of a convolution with TimerContext() as timer: sdos = np.empty((nT, ndoping)) for iT, T in enumerate(Tr): sdos[iT, :] = BoltzTraP2.bandlib.smoothen_DOS_direct( epsilon, dos, T, mur[iT, :]) deltat = timer.get_deltat() info("smoothing the DOS took {:.3g} s".format(deltat)) # The flow from this point is again similar to that in parse_integrate, # with the exception that the chemical potentials are different for # each temperature. cv = np.empty((nT, ndoping)) N = np.empty((nT, ndoping)) L0 = np.empty((nT, ndoping, 3, 3)) L1 = np.empty((nT, ndoping, 3, 3)) L2 = np.empty((nT, ndoping, 3, 3)) Lm11 = np.empty((nT, ndoping, 3, 3, 3)) with TimerContext() as timer: for iT, T in enumerate(Tr): cv[iT] = BoltzTraP2.bandlib.calc_cv(epsilon, dos, mur[iT, :], np.array([T]), dosweight=data.dosweight) (N[iT], L0[iT], L1[iT], L2[iT], Lm11[iT]) = BoltzTraP2.bandlib.fermiintegrals( epsilon, dos, vvdos, mur=mur[iT], Tr=np.array([T]), dosweight=data.dosweight, cdos=cdos) deltat = timer.get_deltat() info("computing the FD moments took {:.3g} s".format(deltat)) L11 = np.empty((nT, ndoping, 3, 3)) seebeck = np.empty((nT, ndoping, 3, 3)) kappa = np.empty((nT, ndoping, 3, 3)) hall = np.empty((nT, ndoping, 3, 3, 3)) for iT, T in enumerate(Tr): (L11[iT], seebeck[iT], kappa[iT], hall[iT]) = BoltzTraP2.bandlib.calc_Onsager_coefficients( L0[[iT]], L1[[iT]], L2[[iT]], mur[iT], np.array([T]), vuc, Lm11[[iT]]) basefn = os.path.splitext(args.bt2_file)[0] tracefn = basefn + ".dope.trace" info("trace output file:", tracefn) BoltzTraP2.io.save_trace(tracefn, data, Tr, mur, N, sdos, cv, L11, seebeck, kappa, hall, scattering_model=args.scattering_model) condtensfn = basefn + ".dope.condtens" info("conductivity/seebeck output file:", condtensfn) BoltzTraP2.io.save_condtens(condtensfn, data, Tr, mur, N, L11, seebeck, kappa, scattering_model=args.scattering_model) halltensfn = basefn + ".dope.halltens" info("Hall coefficient output file:", halltensfn) BoltzTraP2.io.save_halltens(halltensfn, data, Tr, mur, N, hall)