# This code is part of qtealeaves.
#
# 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.
"""
Two-body interactions in a two-dimensional system.
"""
import numpy as np
from qtealeaves.tooling import QTeaLeavesError
from qtealeaves.tooling.mapping import map_selector
from .baseterm import _ModelTerm
__all__ = ["TwoBodyTerm2D", "TwoBodyTerm2DLatticeLayout"]
# pylint: disable-next=too-many-instance-attributes
[docs]
class TwoBodyTerm2D(_ModelTerm):
"""
The term defines an interaction between two sites of the Hilbert space.
For example, the tunneling term in the Bose-Hubbard model can be
represented by this term. This class represents the 2d version.
**Arguments**
operators : list of two strings
String identifier for the operators. Before launching the simulation,
the python API will check that the operator is defined.
shift : list of two ints
Defines the distance of the interaction. In the end,
we iterate over all sites and apply interactions to
sites (x, y) and (x + shift[0], y + shift[1])
strength : str, callable, numeric (optional)
Defines the coupling strength of the local terms. It can
be parameterized via a value in the dictionary for the
simulation or a function.
Default to 1.
prefactor : numeric, scalar (optional)
Scalar prefactor, e.g., in order to take into account signs etc.
Default to 1.
isotropy_xyz : bool, optional
If False, the defined shift will only be applied as is. If true,
we permute the defined shift to cover all spatial directions.
Default to True.
add_complex_conjg : bool, optional
(BUG ticket #1) Aims to automatically add complex conjugated
terms, e.g., for the tunneling term.
has_obc : bool or list of bools, optional
Defines the boundary condition along each spatial dimension.
If scalar is given, the boundary condition along each
spatial dimension is assumed to be equal.
Default to True
mask : callable or ``None``, optional
The true-false-mask allows to apply the Hamiltonians terms
only to specific sites, i.e., with true values. The function
takes the dictionary with the simulation parameters as an
argument. The mask is applied to the site where the left
operator is acting on.
Default to ``None`` (all sites have the interaction)
**Attributes**
map_type : str, optional
Selecting the mapping from a n-dimensional system to the
1d system required for the TTN simulations.
"""
# pylint: disable-next=too-many-arguments
def __init__(
self,
operators,
shift,
strength=1,
prefactor=1,
isotropy_xyz=True,
add_complex_conjg=False,
has_obc=True,
mask=None,
):
super().__init__()
self.operators = operators
self.shift = shift
self.strength = strength
self.prefactor = prefactor
self.isotropy_xyz = isotropy_xyz
self.add_complex_conjg = add_complex_conjg
self.mask = mask
# Will be set when adding Hamiltonian terms
self.map_type = None
if isinstance(has_obc, bool):
self.has_obc = [has_obc] * 2
else:
self.has_obc = has_obc
[docs]
@staticmethod
def check_dim(dim):
"""
See :func:`_ModelTerm.check_dim`
"""
if dim != 2:
raise QTeaLeavesError("Dimension does not match.")
[docs]
def collect_operators(self):
"""
All the required operators are returned to ensure that they are
written by fortran.
"""
yield self.operators[0], "l"
yield self.operators[1], "r"
# Hilbert curvature could swap order of sites
yield self.operators[0], "r"
yield self.operators[1], "l"
[docs]
def iter_shifts(self):
"""
Return all possible shifts, which depends on the isotropy
in the 2d case.
"""
if self.isotropy_xyz:
n_coord = np.sum(np.abs(np.array(self.shift)) > 0)
if n_coord == 0:
raise QTeaLeavesError("Not implemented. Is this case useful?")
if n_coord == 1:
shifts = [
[self.shift[0], self.shift[1]],
[self.shift[1], self.shift[0]],
]
elif n_coord == 2:
shifts = [
[self.shift[0], self.shift[1]],
[self.shift[0], -self.shift[1]],
]
# This takes into account unequal shifts leading
# to four terms, e.g, (1, 2), (1, -2), (2, 1),
# and (2, -1)
if self.shift[0] != self.shift[1]:
shifts.append([self.shift[1], self.shift[0]])
shifts.append([self.shift[1], -self.shift[0]])
else:
shifts = [self.shift]
yield from shifts
# pylint: disable-next=unused-argument
[docs]
def get_entries(self, params):
"""
Entries combine the information about possible shifts based
on the origin coordination (0, 0) and the shift with the
information about the operators and couplings.
**Arguments**
params : dictionary
Contains the simulation parameters.
**Details**
To-Do: adding complex conjugates for general case beyond
b bdagger case.
"""
for shift in self.iter_shifts():
coord_a = [0, 0]
coord_b = shift
coordinates = [coord_a, coord_b]
coupl_ii = {"coordinates": coordinates, "operators": self.operators}
yield coupl_ii
if self.add_complex_conjg:
coupl_ii = {
"coordinates": coordinates,
"operators": self.operators[::-1],
}
yield coupl_ii
# pylint: disable-next=too-many-branches
[docs]
def get_interactions(self, ll, params, **kwargs):
"""
These interactions are closest to the TPO description iterating
over specific sites within the 1d coordinates.
**Arguments**
ll : list of ints
Number of sites along the dimensions, i.e., not the
total number of sites. Assuming list of sites
along all dimension.
params : dictionary
Contains the simulation parameters.
"""
if isinstance(self.map_type, str):
map_type = self.eval_str_param(self.map_type, params)
else:
map_type = self.map_type
map_to_1d = map_selector(2, ll, map_type)
if self.mask is None:
local_mask = np.ones(ll, dtype=bool)
else:
local_mask = self.mask(params)
for elem in self.get_entries(params):
for ix, iy in self.iterate_sites(ll):
coord_a, coord_b = elem["coordinates"]
if np.sum(np.abs(np.array(coord_a))) != 0:
raise QTeaLeavesError("Coordinate A is not the origin.")
jx = ix + coord_b[0]
jy = iy + coord_b[1]
if (jx >= ll[0]) and self.has_obc[0]:
continue
if (jx < 0) and self.has_obc[0]:
continue
if (jy >= ll[1]) and self.has_obc[1]:
continue
if (jy < 0) and self.has_obc[1]:
continue
if jx >= ll[0]:
jx = jx % ll[0]
if jx < 0:
jx += ll[0]
if jy >= ll[1]:
jy = jy % ll[1]
if jy < 0:
jy += ll[1]
if (jx >= ll[0]) or (jx < 0):
raise QTeaLeavesError("Improve handling.")
if (jy >= ll[1]) or (jy < 0):
raise QTeaLeavesError("Improve handling.")
if (ix == jx) and (iy == jy):
raise QTeaLeavesError("Same site ...")
if not local_mask[ix, iy]:
continue
# Convert from python to Hilbert space index starting from 1
coords_1d = [map_to_1d[elem] for elem in [(ix, iy), (jx, jy)]]
yield elem, coords_1d
[docs]
def get_fortran_str(self, ll, params, operator_map, param_map):
"""
See :func:`_ModelTerm.get_fortran_str_two_body`.
"""
return self.get_fortran_str_twobody(ll, params, operator_map, param_map)
[docs]
class TwoBodyTerm2DLatticeLayout(TwoBodyTerm2D):
"""
The term defines an interaction between two sites of the Hilbert space,
for an arbitrary lattice layout. For example, the tunneling term in the
Bose-Hubbard model can be represented by this term. This class represents
the 2d version.
**Arguments**
operators : list of two strings
String identifier for the operators. Before launching the simulation,
the python API will check that the operator is defined.
distance : float
Defines the distance of the interaction.
layout : :class:`LatticeLayout`, str, callable
Instance of the :class:`LatticeLayout`. The `LatticeLayout` class stores
the positions of the (nx)x(ny) grid for an arbitrary lattice layout.
strength : str, callable, numeric (optional)
Defines the coupling strength of the local terms. It can
be parameterized via a value in the dictionary for the
simulation or a function.
Default to 1.
prefactor : numeric, scalar (optional)
Scalar prefactor, e.g., in order to take into account signs etc.
Default to 1.
tolerance : float, optional
Tolerance for the distance of the interaction,
i.e., the absolute distance must be smaller than the tolerance
to consider the sites interacting.
**Attributes**
map_type : str, optional
Selecting the mapping from a n-dimensional system to the
1d system required for the TTN simulations.
"""
# pylint: disable-next=too-many-arguments
def __init__(
self, operators, distance, layout, strength=1, prefactor=1, tolerance=1e-8
):
super().__init__(operators, 1, strength=strength, prefactor=prefactor)
self.distance = distance
self.tolerance = tolerance
self.layout = layout
# Delete unused terms
del self.shift
del self.isotropy_xyz
del self.add_complex_conjg
del self.mask
del self.has_obc
# Will be set when adding Hamiltonian terms
self.map_type = None
[docs]
def iter_shifts(self):
"""
The function is inherited, but we overwrite it since it
has no meaning in this context.
"""
raise QTeaLeavesError("This function is not available.")
[docs]
def get_entries(self, params):
"""
The function is inherited, but we overwrite it since it
has no meaning in this context.
"""
raise QTeaLeavesError("This function is not available.")
# pylint: disable-next=too-many-locals
[docs]
def get_interactions(self, ll, params, **kwargs):
"""
These interactions are closest to the TPO description iterating
over specific sites within the 1d coordinates.
**Arguments**
ll : int
Number of sites along the dimensions, i.e., not the
total number of sites. Assuming equal number of sites
along all dimension.
params : dictionary
Contains the simulation parameters.
"""
map_to_1d = map_selector(2, ll, self.map_type)
layout = self.eval_numeric_param(self.layout, params)
for ix, iy in self.iterate_sites(ll):
for jx, jy in self.iterate_sites(ll):
dist = layout.distance((ix, iy), (jx, jy))
coords_1d_i = map_to_1d[(ix, iy)]
coords_1d_j = map_to_1d[(jx, jy)]
if coords_1d_i < coords_1d_j:
if np.abs(dist - self.distance) <= self.tolerance:
coupl_ij = {
"coordinates": [(ix, iy), (jx, jy)],
"operators": self.operators,
}
yield coupl_ij, (coords_1d_i, coords_1d_j)
[docs]
def get_fortran_str(self, ll, params, operator_map, param_map):
"""
See :func:`_ModelTerm.get_fortran_str_two_body`.
"""
return self.get_fortran_str_twobody(ll, params, operator_map, param_map)