import numpy as np
from warnings import warn
import tbmodels as tbm
import pythtb as ptb
import wannierberri as wberri
from .config import USE_MPI, mpimaster
from .classes import Model
from .postprocessing import pbc_lattice_contraction, average_over_radius
# For backward compatibility with old PythTB versions
ptb_model = ptb.tb_model if ptb.__version__ < "2.0.0" else ptb.tb_model | ptb.TBModel
[docs]
class Supercell(Model):
r"""A class describing a supercell (PBC) of a given model."""
def __init__(
self, model: Model, Lx: int = 1, Ly: int = 1, n_occ: int = None, **kwargs
):
r"""Create a supercell of a given :python:`strawberrypy.Model`, allowing
the calculation of single-point invariants and local geometrical and
topological markers within periodic boundary conditions.
Parameters
----------
model :
A :python:`strawberrypy.Model` instance.
Lx :
Number of unit cells repeated along the :math:`\mathbf{a}_1` direction in
the supercell.
Ly :
Number of unit cells repeated along the :math:`\mathbf{a}_2` direction in
the supercell.
n_occ :
Number of occupied states. If :python:`None`, this is determined based on
the original model.
kwargs :
Additional keyword arguments of the parent class :python:`Model`.
"""
# Store the new dimension of the supercell
self.Lx, self.Ly = Lx, Ly
# For backward compatibility, will be removed eventually
if not isinstance(model, Model):
warn(
"Passing a 3rd party model directly to the Supercell class is deprecated and"
"will be removed in future versions. Please create a strawberrypy.Model"
"instance first and then use the appropriate method to create a Supercell.",
DeprecationWarning,
stacklevel=2,
)
from .backends import get_backend
spinful = kwargs.get("spinful", False)
is_master_rank = get_backend(nblk=Lx)[0].is_master_rank
deprecating = True
else:
spinful = model.spinful
is_master_rank = model.backend.is_master_rank
deprecating = False
# Create supercell
if is_master_rank:
if deprecating:
self.model = self._make_supercell(model)
from . import _tbmodels, _pythtb, _wberri
if isinstance(model, tbm.Model):
st_uc = _tbmodels.calc_states_uc(model)
elif isinstance(model, ptb_model):
st_uc = _pythtb.calc_states_uc(model, spinful)
elif isinstance(model, wberri.System_R):
st_uc = _wberri.calc_states_uc(model)
else:
raise NotImplementedError("Invalid model instance.")
model.states_uc = st_uc
else:
self.model = self._make_supercell(model.model)
else:
self.model = None
super().__init__(
model=self.model,
spinful=spinful,
Lx=self.Lx,
Ly=self.Ly,
n_occ=n_occ,
states_uc=model.states_uc,
disordered=model.disordered,
has_vacancies=model.has_vacancies,
is_crystalline=model.is_crystalline,
**kwargs,
)
def _make_supercell(self, model):
r"""Create :math:`Lx \times Ly` supercell for a 2D tight-binding model.
If a 3D tight-binding model is provided, the supercell is created in the *xy*-plane.
Parameters
----------
model :
A tight-binding model instance.
Returns
-------
supercell_model :
A tight-binding model instance of the supercell.
"""
if self.Lx != 1 or self.Ly != 1:
if isinstance(model, tbm.Model):
return (
model.supercell([self.Lx, self.Ly])
if model.dim == 2
else model.supercell([self.Lx, self.Ly, 1])
)
elif isinstance(model, ptb_model):
# For backward compatibility with old PythTB versions
dimr = model.dim_r if hasattr(model, "dim_r") else model._dim_r
return (
model.make_supercell([[self.Lx, 0], [0, self.Ly]])
if dimr == 2
else model.make_supercell(
[[self.Lx, 0, 0], [0, self.Ly, 0], [0, 0, 1]]
)
)
elif isinstance(model, wberri.System_R):
# TODO
raise NotImplementedError(
"Supercell construction for wannierberri models is not implemented yet."
)
else:
raise ValueError("Invalid model instance.")
else:
return model
def _periodic_gauge(self, u_n0, b, glob_shape_u_n0, n_occ=None) -> np.ndarray:
r"""Returns the matrix of occupied eigenvectors at the edge of the
Brillouin zone imposing periodic gauge upon the eigenvectors at
:math:`\Gamma`.
Parameters
----------
u_n0 :
Matrix of Hamiltonian eigenstates at :math:`\Gamma`-point.
:math:`\mathbf{b}` :
Reciprocal lattice vector indicating the edge of the Brillouin zone.
glob_shape_u_n0:
Global shape (rows,cols) of the Hamiltonian eigenstates without any
slicing or operations to it.
n_occ :
Number of occupied states.
Returns
-------
np.ndarray
Matrix of occupied eigenvectors at the edge of the Brillouin zone
obtained by imposing periodic gauge upon the eigenvectors at
:math:`\Gamma`. The shape of the returned matrix is (n_occ, n_orb).
"""
if n_occ is None:
n_occ = self.n_occ
vec_exp_b = np.exp(-1.0j * self.cart_positions @ b).ravel()
vec_exp_b = self.backend.distribute_diag(vec_exp_b)
u_nb = self.backend.matmul(
u_n0,
vec_exp_b,
glob_shape_u_n0,
(self.n_orb, self.n_orb),
"T",
"N",
[[0, self.n_orb], [0, n_occ]],
)
return u_nb # shape: (n_occ, n_orb)
def _dual_state(
self, u_n0, u_nb, glob_shape_u_n0, spin=None, n_sub=None, n_occ=None
) -> np.ndarray:
r"""Returns the "dual states" reported in Eq. (8) in Ref. `Ceresoli-Resta (2007)
<https://journals.aps.org/prb/abstract/10.1103/PhysRevB.76.012405>`_ for the
calculation of the single-point Chern number and those in Eq. (10) in Ref.
`Favata-Marrazzo (2023) <https://iopscience.iop.org/article/10.1088/2516-1075/acba6f/meta>`_
for the calculation of the single-point spin Chern number.
Parameters
----------
u_n0 :
Matrix of Hamiltonian eigenstates at :math:`\Gamma`-point.
u_nb :
Matrix of Hamiltonian eigenstates at :math:`\mathbf{b}`-point at the
edge of the Brillouin zone.
glob_shape_u_n0:
Global shape (rows,cols) of the Hamiltonian eigenstates without any
slicing or operations to it.
spin :
In case of :python:`spinful` system, indicates which sector of spin
projected operator spectra is considered in the calculation of the
single-point spin Chern number. If :python:`spin` is :python:`'up'`
(:python:`'down'`), eigenstates of spin projected operator with positive
(negative) eigenvalues are considered.
n_sub :
Number of states used in the single-point invariant calculation. If
:python:`n_sub == None`, the number of occupied eigenstates without
smearing is used for the calculation of the single-point Chern number
and the number of eigenstates of spin projected operator with eigenvalues
of a given sign (:math:`\pm`) is used for the calculation of the
single-point spin Chern number.
n_occ:
Number of occupied states. If :python:`n_occ == None`, the number of
occupied eigenstates without smearing is used.
Returns
-------
np.ndarray
Matrix of dual states at the edge of the Brillouin zone
obtained by imposing periodic gauge upon the eigenvectors at
:math:`\Gamma`. The shape of the returned matrix is (n_sub, n_orb).
"""
if n_sub is None:
n_occ = self.n_occ
n_sub = n_occ // 2 if self.spinful else n_occ
else:
if n_occ is None:
n_occ = n_sub * 2 if self.spinful else n_sub
# transpose of the smatrix
s_mat_b_trans = self.backend.matmul(
u_nb,
u_n0.conj(),
[n_occ, self.n_orb],
glob_shape_u_n0,
"N",
"N",
slc_idx_B=[[0, self.n_orb], [0, n_occ]],
)
if spin == "down" or spin is None:
udual_nb = self.backend.linsolve(
s_mat_b_trans,
u_nb,
[n_occ, n_occ],
[n_occ, self.n_orb],
[[0, n_sub], [0, n_sub]],
[[0, n_sub], [0, self.n_orb]],
)
elif spin == "up":
udual_nb = self.backend.linsolve(
s_mat_b_trans,
u_nb,
[n_occ, n_occ],
[n_occ, self.n_orb],
[[n_sub, 2 * n_sub], [n_sub, 2 * n_sub]],
[[n_sub, 2 * n_sub], [0, self.n_orb]],
)
return udual_nb
#################################################
# Single-point invariants
#################################################
[docs]
@mpimaster
def single_point_chern(self, formula: str = "both", return_ham_gap: bool = False):
r"""Evaluate the single-point Chern number provided in Ref.\
`Ceresoli-Resta (2007) <https://journals.aps.org/prb/abstract/10.1103/PhysRevB.76.012405>`_.
Parameters
----------
formula :
Two possible formulas are used for the calculation of the single-point
Chern number: the :python:`'asymmetric'` one is the one originally derived
in Ref. `Ceresoli-Resta (2007)` and uses right-hand discrete derivative;
the :python:`'symmetric'` one uses symmetric derivative centered in
:math:`\Gamma`-point and converges faster to the exact value with the
supercell size. Default value is :python:`'both'`, which returns the
invariants computed with both symmetric and asymmetric formulas.
return_ham_gap :
If :python:`True`, returns the value of the gap between the highest
occupied and the lowest unoccupied eigenstates of the Hamiltonian at
:math:`\Gamma`-point. Default value is :python:`False`.
Returns
-------
chern :
Dictionary with results from :python:`'asymmetric'` and/or :python:`'symmetric'`
formula for the single-point Chern number.
hamiltonian_gap :
Gap between the lowest unoccupied and the highest occupied eigenstates
of the Hamiltonian at :math:`\Gamma`-point if :python:`return_ham_gap == True`.
"""
b1, b2, _ = self.reciprocal_vecs
proj_shape = [self.n_orb, self.n_orb]
# Distribute the Hamiltonian in block-cyclic layout among MPI ranks if using MPI,
# otherwise simply return the global Hamiltonian as the local Hamiltonian.
ham = self.backend.distribute(self.hamiltonian.toarray())
eigvals, u_n0 = self.backend.eigh(
ham,
nev=self.n_occ + 1,
N=self.n_orb,
collect_evec=False,
debug_info=self.debug_info,
)
hamiltonian_gap = (
eigvals[self.n_occ] - eigvals[self.n_occ - 1]
if self.backend.is_master_rank
else None
)
chern = {}
u_nb1 = self._periodic_gauge(u_n0, b1, proj_shape, n_occ=self.n_occ)
udual_nb1 = self._dual_state(u_n0, u_nb1, proj_shape, n_sub=self.n_occ)
del u_nb1
u_nb2 = self._periodic_gauge(u_n0, b2, proj_shape, n_occ=self.n_occ)
udual_nb2 = self._dual_state(u_n0, u_nb2, proj_shape, n_sub=self.n_occ)
del u_nb2
if formula == "asymmetric" or formula == "both":
proj = self.backend.matmul(
udual_nb1.conj(),
udual_nb2,
[self.n_occ, self.n_orb],
[self.n_occ, self.n_orb],
"N",
"T",
)
chern["asymmetric"] = -np.imag(self.backend.trace(proj, self.n_occ)) / np.pi
if formula == "symmetric" or formula == "both":
u_nb1 = self._periodic_gauge(u_n0, -b1, proj_shape, n_occ=self.n_occ)
udual_nmb1 = self._dual_state(u_n0, u_nb1, proj_shape)
del u_nb1
u_nb2 = self._periodic_gauge(u_n0, -b2, proj_shape, n_occ=self.n_occ)
udual_nmb2 = self._dual_state(u_n0, u_nb2, proj_shape)
del u_nb2
proj = self.backend.matmul(
(udual_nmb1 - udual_nb1).conj(),
(udual_nmb2 - udual_nb2),
[self.n_occ, self.n_orb],
[self.n_occ, self.n_orb],
"N",
"T",
)
chern["symmetric"] = -np.imag(self.backend.trace(proj, self.n_occ)) / (
4 * np.pi
)
if return_ham_gap:
return chern, hamiltonian_gap
else:
return chern
[docs]
@mpimaster
def single_point_spin_chern(
self,
spin: str = "down",
formula: str = "both",
return_pszp_gap: bool = False,
return_ham_gap: bool = False,
):
r"""Evaluate the single-point spin Chern number derived in Ref.\ `Favata-Marrazzo
(2023) <https://iopscience.iop.org/article/10.1088/2516-1075/acba6f/meta>`_.
Parameters
----------
spin :
Indicates which sector of the spin projected operator spectra is considered
in the calculation of the single-point spin Chern number. If :python:`spin`
is :python:`'up'` (:python:`'down'`), eigenstates of spin projected operator
with positive (negative) eigevalues are considered. Default value is :python:`'down'`.
formula :
Two possible formulas are used for the calculation of the single-point
spin Chern number: the :python:`'asymmetric'` one uses right-hand discrete
derivative; the :python:`'symmetric'` one uses symmetric derivative centered
in :math:`\Gamma`-point and converges faster to the exact value with the
supercell size. Default value is :python:`'both'`, which returns the
invariants computed with both symmetric and asymmetric formulas.
return_pszp_gap :
If :python:`True`, returns the value of the gap between positive and negative
eigenvalues of the spin operator projected onto occupied states at
:math:`\Gamma`-point. Default value is :python:`False`.
return_ham_gap :
If :python:`True`, returns the value of the gap between the lowest unoccupied
and the highest occupied eigenstates of the Hamiltonian at :math:`\Gamma`-point.
Default value is :python:`False`.
Returns
-------
spin_chern :
Dictionary with results from :python:`'asymmetric'` and/or :python:`'symmetric'`
formula for the single-point spin Chern number.
pszp_gap :
Gap between positive and negative eigenvalues of the spin operator projected
onto occupied states at :math:`\Gamma`-point if :python:`return_pszp_gap == True`.
hamiltonian_gap :
Gap between the lowest unoccupied and the highest occupied eigenstates of
the Hamiltonian at :math:`\Gamma`-point if :python:`return_ham_gap == True`.
"""
n_sub = self.n_occ // 2
b1, b2, _ = self.reciprocal_vecs
proj_shape = [self.n_orb, self.n_orb]
# Distribute the Hamiltonian in block-cyclic layout among MPI ranks if using MPI,
# otherwise simply return the global Hamiltonian as the local Hamiltonian.
ham = self.backend.distribute(self.hamiltonian.toarray())
eigvals, u_n0 = self.backend.eigh(
ham,
nev=self.n_orb,
N=self.n_orb,
collect_evec=False,
debug_info=self.debug_info,
)
hamiltonian_gap = (
eigvals[self.n_occ] - eigvals[self.n_occ - 1]
if self.backend.is_master_rank
else None
)
sz = self.backend.distribute(self.sz.toarray())
tmp = self.backend.matmul(
u_n0.conj(),
sz,
proj_shape,
proj_shape,
"T",
"N",
[[0, self.n_orb], [0, self.n_occ]],
)
del sz
pszp = self.backend.matmul(
tmp,
u_n0,
(self.n_occ, self.n_orb),
proj_shape,
"N",
"N",
slc_idx_B=[[0, self.n_orb], [0, self.n_occ]],
)
del tmp
eval_pszp, evec_pszp = self.backend.eigh(
pszp, self.n_occ, self.n_occ, debug_info=self.debug_info
)
del pszp
spin_chern = {}
pszp_gap = None
if self.backend.is_master_rank:
pszp_gap = eval_pszp[n_sub] - eval_pszp[n_sub - 1]
if pszp_gap < 10 ** (-14):
print("Closing P Sz P gap!")
elif eval_pszp[n_sub] * eval_pszp[n_sub - 1] > 0:
print("P Sz P spectrum is NOT symmetric!")
q_0 = self.backend.matmul(
u_n0,
evec_pszp,
proj_shape,
(self.n_occ, self.n_occ),
"N",
"N",
slc_idx_A=[[0, self.n_orb], [0, self.n_occ]],
)
del evec_pszp
q_b1 = self._periodic_gauge(q_0, b1, [self.n_orb, self.n_occ], n_occ=self.n_occ)
qdual_b1 = self._dual_state(q_0, q_b1, [self.n_orb, self.n_occ], spin)
del q_b1
q_b2 = self._periodic_gauge(q_0, b2, [self.n_orb, self.n_occ], n_occ=self.n_occ)
qdual_b2 = self._dual_state(q_0, q_b2, [self.n_orb, self.n_occ], spin)
del q_b2
if spin.lower() == "down":
slc_idx_states = [[0, n_sub], [0, self.n_orb]]
elif spin.lower() == "up":
slc_idx_states = [[n_sub, 2 * n_sub], [0, self.n_orb]]
# For scalapack linear solver, the dual state vector shape is preserved. For
# spin up (down), only the lower (upper) half of the dual state vector is
# used, but the shape is still (n_occ, n_orb) instead of (n_sub, n_orb).
# This will have a problem when using scipy/numpy linear solvers, which expect
# the shape of the vector to be (n_sub, n_orb). To avoid this problem, we
# slice the dual state vector to have the shape (n_sub, n_orb) when using
# scipy/numpy linear solvers
if not USE_MPI:
slc_idx_states = [[0, n_sub], [0, self.n_orb]]
if formula == "asymmetric" or formula == "both":
proj = self.backend.matmul(
qdual_b1.conj(),
qdual_b2,
[self.n_occ, self.n_orb],
[self.n_occ, self.n_orb],
"N",
"T",
slc_idx_states,
slc_idx_states,
)
spin_chern["asymmetric"] = -np.imag(self.backend.trace(proj, n_sub)) / np.pi
if formula == "symmetric" or formula == "both":
q_b1 = self._periodic_gauge(
q_0, -b1, [self.n_orb, self.n_occ], n_occ=self.n_occ
)
qdual_mb1 = self._dual_state(q_0, q_b1, [self.n_orb, self.n_occ], spin)
del q_b1
q_b2 = self._periodic_gauge(
q_0, -b2, [self.n_orb, self.n_occ], n_occ=self.n_occ
)
qdual_mb2 = self._dual_state(q_0, q_b2, [self.n_orb, self.n_occ], spin)
del q_b2
proj = self.backend.matmul(
(qdual_mb1 - qdual_b1).conj(),
(qdual_mb2 - qdual_b2),
[self.n_occ, self.n_orb],
[self.n_occ, self.n_orb],
"N",
"T",
slc_idx_states,
slc_idx_states,
)
spin_chern["symmetric"] = -np.imag(self.backend.trace(proj, n_sub)) / (
4 * np.pi
)
if return_pszp_gap and return_ham_gap:
return (spin_chern, pszp_gap, hamiltonian_gap)
elif return_pszp_gap:
return spin_chern, pszp_gap
elif return_ham_gap:
return spin_chern, hamiltonian_gap
else:
return spin_chern
#################################################
# Local PBC marker
#################################################
[docs]
@mpimaster
def pbc_local_chern_marker(
self,
direction: int = None,
start: int = 0,
return_projector: bool = False,
input_projector=None,
formula: str = "symmetric",
macroscopic_average: bool = False,
cutoff: float = 0.8,
bare_marker: bool = False,
smearing_temperature: float = 0,
fermidirac_cutoff: float = 0.1,
n_tba: int = 0,
until_gap: bool = False,
gap_tol: float = 1e-8,
):
r"""
Evaluate the PBC local Chern marker provided in Ref.\ `Baù-Marrazzo (2024a)
<https://doi.org/10.1103/PhysRevB.109.014206>`_. This can be evaluated on the whole
lattice if :python:`direction == None` or along :python:`direction` starting from
:python:`start`, where directions can be ``0`` (:math:`\mathbf{a}_1` direction),
or ``1`` (:math:`\mathbf{a}_2` direction).
Parameters
----------
direction :
Direction along which to compute the PBC local Chern marker. Default is
:python:`None` (returns the marker on the whole supercell). Allowed
directions are ``0`` (:math:`\mathbf{a}_1` direction), and ``1``
(:math:`\mathbf{a}_2` direction).
start :
If :python:`direction` is not :python:`None`, is the coordinate of the
unit cell from which the evaluation of the PBC local Chern marker starts.
For instance, if interested on the value of the PBC local marker along the
:math:`\mathbf{a}_1` direction at half height, it should be set
:python:`direction = 0` and :python:`start = Ly // 2`.
return_projector :
If :python:`True`, returns the ground state projector at the end of the
calculation. Default is :python:`False`.
input_projector :
Input the list of projectors :math:`[\mathcal{P}_{\mathbf{b}_1},
\mathcal{P}_{\mathbf{b}_2},\mathcal{P}_{\Gamma}]` (or :math:`[\mathcal{P}_{\mathbf{b}_1},
\mathcal{P}_{\mathbf{b}_2},\mathcal{P}_{-\mathbf{b}_1},\mathcal{P}_{-\mathbf{b}_2},
\mathcal{P}_{\Gamma}]` if :python:`formula == 'symmetric'`) to be used in
the calculation. Default is :python:`None`, which means that it is computed
from the model.
formula :
Formula to be used. Default is :python:`'symmetric'`, which is computationally
more demanding but converges faster. Any other input will result in the
:python:`'asymmetric'` formulation.
macroscopic_average :
If :python:`True`, returns the PBC local Chern marker averaged in real
space over a radius equal to :python:`cutoff`. Default is :python:`False`.
cutoff :
Cutoff set for the calculation of the macroscopic average in real space
of the PBC local Chern marker.
bare_marker :
If :python:`True` returns the bare PBC local Chern marker, without taking
local trace and averaging. Default is :python:`False`.
smearing_temperature :
Set a fictitious temperature :math:`T_s` to be used when weighting the
eigenstates of the Hamiltonian comprising the ground state projector. In
particular, the ground state projector is computed as :math:`\mathcal
P=\sum_{n}f(\epsilon_n, T_s, \mu)|u_n\rangle\langle u_n|` where
:math:`f(\epsilon_n, T_s, \mu)` is the Fermi-Dirac distribution, :math:`\mu`
is the chemical potential and :math:`\mathcal{H}_{\Gamma}|u_n\rangle=\epsilon_n
|u_n\rangle`. Introducing some smearing is particularly useful when dealing
with heterojunctions o inhomogeneous models whose insulating gap is small
in order to improve the convergence of the local marker. Default is ``0``,
so no smearing is introduced.
fermidirac_cutoff :
Cutoff imposed on the Fermi-Dirac distribution to further improve the
convergence, mostly when :math:`T_s\neq0`. Default is ``0.1``, which is
appropriate in most cases.
n_tba :
Number of states to be added relative to the a priori set filling (defined
by the :python:`n_occ` parameter or the default half-filling) when applying
smearing.
until_gap :
If :python:`True`, the minimum number of states in order to have an energy
gap larger than :python:`gap_tol` is added to :python:`n_occ` in the
calculation of the marker. Default is :python:`False`.
gap_tol :
Minimum value of the gap when adding states to :python:`n_occ` in the
calculation of the marker if :python:`until_gap == True`. Default is
python:`1e-8`.
Returns
-------
lattice_chern :
PBC local Chern marker evaluated on the whole lattice if :python:`direction == None`.
lcm_direction :
PBC local Chern marker evaluated along :python:`direction` starting from
:python:`start`.
return_proj :
List of projectors :math:`[\mathcal{P}_{\mathbf{b}_1},\mathcal{P}_{\mathbf{b}_2},
\mathcal{P}_{\Gamma}]` (or :math:`[\mathcal{P}_{\mathbf{b}_1},
\mathcal{P}_{\mathbf{b}_2},\mathcal{P}_{-\mathbf{b}_1}, \mathcal{P}_{-\mathbf{b}_2},
\mathcal{P}_{\Gamma}]` if :python:`formula == 'symmetric'`) used in
the calculation if :python:`return_projector == True`.
local_c :
The bare PBC local Chern marker :math:`C(\mathbf{r})` not averaged,
returned if :python:`bare_marker == True`.
"""
# Check input variables
if direction not in [None, 0, 1]:
raise RuntimeError(
"Direction allowed are None, 0 (which stands for x), and 1 (which stands for y)"
)
if direction is not None:
if direction == 0:
if start not in range(self.Ly):
raise RuntimeError(
"Invalid start parameter (must be within [0, Ly - 1])"
)
else:
if start not in range(self.Lx):
raise RuntimeError(
"Invalid start parameter (must be within [0, Lx - 1])"
)
return_proj = []
b1, b2, _ = self.reciprocal_vecs
proj_shape = [self.n_orb, self.n_orb]
if input_projector is None:
# Distribute the Hamiltonian in block-cyclic layout among MPI ranks if using MPI,
# otherwise simply return the global Hamiltonian as the local Hamiltonian
hamiltonian = self.backend.distribute(self.hamiltonian.toarray())
# Eigenvectors of the Hamiltonian
eigenvals, eigenvecs = self.backend.eigh(
hamiltonian,
nev=self.n_orb if smearing_temperature > 0 else self.n_occ + n_tba + 1,
N=self.n_orb,
collect_evec=False,
debug_info=self.debug_info,
)
del hamiltonian
if not until_gap:
if n_tba == 0:
# Find the chemical potential
mu = self.physics.chemical_potential(
eigenvals, smearing_temperature, self.n_occ
)
# If smearing_temperature > 0 evaluate the number of states whose
# occupation is greater than the cutoff
if smearing_temperature > 1e-6:
rank_occ = self.physics._get_occupied_states(
eigenvals, smearing_temperature, mu, fermidirac_cutoff
)
else:
rank_occ = self.n_occ
else:
rank_occ, smearing_temperature, mu = (
self.physics._add_occupied_states(
eigenvals, fermidirac_cutoff, self.n_occ, n_tba
)
)
else:
rank_occ, smearing_temperature, mu = self.physics._add_states_until_gap(
eigenvals, self.n_occ, gap_tol, fermidirac_cutoff
)
fd = self.physics.fermidirac(
eigenvals[:rank_occ] if self.backend.is_master_rank else None,
smearing_temperature,
mu,
)
fd = self.backend.distribute_diag(fd)
# Dual states at b_1
udual_b1 = self._periodic_gauge(eigenvecs, b1, proj_shape, n_occ=rank_occ)
udual_b1 = self._dual_state(eigenvecs, udual_b1, proj_shape, n_sub=rank_occ)
pb1 = self.physics.get_proj(fd, udual_b1, rank_occ, self.n_orb, is_dual=True)
if self.backend.is_master_rank and return_projector:
return_proj += [self.backend.gather(pb1, self.n_orb, self.n_orb, root=0)]
del udual_b1
# Dual states at b_2
udual_b2 = self._periodic_gauge(eigenvecs, b2, proj_shape, n_occ=rank_occ)
udual_b2 = self._dual_state(eigenvecs, udual_b2, proj_shape, n_sub=rank_occ)
pb2 = self.physics.get_proj(fd, udual_b2, rank_occ, self.n_orb, is_dual=True)
if self.backend.is_master_rank and return_projector:
return_proj += [self.backend.gather(pb2, self.n_orb, self.n_orb, root=0)]
del udual_b2
p = self.backend.commutator(pb1, pb2, proj_shape, proj_shape)
# If I want the symmetric formula I need to do the same also for -b_1 and -b_2
if formula.lower() == "symmetric":
udual_b1 = self._periodic_gauge(
eigenvecs, -b1, proj_shape, n_occ=rank_occ
)
udual_b1 = self._dual_state(
eigenvecs, udual_b1, proj_shape, n_sub=rank_occ
)
pmb1 = self.physics.get_proj(
fd, udual_b1, rank_occ, self.n_orb, is_dual=True
)
if self.backend.is_master_rank and return_projector:
return_proj += [
self.backend.gather(pmb1, self.n_orb, self.n_orb, root=0)
]
del udual_b1
p -= self.backend.commutator(pmb1, pb2, proj_shape, proj_shape)
del pb2
udual_b2 = self._periodic_gauge(
eigenvecs, -b2, proj_shape, n_occ=rank_occ
)
udual_b2 = self._dual_state(
eigenvecs, udual_b2, proj_shape, n_sub=rank_occ
)
pmb2 = self.physics.get_proj(
fd, udual_b2, rank_occ, self.n_orb, is_dual=True
)
if self.backend.is_master_rank and return_projector:
return_proj += [
self.backend.gather(pmb2, self.n_orb, self.n_orb, root=0)
]
del udual_b2
p -= self.backend.commutator(pb1, pmb2, proj_shape, proj_shape)
del pb1
p += self.backend.commutator(pmb1, pmb2, proj_shape, proj_shape)
del pmb1, pmb2
gsp = self.physics.get_proj(
fd, eigenvecs, rank_occ, self.n_orb, is_dual=False
)
if self.backend.is_master_rank and return_projector:
return_proj += [self.backend.gather(gsp, self.n_orb, self.n_orb, root=0)]
else:
gsp, pb1, pb2 = None, None, None
if self.backend.is_master_rank:
pb1 = input_projector[0]
pb2 = input_projector[1]
gsp = input_projector[-1]
pb1 = self.backend.distribute(pb1)
pb2 = self.backend.distribute(pb2)
gsp = self.backend.distribute(gsp)
p = self.backend.commutator(pb1, pb2, proj_shape, proj_shape)
if formula == "symmetric":
pmb1, pmb2 = None, None
if self.backend.is_master_rank:
pmb1 = input_projector[2]
pmb2 = input_projector[3]
pmb1 = self.backend.distribute(pmb1)
pmb2 = self.backend.distribute(pmb2)
p += (
self.backend.commutator(pmb1, pmb2, proj_shape, proj_shape)
- self.backend.commutator(pb1, pmb2, proj_shape, proj_shape)
- self.backend.commutator(pmb1, pb2, proj_shape, proj_shape)
)
loc_chern_op = self.backend.matmul(p, gsp, proj_shape, proj_shape)
del p, gsp
chern_diags = self.backend.get_diag(loc_chern_op, self.n_orb)
del loc_chern_op
chern_diags = (
-np.imag(chern_diags) * (self.Lx * self.Ly) / (np.pi * (8 if formula == "symmetric" else 2))
if self.backend.is_master_rank
else None
)
chern_diags = self.backend.shared_array(chern_diags)
if bare_marker:
return chern_diags
# If macroscopic_average I have to compute the lattice values with the averages
# first (explicit PBC contraction passed)
if macroscopic_average:
contraction = pbc_lattice_contraction(self, cutoff)
chern_diags = average_over_radius(
self, chern_diags, cutoff, contraction=contraction
)
if direction is not None:
pbclcm_line = self._extract_line_marker(
chern_diags, direction, start, macroscopic_average
)
if not return_projector:
return pbclcm_line
else:
return pbclcm_line, np.asarray(return_proj)
if not macroscopic_average:
chern_diags = self._trace_unit_cell(chern_diags)
if not return_projector:
return chern_diags
else:
return chern_diags, np.asarray(return_proj)
[docs]
@mpimaster
def pbc_local_spin_chern_marker(
self,
direction: int = None,
start: int = 0,
return_projector: bool = False,
input_projector=None,
formula: str = "symmetric",
macroscopic_average: bool = False,
cutoff: float = 0.8,
bare_marker: bool = False,
smearing_temperature: float = 0,
fermidirac_cutoff: float = 0.1,
n_tba: int = 0,
until_gap: bool = False,
gap_tol: float = 1e-8,
):
r"""
Evaluate the PBC local spin-Chern marker provided in Ref.\ `Baù-Marrazzo (2024b)
<https://doi.org/10.1103/PhysRevB.110.054203>`_. This can be evaluated on the whole
lattice if :python:`direction == None` or along :python:`direction` starting from
:python:`start`, where directions can be ``0`` (:math:`\mathbf{a}_1` direction),
or ``1`` (:math:`\mathbf{a}_2` direction).
Parameters
----------
direction :
Direction along which to compute the PBC local spin-Chern marker. Default
is :python:`None` (returns the marker on the whole supercell). Allowed
directions are ``0`` (:math:`\mathbf{a}_1` direction), and ``1``
(:math:`\mathbf{a}_2` direction).
start :
If :python:`direction` is not :python:`None`, is the coordinate of the
unit cell from which the evaluation of the PBC local spin-Chern marker
starts. For instance, if interested on the value of the PBC local marker
along the :math:`\mathbf{a}_1` direction at half height, it should be set
:python:`direction = 0` and :python:`start = Ly // 2`.
return_projector :
If :python:`True`, returns the projectors :math:`\mathcal P_{\mathbf b_{1,2}}^{\pm}`
at the end of the calculation. Default is :python:`False`.
input_projector :
List of projectors :math:`[\mathcal{P}_{\mathbf{b}_1}^-,\mathcal{P}_{\mathbf{b}_2}^-,
\mathcal{P}_{\mathbf{b}_1}^+,\mathcal{P}_{\mathbf{b}_2}^+,\mathcal{P}_{\Gamma}^-,
\mathcal{P}_{\Gamma}^+]` (or :math:`[\mathcal{P}_{\mathbf{b}_1}^-,
\mathcal{P}_{\mathbf{b}_2}^-,\mathcal{P}_{\mathbf{b}_1}^+,\mathcal{P}_{\mathbf{b}_2}^+,
\mathcal{P}_{-\mathbf{b}_1}^-,\mathcal{P}_{-\mathbf{b}_2}^-,\mathcal{P}_{-\mathbf{b}_1}^+,
\mathcal{P}_{-\mathbf{b}_2}^+,\mathcal{P}_{\Gamma}^-,\mathcal{P}_{\Gamma}^+]`
if :python:`formula == 'symmetric'`) to be used in the calculation.
Default is :python:`None`, which means that it is computed from the model.
formula :
Formula to be used. Default is :python:`'symmetric'`, which is
computationally more demanding but converges faster. Any other input will
result in the :python:`'asymmetric'` formulation.
macroscopic_average :
If :python:`True`, returns the PBC local spin-Chern marker averaged in
real space over a radius equal to :python:`cutoff`. Default is :python:`False`.
cutoff :
Cutoff set for the calculation of the macroscopic average in real space of
the PBC local spin-Chern marker.
bare_marker :
If :python:`True` returns the bare PBC local individual Chern markers,
without taking local trace and averaging. Default is :python:`False`.
smearing_temperature :
Set a fictitious temperature :math:`T_s` to be used when weighting the
eigenstates of :math:`\mathcal PS_z\mathcal P` comprising the projectors
:math:`\mathcal P_{\pm}`. In particular, the projectors :math:`\mathcal P_{\pm}`
are computed as :math:`\mathcal P_{\pm}=\sum_{m:\sigma=\pm}c_m^{\sigma}(T_s)
|\phi_m^{\sigma}\rangle\langle \phi_m^{\sigma}|` where :math:`|\phi_m^{\sigma}\rangle`
are the eigenstates of :math:`\mathcal PS_z\mathcal P` with eigenvalue
:math:`\lambda_m^{\sigma}` whose sign is :math:`\sigma=\pm`. Introducing
some smearing is particularly useful when dealing with heterojunctions or
inhomogeneous models whose insulating gap is small in order to improve the
convergence of the local marker. Default is ``0``, so that no smearing is
introduced.
fermidirac_cutoff :
Cutoff imposed on the Fermi-Dirac distribution to further improve the
convergence, mostly when :math:`T_s\neq0`. Default is ``0.1``, which is
appropriate in most cases.
n_tba :
Number of states to be added relative to the a priori set filling (defined
by the :python:`n_occ` parameter or the default half-filling) when applying smearing.
until_gap :
If :python:`True`, the minimum number of states in order to have an energy
gap larger than :python:`gap_tol` is added to :python:`n_occ` in the
calculation of the marker. Default is :python:`False`.
gap_tol :
Minimum value of the gap when adding states to :python:`n_occ` in the
calculation of the marker if :python:`until_gap == True`. Default is :python:`1e-8`.
Returns
-------
lattice_spinchern :
PBC local spin-Chern marker evaluated on the whole lattice if :python:`direction == None`.
lscm_direction :
PBC local spin-Chern marker evaluated along :python:`direction` starting
from :python:`start`.
return_proj :
List of projectors :math:`[\mathcal{P}_{\mathbf{b}_1}^-,\mathcal{P}_{\mathbf{b}_2}^-,
\mathcal{P}_{\mathbf{b}_1}^+,\mathcal{P}_{\mathbf{b}_2}^+,\mathcal{P}_{\Gamma}^-,
\mathcal{P}_{\Gamma}^+]` (or :math:`[\mathcal{P}_{\mathbf{b}_1}^-,\mathcal{P}_{\mathbf{b}_2}^-,
\mathcal{P}_{-\mathbf{b}_1}^-,\mathcal{P}_{-\mathbf{b}_2}^-,\mathcal{P}_{\mathbf{b}_1}^+,
\mathcal{P}_{\mathbf{b}_2}^+,\mathcal{P}_{-\mathbf{b}_1}^+,\mathcal{P}_{-\mathbf{b}_2}^+,
\mathcal{P}_{\Gamma}^-, \mathcal{P}_{\Gamma}^+]` if :python:`formula ==
'symmetric'`) used in the calculation if :python:`return_projector == True`.
local_cp, local_cm :
The list of bare PBC local individual Chern markers :math:`C_+(\mathbf{r})`
and :math:`C_-(\mathbf{r})`, returned if :python:`bare_marker == True`.
""" # Check input variables
if direction not in [None, 0, 1]:
raise RuntimeError(
"Direction allowed are None, 0 (which stands for x), and 1 (which stands for y)"
)
if direction is not None:
if direction == 0:
if start not in range(self.Ly):
raise RuntimeError(
"Invalid start parameter (must be within [0, Ly - 1])"
)
else:
if start not in range(self.Lx):
raise RuntimeError(
"Invalid start parameter (must be within [0, Lx - 1])"
)
return_proj = []
b1, b2, _ = self.reciprocal_vecs
proj_shape = [self.n_orb, self.n_orb]
if input_projector is None:
# Distribute the Hamiltonian in block-cyclic layout among MPI ranks if using MPI,
# otherwise simply return the global Hamiltonian as the local Hamiltonian
hamiltonian = self.backend.distribute(self.hamiltonian.toarray())
# Eigenvectors of the Hamiltonian
eigenvals, canonical_to_H = self.backend.eigh(
hamiltonian,
nev=self.n_orb if smearing_temperature > 0 else self.n_occ + n_tba + 1,
N=self.n_orb,
collect_evec=False,
debug_info=self.debug_info,
)
del hamiltonian
if not until_gap:
if n_tba == 0:
# Find the chemical potential
mu = self.physics.chemical_potential(
eigenvals, smearing_temperature, self.n_occ
)
# If smearing_temperature > 0 evaluate the number of states whose
# occupation is greater than the cutoff
if smearing_temperature > 1e-6:
rank_occ = self.physics._get_occupied_states(
eigenvals, smearing_temperature, mu, fermidirac_cutoff
)
else:
rank_occ = self.n_occ
else:
rank_occ, smearing_temperature, mu = (
self.physics._add_occupied_states(
eigenvals, fermidirac_cutoff, self.n_occ, n_tba
)
)
else:
rank_occ, smearing_temperature, mu = self.physics._add_states_until_gap(
eigenvals, self.n_occ, gap_tol, fermidirac_cutoff
)
nsub = rank_occ // 2
sz = self.backend.distribute(self.sz.toarray())
tmp = self.backend.matmul(
canonical_to_H.conj(),
sz,
proj_shape,
proj_shape,
"T",
"N",
[[0, self.n_orb], [0, rank_occ]],
)
del sz
pszp = self.backend.matmul(
tmp,
canonical_to_H,
(rank_occ, self.n_orb),
proj_shape,
"N",
"N",
slc_idx_B=[[0, self.n_orb], [0, rank_occ]],
)
del tmp
_, evecs = self.backend.eigh(
pszp, rank_occ, rank_occ, debug_info=self.debug_info
)
del pszp
evecs = self.backend.matmul(
canonical_to_H,
evecs,
proj_shape,
(rank_occ, rank_occ),
"N",
"N",
[[0, self.n_orb], [0, rank_occ]],
)
### ================= Negative eigenvalues ================= ###
evecs_b1 = self._periodic_gauge(
evecs, b1, [self.n_orb, rank_occ], n_occ=rank_occ
)
udual_b1 = self._dual_state(
evecs, evecs_b1, [self.n_orb, rank_occ], "down", n_sub=nsub
)
del evecs_b1
coeff = self.physics.smearing(
udual_b1,
canonical_to_H,
eigenvals,
smearing_temperature,
mu,
rank_occ,
self.n_orb,
is_dual=True,
spin="down",
)
pb1_m = self.physics.get_proj(
coeff, udual_b1, rank_occ, self.n_orb, is_dual=True, spin="down"
)
del coeff
if self.backend.is_master_rank and return_projector:
return_proj += [
self.backend.gather(pb1_m, self.n_orb, self.n_orb, root=0)
]
del udual_b1
evecs_b2 = self._periodic_gauge(
evecs, b2, [self.n_orb, rank_occ], n_occ=rank_occ
)
udual_b2 = self._dual_state(
evecs, evecs_b2, [self.n_orb, rank_occ], "down", n_sub=nsub
)
del evecs_b2
coeff = self.physics.smearing(
udual_b2,
canonical_to_H,
eigenvals,
smearing_temperature,
mu,
rank_occ,
self.n_orb,
is_dual=True,
spin="down",
)
pb2_m = self.physics.get_proj(
coeff, udual_b2, rank_occ, self.n_orb, is_dual=True, spin="down"
)
del coeff
if self.backend.is_master_rank and return_projector:
return_proj += [
self.backend.gather(pb2_m, self.n_orb, self.n_orb, root=0)
]
del udual_b2
pminus = self.backend.commutator(pb1_m, pb2_m, proj_shape, proj_shape)
if formula.lower() == "symmetric":
evecs_b1 = self._periodic_gauge(
evecs, -b1, [self.n_orb, rank_occ], n_occ=rank_occ
)
udual_b1 = self._dual_state(
evecs, evecs_b1, [self.n_orb, rank_occ], "down", n_sub=nsub
)
del evecs_b1
coeff = self.physics.smearing(
udual_b1,
canonical_to_H,
eigenvals,
smearing_temperature,
mu,
rank_occ,
self.n_orb,
is_dual=True,
spin="down",
)
pmb1_m = self.physics.get_proj(
coeff, udual_b1, rank_occ, self.n_orb, is_dual=True, spin="down"
)
if self.backend.is_master_rank and return_projector:
return_proj += [
self.backend.gather(pmb1_m, self.n_orb, self.n_orb, root=0)
]
del udual_b1
evecs_b2 = self._periodic_gauge(
evecs, -b2, [self.n_orb, rank_occ], n_occ=rank_occ
)
udual_b2 = self._dual_state(
evecs, evecs_b2, [self.n_orb, rank_occ], "down", n_sub=nsub
)
del evecs_b2
coeff = self.physics.smearing(
udual_b2,
canonical_to_H,
eigenvals,
smearing_temperature,
mu,
rank_occ,
self.n_orb,
is_dual=True,
spin="down",
)
pmb2_m = self.physics.get_proj(
coeff, udual_b2, rank_occ, self.n_orb, is_dual=True, spin="down"
)
del coeff
if self.backend.is_master_rank and return_projector:
return_proj += [
self.backend.gather(pmb2_m, self.n_orb, self.n_orb, root=0)
]
del udual_b2
pminus -= self.backend.commutator(pb1_m, pmb2_m, proj_shape, proj_shape)
del pb1_m
pminus -= self.backend.commutator(pmb1_m, pb2_m, proj_shape, proj_shape)
del pb2_m
pminus += self.backend.commutator(pmb1_m, pmb2_m, proj_shape, proj_shape)
### ================= Positive eigenvalues ================= ###
evecs_b1 = self._periodic_gauge(
evecs, b1, [self.n_orb, rank_occ], n_occ=rank_occ
)
udual_b1 = self._dual_state(
evecs, evecs_b1, [self.n_orb, rank_occ], "up", n_sub=nsub
)
del evecs_b1
coeff = self.physics.smearing(
udual_b1,
canonical_to_H,
eigenvals,
smearing_temperature,
mu,
rank_occ,
self.n_orb,
is_dual=True,
spin="up",
)
pb1_p = self.physics.get_proj(
coeff, udual_b1, rank_occ, self.n_orb, is_dual=True, spin="up"
)
del coeff
if self.backend.is_master_rank and return_projector:
return_proj += [
self.backend.gather(pb1_p, self.n_orb, self.n_orb, root=0)
]
del udual_b1
evecs_b2 = self._periodic_gauge(
evecs, b2, [self.n_orb, rank_occ], n_occ=rank_occ
)
udual_b2 = self._dual_state(
evecs, evecs_b2, [self.n_orb, rank_occ], "up", n_sub=nsub
)
del evecs_b2
coeff = self.physics.smearing(
udual_b2,
canonical_to_H,
eigenvals,
smearing_temperature,
mu,
rank_occ,
self.n_orb,
is_dual=True,
spin="up",
)
pb2_p = self.physics.get_proj(
coeff, udual_b2, rank_occ, self.n_orb, is_dual=True, spin="up"
)
del coeff
if self.backend.is_master_rank and return_projector:
return_proj += [
self.backend.gather(pb2_p, self.n_orb, self.n_orb, root=0)
]
del udual_b2
pplus = self.backend.commutator(pb1_p, pb2_p, proj_shape, proj_shape)
if formula.lower() == "symmetric":
evecs_b1 = self._periodic_gauge(
evecs, -b1, [self.n_orb, rank_occ], n_occ=rank_occ
)
udual_b1 = self._dual_state(
evecs, evecs_b1, [self.n_orb, rank_occ], "up", n_sub=nsub
)
del evecs_b1
coeff = self.physics.smearing(
udual_b1,
canonical_to_H,
eigenvals,
smearing_temperature,
mu,
rank_occ,
self.n_orb,
is_dual=True,
spin="up",
)
pmb1_p = self.physics.get_proj(
coeff, udual_b1, rank_occ, self.n_orb, is_dual=True, spin="up"
)
del coeff
if self.backend.is_master_rank and return_projector:
return_proj += [
self.backend.gather(pmb1_p, self.n_orb, self.n_orb, root=0)
]
del udual_b1
evecs_b2 = self._periodic_gauge(
evecs, -b2, [self.n_orb, rank_occ], n_occ=rank_occ
)
udual_b2 = self._dual_state(
evecs, evecs_b2, [self.n_orb, rank_occ], "up", n_sub=nsub
)
del evecs_b2
coeff = self.physics.smearing(
udual_b2,
canonical_to_H,
eigenvals,
smearing_temperature,
mu,
rank_occ,
self.n_orb,
is_dual=True,
spin="up",
)
pmb2_p = self.physics.get_proj(
coeff, udual_b2, rank_occ, self.n_orb, is_dual=True, spin="up"
)
del coeff
if self.backend.is_master_rank and return_projector:
return_proj += [
self.backend.gather(pmb2_p, self.n_orb, self.n_orb, root=0)
]
del udual_b2
pplus -= self.backend.commutator(pb1_p, pmb2_p, proj_shape, proj_shape)
del pb1_p
pplus -= self.backend.commutator(pmb1_p, pb2_p, proj_shape, proj_shape)
del pb2_p
pplus += self.backend.commutator(pmb1_p, pmb2_p, proj_shape, proj_shape)
else:
gsp_m, gsp_p, pb1_m, pb2_m, pb1_p, pb2_p = (
None,
None,
None,
None,
None,
None,
)
if self.backend.is_master_rank:
pb1_m = input_projector[0]
pb2_m = input_projector[1]
pb1_p = input_projector[2]
pb2_p = input_projector[3]
gsp_m = input_projector[-2]
gsp_p = input_projector[-1]
pb1_m = self.backend.distribute(pb1_m)
pb2_m = self.backend.distribute(pb2_m)
pb1_p = self.backend.distribute(pb1_p)
pb2_p = self.backend.distribute(pb2_p)
gsp_m = self.backend.distribute(gsp_m)
gsp_p = self.backend.distribute(gsp_p)
pminus = self.backend.commutator(pb1_m, pb2_m, proj_shape, proj_shape)
pplus = self.backend.commutator(pb1_p, pb2_p, proj_shape, proj_shape)
if formula == "symmetric":
pmb1_m, pmb2_m, pmb1_p, pmb2_p = None, None, None, None
if self.backend.is_master_rank:
pmb1_m = input_projector[4]
pmb2_m = input_projector[5]
pmb1_p = input_projector[6]
pmb2_p = input_projector[7]
pmb1_m = self.backend.distribute(pmb1_m)
pmb2_m = self.backend.distribute(pmb2_m)
pmb1_p = self.backend.distribute(pmb1_p)
pmb2_p = self.backend.distribute(pmb2_p)
pminus += (
self.backend.commutator(pmb1_m, pmb2_m, proj_shape, proj_shape)
- self.backend.commutator(pb1_m, pmb2_m, proj_shape, proj_shape)
- self.backend.commutator(pmb1_m, pb2_m, proj_shape, proj_shape)
)
pplus += (
self.backend.commutator(pmb1_p, pmb2_p, proj_shape, proj_shape)
- self.backend.commutator(pb1_p, pmb2_p, proj_shape, proj_shape)
- self.backend.commutator(pmb1_p, pb2_p, proj_shape, proj_shape)
)
### ================= PBC local spin-Chern markers ================= ###
coeff = self.physics.smearing(
evecs,
canonical_to_H,
eigenvals,
smearing_temperature,
mu,
rank_occ,
self.n_orb,
is_dual=False,
spin="down",
)
gsp_m = self.physics.get_proj(
coeff, evecs, rank_occ, self.n_orb, is_dual=False, spin="down"
)
del coeff
loc_spinchern_op_minus = self.backend.matmul(
pminus, gsp_m, proj_shape, proj_shape
)
if self.backend.is_master_rank and return_projector:
return_proj += [self.backend.gather(gsp_m, self.n_orb, self.n_orb, root=0)]
del pminus, gsp_m
coeff = self.physics.smearing(
evecs,
canonical_to_H,
eigenvals,
smearing_temperature,
mu,
rank_occ,
self.n_orb,
is_dual=False,
spin="up",
)
gsp_p = self.physics.get_proj(
coeff, evecs, rank_occ, self.n_orb, is_dual=False, spin="up"
)
del coeff
loc_spinchern_op_plus = self.backend.matmul(pplus, gsp_p, proj_shape, proj_shape)
if self.backend.is_master_rank and return_projector:
return_proj += [self.backend.gather(gsp_p, self.n_orb, self.n_orb, root=0)]
del pplus, gsp_p
# Get the diagonal elements
chern_diags_minus = self.backend.get_diag(loc_spinchern_op_minus, N=self.n_orb)
del loc_spinchern_op_minus
chern_diags_plus = self.backend.get_diag(loc_spinchern_op_plus, N=self.n_orb)
del loc_spinchern_op_plus
chern_diags_minus = (
-np.imag(chern_diags_minus) * (self.Lx * self.Ly) / (np.pi * (8 if formula == "symmetric" else 2))
if self.backend.is_master_rank
else None
)
chern_diags_plus = (
-np.imag(chern_diags_plus) * (self.Lx * self.Ly) / (np.pi * (8 if formula == "symmetric" else 2))
if self.backend.is_master_rank
else None
)
chern_diags_minus = self.backend.shared_array(chern_diags_minus)
chern_diags_plus = self.backend.shared_array(chern_diags_plus)
if bare_marker:
return np.array([chern_diags_plus, chern_diags_minus])
# If macroscopic_average I have to compute the lattice values with the averages
# first (explicit PBC contraction passed)
if macroscopic_average:
contraction = pbc_lattice_contraction(self, cutoff)
chern_diags_plus = average_over_radius(
self, chern_diags_plus, cutoff, contraction=contraction
)
chern_diags_minus = average_over_radius(
self, chern_diags_minus, cutoff, contraction=contraction
)
if direction is not None:
pbclcm_plus = self._extract_line_marker(
chern_diags_plus, direction, start, macroscopic_average
)
pbclcm_minus = self._extract_line_marker(
chern_diags_minus, direction, start, macroscopic_average
)
if not return_projector:
return np.abs(np.fmod(0.5 * (pbclcm_plus - pbclcm_minus), 2))
else:
return np.abs(np.fmod(0.5 * (pbclcm_plus - pbclcm_minus), 2)), np.asarray(
return_proj
)
# If not macroscopic averages I sum the values of the Chern operators of the unit cell
if not macroscopic_average:
chern_diags_minus = self._trace_unit_cell(chern_diags_minus)
chern_diags_plus = self._trace_unit_cell(chern_diags_plus)
if not return_projector:
return np.abs(np.fmod(0.5 * (chern_diags_plus - chern_diags_minus), 2))
else:
return np.abs(
np.fmod(0.5 * (chern_diags_plus - chern_diags_minus), 2)
), np.array(return_proj)
[docs]
@mpimaster
def pbc_local_z2_marker(
self,
direction: int = None,
start: int = 0,
return_projector: bool = False,
input_projector=None,
formula: str = "symmetric",
macroscopic_average: bool = False,
cutoff: float = 0.8,
bare_marker: bool = False,
smearing_temperature: float = 0,
fermidirac_cutoff: float = 0.1,
trial_projections=None,
):
r"""
Evaluate the local PBC :math:`\mathbb{Z}_2` marker provided in Ref.\ `Baù-Marrazzo
(2024b) <https://doi.org/10.1103/PhysRevB.110.054203>`_. This can be evaluated on
the whole lattice if :python:`direction == None` or along :python:`direction`
starting from :python:`start`, where directions can be ``0`` (:math:`\mathbf{a}_1`
direction), or ``1`` (:math:`\mathbf{a}_2` direction).
Parameters
----------
direction :
Direction along which the local :math:`\mathbb{Z}_2` marker is computed.
Default is :python:`None` (returns the marker on the whole lattice). Allowed
directions are ``0`` (:math:`\mathbf{a}_1` direction), and ``1``
(:math:`\mathbf{a}_2` direction).
start :
If :python:`direction` is not :python:`None`, indicates the coordinate of
the unit cell from which the evaluation of the local :math:`\mathbb{Z}_2`
marker starts. For instance, if interested on the value of the local marker
along the :math:`\mathbf{a}_1` direction at half height, it should be set
:python:`direction = 0` and :python:`start = Ly // 2`.
return_projector :
If :python:`True`, returns the list of projectors :math:`[\mathcal{P}_{1,\Gamma},
\Theta\mathcal{P}_{1,\Gamma}\Theta^{-1}, \mathcal{P}_{1,\mathbf{b}_1},
\mathcal{P}_{1,\mathbf{b}_2}, \Theta\mathcal{P}_{1,\mathbf{b}_1}\Theta^{-1},
\Theta\mathcal{P}_{1,\mathbf{b}_2}\Theta^{-1}]` (or :math:`[\mathcal{P}_{1,\Gamma},
\Theta\mathcal{P}_{1,\Gamma}\Theta^{-1}, \mathcal{P}_{1,\mathbf{b}_1},
\mathcal{P}_{1,\mathbf{b}_2}, \Theta\mathcal{P}_{1,\mathbf{b}_1}\Theta^{-1},
\Theta\mathcal{P}_{1,\mathbf{b}_2}\Theta^{-1}, \mathcal{P}_{1,-\mathbf{b}_1},
\mathcal{P}_{1, -\mathbf{b}_2}, \Theta\mathcal{P}_{1, -\mathbf{b}_1}\Theta^{-1},
\Theta\mathcal{P}_{1, -\mathbf{b}_2}\Theta^{-1}]` if :python:`formula ==
'symmetric'`) used in the calculation, where :math:`\Theta` is the time
reversal operator. Default is :python:`False`.
input_projector :
List of projectors :math:`[\mathcal{P}_{1,\Gamma}, \Theta\mathcal{P}_{1,\Gamma}\Theta^{-1},
\mathcal{P}_{1,\mathbf{b}_1}, \mathcal{P}_{1,\mathbf{b}_2}, \Theta\mathcal{P}_{1,\mathbf{b}_1}\Theta^{-1},
\Theta\mathcal{P}_{1,\mathbf{b}_2}\Theta^{-1}]` (or :math:`[\mathcal{P}_{1,\Gamma},
\Theta\mathcal{P}_{1,\Gamma}\Theta^{-1}, \mathcal{P}_{1,\mathbf{b}_1},
\mathcal{P}_{1,\mathbf{b}_2}, \Theta\mathcal{P}_{1,\mathbf{b}_1}\Theta^{-1},
\Theta\mathcal{P}_{1,\mathbf{b}_2}\Theta^{-1}, \mathcal{P}_{1,-\mathbf{b}_1},
\mathcal{P}_{1, -\mathbf{b}_2}, \Theta\mathcal{P}_{1, -\mathbf{b}_1}\Theta^{-1},
\Theta\mathcal{P}_{1, -\mathbf{b}_2}\Theta^{-1}]` if :python:`formula == 'symmetric'`)
to be used in the calculation. Default is :python:`None`, which means that
it is computed from the model.
formula :
Formula to be used. Default is :python:`'symmetric'`, which is computationally
more demanding but converges faster. Any other input will result in the
:python:`'asymmetric'` formulation.
macroscopic_average :
If :python:`True`, returns the local :math:`\mathbb{Z}_2` marker averaged
in real space over a radius equal to :python:`cutoff`. Default is :python:`False`.
cutoff :
Cutoff set for the calculation of the macroscopic average in real space of
the local :math:`\mathbb{Z}_2` marker.
bare_marker :
If :python:`True` returns the bare PBC local individual Chern markers,
without taking local trace and averaging. Default is :python:`False`.
smearing_temperature :
Set a fictitious temperature :math:`T_s` to be used when weighting the quasi
Wannier functions comprising the projector :math:`\mathcal P_{1}`. In particular,
the projector :math:`\mathcal P_{1}` is computed as
:math:`\mathcal P_{1}=\sum_{m}c_m(T_s)|w_m\rangle\langle w_m|` where
:math:`|w_m\rangle` are the quasi Wannier functions. These are obtained
minimizing the spillage between :math:`\mathcal P=\sum_n f(\epsilon_n, T_s, \mu)
|u_n\rangle\langle u_n|` and :math:`\mathcal P_{\Theta}=\mathcal P_1+\Theta\mathcal P_1\Theta^{-1}`,
where :math:`|u_n\rangle` is the periodic part of the *n*-th Bloch function
and :math:`f(\epsilon, T, \mu)` is the Fermi-Dirac distribution. Introducing
some smearing is particularly useful when dealing with heterojunctions or
inhomogeneous models whose insulating gap is small in order to improve the
convergence of the local marker. Default is ``0``, so that no smearing is
introduced.
fermidirac_cutoff :
Cutoff imposed on the Fermi-Dirac distribution to further improve the
convergence, mostly when :math:`T_s\neq0`. Default is ``0.1``, which is
appropriate in most cases.
trial_projections :
The :math:`N\times N` matrix of trial projections to be used when computing
the quasi-Wannier functions, where :math:`N` is the number of states per
primitive cell. Default is :python:`None`, meaning a default choice projecting
on spin-up and spin-down states is employed.
Returns
-------
lattice_lz2m :
Local :math:`\mathbb{Z}_2` marker evaluated on the whole lattice if
:python:`direction == None`.
lz2m_direction :
Local :math:`\mathbb{Z}_2` marker evaluated along :python:`direction`
starting from :python:`start`.
return_proj :
The list of projectors :math:`[\mathcal P_1, \Theta\mathcal P_1\Theta^{-1}]`,
returned if :python:`return_projector == True`.
local_c1, local_c2 :
The bare PBC local individual Chern markers :math:`C_1(\mathbf{r})` and
:math:`C_2(\mathbf{r})`, returned if :python:`bare_marker == True`.
.. note::
The local :math:`\mathbb{Z}_2` marker does not currently support the
:python:`until_gap` and :python:`n_tba` options as the other PBC local
topological markers and it will updated in the future.
"""
# Check input variables
if direction not in [None, 0, 1]:
raise RuntimeError(
"Direction allowed are None, 0 (which stands for x), and 1 (which stands for y)"
)
if direction is not None:
if direction == 0:
if start not in range(self.Ly):
raise RuntimeError(
"Invalid start parameter (must be within [0, Ly - 1])"
)
else:
if start not in range(self.Lx):
raise RuntimeError(
"Invalid start parameter (must be within [0, Lx - 1])"
)
return_proj = []
b1, b2, _ = self.reciprocal_vecs
proj_shape = [self.n_orb, self.n_orb]
if input_projector is None:
# Distribute the Hamiltonian in block-cyclic layout among MPI ranks if using
# MPI, otherwise simply return the global Hamiltonian as the local
# Hamiltonian
hamiltonian = self.backend.distribute(self.hamiltonian.toarray())
# Eigenvectors of the Hamiltonian
eigenvals, eigenvecs = self.backend.eigh(
hamiltonian,
nev=self.n_orb if smearing_temperature > 0 else self.n_occ + 2,
N=self.n_orb,
collect_evec=False,
debug_info=self.debug_info,
)
del hamiltonian
# Find the chemical potential
mu = self.physics.chemical_potential(
eigenvals, smearing_temperature, self.n_occ
)
# If smearing_temperature > 0 evaluate the number of states whose occupation
# is greater than the cutoff
if smearing_temperature > 1e-6:
rank_occ = self.physics._get_occupied_states(
eigenvals, smearing_temperature, mu, fermidirac_cutoff
)
else:
rank_occ = self.n_occ
nsub = rank_occ // 2
# Compute qWFs via projection
eigenvecs_projected = self.physics._delta_projection(
eigenvecs, rank_occ, trial_projections, self.states_uc, self.n_orb
)
vectors, tr_vectors = self.physics._time_reversal_separation(
eigenvecs_projected, rank_occ, self.n_orb
)
########### ======= Subspace spanned by 'vectors' ========###########
evecs_b1 = self._periodic_gauge(vectors, b1, [self.n_orb, nsub], n_occ=nsub)
udual_b1 = self._dual_state(
vectors, evecs_b1, [self.n_orb, nsub], n_sub=nsub, n_occ=nsub
)
del evecs_b1
coeff = self.physics.smearing(
udual_b1,
eigenvecs,
eigenvals,
smearing_temperature,
mu,
nsub,
self.n_orb,
is_dual=True,
spin=None,
)
pb1_1 = self.physics.get_proj(coeff, udual_b1, nsub, self.n_orb, is_dual=True)
del coeff, udual_b1
evecs_b2 = self._periodic_gauge(vectors, b2, [self.n_orb, nsub], n_occ=nsub)
udual_b2 = self._dual_state(
vectors, evecs_b2, [self.n_orb, nsub], n_sub=nsub, n_occ=nsub
)
del evecs_b2
coeff = self.physics.smearing(
udual_b2,
eigenvecs,
eigenvals,
smearing_temperature,
mu,
nsub,
self.n_orb,
is_dual=True,
spin=None,
)
pb2_1 = self.physics.get_proj(coeff, udual_b2, nsub, self.n_orb, is_dual=True)
del coeff, udual_b2
p1 = self.backend.commutator(pb1_1, pb2_1, proj_shape, proj_shape)
if formula.lower() == "symmetric":
evecs_b1 = self._periodic_gauge(
vectors, -b1, [self.n_orb, nsub], n_occ=nsub
)
udual_b1 = self._dual_state(
vectors, evecs_b1, [self.n_orb, nsub], n_sub=nsub, n_occ=nsub
)
del evecs_b1
coeff = self.physics.smearing(
udual_b1,
eigenvecs,
eigenvals,
smearing_temperature,
mu,
nsub,
self.n_orb,
is_dual=True,
spin=None,
)
pmb1_1 = self.physics.get_proj(
coeff, udual_b1, nsub, self.n_orb, is_dual=True
)
del coeff, udual_b1
evecs_b2 = self._periodic_gauge(
vectors, -b2, [self.n_orb, nsub], n_occ=nsub
)
udual_b2 = self._dual_state(
vectors, evecs_b2, [self.n_orb, nsub], n_sub=nsub, n_occ=nsub
)
del evecs_b2
coeff = self.physics.smearing(
udual_b2,
eigenvecs,
eigenvals,
smearing_temperature,
mu,
nsub,
self.n_orb,
is_dual=True,
spin=None,
)
pmb2_1 = self.physics.get_proj(
coeff, udual_b2, nsub, self.n_orb, is_dual=True
)
del coeff, udual_b2
p1 -= self.backend.commutator(pb1_1, pmb2_1, proj_shape, proj_shape)
del pb1_1
p1 -= self.backend.commutator(pmb1_1, pb2_1, proj_shape, proj_shape)
del pb2_1
p1 += self.backend.commutator(pmb1_1, pmb2_1, proj_shape, proj_shape)
del pmb1_1, pmb2_1
########### ======= Subspace spanned by 'tr_vectors' ========###########
evecs_b1 = self._periodic_gauge(
tr_vectors, b1, [self.n_orb, nsub], n_occ=nsub
)
udual_b1 = self._dual_state(
tr_vectors, evecs_b1, [self.n_orb, nsub], n_sub=nsub, n_occ=nsub
)
del evecs_b1
coeff = self.physics.smearing(
udual_b1,
eigenvecs,
eigenvals,
smearing_temperature,
mu,
nsub,
self.n_orb,
is_dual=True,
spin=None,
)
pb1_2 = self.physics.get_proj(coeff, udual_b1, nsub, self.n_orb, is_dual=True)
del coeff, udual_b1
evecs_b2 = self._periodic_gauge(
tr_vectors, b2, [self.n_orb, nsub], n_occ=nsub
)
udual_b2 = self._dual_state(
tr_vectors, evecs_b2, [self.n_orb, nsub], n_sub=nsub, n_occ=nsub
)
del evecs_b2
coeff = self.physics.smearing(
udual_b2,
eigenvecs,
eigenvals,
smearing_temperature,
mu,
nsub,
self.n_orb,
is_dual=True,
spin=None,
)
pb2_2 = self.physics.get_proj(coeff, udual_b2, nsub, self.n_orb, is_dual=True)
del coeff, udual_b2
p2 = self.backend.commutator(pb1_2, pb2_2, proj_shape, proj_shape)
if formula.lower() == "symmetric":
evecs_b1 = self._periodic_gauge(
tr_vectors, -b1, [self.n_orb, nsub], n_occ=nsub
)
udual_b1 = self._dual_state(
tr_vectors, evecs_b1, [self.n_orb, nsub], n_sub=nsub, n_occ=nsub
)
del evecs_b1
coeff = self.physics.smearing(
udual_b1,
eigenvecs,
eigenvals,
smearing_temperature,
mu,
nsub,
self.n_orb,
is_dual=True,
spin=None,
)
pmb1_2 = self.physics.get_proj(
coeff, udual_b1, nsub, self.n_orb, is_dual=True
)
del coeff, udual_b1
evecs_b2 = self._periodic_gauge(
tr_vectors, -b2, [self.n_orb, nsub], n_occ=nsub
)
udual_b2 = self._dual_state(
tr_vectors, evecs_b2, [self.n_orb, nsub], n_sub=nsub, n_occ=nsub
)
del evecs_b2
coeff = self.physics.smearing(
udual_b2,
eigenvecs,
eigenvals,
smearing_temperature,
mu,
nsub,
self.n_orb,
is_dual=True,
spin=None,
)
pmb2_2 = self.physics.get_proj(
coeff, udual_b2, nsub, self.n_orb, is_dual=True
)
del coeff, udual_b2
p2 -= self.backend.commutator(pb1_2, pmb2_2, proj_shape, proj_shape)
del pb1_2
p2 -= self.backend.commutator(pmb1_2, pb2_2, proj_shape, proj_shape)
del pb2_2
p2 += self.backend.commutator(pmb1_2, pmb2_2, proj_shape, proj_shape)
del pmb1_2, pmb2_2
else:
gsp_1 = input_projector[0]
gsp_2 = input_projector[1]
pb1_1 = input_projector[2]
pb2_1 = input_projector[3]
p1 = pb1_1 @ pb2_1 - pb2_1 @ pb1_1
pb1_2 = input_projector[4]
pb2_2 = input_projector[5]
p2 = pb1_2 @ pb2_2 - pb2_2 @ pb1_2
if formula == "symmetric":
pmb1_1 = input_projector[6]
pmb2_1 = input_projector[7]
p1 += (
(pmb1_1 @ pmb2_1 - pmb2_1 @ pmb1_1)
- (pb1_1 @ pmb2_1 - pmb2_1 @ pb1_1)
- (pmb1_1 @ pb2_1 - pb2_1 @ pmb1_1)
)
pmb1_2 = input_projector[8]
pmb2_2 = input_projector[9]
p2 += (
(pmb1_2 @ pmb2_2 - pmb2_2 @ pmb1_2)
- (pb1_2 @ pmb2_2 - pmb2_2 @ pb1_2)
- (pmb1_2 @ pb2_2 - pb2_2 @ pmb1_2)
)
########### ======= PBC local Z2 markers ========###########
coeff = self.physics.smearing(
vectors,
eigenvecs,
eigenvals,
smearing_temperature,
mu,
nsub,
self.n_orb,
is_dual=False,
spin=None,
)
gsp_1 = self.physics.get_proj(coeff, vectors, nsub, self.n_orb, is_dual=False)
del coeff, vectors
loc_z2_op_1 = self.backend.matmul(p1, gsp_1, proj_shape, proj_shape)
del p1, gsp_1
coeff = self.physics.smearing(
tr_vectors,
eigenvecs,
eigenvals,
smearing_temperature,
mu,
nsub,
self.n_orb,
is_dual=False,
spin=None,
)
gsp_2 = self.physics.get_proj(coeff, tr_vectors, nsub, self.n_orb, is_dual=False)
del coeff, tr_vectors
loc_z2_op_2 = self.backend.matmul(p2, gsp_2, proj_shape, proj_shape)
del p2, gsp_2
# get the diagonal elements
chern_diags_1 = self.backend.get_diag(loc_z2_op_1, N=self.n_orb)
del loc_z2_op_1
chern_diags_2 = self.backend.get_diag(loc_z2_op_2, N=self.n_orb)
del loc_z2_op_2
chern_diags_1 = (
(
-np.imag(chern_diags_1)
* (self.Lx * self.Ly)
/ (np.pi * (8 if formula == "symmetric" else 2))
)
if self.backend.is_master_rank
else None
)
chern_diags_2 = (
(
-np.imag(chern_diags_2)
* (self.Lx * self.Ly)
/ (np.pi * (8 if formula == "symmetric" else 2))
)
if self.backend.is_master_rank
else None
)
chern_diags_1 = self.backend.shared_array(chern_diags_1)
chern_diags_2 = self.backend.shared_array(chern_diags_2)
if bare_marker:
return np.array([chern_diags_1, chern_diags_2])
# If macroscopic_average I have to compute the lattice values with the averages
# first (explicit PBC contraction passed)
if macroscopic_average:
contraction = pbc_lattice_contraction(self, cutoff)
chern_diags_1 = average_over_radius(
self, chern_diags_1, cutoff, contraction=contraction
)
chern_diags_2 = average_over_radius(
self, chern_diags_2, cutoff, contraction=contraction
)
if direction is not None:
pbclcm_1 = self._extract_line_marker(
chern_diags_1, direction, start, macroscopic_average
)
pbclcm_2 = self._extract_line_marker(
chern_diags_2, direction, start, macroscopic_average
)
if not return_projector:
return np.abs(np.fmod(0.5 * (pbclcm_1 - pbclcm_2), 2))
else:
return np.abs(np.fmod(0.5 * (pbclcm_1 - pbclcm_2), 2)), np.asarray(
return_proj
)
# If not macroscopic averages I sum the values of the Chern operators of the unit cell
if not macroscopic_average:
chern_diags_1 = self._trace_unit_cell(chern_diags_1)
chern_diags_2 = self._trace_unit_cell(chern_diags_2)
if not return_projector:
return np.abs(np.fmod(0.5 * (chern_diags_1 - chern_diags_2), 2))
else:
return np.abs(np.fmod(0.5 * (chern_diags_1 - chern_diags_2), 2)), np.array(
return_proj
)