Source code for hqs_spin_mapper.transformables

"""Module containing the provided transformable objects for the HQS Spin Mapper."""

# Copyright © 2021-2024 HQS Quantum Simulations GmbH. All Rights Reserved.
import sys
import textwrap
import numpy as np
import yaml

from typing import Optional, Union, Tuple, Set, cast

import lattice_builder as lb
import lattice_validator as lv

from hqs_spin_mapper.spin_mapper_protocols import (
    Supports_SW_Transformation,
    Supports_Spin_Optimization,
    Hamiltonian_Matrices,
)

from lattice_builder import Builder
from struqture_py.fermions import FermionHamiltonianSystem

try:
    from lattice_functions.Fermions import (
        ExpressionSpinful,
        ExpressionSpinful_complex,
    )
except ImportError as e:
    if "mkl" in str(e):
        print(
            textwrap.dedent(
                """\
                ===== ERROR: Missing MKL =====

                The Intel Math Kernel Library (MKL) could not be found. Please ensure
                that the MKL is installed properly.

                When using HQStage, run

                   hqstage envs install-mkl

                to install the MKL. In other cases, please consult the manual."""
            ),
            file=sys.stderr,
        )
        sys.exit(1)
    else:
        raise

__all__ = [
    "Transformable",
    "Transformable_Matrices",
    "Transformable_Struqture",
    "Transformable_LatticeBuilder",
    "Transformable_QuantumChemistry",
    "SpinFinder",
]


[docs] class Transformable(Supports_SW_Transformation): r"""Base Transformable class to avoid code duplication between the transformables."""
[docs] def __init__(self) -> None: r"""Construct empty base class (variables specified for linting purposes only!).""" self._spin_indices: Set[int] = set() self._system_size = 0 self._site_type = "spinful_fermions" self._prefactor_cutoff = 1.0
@property def hamiltonians(self) -> Hamiltonian_Matrices: r"""Return the matrix descriptions of the terms in the system Hamiltonian. Returns: Hamiltonian_Matrices: The stored Hamiltonian matrices """ return self._hamiltonians @hamiltonians.setter def hamiltonians(self, value: Hamiltonian_Matrices) -> None: r"""Set the matrix descriptions of the terms in the system Hamiltonian. Args: value (Hamiltonian_Matrices): New collection of Hamiltonian matrices """ self._hamiltonians = value # TODO: Modify to use Union[np.ndarray, Tuple[np.ndarray, np.ndarray]] @property def rotation_matrix(self) -> np.ndarray: r"""Return the rotation matrix that transforms the system to the spin-optimized basis. Returns: np.ndarray: The stored rotation matrix :math:`U_{iq}` """ return self._rotation_matrix @rotation_matrix.setter def rotation_matrix(self, value: np.ndarray) -> None: r"""Set the rotation matrix that transforms the system to the spin-optimized basis. Args: value (np.ndarray): Updated rotation matrix :math:`U_{iq}` """ self._rotation_matrix = value @property def spin_indices(self) -> Set[int]: r"""Return the set of indices of the spin-like orbitals. Returns: Set[int]: The indices of the spin-like orbitals """ return self._spin_indices @spin_indices.setter def spin_indices(self, value: Set[int]) -> None: r"""Set the set of indices of the spin-like orbitals. Args: value (Set[int]): Updated indices of the spin-like orbitals """ self._spin_indices = value @property def system_size(self) -> int: r"""Return the system size i.e. the number of orbitals. Returns: int: The combined size of the system """ return self._system_size @property def site_type(self) -> str: r"""Return the type of orbitals (e.g. ``spinful_fermions`` or ``spinless_fermions``). Returns: str: The type of the system sites """ return self._site_type @property def prefactor_cutoff(self) -> float: r"""Return the cutoff for the prefactors of terms to be considered in the transformation. Returns: float: The cutoff below which terms are dropped for the transformation """ return self._prefactor_cutoff @prefactor_cutoff.setter def prefactor_cutoff(self, value: float) -> None: r"""Set the prefactor cutoff for terms considered in the Schrieffer-Woff transformation. Args: value (float): Lower bound for the absolute value of prefactors of terms considered. """ self._prefactor_cutoff = value @property def generator(self) -> Union[ExpressionSpinful, ExpressionSpinful_complex]: r"""Return the generator :math:`S` of the Schrieffer-Wolff transformation. Returns: Union[ExpressionSpinful, ExpressionSpinfulComplex]: The generator of the Schrieffer-Wolff transformation """ return self._generator @generator.setter def generator(self, value: Union[ExpressionSpinful, ExpressionSpinful_complex]) -> None: r"""Set the generator :math:`S` of the Schrieffer-Wolff transformation. Args: value (Union[ExpressionSpinful, ExpressionSpinful_complex]): Generator of the SW transformation """ self._generator = value @property def transformed_hamiltonian(self) -> Union[ExpressionSpinful, ExpressionSpinful_complex]: r"""Return the Schrieffer-Wolff transformed Hamiltonian :math:`\tilde{H}`. Returns: Union[ExpressionSpinful, ExpressionSpinful_complex]: The transformed Hamiltonian """ return self._transformed_hamiltonian @transformed_hamiltonian.setter def transformed_hamiltonian( self, value: Union[ExpressionSpinful, ExpressionSpinful_complex] ) -> None: r"""Set the Schrieffer-Wolff transformed Hamiltonian :math:`\tilde{H}`. Args: value (Union[ExpressionSpinful, ExpressionSpinful_complex]): Schrieffer-Wolff transformed Hamiltonian """ self._transformed_hamiltonian = value
[docs] class Transformable_LatticeBuilder(Transformable): r"""Transformable system from a LatticeBuilder input."""
[docs] def __init__( self, builder: Union[Builder, str], rotation_matrix: Optional[np.ndarray] = None, prefactor_cutoff: float = 1e-6, spin_indices: Optional[Set[int]] = None, ) -> None: r"""Construct the transformable system obtained from a LatticeBuilder input. Args: builder (Union[Builder, str]): LatticeBuilder input describing the system rotation_matrix (np.ndarray): Transformation matrix :math:`U_{iq}` to the spin-optimized basis prefactor_cutoff (float): Cutoff for the absolute value of the coupling strength of terms to be considered in the Schrieffer-Wolff transformation. spin_indices (Set[int]): Set of indices of the dedicated spin-like orbitals. Raises: AssertionError: Input is not conforming to lattice validator requirements. TypeError: The type of the input is incorrect. """ # If the input is a filename i.e. str, create a LatticeBuilder instance from it. def load_from_lattice_builder_input( lattice_builder_input: str, ) -> Builder: """Load LatticeBuilder yaml input file and create a transformable system object from it. Args: lattice_builder_input (str): Path to the lattice builder input yaml file Returns: Builder: Instance of LatticeBuilder for the input Raises: AssertionError: Input is not conforming to lattice validator requirements. """ # Open the yaml file containing the lattice builder description of the system with open(lattice_builder_input, mode="r") as stream: config = yaml.safe_load(stream) # Validate the system description validator = lv.Validator() is_valid, _config = validator.validateConfiguration(config) if not is_valid: print(_config) raise AssertionError() # Turn configuration data in lattice builder description of the system. lattice_builder = lb.Builder(_config) return lattice_builder if isinstance(builder, str): try: _builder = cast(Builder, load_from_lattice_builder_input(builder)) except AssertionError as lv_error: raise AssertionError("Not a valid LatticeBuilder description!") from lv_error # If the input is alread an instance of LatticeBuilder, continue elif isinstance(builder, Builder): _builder = cast(Builder, builder) else: raise TypeError("The given input has an invalid type!") self._system_size = int( np.prod(_builder.size) * np.prod(_builder.cluster_size) * _builder.M_uc ) if _builder.is_spin_matrix: (H0_uu, H0_dd, H0_ud) = _builder.getH0() H0_uu = cast(np.ndarray, H0_uu.toarray()) H0_dd = cast(np.ndarray, H0_dd.toarray()) H0_ud = cast(np.ndarray, H0_ud.toarray()) else: H0_uu = cast(np.ndarray, _builder.getH0().toarray()) H0_dd = cast(np.ndarray, _builder.getH0().toarray()) H0_ud = np.zeros_like(H0_uu) HU = np.zeros_like(H0_uu) HU += _builder.export_U_to_matrix() # Particle hole symmetry compensation (including the constant energy shift) H0_uu -= 0.5 * np.diag(np.diag(HU)) H0_dd -= 0.5 * np.diag(np.diag(HU)) self._particle_hole_symmetry_shift = np.real(0.25 * np.trace(HU)) if _builder.is_complex: self._hamiltonians = Hamiltonian_Matrices( H0_uu, H0_dd, H0_ud, HU, Jz=_builder.export_Jz_to_matrix(), Jp=_builder.export_Jperp_to_matrix(), Jc=_builder.export_Jcross_to_matrix(), ) self._transformed_hamiltonian = ExpressionSpinful_complex() self._generator = ExpressionSpinful_complex() else: self._hamiltonians = Hamiltonian_Matrices( H0_uu.real, H0_dd.real, H0_ud.real, HU.real, Jz=_builder.export_Jz_to_matrix().real, Jp=_builder.export_Jperp_to_matrix().real, Jc=_builder.export_Jcross_to_matrix().real, ) self._transformed_hamiltonian = ExpressionSpinful() self._generator = ExpressionSpinful() self._site_type = _builder.site_type if spin_indices is None: self._spin_indices: Set[int] = set() else: self._spin_indices = spin_indices if rotation_matrix is None: self._rotation_matrix = np.eye(self._system_size) else: self._rotation_matrix = rotation_matrix self._prefactor_cutoff = prefactor_cutoff
@property def particle_hole_symmetry_shift(self) -> float: r"""Return the constant energy shift due to the particle-hole symetry of the interaction. Returns: float: The the value of the constant energy shift """ return self._particle_hole_symmetry_shift
[docs] class Transformable_Struqture(Transformable): r"""Transformable system from a LatticeBuilder input."""
[docs] def __init__( self, system: FermionHamiltonianSystem, prefactor_cutoff: float = 1e-6, spin_indices: Optional[Set[int]] = None, ) -> None: r"""Construct the transformable system obtained from a Struqture input. Args: system (FermionHamiltonianSystem): Input Hamiltonian as struqture prefactor_cutoff (float): Cutoff for the absolute value of the coupling strength of terms to be considered in the Schrieffer-Wolff transformation. spin_indices (Set[int]): Set of indices of the dedicated spin-like orbitals. Raises: ValueError: Struqture incompatible with HQS Spin Mapper """ ERROR_MESSAGE = "Invalid structure of the input system." self._system_size = int(system.current_number_modes() // 2) # Obtain the quadratic terms, i.e. 1 creator & 1 annihilator system_H0 = system.separate_into_n_terms((1, 1))[0] # Obtain the quartic terms, i.e. 2 creators & 2 annihilators system_HU = system.separate_into_n_terms((2, 2))[0] H0_uu = np.zeros((self._system_size, self._system_size), dtype=complex) H0_dd = np.zeros((self._system_size, self._system_size), dtype=complex) H0_ud = np.zeros((self._system_size, self._system_size), dtype=complex) for term in system_H0.keys(): i0 = term.creators()[0] j0 = term.annihilators()[0] prefactor = system_H0.get(term) value = prefactor.real.value + 1j * prefactor.imag.value # Separate into spin-up and spin-down terms if i0 % 2 == 0 and j0 % 2 == 0: H0_uu[i0 // 2, j0 // 2] += value elif i0 % 2 == 1 and j0 % 2 == 1: H0_dd[i0 // 2, j0 // 2] += value else: H0_ud[i0 // 2, j0 // 2] += value / 2 HU = np.zeros( (self._system_size, self._system_size, self._system_size, self._system_size), dtype=complex, ) for term in system_HU.keys(): # Assign the indices of the tensor [ijkl] -> c^+_u c^+_d c_d c_u iU, jU = tuple(term.creators()) kU, lU = tuple(term.annihilators()) prefactor = system_HU.get(term) value = prefactor.real.value + 1j * prefactor.imag.value if iU % 2 == 0: if jU % 2 != 1: raise ValueError(ERROR_MESSAGE) else: if jU % 2 != 0: raise ValueError(ERROR_MESSAGE) iU, jU = jU, iU value *= -1.0 if lU % 2 == 0: if kU % 2 != 1: raise ValueError(ERROR_MESSAGE) else: if kU % 2 != 0: raise ValueError(ERROR_MESSAGE) kU, lU = lU, kU value *= -1.0 # Struqture orbitals are spinless -> thus mapping to spinful orbitals necessary HU[iU // 2, jU // 2, kU // 2, lU // 2] += value self._hamiltonians = Hamiltonian_Matrices(H0_uu=H0_uu, H0_dd=H0_dd, H0_ud=H0_ud, HU=HU) self._site_type: str = "spinful_fermions" if spin_indices is None: self._spin_indices: Set[int] = set() else: self._spin_indices = spin_indices self._rotation_matrix = np.eye(self._system_size) self._prefactor_cutoff = prefactor_cutoff if np.all(np.isreal(H0_uu)): self._transformed_hamiltonian = ExpressionSpinful() self._generator = ExpressionSpinful() else: self._transformed_hamiltonian = ExpressionSpinful_complex() self._generator = ExpressionSpinful_complex()
[docs] class Transformable_Matrices(Transformable): r"""Transformable system from input matrices for the Hamiltonian."""
[docs] def __init__( self, hamiltonians: Hamiltonian_Matrices, system_size: Optional[int] = None, rotation_matrix: Optional[np.ndarray] = None, prefactor_cutoff: float = 1e-6, site_type: str = "spinful_fermions", spin_indices: Optional[Set[int]] = None, ) -> None: r"""Construct the transformable system obtained from a matrix collection input. Args: hamiltonians (Hamiltonian_Matrices): Collection of matrix descriptions of the system Hamiltonian system_size (int): Number of orbitals in the system rotation_matrix (np.ndarray): Transformation matrix :math:`U_{iq}` to the spin-optimized basis prefactor_cutoff (float): Cutoff for the absolute value of the coupling strength of terms to be considered in the Schrieffer-Wolff transformation. site_type (str): Type of excitations in the orbitals. spin_indices (Set[int]): Set of indices of the dedicated spin-like orbitals. """ if spin_indices is None: self._spin_indices: Set[int] = set() else: self._spin_indices = spin_indices self._hamiltonians = hamiltonians if system_size is None: self._system_size = self._hamiltonians.H0_uu.shape[0] else: self._system_size = system_size if rotation_matrix is None: self._rotation_matrix = np.eye(self._system_size) else: self._rotation_matrix = rotation_matrix self._prefactor_cutoff = prefactor_cutoff self._site_type = site_type if np.all(np.isreal(self._hamiltonians.H0_uu)): self._transformed_hamiltonian = ExpressionSpinful() self._generator = ExpressionSpinful() else: self._transformed_hamiltonian = ExpressionSpinful_complex() self._generator = ExpressionSpinful_complex()
[docs] class Transformable_QuantumChemistry(Transformable, Supports_Spin_Optimization): r"""Transformable system from an electronic structure calculation input."""
[docs] def __init__( self, hamiltonians: Hamiltonian_Matrices, rdm1: Union[np.ndarray, Tuple[np.ndarray, ...]], rdm2: np.ndarray, system_size: Optional[int] = None, rotation_matrix: Optional[np.ndarray] = None, tolerance: float = 1e-1, prefactor_cutoff: float = 1e-6, optimization_steps: int = 4, site_type: str = "spinful_fermions", spin_indices: Optional[Set[int]] = None, ) -> None: r"""Construct the transformable system obtained from ab-initio calculation input. Args: hamiltonians (Hamiltonian_Matrices): Collection of matrix descriptions of the system Hamiltonian rdm1 (Union[np.ndarray, Tuple[np.ndarray, ...]]): Matrix description of the 1-RDM rdm2 (np.ndarray): Tensor description of the 2-RDM system_size (int): Number of orbitals in the system rotation_matrix (np.ndarray): Transformation matrix :math:`U_{iq}` to the spin-optimized basis tolerance (float): Parity tolerance :math:`P_i=(-1 + \delta)` for an orbital to be spin-like prefactor_cutoff (float): Cutoff for the absolute value of the coupling strength of terms to be considered in the Schrieffer-Wolff transformation. optimization_steps (int): Number of optimization loops. site_type (str): Type of excitations in the orbitals. spin_indices (Set[int]): Set of indices of the dedicated spin-like orbitals. Raises: ValueError: HU does not conform to the requirements needed for basis rotation ValueError: The 2-RDM does not conform to the requirements needed for spin optimization """ if spin_indices is None: self._spin_indices: Set[int] = set() else: self._spin_indices = spin_indices self._immutable_indices: Set[int] = set() self._hamiltonians = hamiltonians # Reordering the two-electron integrals to conform with second quantization standards # c^+_s c_s c^+_s' c_s' -> c^+_s c^+_s' c_s' c_s if len(self._hamiltonians.HU.shape) != 4: raise ValueError( "The two-particle integrals need to specified as a four index tensor!" ) self._hamiltonians.HU = np.einsum("ijkl -> iklj", self._hamiltonians.HU) if np.all(np.isreal(self._hamiltonians.H0_uu)): self._transformed_hamiltonian = ExpressionSpinful() self._generator = ExpressionSpinful() else: self._transformed_hamiltonian = ExpressionSpinful_complex() self._generator = ExpressionSpinful_complex() self._rdm1 = rdm1 self._rdm2 = rdm2 if len(self._rdm2.shape) != 4: raise ValueError("The 2-RDM needs to specified as a four index tensor!") # <c^+_s c_s c^+_s' c_s'> -> <c^+_s c^+_s' c_s' c_s> self._rdm2 = np.einsum("ijkl -> iklj", self._rdm2) if system_size is None: self._system_size = self._hamiltonians.H0_uu.shape[0] else: self._system_size = system_size if rotation_matrix is None: self._rotation_matrix = np.eye(self._system_size) else: self._rotation_matrix = rotation_matrix self._tolerance = tolerance self._prefactor_cutoff = prefactor_cutoff self._optimization_steps = optimization_steps self._site_type = site_type # EXPERIMENTAL # self.energy_shift = 0.0 # Warning if a spin-summed 2RDM has been provided for i in range(self._system_size): if self._rdm2[i, i, i, i] > 1.0: raise ValueError("The provided 2-RDM is not suitable for spin optimization!")
@property def tolerance(self) -> float: r"""Return the tolerated parity deviation :math:`\delta` in :math:`P=(-1+\delta)` to be considered a spin-orbital. Returns: float: The stored value for the tolerance :maht:`delta` of the parity """ return self._tolerance @property def optimization_steps(self) -> int: r"""Return the number of optimization loops. Returns: int: The number of optimization loops """ return self._optimization_steps @property def immutable_indices(self) -> Set[int]: r"""Return the orbital indices that should remained unaltered by the optimization. Returns: Set[int]: The set of immutable indices """ return self._immutable_indices @immutable_indices.setter def immutable_indices(self, value: Set[int]) -> None: r"""Set the orbital indices that should remained unaltered by the optimization. Args: value (Set[int]): Indices that should remain unaltered """ self._immutable_indices = value @property def rdm1(self) -> Union[np.ndarray, Tuple[np.ndarray, ...]]: r"""Return the matrix descriptions of the 1-RDM (:math:`\langle c^{\dagger}_{i\sigma} c_{j\sigma'}\rangle`). Returns: Union[np.ndarray, Tuple[np.ndarray, ...]]: The (spin-resolved) 1-RDM(s) """ return self._rdm1 @rdm1.setter def rdm1(self, value: Union[np.ndarray, Tuple[np.ndarray, ...]]) -> None: r"""Set the matrix descriptions of the 1-RDM (:math:`\langle c^{\dagger}_{i\sigma} c_{j\sigma'}\rangle`). Args: value (np.ndarray): Matrix descriptions of the 1-RDM """ self._rdm1 = value @property def rdm2(self) -> np.ndarray: r"""Return the tensor of the 2-RDM (:math:`\langle c^{\dagger}_{i\uparrow} c^{\dagger}_{j\downarrow} c_{k\downarrow} c_{l\uparrow}\rangle`). Returns: np.ndarray: Tensor description of the 2-RDM """ return self._rdm2 @rdm2.setter def rdm2(self, value: np.ndarray) -> None: r"""Set the tensor description of the 2-RDM (:math:`\langle c^{\dagger}_{i\uparrow} c^{\dagger}_{j\downarrow} c_{k\downarrow} c_{l\uparrow}\rangle`). Args: value (np.ndarray): Tensor description of the 2-RDM """ self._rdm2 = value
[docs] class SpinFinder(Supports_Spin_Optimization): r"""Data object for use in a spin determination calculation."""
[docs] def __init__( self, rdm1: Union[np.ndarray, Tuple[np.ndarray, ...]], rdm2: np.ndarray, tolerance: float = 1e-1, optimization_steps: int = 4, spin_indices: Optional[Set[int]] = None, ) -> None: r"""Construct the object ab-initio calculation input. Args: rdm1 (Union[np.ndarray, Tuple[np.ndarray, ...]]): Matrix descriptions of the 1-RDM rdm2 (np.ndarray): Tensor description of the 2-RDM tolerance (float): Parity tolerance :math:`delta` in :math:`P=(-1 + \delta)` for an orbital to be spin-like optimization_steps (int): Number of optimization loops spin_indices (Set[int]): Set of indices of the dedicated spinful orbitals Raises: ValueError: The 2-RDM does not conform to the requirements needed for spin optimization """ if spin_indices is None: self._spin_indices: Set[int] = set() else: self._spin_indices = spin_indices self._immutable_indices: Set[int] = set() self._rdm1 = rdm1 self._rdm2 = rdm2 if len(self._rdm2.shape) != 4: raise ValueError("The 2-RDM needs to specified as a four index tensor!") # <c^+_s c_s c^+_s' c_s'> -> <c^+_s c^+_s' c_s' c_s> self._rdm2 = np.einsum("ijkl -> iklj", self._rdm2) self._system_size = self._rdm2.shape[0] self._rotation_matrix = np.eye(self._system_size) self._tolerance = tolerance self._optimization_steps = optimization_steps # Warning if a spin-summed 2RDM has been provided for i in range(self._system_size): if self._rdm2[i, i, i, i] > 1.0: raise ValueError( "The provided 2-RDM is not suitable for spin optimization!" + " It is probably summed over both spin directions." )
@property def rotation_matrix(self) -> np.ndarray: r"""Return the rotation matrix that transforms the system to the spin-optimized basis. Returns: np.ndarray: The stored rotation matrix :math:`U_{iq}` """ return self._rotation_matrix @rotation_matrix.setter def rotation_matrix(self, value: np.ndarray) -> None: r"""Set the rotation matrix that transforms the system to the spin-optimized basis. Args: value (np.ndarray): Updated rotation matrix :math:`U_{iq}` """ self._rotation_matrix = value @property def spin_indices(self) -> Set[int]: r"""Return the set of indices of the spin-like orbitals. Returns: Set[int]: The indices of the spin-like orbitals """ return self._spin_indices @spin_indices.setter def spin_indices(self, value: Set[int]) -> None: r"""Set the set of indices of the spin-like orbitals. Args: value (Set[int]): Updated indices of the spin-like orbitals """ self._spin_indices = value @property def system_size(self) -> int: r"""Return the system size i.e. the number of orbitals. Returns: int: The combined size of the system """ return self._system_size @property def tolerance(self) -> float: r"""Return the tolerated parity deviation :math:`\delta` in :math:`P=(-1+\delta)` to be considered a spin-orbital. Returns: float: The stored parity tolerance """ return self._tolerance @property def optimization_steps(self) -> int: r"""Return the number of optimization loops. Returns: int: The number of optimization loops """ return self._optimization_steps @property def immutable_indices(self) -> Set[int]: r"""Return the orbital indices that should remained unaltered by the optimization. Returns: Set[int]: The set of immutable indices """ return self._immutable_indices @immutable_indices.setter def immutable_indices(self, value: Set[int]) -> None: r"""Set the orbital indices that should remained unaltered by the optimization. Args: value (Set[int]): Indices that should remain unaltered """ self._immutable_indices = value @property def rdm1(self) -> Union[np.ndarray, Tuple[np.ndarray, ...]]: r"""Return the matrix/ces of the 1-RDM (:math:`\langle c^{\dagger}_{i\sigma} c_{j\sigma'}\rangle`). Returns: Union[np.ndarray, Tuple[np.ndarray, ...]: The matrix descriptions of the 1-RDM """ return self._rdm1 @rdm1.setter def rdm1(self, value: Union[np.ndarray, Tuple[np.ndarray, ...]]) -> None: r"""Set the matrix description of the 1-RDM (:math:`\langle c^{\dagger}_{i\sigma} c_{j\sigma'}\rangle`). Args: value (np.ndarray): Matrix descriptions of the 1-RDM """ self._rdm1 = value @property def rdm2(self) -> np.ndarray: r"""Return the tensor description of the 2-RDM (:math:`\langle c^{\dagger}_{i\uparrow} c^{\dagger}_{j\downarrow} c_{k\downarrow} c_{l\uparrow}\rangle`). Returns: np.ndarray: The tensor description of the 2-RDM """ return self._rdm2 @rdm2.setter def rdm2(self, value: np.ndarray) -> None: r"""Set the tensor of the 2-RDM (:math:`\langle c^{\dagger}_{i\uparrow} c^{\dagger}_{j\downarrow} c_{k\downarrow} c_{l\uparrow} \rangle`). Args: value (np.ndarray): Tensor of the 2-RDM """ self._rdm2 = value