# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2022, 2023.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""The Heisenberg model."""
from __future__ import annotations
from fractions import Fraction
import numpy as np
from qiskit_nature.second_q.operators import SpinOp
from .lattice_model import LatticeModel
from .lattices import Lattice
[docs]class HeisenbergModel(LatticeModel):
    r"""The Heisenberg model.
    This class implements the following Hamiltonian:
    .. math::
        H = - \vec{J} \sum_{\langle i, j \rangle} \vec{\sigma}_{i} \otimes \vec{\sigma}_{j}
        - \vec{h} \sum_{i} \vec{\sigma}_{i}
    where :math:`i,j` refer to lattice nodes. The :math:`\sum_{\langle i, j \rangle}` is performed
    over adjacent lattice nodes. This model assumes spin-:math:`\frac{1}{2}` particles. Thus,
    :math:`\vec{\sigma}_{i} = (X_i, Y_i, Z_i)` is a vector containing the Pauli matrices.
    :math:`\vec{J}` is the coupling constant and :math:`\vec{h}` is the external magnetic field,
    both with dimensions of energy.
    This model is instantiated using a
    :class:`~qiskit_nature.second_q.hamiltonians.lattices.Lattice`. For example, using a
    :class:`~qiskit_nature.second_q.hamiltonians.lattices.LineLattice`:
    .. code-block:: python
        line_lattice = LineLattice(num_nodes=10, boundary_condition=BoundaryCondition.OPEN)
        heisenberg_model = HeisenbergModel(line_lattice, (1.0, 1.0, 1.0), (0.0, 0.0, 1.0))
    The transverse-field Ising model can be recovered as a special case of the Heisenberg model
    by limiting the model to spins that are parallel/antiparallel with respect to a transverse
    magnetic field:
    .. code-block:: python
        heisenberg_model = HeisenbergModel(line_lattice, (0.0, 0.0, 1.0), (1.0, 0.0, 0.0))
    """
    def __init__(
        self,
        lattice: Lattice,
        coupling_constants: tuple = (1.0, 1.0, 1.0),
        ext_magnetic_field: tuple = (0.0, 0.0, 0.0),
    ) -> None:
        """
        Args:
            lattice: Lattice on which the model is defined.
            coupling_constants: The coupling constants in each Cartesian axis.
                Defaults to ``(1.0, 1.0, 1.0)``.
            ext_magnetic_field: Represents a magnetic field in Cartesian coordinates.
                Defaults to ``(0.0, 0.0, 0.0)``.
        """
        super().__init__(lattice)
        self.coupling_constants = coupling_constants
        self.ext_magnetic_field = ext_magnetic_field
    @property
    def register_length(self) -> int:
        return self._lattice.num_nodes
[docs]    def second_q_op(self) -> SpinOp:
        """Return the Hamiltonian of the Heisenberg model in terms of ``SpinOp``.
        Returns:
            SpinOp: The Hamiltonian of the Heisenberg model.
        """
        hamiltonian = {}
        weighted_edge_list = self.lattice.weighted_edge_list
        for node_a, node_b, _ in weighted_edge_list:
            if node_a == node_b:
                index = node_a
                for axis, coeff in zip("XYZ", self.ext_magnetic_field):
                    if not np.isclose(coeff, 0.0):
                        hamiltonian[f"{axis}_{index}"] = coeff
            else:
                index_left = node_a
                index_right = node_b
                for axis, coeff in zip("XYZ", self.coupling_constants):
                    if not np.isclose(coeff, 0.0):
                        hamiltonian[f"{axis}_{index_left} {axis}_{index_right}"] = coeff
        return SpinOp(hamiltonian, spin=Fraction(1, 2), num_spins=self.lattice.num_nodes)