Source code for pytdscf.hamiltonian_cls

"""
The operator modules consists Hamiltonian.
"""

from __future__ import annotations

import itertools
import math
import random
from typing import Literal

import jax
import jax.numpy as jnp
import numpy as np
from loguru import logger as _logger

from pytdscf import units
from pytdscf._const_cls import const
from pytdscf._mpo_cls import MatrixProductOperators, OperatorCore
from pytdscf.dvr_operator_cls import TensorOperator

logger = _logger.bind(name="main")


[docs] class TermProductForm: """ Hold a product form of operators, such as :math:`1.0 q_1 q_2^2`. Attributes: coef (float or complex) : The coefficient of the product operator, e.g. ``1.0``. op_dofs (List[int]) : The MPS lattice (0-)index of operator, e.g. ``[0, 1]``. op_keys (List[str]) : The type of operators for each legs, e.g. ``['q', 'q^2']``. mode_ops (Dict[Tuple[int], str]) : The pair of ``op_dofs`` and ``op_keys``. blockop_key_sites (Dict[str, List[str]]) : Operator legs divided by central site. E.g. \ 3 DOFs, ``'q_0q^2_1'`` gives \ ``{left:['ovlp','q_0', 'q_0q^2_1'], centr:['q_0', 'q^2_1', 'ovlp'], right:['q^2_1', 'ovlp', 'ovlp']}``. """ def __init__( self, coef: float | complex, op_dofs: list[int], op_keys: list[str] ): self.coef = coef self.op_dofs = op_dofs self.op_keys = op_keys self.mode_ops = { op_dof: op_key for op_dof, op_key in zip(op_dofs, op_keys, strict=True) } assert len(op_dofs) == len(op_keys) def __str__(self) -> str: if isinstance(self.coef, float): sign = "+" if self.coef > 0.0 else "-" return ( sign + f"{abs(self.coef):.4e}" + " " + " * ".join( [ op_key + "_" + str(op_dof) for (op_dof, op_key) in zip( self.op_dofs, self.op_keys, strict=True ) ] ) ) else: assert isinstance(self.coef, complex) real_part = f"{self.coef.real:.4e}" imag_part = f"{self.coef.imag:.4e}" sign_real = "+" if self.coef.real > 0.0 else "-" sign_imag = "+" if self.coef.imag > 0.0 else "-" return ( sign_real + real_part + sign_imag + imag_part + "j " + " * ".join( [ op_key + "_" + str(op_dof) for (op_dof, op_key) in zip( self.op_dofs, self.op_keys, strict=True ) ] ) )
[docs] @staticmethod def convert_key(op_dofs: list[int], op_keys: list[str]) -> str: """ convert product operator to string key Args: op_dofs (List[int]) : The MPS lattice (0-)index of operator, e.g. ``[0, 1]``. op_keys (List[str]) : The type of operators for each legs, e.g. ``['q', 'q^2']``. Returns: str : string key, e.g. ``q_0 q^2_1``. Examples: >>> TermProductForm.convert_key([1, 2], ['q', 'q^2']) 'q_1 q^2_2' """ key = "" for op_dof, op_key in zip(op_dofs, op_keys, strict=True): key += op_key + "_" + str(op_dof) + " " return key.rstrip()
[docs] def term_key(self) -> str: """ call `convert_key` attributes in this class. Returns: str : string key, e.g. ``q_1 q^2_2``. """ return self.convert_key(self.op_dofs, self.op_keys)
[docs] def is_op_ovlp(self, block_lcr_list: list[str], psite: int) -> bool: """ check MPS lattice block is ``'ovlp'`` operator or not. Args: block_lcr_list (List[str]) : ``'left'``, or ``'centr'``, or ``'right'`` psite (int) : (0-) index of center site. LCR of C. Returns: bool : Return ``True`` if all operator types with legs in the given block\ are ``'ovlp'`` when regard centered site as the ``psite``-th site. """ assert hasattr(self, "blockop_key_sites"), ( "blockop_key_sites attribute \ has not set. Call set_blockp_key attribute before call this attribute." ) for block_lcr in block_lcr_list: if self.blockop_key_sites[block_lcr][psite] != "ovlp": return False return True
[docs] def set_blockop_key(self, ndof: int, *, print_out: bool = False): """ For the complementary operator, the product operator is set to an attribute\ `blockop_key_sites` that is classified according to the center site. Args: ndof (int) : The length of MPS lattice, that is degree of freedoms. print_out (bool) : Print ``blockop_key_sites`` or not. Defaults to ``False``. """ blockop_key_l = [] blockop_key_c = [] blockop_key_r = [] for centr_dof in range(ndof): op_l_key = "" op_r_key = "" for kdof in self.op_dofs: if kdof < centr_dof: op_l_key += ( ("" if len(op_l_key) == 0 else " ") + self.mode_ops[kdof] + "_" + str(kdof) ) if kdof > centr_dof: op_r_key += ( ("" if len(op_r_key) == 0 else " ") + self.mode_ops[kdof] + "_" + str(kdof) ) blockop_key_l.append("ovlp" if len(op_l_key) == 0 else op_l_key) blockop_key_c.append( "ovlp" if centr_dof not in self.op_dofs else self.mode_ops[centr_dof] ) blockop_key_r.append("ovlp" if len(op_r_key) == 0 else op_r_key) self.blockop_key_sites = { "left": blockop_key_l, "centr": blockop_key_c, "right": blockop_key_r, } if print_out and const.verbose == 4: for idof in range(ndof): logger.debug( "(" + self.blockop_key_sites["left"][idof] + ", " + self.blockop_key_sites["centr"][idof] + ", " + self.blockop_key_sites["right"][idof] + ")" )
[docs] class TermOneSiteForm: """Operator with legs in only one degree of freedom Attributes: coef (float or complex) : The coefficient of the product operator, e.g. ``1.0``. op_dofs (int) : The MPS lattice (0-)index of operator, e.g. ``0``. op_keys (str) : The type of operators for each legs, e.g. ``'q'``, ``'d^2'``. """ def __init__(self, coef: float | complex, op_dof: int, op_key: str): self.coef = coef self.op_dof = op_dof self.op_key = op_key def __str__(self) -> str: if isinstance(self.coef, complex): real_part = f"{self.coef.real:.4e}" imag_part = f"{self.coef.imag:.4e}" sign_real = "+" if self.coef.real > 0.0 else "-" sign_imag = "+" if self.coef.imag > 0.0 else "-" return ( sign_real + real_part + sign_imag + imag_part + "j " + self.op_key + "_" + str(self.op_dof) ) else: sign = "+" if self.coef > 0.0 else "-" return ( sign + f"{abs(self.coef):.4e}" + " " + self.op_key + "_" + str(self.op_dof) )
[docs] def truncate_terms( terms: list[TermProductForm], cut_off: float = 0.0 ) -> list[TermProductForm]: """ Truncate operators by a certain cut_off. Args: terms (List[TermProductForm]) : Terms list of sum of products form before truncation. cut_off (float) : The threshold of truncation. unit is a.u. Returns: terms (List[TermProductForm]) : Terms list of sum of products form after truncation. """ terms_after = [] for term in terms: if abs(term.coef) >= cut_off: terms_after.append(term) return terms_after
def _accumulate_q0_terms(terms: list[TermProductForm]): """Regard ovlp operator as q_0 operator""" coef_q0 = 0.0 terms_wo_const = [] for term in terms: if "ovlp" in term.op_keys: coef_q0 += term.coef # type: ignore else: terms_wo_const.append(term) return (terms_wo_const, coef_q0) def _accumulate_q0_terms_with_const(terms: list[TermProductForm], ndof: int): """Regard ovlp operator as random dofs operator""" coef_q0 = 0.0 terms_with_const = [] for term in terms: if "ovlp" in term.op_keys: coef_q0 += term.coef # type: ignore else: terms_with_const.append(term) dof_any = random.choice(range(ndof)) terms_with_const.append(TermProductForm(coef_q0, [dof_any], ["ovlp"])) return (terms_with_const, 0.0) def _extract_onesite( terms_general: list[TermProductForm], ) -> tuple[list[TermProductForm], list[TermOneSiteForm]]: """ Extract onesite operators, such as 'q^2_1', 'd^2_3' from all terms. Args: terms_general (List[TermProductForm]) : \ List of all sume of products operators. Returns: Tuple(List[TermProductForm], List[TermOneSiteForm])) : \ terms_multisite, terms_onesite """ terms_multisite = [] terms_onesite = [] for term in terms_general: assert len(term.op_dofs) > 0, ( 'define PolynomialHamiltonian scalar term as _matH attribute (not in "general" type operator)' ) if len(term.op_dofs) == 1: terms_onesite.append( TermOneSiteForm(term.coef, term.op_dofs[0], term.op_keys[0]) ) else: terms_multisite.append(term) return (terms_multisite, terms_onesite)
[docs] class HamiltonianMixin: """Hamiltonian abstract class. Attributes: name (str) : The name of operator. onesite_name (str) : The name of onesite term. Defaults tp ``'onesite'``. nstete (int) : The number of electronic states. ndof (int) : The degree of freedoms. coupleJ (List[List[complex]]) : The couplings of electronic states. \ ``coupleJ[i][j]`` denotes the couplings between `i`-states (bra) \ and `j`-states (ket). This contains scalar operator. """ def __init__( self, name: str, nstate: int, ndof: int, matJ: list[list[complex | float]] | None = None, ): self.name = name self.nstate = nstate self.ndof = ndof if matJ is None: self.coupleJ = [ [complex(0.0) for j in range(nstate)] for i in range(nstate) ] else: assert len(matJ) == nstate and len(matJ[0]) == nstate, ( "matJ must be square matrix" ) self.coupleJ = [ [complex(matJ[i][j]) for j in range(nstate)] for i in range(nstate) ]
[docs] class PolynomialHamiltonian(HamiltonianMixin): """ PolynomialHamiltonian package, contains some model Hamiltonian. Sometimes, this \ class also manage observable operators, such as dipole operator Attributes: coupleJ (List[List[complex]]) : The couplings of electronic states. \ ``coupleJ[i][j]`` denotes the couplings between `i`-states (bra) \ and `j`-states (ket). This contains scalar operator. onesite (List[List[List[TermProductForm or TermOneSiteForm]]]) : \ The onesite operators. ``onesite[i][j][k]`` \ denotes `k`-th onesite operator (TermOneSiteForm) between \ `i`-states (bra) and `j`-states(ket). general (List[List[List[TermProductForm or TermOneSiteForm]]]) : \ The multisite operators. The definition is \ almost the same as ``onesite``. """ def __init__( self, ndof: int, nstate: int = 1, name: str = "hamiltonian", matJ=None ): super().__init__(name, nstate, ndof, matJ) self.onesite_name = ( "onesite" if name == "hamiltonian" else "onesite_" + name ) self.onesite: list[list[list[TermProductForm | TermOneSiteForm]]] = [ [[] for j in range(nstate)] for i in range(nstate) ] self.general: list[list[list[TermProductForm | TermOneSiteForm]]] = [ [[] for j in range(nstate)] for i in range(nstate) ]
[docs] def set_HO_potential_ham1(self, basinfo): for istate in range(self.nstate): terms = [] for idof in range(self.ndof): terms.append(TermOneSiteForm(1.0, idof, "ham1")) self.onesite[istate][istate] += terms
[docs] def set_HO_potential(self, basinfo, *, enable_onesite=True) -> None: """Setting Harmonic Oscillator polynomial-based potential""" """Intra-state terms |i><i|""" for istate in range(self.nstate): terms = [] for idof in range(self.ndof): pbas = basinfo.get_primbas(istate, idof) """V(Q) = (w**2/2) (q-q0)**2 [= (w/2) (Q-Q0)**2] = (w**2/2) (q**2 -(2*q0)*q +(q0**2)) """ q0 = pbas.origin_mwc w = pbas.freq_au terms.append(TermProductForm(-1 / 2, [idof], ["d^2"])) terms.append(TermProductForm(w**2 / 2, [idof], ["q^2"])) if q0 != 0.0: terms.append( TermProductForm(w**2 / 2 * (-2 * q0), [idof], ["q^1"]) ) terms.append( TermProductForm(w**2 / 2 * (q0**2), [idof], ["ovlp"]) ) terms = truncate_terms(terms) terms_general, coupleJ = _accumulate_q0_terms_with_const( terms, self.ndof ) if enable_onesite: terms_general, terms_onesite = _extract_onesite(terms_general) self.onesite[istate][istate] += terms_onesite self.coupleJ[istate][istate] += coupleJ self.general[istate][istate] += terms_general
[docs] def set_LVC( self, bas_info, first_order_coupling: dict[tuple[int, int], dict[int, float]], ): """Setting Linear Vibronic Coupling (LVC) Model Args: bas_info (BasisInfo): Basis information first_order_coupling (Dict[Tuple[int, int], Dict[int, float]]): First order coupling if {(0,1): {2: 0.1}} is given, then the coupling between 0-state and 1-state is 0.1*Q_2 """ self.set_HO_potential(bas_info, enable_onesite=True) for (istate, jstate), coupling in first_order_coupling.items(): for idof, coef in coupling.items(): self.onesite[istate][jstate].append( TermOneSiteForm(coef, idof, "q^1") )
[docs] def is_hermitian(self): raise NotImplementedError
[docs] def set_ConIns_potential(self, basinfo): """Intra-state terms i><i""" for istate in range(self.nstate): terms = [] for idof in range(self.ndof): pbas = basinfo.get_primbas(istate, idof) """V(Q) = (w**2/2) (q-q0)**2 [= (w/2) (Q-Q0)**2] = (w**2/2) (q**2 -(2*q0)*q +(q0**2)) """ q0 = pbas.origin_mwc w = pbas.freq_au # pbas.nprim terms.append(TermProductForm(-1 / 2, [idof], ["d^2"])) terms.append(TermProductForm(w**2 / 2, [idof], ["q^2"])) terms.append( TermProductForm(w**2 / 2 * (-2 * q0), [idof], ["q^1"]) ) terms.append( TermProductForm(w**2 / 2 * (q0**2), [idof], ["ovlp"]) ) terms = truncate_terms(terms) terms_general, coupleJ = _accumulate_q0_terms_with_const( terms, self.ndof ) terms_general, terms_onesite = _extract_onesite(terms_general) self.coupleJ[istate][istate] += coupleJ self.general[istate][istate] += terms_general self.onesite[istate][istate] += terms_onesite """|al><be|op terms """ for istate, jstate in itertools.product(range(self.nstate), repeat=2): if istate != jstate: terms_onesite = [] for idof in [0]: coupleL2 = (50 / units.au_in_cm1) * ( 206.0 / units.au_in_cm1 ) ** 1.0 terms_onesite.append(TermOneSiteForm(coupleL2, idof, "q^2")) self.onesite[istate][jstate] += terms_onesite
[docs] def set_henon_heiles( self, omega: float, lam: float, f: int, omega_unit: str = "cm-1", lam_unit: str = "a.u.", ) -> list[list[TermProductForm]]: r"""Setting Henon-Heiles potential In dimensionless form, the Hamiltonian is given by .. math:: \hat{H} = \frac{\omega}{2}\sum_{i=1}^{f} \left( - \frac{\partial^2}{\partial q_i^2} + q_i^2 \right) \ + \lambda \left( \sum_{i=1}^{f-1} q_i^2 q_{i+1} - \frac{1}{3} q_{i+1}^3 \right) But PyTDSCF adopts mass-weighted coordinate, thus the Hamiltonian is given by .. math:: \hat{H} = \frac{1}{2}\sum_{i=1}^{f} \left( - \frac{\partial^2}{\partial Q_i^2} + \omega^2 Q_i^2 \right) \ + \lambda \omega^{\frac{3}{2}} \left( \sum_{i=1}^{f-1} Q_i^2 Q_{i+1} - \frac{1}{3} Q_{i+1}^3 \right) Args: omega (float): Harmonic frequency in a.u. lam (float): Coupling constant in a.u. f (int): Number of degrees of freedom Returns: List[List[TermProductForm]]: List of Hamiltonian terms """ terms = [] if omega_unit == "cm-1": omega /= units.au_in_cm1 elif omega_unit.lower() in ["au", "a.u.", "hartree"]: pass else: raise ValueError("omega_unit must be cm-1 or a.u.") if lam_unit == "cm-1": lam = lam / units.au_in_cm1 elif lam_unit.lower() in ["au", "a.u.", "hartree"]: pass else: raise ValueError("lam_unit must be cm-1 or a.u.") for idof in range(f): terms.append(TermProductForm(-1 / 2, [idof], ["d^2"])) terms.append(TermProductForm(pow(omega, 2) / 2, [idof], ["q^2"])) for idof in range(f - 1): terms.append( TermProductForm( lam * pow(omega, 1.5), [idof, idof + 1], ["q^2", "q^1"] ) ) terms.append( TermProductForm(-lam * pow(omega, 1.5) / 3, [idof + 1], ["q^3"]) ) istate = 0 terms_general, terms_onesite = _extract_onesite(terms) self.coupleJ[istate][istate] = 0.0 self.general[istate][istate] += terms_general self.onesite[istate][istate] += terms_onesite return [terms]
[docs] def set_henon_heiles_2D_4th( self, lam: float = 0.2 ) -> list[list[TermProductForm]]: r""" .. math:: H(x,y) = -\frac{1}{2}\left(\frac{d^2}{dx^2} + \frac{d^2}{dy^2}\right) \ + \frac{1}{2}(x^2 + y^2) + \lambda \left(xy^2 - \frac{1}{3}x^3\right)\ + \lambda^2 \left(\frac{1}{16}(x^4 + y^4) + \frac{1}{8}x^2y^2\right) """ terms = [] xdof = 0 ydof = 1 """-(1/2)(d^2_x + d^2_y)""" terms.append(TermProductForm(-1 / 2, [xdof], ["d^2"])) terms.append(TermProductForm(-1 / 2, [ydof], ["d^2"])) """(1/2)(x^2 + y^2)""" terms.append(TermProductForm(+1 / 2, [xdof], ["q^2"])) terms.append(TermProductForm(+1 / 2, [ydof], ["q^2"])) """lam (x*y^2 - (1/3)*x^3)""" terms.append(TermProductForm(lam, [xdof, ydof], ["q^1", "q^2"])) terms.append(TermProductForm(-(1 / 3) * lam, [xdof], ["q^3"])) """lam^2 ((x^4 + y^4)/16 + (x^2*y^2)/8""" terms.append(TermProductForm((1 / 16) * lam**2, [xdof], ["q^4"])) terms.append(TermProductForm((1 / 16) * lam**2, [ydof], ["q^4"])) terms.append( TermProductForm((1 / 8) * lam**2, [xdof, ydof], ["q^2", "q^2"]) ) """""" for term_prod in terms: term_prod.set_blockop_key(self.ndof) istate = 0 terms_general, terms_onesite = _extract_onesite(terms) self.coupleJ[istate][istate] = 0.0 self.onesite[istate][istate] += terms_onesite self.general[istate][istate] += terms_general return [terms]
[docs] class TensorHamiltonian(HamiltonianMixin): """Hamiltonian in tensor formulation. Attributes: mpo (List[List[MatrixProductOperators]]) : MPO between i-state and j-state """ mpo: list[list[MatrixProductOperators | None]] def __init__( self, ndof: int, potential: list[ list[dict[tuple[int | tuple[int, int], ...], TensorOperator]] ], name: str = "hamiltonian", kinetic: list[ list[dict[tuple[tuple[int, int], ...], TensorOperator] | None] ] | None = None, decompose_type: Literal["QRD", "SVD"] = "QRD", rate: float | None = None, bond_dimension: list[int] | int | None = None, backend: Literal["jax", "numpy"] = "jax", ): """ Args: ndof (int): degree of freedom, equals to number os sites potential (List[List[Dict[Tuple[int], TensorOperator]]]): [istate][jstate] PES name (str, optional): Defaults to 'hamiltonian'. kinetic (Dict[Tuple[int], TensorOperator], optional): kinetic term. Defaults to None. \ kinetic term are the same for all states. decompose_type (Optional[str], optional): MPO decompose algorithm. Defaults to 'QRD'. rate (Optional[float], optional): SVD-MPO contribution rate. Defaults to None. bond_dimension (Optional[List[int] or int], optional): SVD-MPO bond dimension. Defaults to None. """ if const.mpi_rank != 0: nstate = 1 super().__init__(name, nstate, ndof) self.mpo = [[None for j in range(nstate)] for i in range(nstate)] return nstate = len(potential) super().__init__(name, nstate, ndof) self.mpo = [[None for j in range(nstate)] for i in range(nstate)] for i, j in itertools.product(range(nstate), range(nstate)): operators: dict[ tuple[int | tuple[int, int], ...], list[np.ndarray] | list[jax.Array], ] = {} if potential[i][j] is not None: for key, tensor in potential[i][j].items(): if key == (): if not (isinstance(tensor, float | complex | int)): raise ValueError( f"scalar term must be scalar but {tensor} is {type(tensor)}" ) self.coupleJ[i][j] = tensor continue else: # key is (0,1,...) or ((0,0),(1,1),...) # if key is (0,1,...), then return (0,1,...) # if key is ((0,0),(1,1),...), then return (0,0,1,1,...) flatten_key = tuple() for k in key: if type(k) is tuple: flatten_key += k else: flatten_key += (k,) if flatten_key != tensor.legs: raise ValueError( f"Given potential key {key} is not consistent with tensor legs {tensor.legs}" ) if backend.lower() == "jax": if tensor.dtype in [jnp.complex128, np.complex128]: dtype = jnp.complex128 elif tensor.dtype in [jnp.float64, np.float64]: dtype = jnp.float64 else: raise ValueError( f"core dtype must be complex128 or float64 but {tensor.dtype} is given" ) operators[key] = [ jnp.array(core, dtype=dtype) for core in tensor.decompose( bond_dimension=bond_dimension, decompose_type=decompose_type, rate=rate, ) ] elif backend.lower() == "numpy": operators[key] = [ core for core in tensor.decompose( bond_dimension=bond_dimension, decompose_type=decompose_type, rate=rate, ) ] else: raise ValueError( f"backend must be jax, or numpy but {backend} is given" ) if kinetic is not None and kinetic[i][j] is not None: assert isinstance(kinetic, list) K_ij = kinetic[i][j] assert isinstance(K_ij, dict) for key, d2 in K_ij.items(): if backend.lower() == "jax": if d2.dtype in [jnp.complex128, np.complex128]: dtype = jnp.complex128 elif d2.dtype in [jnp.float64, np.float64]: dtype = jnp.float64 if key in operators: raise ValueError( f"key {key} is already set in potential. " + "Concatenate KEO and PEO or set KEO as SOP" ) operators[key] = [ jnp.array(core, dtype=dtype) for core in d2.decompose() ] else: operators[key] = d2.decompose() self.mpo[i][j] = MatrixProductOperators( nsite=ndof, operators=operators, backend=backend )
[docs] def interaction_picture(self, U: TensorHamiltonian): """ H_int = U^† H U """ assert len(self.mpo) == 1, "Only one state is supported" assert len(U.mpo) == 1, "Only one state is supported" H_mpo = self.mpo[0][0] U_mpo = U.mpo[0][0] assert isinstance(U_mpo, MatrixProductOperators) assert isinstance(H_mpo, MatrixProductOperators) for i, op_cores in enumerate(U_mpo.calc_point): if len(op_cores) != 1: continue if H_mpo.calc_point[i] is None: continue else: H_cores = H_mpo.calc_point[i] op_core = op_cores[0] assert isinstance(op_core, OperatorCore) assert op_core.key in [(i,), ((i, i),)], f"{op_core.key=}" if op_core.key == (i,): subscripts = "b,cbde,d->cbde" elif op_core.key == ((i, i),): subscripts = "ab,cbde,df->cafe" else: raise ValueError(f"{op_core.key=} is not supported") for H_core in H_cores: assert isinstance(H_core, OperatorCore) assert isinstance(H_core.data, jnp.ndarray | np.ndarray) assert H_core.data.ndim == 4, ( f"{H_core.data.ndim=} is not yet implemented" ) if isinstance(H_core.data, jnp.ndarray): einsum = jnp.einsum else: einsum = np.einsum # type: ignore assert isinstance(op_core.data, jnp.ndarray | np.ndarray) H_core.data = einsum( subscripts, op_core.data[0, ..., 0].conj(), H_core.data, op_core.data[0, ..., 0], )
[docs] def distribute_mpo_cores(self): import copy import mpi4py comm = mpi4py.MPI.COMM_WORLD if const.mpi_rank == 0: self.mpo_all = copy.deepcopy(self.mpo) send_data_all = [] split_indices = const.split_indices for k in range(comm.size): send_data_k: dict[ tuple[int, int], list[list[OperatorCore]] | None ] = {} for i, j in itertools.product( range(self.nstate), range(self.nstate) ): mpo_ij = self.mpo[i][j] if isinstance(mpo_ij, MatrixProductOperators): if k < len(split_indices) - 1: calc_point = mpo_ij.calc_point[ split_indices[k] : split_indices[k + 1] + 1 ] # for adaptive calculation, terminate site should be shared. else: calc_point = mpo_ij.calc_point[split_indices[k] :] send_data_k[(i, j)] = calc_point else: send_data_k[(i, j)] = None send_data_all.append(send_data_k) else: send_data_all = None recv_data = comm.scatter(send_data_all, root=0) for i, j in itertools.product(range(self.nstate), range(self.nstate)): if recv_data[(i, j)] is not None: nsite = const.end_site_rank - const.bgn_site_rank + 1 if const.mpi_rank != const.mpi_size - 1: nsite += 1 mpo_ij = MatrixProductOperators( nsite=nsite, operators={}, backend="jax" if const.use_jax else "numpy", ) mpo_ij.calc_point = recv_data[(i, j)] self.mpo[i][j] = mpo_ij
[docs] def project_subspace(self, subspace_inds: dict[int, tuple[int, ...]]): assert len(self.mpo) == 1, "Only one state is supported" mpo = self.mpo[0][0] if mpo is None: return for isite, P_inds in subspace_inds.items(): if mpo.calc_point[isite] is None: continue for core in mpo.calc_point[isite]: assert isinstance(core, OperatorCore) if isinstance(core.data, int): continue elif len(core.data.shape) == 3: core.data = core.data[:, P_inds, :] elif len(core.data.shape) == 4: is_hermitian = core.is_hermitian() # Reduce bra and ket indices according to projection. ket_inds, bra_inds = np.ix_(P_inds, P_inds) core.data = core.data[:, ket_inds, bra_inds, :] is_hermitian_after = core.is_hermitian() if is_hermitian != is_hermitian_after: logger.warning( f"The operator {core.key} is {is_hermitian} hermitian, but after subspace projection, it is {is_hermitian_after} hermitian. You might need to change integrator." ) else: raise ValueError( f"core.data.shape = {core.data.shape} is not supported" )
[docs] def read_potential_nMR( potential_emu: dict[tuple[int, ...], float | complex], *, active_modes: list[int] | None = None, name: str = "hamiltonian", cut_off: float | None = None, dipole_emu: dict[tuple[int, ...], tuple[float, float, float]] | None = None, print_out: bool = False, active_momentum=None, div_factorial: bool = True, efield: tuple[float, float, float] = (1.0, 1.0, 1.0), ) -> PolynomialHamiltonian: """ Construct polynomial potential, such as n-Mode Representation form. \ multi-state nMR is not yet implemented. Args: potential_emu (Dict[Tuple[int], float or complex]) : The coeffcients of operators. \ The index is start from `1`. Factorial factor is not included in the coefficients.\ E.g. V(q_1) = 0.123 * 1/(2!) * q_1^2 gives ``{(1,1):0.123}``.\n units are all a.u.:\n k_orig{ij} in [Hartree/emu/bohr**2]\n k_orig{ijk} in [Hartree/emu**i(3/2)/BOHR**3]\n k_orig{ijkl} in [Hartree/emu**(2)/BOHR**4]\n active_modes (List[int]) : The acctive degree of freedoms.\ Note that, The order of the MPS lattice follows this list.\ The index is start from `1`. name (str) : The name of operator. cut_off (float) : The threshold of truncatig terms by coeffcients after \ factorial division. dipole_emu (Dict[Tuple[int], float or complex]) : Dipole moment operator to generate a vibrational \ excited states. Defaults to ``None``. The definition is almost the \ same as `potential_emu`, but this value is 3d vector (list).\n mu{ij} in [Debye/emu/bohr**2]\n mu{ijk} in [Debye/emu**(3/2)/BOHR**3]\n mu{ijkl} in [Debye/emu**(2)/BOHR**4]\n print_out (bool) : Defaults to ``False`` div_factorial(bool) : Whether or not divided by factorial term. \ Defaults to ``True``. active_momentum (List[Tuple[int, float]]) : [(DOF, coef)]. Defaults to ``None``\ When active_momentum are set, unselected kinetic energy operators \ are excluded. Default option include all kinetic energy operators. efield (List[float]) : The electric field in [Hartree Bohr^-1 e^-1]. Defaults to ``[1.0, 1.0, 1.0]`` Returns: PolynomialHamiltonian : operator """ if active_modes is None: if dipole_emu is not None: active_modes = sorted( list(set(itertools.chain.from_iterable(dipole_emu.keys()))) ) elif potential_emu is not None: active_modes = sorted( list(set(itertools.chain.from_iterable(potential_emu.keys()))) ) else: raise ValueError("active_modes must be set") logger.info("Construct anharmonic polynomial operator") k_orig = potential_emu scalar_term: float = 0.0 if dipole_emu is not None: mu = dipole_emu active_momentum = False k_orig = {} for key, val in mu.items(): if key == (): scalar_term += float(np.dot(val, efield)) else: k_orig[key] = np.dot(val, efield) # mode_set = set(itertools.chain.from_iterable([key for key in k_orig.keys()])) dic = {} for i in range(len(active_modes)): dic[active_modes[i]] = i + 1 nmode = len(active_modes) # max([len(key) for key in k_orig.keys()]) nstate = 1 ndof = nmode k = {} for key, value in k_orig.items(): """ e.g. k_orig[(1,2,3,3)] -means-> coef[(1,1,2)] q_0 q_1 (q_2)^2 k_orig[(3,3,1)] -means-> coef[(1,0,2)] q_0 (q_2)^2 """ assert isinstance(value, float) if key == (): scalar_term = value continue if set(key) & set(active_modes) != set(key): continue degree_of_q = [0 for i in range(nmode)] for imode in key: imode = dic[imode] degree_of_q[imode - 1] += 1 if print_out: logger.debug(degree_of_q) key_new = tuple(degree_of_q) assert key_new not in k, "duplicated keys in k_orig" k[key_new] = value matJ = [[scalar_term]] hamiltonian = PolynomialHamiltonian(ndof, nstate, name, matJ) terms = [] """kinetic terms""" if active_momentum is None: for idof in range(nmode): terms.append(TermProductForm(-1 / 2, [idof], ["d^2"])) elif active_momentum: for idof, coef in active_momentum.items(): idof = dic[idof] - 1 terms.append(TermProductForm(coef, [idof], ["d^2"])) """potential terms""" for key, value in k.items(): op_dofs = [] op_keys = [] fac = 1.0 for idof, order in enumerate(key): if order > 0: op_dofs.append(idof) op_keys.append("q^" + str(order)) if div_factorial: fac /= math.factorial(order) coef = fac * value terms.append(TermProductForm(coef, op_dofs, op_keys)) if cut_off is not None: terms = truncate_terms(terms, cut_off=cut_off) if const.verbose > 3: logger.debug(f"{name} sum of products term = {len(terms)}") terms_multisite, terms_onesite = _extract_onesite(terms) for term_prod in terms_multisite: term_prod.set_blockop_key(hamiltonian.ndof, print_out=print_out) hamiltonian.onesite[0][0] += terms_onesite hamiltonian.general[0][0] += terms_multisite return hamiltonian