Source code for grogupy.physics.hamiltonian

# Copyright (c) [2024-2025] [Laszlo Oroszlany, 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.
import copy
import warnings
from typing import Union

import numpy as np
import sisl
from numpy.typing import NDArray

from .._core.constants import TAU_X, TAU_Y, TAU_Z
from .._core.utilities import RotMa2b, build_hh_ss, hsk
from .._tqdm import _tqdm
from ..batch.timing import DefaultTimer
from ..config import CONFIG
from .utilities import spin_tracer

if CONFIG.is_GPU:
    import cupy as cp
    from cupy.typing import NDArray as CNDArray


[docs] class Hamiltonian: """This class contains the data and the methods related to the Hamiltonian and geometry. It sets up the instance based on the path to the Hamiltonian of the DFT calculation and the DFT exchange orientation. Parameters ---------- infile: Union[str, tuple[sisl.physics.Hamiltonian, Union[sisl.physics.DensityMatrix, None]]] Path to the .fdf file or the sisl Hamiltonian and Density matrix, DM is optional scf_xcf_orientation: Union[list, NDArray]. optional The reference orientation, by default [0,0,1] Examples -------- Creating a Hamiltonian from the DFT calculation. >>> fdf_path = "/Users/danielpozsar/Downloads/Fe3GeTe2/Fe3GeTe2.fdf" >>> scf_xcf_orientation = np.array([0,0,1]) >>> hamiltonian = Hamiltonian(fdf_path, scf_xcf_orientation) >>> print(hamiltonian) <grogupy.Hamiltonian scf_xcf_orientation=[0 0 1], orientation=[0 0 1], NO=84> Methods ------- rotate(orientation) : It rotates the exchange field of the Hamiltonian. HkSk(k) : Sets up the Hamiltonian and the overlap matrix at a given k-point. copy() : Return a copy of this Pair Attributes ---------- _dh: sisl.physics.Hamiltonian The sisl Hamiltonian _ds: Union[sisl.physics.DensityMatrix, None] The sisl density matrix or None if it is not given infile: str The path to the .fdf file H: NDArray Hamiltonian built from the sisl Hamiltonian S: NDArray Overlap matrix built from the sisl Hamiltonian scf_xcf_orientation: NDArray Orientation of the DFT exchange field orientation: NDArray Current orientation of the XCF filed hTRS: NDArray Time reversal symmetric part of the Hamiltonian hTRB: NDArray Time reversal broken symmetric part of the Hamiltonian H_XCF: NDArray Exchange Hamiltonian XCF: NDArray Exchange field NO: int Number of orbitals in the Hamiltonian cell: NDArray The unit cell vectors nsc: NDArray Number of supercells in each direction sc_off: NDArray Supercell indices uc_in_sc_index: int Unit cell index H_uc: NDArray Unit cell Hamiltonian H_XCF_uc: NDArray Unit cell exchange part of the Hamiltonian times: grogupy.batch.timing.DefaultTimer It contains and measures runtime """ number_of_hamiltonians = 0
[docs] def __init__( self, infile: Union[ str, tuple[sisl.physics.Hamiltonian, Union[sisl.physics.DensityMatrix, None]], ], scf_xcf_orientation: Union[list, NDArray] = np.array([0, 0, 1]), ) -> None: """Initialize hamiltonian""" self.times: DefaultTimer = DefaultTimer() if isinstance(infile, str): # get sisl sile sile = sisl.io.get_sile(infile) # load hamiltonian self._dh: sisl.physics.Hamiltonian = sile.read_hamiltonian() try: self._ds: sisl.physics.DensityMatrix = sile.read_density_matrix() except: self._ds = None self.infile: str = infile elif isinstance(infile, tuple): if isinstance(infile[0], sisl.physics.Hamiltonian) and isinstance( infile[1], sisl.physics.DensityMatrix ): self._dh = infile[0] self._ds = infile[1] self.infile = "Unknown!" else: raise Exception("Not valid input:", (type(infile[0]), type(infile[1]))) else: raise Exception("Not valid input:", type(infile)) # if the Hamiltonian is unpolarized then there is no spin information if self._dh.spin.kind not in {1, 2, 3}: raise Exception("Unpolarized DFT calculation cannot be used!") if self._dh.spin.kind == 1: self._spin_state = "POLARIZED" if self._dh.spin.kind == 2: self._spin_state = "NON-COLINEAR" if self._dh.spin.kind == 3: self._spin_state = "SPIN-ORBIT" H, S = build_hh_ss(self._dh) self.H: Union[NDArray, None] = H self.S: Union[NDArray, None] = S self.scf_xcf_orientation: NDArray = np.array(scf_xcf_orientation) if (self.scf_xcf_orientation != 0).sum() != 1: warnings.warn( "Tilted exchange field in the DFT calculation: ", self.scf_xcf_orientation, ) self.orientation: NDArray = scf_xcf_orientation if CONFIG.is_CPU: # identifying TRS and TRB parts of the Hamiltonian TAUY: NDArray = np.kron(np.eye(self.NO), TAU_Y) hTR: NDArray = [] for i in _tqdm(range(self.nsc.prod()), desc="Transpose Hamiltonian"): hTR.append(TAUY @ self.H[i].conj() @ TAUY) hTR = np.array(hTR) hTRS: NDArray = (self.H + hTR) / 2 hTRB: NDArray = (self.H - hTR) / 2 # extracting the exchange field traced: NDArray = [] for i in _tqdm(range(self.nsc.prod()), desc="Calculate V_XCF"): traced.append(spin_tracer(hTRB[i])) traced = np.array(traced) # equation 77 XCF: NDArray = np.array( [ np.array([f["x"] / 2 for f in traced]), np.array([f["y"] / 2 for f in traced]), np.array([f["z"] / 2 for f in traced]), ] ) H_XCF: NDArray = np.zeros( (self.nsc.prod(), self.NO * 2, self.NO * 2), dtype="complex128" ) for i, tau in _tqdm( enumerate([TAU_X, TAU_Y, TAU_Z]), total=3, desc="Calculate H_XC" ): H_XCF += np.kron(XCF[i], tau) elif CONFIG.is_GPU: # identifying TRS and TRB parts of the Hamiltonian TAUY: CNDArray = cp.kron(cp.eye(self.NO), cp.array(TAU_Y)) hTR: CNDArray = [] for i in _tqdm(range(self.nsc.prod()), desc="Transpose Hamiltonian"): hTR.append(TAUY @ cp.array(self.H[i]).conj() @ TAUY) hTR = cp.array(hTR) hTRS: NDArray = (self.H + hTR.get()) / 2 hTRB: NDArray = (self.H - hTR.get()) / 2 # extracting the exchange field equation 77 traced: list = [] for i in _tqdm(range(self.nsc.prod()), desc="Calculate V_XCF"): traced.append(spin_tracer(hTRB[i])) XCF: NDArray = cp.array( [ cp.array([f["x"] / 2 for f in traced]), cp.array([f["y"] / 2 for f in traced]), cp.array([f["z"] / 2 for f in traced]), ] ) H_XCF: NDArray = cp.zeros( (self.nsc.prod(), self.NO * 2, self.NO * 2), dtype="complex128" ) for i, tau in _tqdm( enumerate([TAU_X, TAU_Y, TAU_Z]), total=3, desc="Calculate H_XC" ): H_XCF += np.kron(XCF[i], cp.array(tau)) XCF, H_XCF = XCF.get(), H_XCF.get() else: raise ValueError(f"Unknown architecture: {CONFIG.architecture}") # check if exchange field has scalar part max_xcfs: float = abs(np.array([f["c"] / 2 for f in traced])).max() if max_xcfs > 1e-12: warnings.warn( f"Exchange field has non negligible scalar part. Largest value is {max_xcfs}" ) self.hTRS, self.hTRB, self.XCF, self.H_XCF = hTRS, hTRB, XCF, H_XCF # pre calculate hidden unuseed properties # they are here so they are dumped to the self.__dict__ upon saving self.__no = self._dh.no self.__cell = self._dh.geometry.cell self.__sc_off = self._dh.geometry.sc_off self.__uc_in_sc_index = self._dh.lattice.sc_index([0, 0, 0]) self.times.measure("setup", restart=True) Hamiltonian.number_of_hamiltonians += 1
def __getstate__(self): state = self.__dict__.copy() state["times"] = state["times"].__getstate__() return state def __setstate__(self, state): times = object.__new__(DefaultTimer) times.__setstate__(state["times"]) state["times"] = times self.__dict__ = state def __eq__(self, value): if isinstance(value, Hamiltonian): if ( np.allclose(self._dh.Hk().toarray(), value._dh.Hk().toarray()) and np.allclose(self._dh.Sk().toarray(), value._dh.Sk().toarray()) and self.infile == value.infile and self._spin_state == value._spin_state and np.allclose(self.H, value.H) and np.allclose(self.S, value.S) and np.allclose(self.scf_xcf_orientation, value.scf_xcf_orientation) and np.allclose(self.orientation, value.orientation) and np.allclose(self.hTRS, value.hTRS) and np.allclose(self.hTRB, value.hTRB) and np.allclose(self.XCF, value.XCF) and np.allclose(self.H_XCF, value.H_XCF) ): # if DM is None return True if self._ds is None and value._ds is None: return True # if DM is not None, then compare else: if np.allclose( self._ds.Dk().toarray(), value._ds.Dk().toarray() ) and np.allclose( self._ds.Sk().toarray(), value._ds.Sk().toarray() ): return True else: return False return False return False def __repr__(self) -> str: """String representation of the instance.""" out = f"<grogupy.Hamiltonian scf_xcf_orientation={self.scf_xcf_orientation}, orientation={self.orientation}, NO={self.NO}>" return out @property def geometry(self) -> sisl.geometry: return self._dh.geometry @property def NO(self) -> int: try: self.__no = self._dh.no except: warnings.warn( "Property could not be calculated. This is only acceptable for loaded Hamiltonian!" ) return self.__no @property def cell(self) -> NDArray: try: self.__cell = self._dh.geometry.cell except: warnings.warn( "Property could not be calculated. This is only acceptable for loaded Hamiltonian!" ) return self.__cell @property def nsc(self) -> int: return self._dh.geometry.nsc @property def sc_off(self) -> NDArray: try: self.__sc_off = self._dh.geometry.sc_off except: warnings.warn( "Property could not be calculated. This is only acceptable for loaded Hamiltonian!" ) return self.__sc_off @property def uc_in_sc_index(self) -> int: self.__uc_in_sc_index = self._dh.sc_index([0, 0, 0]) return self.__uc_in_sc_index @property def H_uc(self) -> NDArray: return self.H[self.uc_in_sc_index] @property def H_XCF_uc(self) -> NDArray: return self.H_XCF[self.uc_in_sc_index]
[docs] def rotate(self, orientation: NDArray) -> None: """It rotates the exchange field of the Hamiltonian. It dumps the solutions to the `XCF`, `H_XCF`, `H` and `orientation` properties. Parameters ---------- orientation: NDArray The rotation where it rotates """ # obtain rotated exchange field and Hamiltonian R: NDArray = RotMa2b(self.scf_xcf_orientation, orientation) self.XCF: NDArray = np.einsum("ij,jklm->iklm", R, self.XCF) if CONFIG.is_CPU: self.H_XCF: NDArray = np.zeros( (self.nsc.prod(), self.NO * 2, self.NO * 2), dtype=np.complex128 ) for i, tau in _tqdm( enumerate([TAU_X, TAU_Y, TAU_Z]), total=3, desc="Rotating Exchange field", ): self.H_XCF += np.kron(self.XCF[i], tau) elif CONFIG.is_GPU: self.H_XCF: CNDArray = cp.zeros( (self.nsc.prod(), self.NO * 2, self.NO * 2), dtype=np.complex128 ) self.XCF = cp.array(self.XCF) for i, tau in _tqdm( enumerate([TAU_X, TAU_Y, TAU_Z]), total=3, desc="Rotating Exchange field", ): self.H_XCF += cp.kron(self.XCF[i], cp.array(tau)) self.H_XCF = self.H_XCF.get() else: raise Exception(f"Unknown architecture: {CONFIG.architecture}") # obtain total Hamiltonian with the rotated exchange field self.H: NDArray = self.hTRS + self.H_XCF # equation 76 self.orientation = orientation
[docs] def HkSk(self, k: tuple = (0, 0, 0)) -> tuple[NDArray, NDArray]: """Sets up the Hamiltonian and the overlap matrix at a given k-point. Parameters ---------- k: tuple, optional The given k-point, by default (0, 0, 0) Returns ------- Hk: NDArray The Hamiltonian at the given k point Sk: NDArray The Ovelap matrix at the given k point """ return hsk(self.H, self.S, self.sc_off, k)
[docs] def copy(self): """Returns the deepcopy of the instance. Returns ------- Hamiltonian The copied instance. """ return copy.deepcopy(self)
if __name__ == "__main__": pass