##########################################################################
#
# TRIQS: a Toolbox for Research in Interacting Quantum Systems
#
# Copyright (C) 2011 by M. Aichhorn, L. Pourovskii, V. Vildosola
#
# TRIQS is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# TRIQS is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# TRIQS. If not, see <http://www.gnu.org/licenses/>.
#
##########################################################################
"""
Wannier90 to HDF5 converter for TRIQS/dft_tools
Written by Gabriele Sclauzero (Materials Theory, ETH Zurich), Dec 2015 -- Jan 2016,
updated by Maximilian Merkel (Materials Theory, ETH Zurich), Aug 2020 -- Feb 2022,
and by Sophie Beck (Materials Theory, ETH Zurich), Sep 2020 -- Apr 2021,
under the supervision of Claude Ederer (Materials Theory).
Partially based on previous work by K. Dymkovski and the TRIQS/dft_tools team.
Limitations of the current implementation:
- the T rotation matrices are not used in this implementation
Things to be improved/checked:
- the case with SP=1 is only half implemented and never tested (do we need to
define rot_mat_time_inv also if symm_op = 0?)
- the calculation of rot_mat in find_rot_mat() relies on the eigenvalues of H(0);
this might fail in presence of degenerate eigenvalues (now just prints warning)
- make the code more MPI safe (error handling): if we run with more than one process
and an error occurs on the masternode, the calculation does not abort
- in case of disentanglement, the outer window being close to Kohn-Sham energies
can cause a problem in creating the udis_mat_spin in read_wannier90data
- bloch_basis on SO coupled calculations has never been tested but might work
- would be helpful to read the order of orbitals from the nnkp or wout file
and save it to, e.g., misc_subgrp for codes working on the generated h5
"""
import os.path
from itertools import product
import numpy as np
from h5 import HDFArchive
from triqs.utility import mpi
from .converter_tools import ConverterTools
[docs]
class Wannier90Converter(ConverterTools):
"""
Conversion from Wannier90 output to an hdf5 file that can be used as input
for the SumkDFT class.
"""
[docs]
def __init__(self, seedname, hdf_filename=None, dft_subgrp='dft_input',
symmcorr_subgrp='dft_symmcorr_input', misc_subgrp='dft_misc_input',
repacking=False, rot_mat_type='hloc_diag', bloch_basis=False, add_lambda=None,
w90zero=2e-6, reorder_orbital_and_spin_vasp5=False):
r"""
Initialise the class.
Parameters
----------
seedname : string
Base name of Wannier90 files
hdf_filename : string, optional
Name of hdf5 archive to be created
dft_subgrp : string, optional
Name of subgroup storing necessary DFT data
symmcorr_subgrp : string, optional
Name of subgroup storing correlated-shell symmetry data
misc_subgrp : string, optional
Name of subgroup storing miscellaneous DFT data.
repacking : boolean, optional
Does the hdf5 archive need to be repacked to save space?
rot_mat_type : string, optional
Type of rot_mat used
Can be 'hloc_diag', 'wannier', 'none'
bloch_basis : boolean, optional
Should the Hamiltonian be written in Bloch rather than Wannier basis?
add_lambda : list of floats, optional
Add local spin-orbit term
w90zero : float, optional
Threshold on symmetry checks of Hamiltonian and rot_mat
reorder_orbital_and_spin_vasp5 : bool, optional
Is false for output from VASP 6 and Quantum Espresso
Reorder orbitals and spins from the VASP 5 convention of first all
orbitals with up and then all orbitals with down to the "usual"
convention of every up orbital immediately being followed by its
corresponding down orbital
"""
self._name = 'Wannier90Converter'
assert isinstance(seedname, str), self._name + \
': Please provide the DFT files\' base name as a string.'
if hdf_filename is None:
hdf_filename = seedname + '.h5'
self.hdf_file = hdf_filename
# if the w90 output is seedname_hr.dat, the input file for the
# converter must be called seedname.inp
self.inp_file = seedname + '.inp'
self.w90_seed = seedname
self.dft_subgrp = dft_subgrp
self.symmcorr_subgrp = symmcorr_subgrp
self.misc_subgrp = misc_subgrp
self.fortran_to_replace = {'D': 'E'}
# threshold below which matrix elements from wannier90 should be
# considered equal
self._w90zero = w90zero
self.rot_mat_type = rot_mat_type
self.bloch_basis = bloch_basis
self.add_lambda = add_lambda
self.reorder_orbital_and_spin_vasp5 = reorder_orbital_and_spin_vasp5
if self.add_lambda is not None and len(self.add_lambda) != 3:
raise ValueError('If specifying add_lambda, give three values.')
if self.rot_mat_type not in ('hloc_diag', 'wannier', 'none'):
raise ValueError('Parameter rot_mat_type invalid, should be one of'
+ '"hloc_diag", "wannier", "none"')
# Checks if h5 file is there and repacks it if wanted:
if (os.path.exists(self.hdf_file) and repacking):
ConverterTools.repack(self)
[docs]
def check_and_adapt_for_soc(shells, corr_shells, bloch_basis, add_lambda):
"""
Checks compatibilities, modifies shells and corr_shells and sets variables
needed for spin-orbit coupled systems.
"""
# Determines if any shell requires SO
if any(sh['SO'] == 1 for sh in corr_shells):
SO = 1
SP = 1
mpi.report('Spin-orbit interaction turned on')
else:
SO = 0
SP = 0
# Only one block supported - either non-spin-polarized or spin-orbit coupled
assert SP == SO, 'Spin-polarized calculations not implemented'
# If adding a local SOC term, turn on SOC
if add_lambda:
assert all(sh['dim'] == 3 for sh in corr_shells), 'add_lambda only implemented for t2g shell'
assert SO == SP == 0, 'add_lambda not implemented for SO = SP = 1'
assert not bloch_basis, 'add_lambda not implemented for bloch_basis = True'
# now setting SO and SP to 1
SO = SP = 1
# this is more general
n_spin_blocks = SP + 1 - SO
assert n_spin_blocks > 0, 'Input error, if SO=1, SP must be 1.'
# Doubles dimension of all shells if SOC is on
if SO == 1:
for shell_list in [shells, corr_shells]:
for entry in shell_list:
entry['dim'] *= 2
if 'SO' in entry.keys() and add_lambda:
entry['SO'] = 1
return shells, corr_shells, SO, SP, n_spin_blocks
[docs]
def read_wannier90_hr_data(wannier_seed):
"""
Method for reading the seedname_hr.dat file produced by Wannier90 (http://wannier.org).
If spin polarized, reads in the properties for a given spin
Parameters
----------
wannier_seed : string
seedname to read H(R) file produced by Wannier90, seedname_hr.dat
Returns
-------
n_r_spin : integer
number of R vectors found in the file
r_vector_spin : np.ndarray of integers
Miller indices of the R vectors
r_degeneracy_spin : np.ndarray of floats
weight of the R vectors
n_wannier_spin : integer
number of Wannier functions found
wannier_hr_spin : np.ndarray, # R points x # wannier funcs x # wannier funcs
<w_i|H(R)|w_j> = Hamilonian matrix elements in the Wannier basis
"""
hr_filename = wannier_seed + '_hr.dat'
with open(hr_filename, 'r') as file:
hr_data = file.readlines()
mpi.report('Reading {}: {}'.format(hr_filename, hr_data[0].strip()))
n_wannier_spin = int(hr_data[1])
n_r_spin = int(hr_data[2])
r_vector_spin = np.zeros((n_r_spin, 3), dtype=int)
r_degeneracy_spin = np.zeros(n_r_spin, dtype=int)
wannier_hr_spin = np.zeros((n_r_spin, n_wannier_spin, n_wannier_spin), dtype=complex)
currpos = 2
ir = 0
# read the degeneracy of the R vectors (needed for the Fourier
# transform)
while ir < n_r_spin:
currpos += 1
for x in hr_data[currpos].split():
if ir >= n_r_spin:
raise IndexError('wrong number of R vectors??')
r_degeneracy_spin[ir] = int(x)
ir += 1
# for each direct lattice vector R read the block of the
# Hamiltonian H(R)
for ir, jj, ii in product(range(n_r_spin), range(n_wannier_spin), range(n_wannier_spin)):
# advance one line, split the line into tokens
currpos += 1
cline = hr_data[currpos].split()
# check if the orbital indexes in the file make sense
assert int(cline[3]) == ii + 1 and int(cline[4]) == jj + 1, 'Inconsistent indices for R vector n. {}'.format(ir)
rcurr = np.array([int(cline[0]), int(cline[1]), int(cline[2])])
if ii == 0 and jj == 0:
r_vector_spin[ir] = rcurr
rprev = rcurr
else:
# check if the vector indices are consistent
assert np.all(rcurr == rprev), 'Inconsistent indices for R vector n. {}'.format(ir)
# fill wannier_hr_spin with the matrix elements of the Hamiltonian
wannier_hr_spin[ir, ii, jj] = complex(float(cline[5]), float(cline[6]))
return n_r_spin, r_vector_spin, r_degeneracy_spin, n_wannier_spin, wannier_hr_spin
[docs]
def read_wannier90_blochbasis_data(wannier_seed, n_wannier_spin):
"""
Method for reading the files needed in the bloch_basis: seedname_u.mat,
seedname.eig and potentially seedname_u_dis.mat.
Parameters
----------
wannier_seed : string
seedname to Wannier90 output
Returns
-------
u_mat_spin : np.ndarray
U_mn^k = unitary matrix elements which mix the Kohn-Sham states
udis_mat_spin : np.ndarray
U^dis(k) = rectangular matrix for entangled bands
ks_eigenvals_spin : np.ndarray
\epsilon_nk = Kohn-Sham eigenvalues (in eV) needed for entangled bands
k_mesh : np.ndarray
The k mesh read from the seedname_u.mat file to ensure consistency
"""
mpi.report('Writing h5 archive in projector formalism: H(k) defined in KS Bloch basis')
# ------------- Reads seedname_u.mat
# first, read u matrices from 'seedname_u.mat'
u_filename = wannier_seed + '_u.mat'
with open(u_filename,'r') as u_file:
u_data = u_file.readlines()
# reads number of kpoints and number of wannier functions
n_k, n_wannier_spin_u, _ = map(int, u_data[1].split())
assert n_wannier_spin_u == n_wannier_spin, '#WFs must be identical for *_u.mat and *_hr.dat'
mpi.report('Reading {}: {}'.format(u_filename, u_data[0].strip()))
# Reads k mesh from all lines with 3 floats
k_mesh = np.loadtxt((line for line in u_data if line.count('.') == 3), ndmin=2)
assert k_mesh.shape == (n_k, 3)
# Reads u matrices from all lines with 2 floats
u_mat_spin = np.loadtxt((line for line in u_data if line.count('.') == 2))
assert u_mat_spin.shape == (n_k*n_wannier_spin*n_wannier_spin, 2)
# first, get the input for u_mat_spin
u_mat_spin = u_mat_spin[:, 0] + 1j * u_mat_spin[:, 1]
u_mat_spin = u_mat_spin.reshape((n_k, n_wannier_spin, n_wannier_spin)).transpose((0, 2, 1))
# ------------- Reading seedname.eig
band_filename = wannier_seed + '.eig'
# read Kohn-Sham eigenvalues from 'seedname.eig'
mpi.report('Reading {}'.format(band_filename))
band_data = np.loadtxt(band_filename, usecols=2)
ks_eigenvals_spin = band_data.reshape(n_k, -1)
# ------------- Reading seedname_u_dis.mat
udis_filename = wannier_seed + '_u_dis.mat'
disentangle = os.path.isfile(udis_filename)
if disentangle:
with open(udis_filename,'r') as udis_file:
udis_data = udis_file.readlines()
else:
mpi.report('WARNING: File {} missing.'.format(udis_filename))
mpi.report('Assuming an isolated set of bands. Check if this is what you want!')
if disentangle:
# if disentangle the window is needed
wout_filename = wannier_seed + '.wout'
# reads number of kpoints, number of wannier functions and bands
num_k_udis, n_wannier_spin_udis, num_ks_bands = map(int, udis_data[1].split())
assert num_k_udis == n_k, '#k points must be identical for *.inp and *_u_dis.mat'
assert n_wannier_spin_udis == n_wannier_spin, '#WFs must be identical for *_u.mat and *_hr.dat'
mpi.report('Reading {}: {}, '.format(udis_filename, udis_data[0].strip()))
udis_data = np.loadtxt(udis_data, usecols=(0, 1), skiprows=2)
# read disentanglement window from 'seedname.wout'
with open(wout_filename) as wout_file:
for line in wout_file:
if 'Outer:' in line:
content = line.split()
index = content.index('Outer:') + 1
dis_window_min = float(content[index])
dis_window_max = float(content[index+2])
break
mpi.report('and {} for disentanglement energy window.'.format(wout_filename))
else:
num_ks_bands = n_wannier_spin
# now, check what is needed in the case of disentanglement:
# The file seedname_u_dis.mat contains only the bands inside the window
# and then fills the rest up with zeros. Therefore, we need to put the
# entries from udis_data in the correct position in udis_mat_spin, i.e.
# shifting by the number of bands below dis_window_min
# reshape band_data
assert ks_eigenvals_spin.shape[1] == num_ks_bands, '.eig and u_dis.mat data inconsistent'
if disentangle:
# In case the disentanglement window is not set by the user, change manually both limits to
# larger window to avoid possible counting error in next line
dis_tol = 1e-5
shift_dis_down = np.any(np.isclose(np.min(ks_eigenvals_spin, axis=1), dis_window_min, atol=dis_tol, rtol=0.) == True)
shift_dis_up = np.any(np.isclose(np.max(ks_eigenvals_spin, axis=1), dis_window_max, atol=dis_tol, rtol=0.) == True)
if shift_dis_down or shift_dis_up:
if shift_dis_down:
mpi.report('WARNING: dis_win_min too close to value in .eig, manually shifting it down by '
+ '{} to avoid counting error'.format(2*dis_tol))
dis_window_min -= 2*dis_tol
if shift_dis_up:
mpi.report('WARNING: dis_win_max too close to value in .eig, manually shifting it up by '
+ '{} to avoid counting error'.format(2*dis_tol))
dis_window_max += 2*dis_tol
mpi.report('This can happen when the user did not specify disentanglement windows in .win. '
+ 'Check if this is the case here.')
# Determine which bands are inside the band window
inside_window = np.logical_and(ks_eigenvals_spin >= dis_window_min,
ks_eigenvals_spin <= dis_window_max)
n_inside_per_k = np.sum(inside_window, axis=1)
# Reformats udis_data as complex, without header
udis_data = udis_data[:, 0] + 1j * udis_data[:, 1]
udis_data = udis_data.reshape((n_k, n_wannier_spin*num_ks_bands+1))[:, 1:]
udis_data = udis_data.reshape((n_k, n_wannier_spin, num_ks_bands))
#initiate U disentanglement matrices and fill from file 'seedname_u_dis.mat'
udis_mat_spin = np.zeros([n_k, num_ks_bands, n_wannier_spin], dtype=complex)
for ik in range(n_k):
# this assumes that u_dis_mat first index is the first state in the dis window
# which is different from the eig file which starts with the first KS ev given to w90
udis_mat_spin[ik, inside_window[ik]] = udis_data[ik, :, :n_inside_per_k[ik]].T
if not np.allclose(udis_data[ik, :, n_inside_per_k[ik]:], 0):
raise ValueError('This error could come from rounding of the band window in the seedname.wout. '
+ 'Never use default outer window but something wider and '
+ 'check that your outer window is not close to any band energy.')
else:
# no disentanglement; fill udis_mat_spin with identity
udis_mat_spin = np.array([np.identity(n_wannier_spin, dtype=complex)] * n_k)
# return the data into variables
return u_mat_spin, udis_mat_spin, ks_eigenvals_spin, k_mesh
[docs]
def read_all_wannier90_data(n_spin_blocks, dim_corr_shells, w90_seed, add_lambda, bloch_basis):
"""
Reads in all the wannier90 data using the functions read_wannier90_hr_data
and read_wannier90_blochbasis_data. Reads in everything for each spin
channel (into the variables marked with _spin) and runs consistency checks
on them or combines them.
Returns
-------
wannier_hr : np.ndarray[n_spin_blocks, n_r, n_wannier, n_wannier] of complex
Hamilonian matrix elements in the Wannier basis
u_total : np.ndarray[n_spin_blocks, n_k, n_wannier, n_bands] of complex
The projection matrix as read from wannier. None if not bloch_basis
ks_eigenvals : np.ndarray[n_spin_blocks, n_k, n_bands] of float
The KS eigenvalues of the bands per k point. None if not bloch_basis
r_vector : np.ndarray[n_r, 3] of int
The r vectors of the wannier Hamiltonian
r_degeneracy : np.ndarray[n_r] of int
The degeneracy per r point
n_wannier: int
Number of wannier functions
n_bands : int
Number of bands
k_mesh_from_umat : np.ndarray[n_k, 3] of float
The k points as used in wannier for consistency. None if not bloch_basis
"""
spin_w90name = ['_up', '_down']
wannier_hr = []
if bloch_basis:
u_mat = []
udis_mat = []
ks_eigenvals = []
for isp in range(n_spin_blocks):
# build filename according to wannier90 conventions
if n_spin_blocks == 2:
mpi.report('Reading information for spin component n. {}'.format(isp))
file_seed = w90_seed + spin_w90name[isp]
else:
file_seed = w90_seed
# now grab the data from the wannier90 output files
mpi.report('\nThe Hamiltonian in MLWF basis is extracted from {} files'.format(file_seed))
w90_hr_results = None
if mpi.is_master_node():
w90_hr_results = read_wannier90_hr_data(file_seed)
n_r_spin, r_vector_spin, r_degeneracy_spin, n_wannier_spin, wannier_hr_spin = mpi.bcast(w90_hr_results)
if bloch_basis:
w90_results = None
if mpi.is_master_node():
w90_results = read_wannier90_blochbasis_data(file_seed, n_wannier_spin)
# number of R vectors, their indices, their degeneracy, number of WFs, H(R),
# U matrices, U(dis) matrices, band energies, k_mesh of U matrices
u_mat_spin, udis_mat_spin, ks_eigenvals_spin, k_mesh_from_umat = mpi.bcast(w90_results)
mpi.report('\n... done: {} R vectors, {} WFs found'.format(n_r_spin, n_wannier_spin))
if add_lambda:
mpi.report('Adding local spin-orbit term to Hamiltonian (assuming dxz, dyz, dxy as orbital order)')
# doubling number of wannier functions
n_wannier_spin *= 2
if isp == 0:
# set the R vectors and their degeneracy
n_r = n_r_spin
r_vector = r_vector_spin
r_degeneracy = r_degeneracy_spin
n_wannier = n_wannier_spin
# check that the total number of WFs makes sense
if n_wannier < dim_corr_shells:
mpi.report('ERROR: number of WFs in the file smaller than number of correlated orbitals!')
elif n_wannier > dim_corr_shells:
# NOTE: correlated shells must appear before uncorrelated
# ones inside the file
mpi.report('Number of WFs larger than correlated orbitals:',
'WFs from {} to {} treated as uncorrelated'.format(dim_corr_shells + 1, n_wannier))
else:
mpi.report('Number of WFs equal to number of correlated orbitals')
# we assume spin up and spin down always have same total number
# of WFs
# get second dimension of udis_mat_spin which corresponds to number of bands in window
if bloch_basis:
n_bands = udis_mat_spin.shape[1]
if add_lambda:
n_bands *= 2
else:
n_bands = n_wannier
else:
# consistency check between the _up and _down file contents
assert n_r_spin == n_r, 'Different number of R vectors for spin-up/spin-down!'
assert n_wannier_spin == n_wannier, 'Different number of WFs for spin-up/spin-down!'
assert np.all(r_vector_spin == r_vector), 'R vectors different between spin components'
assert np.all(r_degeneracy_spin == r_degeneracy), 'R vec. degeneracy different between spin components'
wannier_hr.append(wannier_hr_spin)
if bloch_basis:
u_mat.append(u_mat_spin)
udis_mat.append(udis_mat_spin)
ks_eigenvals.append(ks_eigenvals_spin)
if bloch_basis:
# Definition of projectors in Wannier and Triqs different by Hermitian conjugate
u_total = np.einsum('skab,skbc->skca', udis_mat, u_mat).conj()
ks_eigenvals = np.array(ks_eigenvals)
else:
u_total = None
ks_eigenvals = None
k_mesh_from_umat = None
wannier_hr = np.array(wannier_hr)
return (wannier_hr, u_total, ks_eigenvals, r_vector, r_degeneracy,
n_wannier, n_bands, k_mesh_from_umat)
[docs]
def build_kmesh(kmesh_size, kmesh_mode=0):
"""
Method for the generation of the k-point mesh. Right now it only supports
the option for generating a full grid containing k=0,0,0.
Parameters
----------
kmesh_size : list of 3 integers
the dimensions of the mesh
kmesh_mode : integer
mesh generation mode (right now, only full grid available)
Returns
-------
n_k : integer
total number of k-points in the mesh
kpts : np.ndarray[n_k, 3] of floats
the coordinates of all k-points
kpt_weights : np.ndarray[n_k] of floats
the weight of each k-point
"""
if kmesh_mode != 0:
raise ValueError('Mesh generation mode not supported: {}'.format(kmesh_mode))
assert len(kmesh_size) == 3
# a regular mesh including Gamma point
# total number of k-points
n_k = np.prod(kmesh_size)
kpts = np.array(list(product(range(kmesh_size[0]), range(kmesh_size[1]), range(kmesh_size[2]))))
kpts = kpts / np.array(kmesh_size)
# weight is equal for all k-points because wannier90 uses uniform grid on whole BZ
# (normalization is always 1 and takes into account spin degeneracy)
kpt_weights = np.full(n_k, 1/n_k)
return n_k, kpts, kpt_weights
[docs]
def reorder_orbital_and_spin(nwfs, wannier_hr, u_total):
"""
Changes order from VASP5 + wannier90 (first all up spin wannier orbitals,
then all down spin) to usual order (every up orbital is followed directly
by the corresponding down orbital). The order between orbital degrees of
freedom is not changed.
Returns
-------
wannier_hr : np.ndarray
The re-ordered wannier real-space Hamiltonian.
u_total : list of np.ndarray
The re-ordered projection matrix.
"""
reorder = [i+shift for i in range(nwfs//2) for shift in (0, nwfs//2)]
mpi.report('Reordering the orbitals because of SOC, new order:', reorder)
wannier_hr = wannier_hr[:, :, reorder][:, :, :, reorder]
if u_total is not None:
u_total = u_total[:, :, reorder, :]
return wannier_hr, u_total
[docs]
def generate_local_so_matrix_t2g(add_lambda, n_corr_shells, nwfs):
"""
Adds local spin-orbit interaction term to the t2g subspace. Orbital order
is assumed to be the wannier90/triqs order xz_up, xz_dn, yz_up, yz_dn,
xy_up, xy_dn.
Parameters are defined as add_lambda = [lambda_x, lambda_y, lambda_z],
representative of the orbital coupling terms perpendicular to [x, y, z],
i.e., [d_yz, d_xz, d_xy], respectively.
Returns
-------
lambda_matrix : np.ndarray[6, 6] of complex
local spin-orbit term to be added to H(0)
"""
lambda_x, lambda_y, lambda_z = add_lambda
lambda_matrix = np.zeros((nwfs, nwfs), dtype=complex)
for icrsh in range(n_corr_shells):
ind = 6*icrsh
lambda_matrix[ind , ind+2] = -1j*lambda_z/2.0
lambda_matrix[ind , ind+5] = 1j*lambda_x/2.0
lambda_matrix[ind+2, ind+5] = -lambda_y/2.0
lambda_matrix[ind+4, ind+1] = -1j*lambda_x/2.0
lambda_matrix[ind+4, ind+3] = lambda_y/2.0
lambda_matrix[ind+1, ind+3] = 1j*lambda_z/2.0
lambda_matrix += lambda_matrix.T.conj()
return lambda_matrix
[docs]
def check_hr(wannier_hr, w90_zero, r_zero_index):
"""
Checks of the real-space Hamiltonian. Prints a warning if Hamiltonian is
not real and raises a ValueError if Hamiltonian at R=0 is not Hermitian.
"""
# Tests if Hamiltonian is real
imag_components_hr = np.isclose(np.imag(wannier_hr), 0, atol=w90_zero, rtol=0)
index_imag_components = np.nonzero(np.logical_not(np.all(imag_components_hr, axis=(2, 3))))
for ind in np.transpose(index_imag_components):
mpi.report('H(R) has large complex components at R {}'.format(ind[1])
+ ' and spin component {}'.format(ind[0]))
# Checks if Hamiltonian at R=0 is hermitian
wannier_hr0 = wannier_hr[:, r_zero_index]
if not np.allclose(wannier_hr0.transpose((0, 2, 1)).conj(), wannier_hr0, atol=w90_zero, rtol=0):
raise ValueError('H(R=0) matrix is not Hermitian')
[docs]
def find_rot_mat(n_corr_shells, corr_shells, shells_map, wannier_hr0,
rot_mat_type, w90_zero, n_spin_blocks):
"""
Method for finding the matrices that bring from local to global coordinate
systems, based on the eigenvalues of H(R=0).
Returns
-------
rot_mat : list of list of np.ndarray[dim, dim] of complex
Rotation matrix for each of the shell. None if construction failed
"""
rot_mat_full = [None] * n_spin_blocks
for isp in range(n_spin_blocks):
# initialize the rotation matrices to identities
rot_mat = [np.identity(corr_shells[icrsh]['dim'], dtype=complex)
for icrsh in range(n_corr_shells)]
# Method none only physical if no equivalent impurities, otherwise
# potentially useful for debugging
# Returns identity matrices as rotation matrices
if rot_mat_type == 'none':
mpi.report('WARNING: using the method "none" leads to physically wrong results '
+ 'if there is a mapping of multiple correlated shells on one impurity.')
rot_mat_full[isp] = rot_mat
continue
# TODO: better handling of degenerate eigenvalue case
hr0_list = [None] * n_corr_shells
eigval_lst = [None] * n_corr_shells
eigvec_lst = [None] * n_corr_shells
ind = 0
for icrsh in range(n_corr_shells):
dim = corr_shells[icrsh]['dim']
# Saves the sub-block of H(0) corresponding to this shell
hr0_list[icrsh] = np.zeros((dim, dim), dtype=complex)
hr0_list[icrsh] = wannier_hr0[isp, ind:ind+dim, ind:ind+dim]
ind += dim
# Diagonalizes the sub-block for this shell
eigval_lst[icrsh], eigvec_lst[icrsh] = np.linalg.eigh(hr0_list[icrsh])
# Checks for degenerate eigenvalues if there are equivalent shells
# TODO: better handling of degenerate eigenvalue case
for iineq in set(shells_map):
if shells_map.count(iineq) > 1:
icrsh = shells_map.index(iineq)
dim = corr_shells[icrsh]['dim']
if any(abs(eigval_lst[icrsh][j] - eigval_lst[icrsh][i]) < w90_zero
for i in range(dim) for j in range(i+1, dim)):
mpi.report('WARNING: degenerate eigenvalue of H(0) detected for shell {}: '.format(icrsh) +
'global-to-local transformation might not work!')
for icrsh in range(n_corr_shells):
# build rotation matrices either...
if rot_mat_type == 'hloc_diag':
# using the unitary transformations that diagonalize H(0)
rot_mat[icrsh] = eigvec_lst[icrsh]
elif rot_mat_type == 'wannier':
# or by combining those transformations (i.e. for each group,
# the representative site is chosen as the global frame of reference)
rot_mat[icrsh] = np.dot(eigvec_lst[icrsh], eigvec_lst[shells_map[icrsh]].T.conj())
# check that eigenvalues are the same (within accuracy) for
# equivalent shells
if not np.allclose(eigval_lst[icrsh], eigval_lst[shells_map[icrsh]], atol=w90_zero, rtol=0):
mpi.report(f'ERROR: eigenvalue mismatch between equivalent shells! {icrsh}, {shells_map[icrsh]}')
eigval_diff = eigval_lst[icrsh] - eigval_lst[shells_map[icrsh]]
mpi.report(f'Eigenvalue difference {eigval_diff}, but threshold set to {w90_zero:.1e}.')
mpi.report('Consider lowering threshold if you are certain the mapping is correct.')
return None
# check that rotation matrices are unitary
# dim = number of orbitals in this shell
dim = corr_shells[icrsh]['dim']
tmp_mat = np.dot(rot_mat[icrsh], rot_mat[icrsh].conj().T)
if not np.allclose(tmp_mat, np.identity(dim), atol=w90_zero, rtol=0):
mpi.report(f'ERROR: rot_mat for shell {icrsh:d} is not unitary!')
return None
# check that rotation matrices map equivalent H(0) blocks as they should
# (assuming representative shell as global frame of reference)
if rot_mat_type == 'hloc_diag':
tmp_mat = np.dot(rot_mat[icrsh], rot_mat[shells_map[icrsh]].conj().T)
elif rot_mat_type == 'wannier':
tmp_mat = rot_mat[icrsh]
tmp_mat = np.dot(tmp_mat.conj().T, np.dot(hr0_list[icrsh], tmp_mat))
if not np.allclose(tmp_mat, hr0_list[shells_map[icrsh]], atol=w90_zero, rtol=0):
mpi.report(f'ERROR: rot_mat does not map H(0) correctly! {icrsh:d}')
return None
rot_mat_full[isp] = rot_mat
# Equality check between rot_mats for different spins
if n_spin_blocks == 2 and not all(np.allclose(r0, r1, atol=w90_zero, rtol=0)
for r0, r1 in zip(rot_mat_full[0], rot_mat_full[1])):
mpi.report('Rotations between spin components do not match!')
return None
return rot_mat_full[0]
[docs]
def check_bloch_basis_hk(n_corr_shells, corr_shells, n_k, n_spin_blocks, n_bands,
proj_mat, dim_corr_shells, wannier_hk, hopping):
"""
Check of the reciprocal-space Hamiltonian in bloch basis. Prints warning if
the local downfolded Hamiltonian with the projector method does not
correspond to the W90 result.
"""
proj_mat_flattened = np.zeros((n_k, n_spin_blocks, dim_corr_shells, n_bands), dtype=complex)
iorb = 0
for icrsh in range(n_corr_shells):
dim = corr_shells[icrsh]['dim']
proj_mat_flattened[:, :, iorb:iorb+dim, :] = proj_mat[:, :, icrsh, :dim, :]
iorb += dim
downfolded_ham = np.einsum('ksab,ksbc,ksdc->skad', proj_mat_flattened, hopping,
proj_mat_flattened.conj())
if dim_corr_shells < n_bands:
wannier_ham_corr = wannier_hk[:, :, :dim_corr_shells, :dim_corr_shells]
else:
wannier_ham_corr = wannier_hk
hks_are_equal = np.isclose(downfolded_ham, wannier_ham_corr, atol=1e-4, rtol=0)
if not np.all(hks_are_equal):
index_difference = np.nonzero(np.logical_not(np.all(hks_are_equal, axis=2)))
isp = index_difference[0][0]
ik = index_difference[1][0]
mpi.report('WARNING: mismatch between downfolded Hamiltonian and Fourier transformed '
+ 'H(R). First occurred at kpt {} and spin {}:'.format(ik, isp))
with np.printoptions(formatter={'complexfloat': '{:+.4f}'.format}):
mpi.report('Downfolded Hamiltonian, P H_eig P')
mpi.report(downfolded_ham[isp, ik])
mpi.report('\nWannier Hamiltonian, Fourier(H(r))')
mpi.report(wannier_ham_corr[isp, ik])
[docs]
def check_wannier_basis_hk(hopping, dim_corr_shells):
"""
Check of the reciprocal-space Hamiltonian in wannier basis. Raises error
if imaginary diagonal elements are not zero, because otherwise there can be
instabilties in lattice Gf.
"""
#TODO: do we want to apply this on bloch_basis downfolded Hamiltonian as well?
diag_iterator = range(dim_corr_shells)
hk_has_imag_diag = np.isclose(hopping[:, :, diag_iterator, diag_iterator].imag, 0, atol=1e-10, rtol=0)
if not np.all(hk_has_imag_diag):
index_imag = np.nonzero(np.logical_not(np.all(hk_has_imag_diag, axis=2)))
ik, isp = np.transpose(index_imag)[0]
mpi.report('ERROR: Wannier Hamiltonian has complex diagonal entries. '
+ 'First occurred at kpt {} and spin {}:'.format(ik, isp))
with np.printoptions(formatter={'float': '{:+.10f}'.format}):
mpi.report('\nWannier Hamiltonian diagonal, Fourier(H(r)), imaginary')
mpi.report(hopping[ik, isp, diag_iterator, diag_iterator].imag)
raise ValueError