# Copyright (c) [2024] [Daniel Pozsar] # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. if __name__ == "__main__": import argparse import sys from itertools import permutations, product from timeit import default_timer as timer import numpy as np import numpy.linalg as nl import sisl import tqdm from mpi4py import MPI from scipy.special import roots_legendre from grogu_magn.useful import hsk, make_atran, make_contour, make_kset start = timer() # Some input parsing parser = argparse.ArgumentParser() parser.add_argument( "--kset", dest="kset", default=2, type=int, help="k-space resolution of Jij calculation", ) parser.add_argument( "--kdirs", dest="kdirs", default="xyz", help="Definition of k-space dimensionality", ) parser.add_argument( "--eset", dest="eset", default=42, type=int, help="Number of energy points on the contour", ) parser.add_argument( "--eset-p", dest="esetp", default=10, type=int, help="Parameter tuning the distribution on the contour", ) parser.add_argument("--input", dest="infile", required=True, help="Input file name") parser.add_argument( "--output", dest="outfile", required=True, help="Output file name" ) parser.add_argument( "--Ebot", dest="Ebot", default=-20.0, type=float, help="Bottom energy of the contour", ) parser.add_argument( "--npairs", dest="npairs", default=1, type=int, help="Number of unitcell pairs in each direction for Jij calculation", ) parser.add_argument( "--adirs", dest="adirs", default=False, help="Definition of pair directions" ) parser.add_argument( "--use-tqdm", dest="usetqdm", default="not", help="Use tqdm for progressbars or not", ) parser.add_argument( "--cutoff", dest="cutoff", default=100.0, type=float, help="Real space cutoff for pair generation in Angs", ) parser.add_argument( "--pairfile", dest="pairfile", default=False, help="File to read pair information", ) args = parser.parse_args() # ---------------------------------------------------------------------- # MPI init comm = MPI.COMM_WORLD size = comm.Get_size() rank = comm.Get_rank() root_node = 0 if rank == root_node: print("Number of nodes in the parallel cluster: ", size) # ---------------------------------------------------------------------- # importing the necessary structures from SIESTA output dat = sisl.get_sile(args.infile) dh = dat.read_hamiltonian() # update datastructure of the hamiltonian # this is needed for quick Hk building dh.hup = ( dh.tocsr(0) .toarray() .reshape(dh.no, dh.n_s, dh.no) .transpose(0, 2, 1) .astype("complex128") ) dh.hdo = ( dh.tocsr(1) .toarray() .reshape(dh.no, dh.n_s, dh.no) .transpose(0, 2, 1) .astype("complex128") ) dh.sov = ( dh.tocsr(2) .toarray() .reshape(dh.no, dh.n_s, dh.no) .transpose(0, 2, 1) .astype("complex128") ) # ---------------------------------------------------------------------- # generate k space sampling kset = make_kset(dirs=args.kdirs, NUMK=args.kset) wk = 1 / len(kset) # weight of a kpoint in BZ integral kpcs = np.array_split(kset, size) if "k" in args.usetqdm: kpcs[root_node] = tqdm.tqdm(kpcs[root_node], desc="k loop") # ---------------------------------------------------------------------- # define pairs if args.pairfile: # if pair file is specified read in pair information from pairfile if rank == root_node: # read in pair on root node file in a format of five integer columns # first two integers define sublattice # second three define distance vectors of unitcells dummy = np.loadtxt(args.pairfile, dtype=int) atran = [ (dummy[p, 0], dummy[p, 1], [dummy[p, 2], dummy[p, 3], dummy[p, 4]]) for p in range(len(dummy)) ] else: atran = None # syncronize atran over all nodes atran = comm.bcast(atran, root=root_node) else: # if pairfile is not specified generate pair as defined by npairs and adirs # Take pair directions for k directions if adirs is not set args.adirs = args.adirs if args.adirs else args.kdirs # definition of pairs in terms of integer coordinates refering # to unicell distances and atomic positions atran = make_atran(len(dh.atoms), args.adirs, dist=args.npairs) pairs = [] for i, j, uc in atran: if nl.norm(np.dot(uc, dh.cell)) < args.cutoff: pairs.append( dict( offset=uc, # lattice vector offset between the unitcells the two atoms are aiij=[i, j], # indecies of the atoms in the unitcell noij=[ dh.atoms.orbitals[i], dh.atoms.orbitals[j], ], # number of orbitals on the appropriate atoms slij=[ slice( *(lambda x: [min(x), max(x) + 1])(dh.a2o(i, all=True)) ), # slices for slice(*(lambda x: [min(x), max(x) + 1])(dh.a2o(j, all=True))), ], # appropriate orbitals rirj=[ dh.axyz()[i], dh.axyz()[j], ], # real space vectors of atoms in the unit cell Rij=np.dot( uc, dh.cell ), # real space distance vector between unit cells rij=np.dot(uc, dh.cell) - dh.axyz()[i] + dh.axyz()[j], # real space vector between atoms Jijz=[], # in this empty list are we going to gather the integrad of the energy integral Jij=0, # the final results of the calculation are going to be here on the root node ) ) if rank == root_node: print("Number of pairs beeing calculated: ", len(pairs)) comm.Barrier() # ---------------------------------------------------------------------- # make energy contour # we are working in eV now ! # and sisil shifts E_F to 0 ! cont = make_contour(emin=args.Ebot, enum=args.eset, p=args.esetp) eran = cont.ze # ---------------------------------------------------------------------- # generating onsite matrix and overalp elements of all the atoms in the unitcell # onsite of the origin supercell orig_indx = np.arange(0, dh.no) + dh.sc_index([0, 0, 0]) * dh.no # spin up uc_up = dh.tocsr(dh.UP)[:, orig_indx].toarray() # spin down uc_down = dh.tocsr(dh.DOWN)[:, orig_indx].toarray() Hs = [] # get number of atoms in the unit cell for i in range(len(dh.atoms)): at_indx = dh.a2o(i, all=True) Hs.append(uc_up[:, at_indx][at_indx, :] - uc_down[:, at_indx][at_indx, :]) # ---------------------------------------------------------------------- # sampling the integrand on the contour and the BZ for pair in pairs: noi, noj = pair["noij"] pair["Guij"] = np.zeros((args.eset, noi, noj), dtype="complex128") pair["Gdji"] = np.zeros((args.eset, noj, noi), dtype="complex128") pair["Guij_tmp"] = np.zeros((args.eset, noi, noj), dtype="complex128") pair["Gdji_tmp"] = np.zeros((args.eset, noj, noi), dtype="complex128") for k in kpcs[rank]: HKU, HKD, SK = hsk(dh, k) Gku = nl.inv(SK * eran.reshape(args.eset, 1, 1) - HKU) Gkd = nl.inv(SK * eran.reshape(args.eset, 1, 1) - HKD) for pair in pairs: phase = np.exp(1j * np.dot(np.dot(k, dh.rcell), pair["Rij"])) si, sj = pair["slij"] pair["Guij_tmp"] += Gku[:, si, sj] * phase * wk pair["Gdji_tmp"] += Gkd[:, sj, si] / phase * wk # summ reduce partial results of mpi nodes for pair in pairs: comm.Reduce(pair["Guij_tmp"], pair["Guij"], root=root_node) comm.Reduce(pair["Gdji_tmp"], pair["Gdji"], root=root_node) if rank == root_node: for pair in pairs: i, j = pair["aiij"] # The Szunyogh-Lichtenstein formula pair["Jijz"] = np.trace( (Hs[i] @ pair["Guij"]) @ (Hs[j] @ pair["Gdji"]), axis1=1, axis2=2 ) # evaluation of the contour integral pair["Jij"] = np.trapz(np.imag(pair["Jijz"] * cont.we) / (2 * np.pi)) end = timer() # ---------------------------------------------------------------------- # and saveing output of the calculation np.savetxt( args.outfile, np.array( [ [nl.norm(p["rij"]), p["Jij"] * sisl.unit_convert("eV", "Ry") * 1000] + p["aiij"] + list(p["offset"]) + list(p["rij"]) for p in pairs ], dtype=object, ), header=str(args) + "\nnumber of cores = " + str(size) + "\ntime of calculation = " + str(end - start) + "\nnorm(rij),Jij[mRy],aiij,offset,rij", fmt="%s", )