# 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.
"""
Tensor Ordering Utilities (:mod:`qiskit_nature.second_q.operators.tensor_ordering`)
===================================================================================
.. currentmodule:: qiskit_nature.second_q.operators.tensor_ordering
Utility functions to detect and transform the index-ordering convention of two-body integrals
.. autosummary::
:toctree: ../stubs/
to_chemist_ordering
to_physicist_ordering
find_index_order
IndexType
"""
from __future__ import annotations
from enum import Enum
from typing import TYPE_CHECKING
import numpy as np
from qiskit_nature import QiskitNatureError
import qiskit_nature.optionals as _optionals
if TYPE_CHECKING:
from .symmetric_two_body import SymmetricTwoBodyIntegrals
if _optionals.HAS_SPARSE:
# pylint: disable=import-error
from sparse import SparseArray
else:
class SparseArray: # type: ignore
"""Empty SparseArray class
Replacement if sparse.SparseArray is not present.
"""
pass
[docs]def to_chemist_ordering(
two_body_tensor: np.ndarray | SparseArray,
*,
index_order: IndexType | None = None,
) -> np.ndarray | SparseArray:
"""Convert a two-body tensor to chemists' index order.
Coverts the rank-four tensor two-body tensor representing two-body integrals from
physicists', or intermediate, index order to chemists' index order:
``i,j,k,l -> i,l,j,k``
Args:
two_body_tensor: the rank-four tensor to be converted.
index_order: when supplied this will hard-code the ``IndexType`` value. If ``None`` (the
default), the ``index_order`` will be determined automatically based on the symmetries of
the ``two_body_tensor``.
Returns:
The same rank-four tensor, now in chemists' index order.
Raises:
QiskitNatureError: when an unknown index type is encountered.
"""
if index_order is None:
index_order = find_index_order(two_body_tensor)
if index_order == IndexType.CHEMIST:
return two_body_tensor
if index_order == IndexType.PHYSICIST:
chem_tensor = _phys_to_chem(two_body_tensor)
return chem_tensor
if index_order == IndexType.INTERMEDIATE:
chem_tensor = _chem_to_phys(two_body_tensor)
return chem_tensor
else:
raise QiskitNatureError(
"""
Unknown index order type, input tensor must be chemists', physicists',
or intermediate index order
"""
)
[docs]def to_physicist_ordering(
two_body_tensor: np.ndarray | SparseArray,
*,
index_order: IndexType | None = None,
) -> np.ndarray | SparseArray:
"""Convert a two-body tensor to physicists' index order.
Converts the rank-four tensor two-body tensor representing two-body integrals from
chemists', or intermediate, index order to physicists' index order: ``i,j,k,l -> i,l,j,k``
Args:
two_body_tensor: the rank-four tensor to be converted.
index_order: when supplied this will hard-code the ``IndexType`` value. If ``None`` (the
default), the ``index_order`` will be determined automatically based on the symmetries of
the ``two_body_tensor``.
Returns:
The same rank-four tensor, now in physicists' index order.
Raises:
QiskitNatureError: when an unknown index type is encountered.
"""
if index_order is None:
index_order = find_index_order(two_body_tensor)
if index_order == IndexType.PHYSICIST:
return two_body_tensor
if index_order == IndexType.CHEMIST:
phys_tensor = _chem_to_phys(two_body_tensor)
return phys_tensor
if index_order == IndexType.INTERMEDIATE:
phys_tensor = _phys_to_chem(two_body_tensor)
return phys_tensor
else:
raise QiskitNatureError(
"""
Unknown index order type, input tensor must be chemists', physicists',
or intermediate index order
"""
)
def _phys_to_chem(two_body_tensor: np.ndarray | SparseArray) -> np.ndarray | SparseArray:
"""Convert the rank-four tensor `two_body_tensor` representing two-body integrals from
physicists' index order to chemists' index order: i,j,k,l -> i,l,j,k
See also `_chem_to_phys`, `_check_two_body_symmetries`.
.. note::
Denote `_chem_to_phys` by `g` and `_phys_to_chem` by `h`. The elements `g`, `h`, `I` form
a group with `gh = hg = I`, `g^2=h`, and `h^2=g`.
Args:
two_body_tensor: the rank-four tensor in physicists' to be converted.
Returns:
The same rank-four tensor, now in chemists' index order.
"""
permuted_tensor = np.moveaxis(two_body_tensor, (1, 2), (2, 3))
return permuted_tensor
def _chem_to_phys(two_body_tensor: np.ndarray | SparseArray) -> np.ndarray | SparseArray:
"""Convert the rank-four tensor `two_body_tensor` representing two-body integrals from chemists'
index order to physicists' index order: i,j,k,l -> i,k,l,j
See also `_phys_to_chem`, `_check_two_body_symmetries`.
.. note::
Denote `_chem_to_phys` by `g` and `_phys_to_chem` by `h`. The elements `g`, `h`, `I` form
a group with `gh = hg = I`, `g^2=h`, and `h^2=g`.
Args:
two_body_tensor: the rank-four tensor in chemists' to be converted.
Returns:
The same rank-four tensor, now in physicists' index order.
"""
permuted_tensor = np.moveaxis(two_body_tensor, (1,), (3,))
return permuted_tensor
def _check_two_body_symmetry(
two_body_tensor: np.ndarray | SparseArray,
permutation: tuple[tuple[int, ...], tuple[int, ...]],
*,
rtol: float = 1e-5,
atol: float = 1e-8,
) -> bool:
"""Return whether the provided tensor remains identical under the provided permutation.
Args:
two_body_tensor: the tensor to test.
permutation: the source and destination indices of the axis permutations.
rtol: the relative tolerance used during the comparison.
atol: the absolute tolerance used during the comparison.
Returns:
Whether the tensor remains unchanged under the applied permutation.
"""
permuted_tensor = np.moveaxis(two_body_tensor, permutation[0], permutation[1])
if isinstance(two_body_tensor, SparseArray):
return np.allclose(
two_body_tensor.data, permuted_tensor.data, rtol=rtol, atol=atol
) and np.array_equal(
two_body_tensor.coords, permuted_tensor.coords # type: ignore[attr-defined]
)
return np.allclose(two_body_tensor, permuted_tensor, rtol=rtol, atol=atol)
def _check_two_body_symmetries(
two_body_tensor: np.ndarray | SparseArray,
*,
rtol: float = 1e-5,
atol: float = 1e-8,
) -> bool:
"""Return whether a tensor has the required symmetries to represent two-electron terms.
Return `True` if the rank-4 tensor `two_body_tensor` has the required symmetries for
coefficients of the two-electron terms.
If `two_body_tensor` is a correct tensor of indices, with the correct index order, it must pass
the tests. If `two_body_tensor` is a correct tensor of indices, but the flag `chemist` is
incorrect, it will fail the tests, unless the tensor has accidental symmetries. This test may be
used with care to discriminate between the orderings.
References: HJO Molecular Electronic-Structure Theory (1.4.17), (1.4.38)
See also `_phys_to_chem`, `_chem_to_phys`.
Args:
two_body_tensor: the tensor to test.
rtol: the relative tolerance used during the comparison.
atol: the absolute tolerance used during the comparison.
Returns:
Whether the tensor has the required symmetries to represent two-electron terms.
"""
for permutation in _ChemIndexPermutations:
if not _check_two_body_symmetry(two_body_tensor, permutation.value, rtol=rtol, atol=atol):
return False
return True
[docs]def find_index_order(
two_body_tensor: np.ndarray | SparseArray | SymmetricTwoBodyIntegrals,
*,
rtol: float = 1e-5,
atol: float = 1e-8,
) -> IndexType:
"""Return the index-order convention of the provided rank-four tensor.
The index convention is determined by checking symmetries of the tensor.
If the indexing convention can be determined, then one of :class:`IndexType.CHEMIST`,
:class:`IndexType.PHYSICIST`, or :class:`IndexType.INTERMEDIATE` is returned. The
:class:`IndexType.INTERMEDIATE` indexing may be obtained by applying :meth:`_chem_to_phys` to
the physicists' convention or :meth:`_phys_to_chem` to the chemists' convention. If the tests
for each of these conventions fail, then :class:`IndexType.UNKNOWN` is returned.
.. note::
The first of :class:`IndexType.CHEMIST`, :class:`IndexType.PHYSICIST`, and
:class:`IndexType.INTERMEDIATE`, in that order, to pass the tests is returned. If
``two_body_tensor`` has accidental symmetries, it may in fact satisfy more than one set of
symmetry tests. For example, if all elements have the same value, then the symmetries for all
three index orders are satisfied.
Args:
two_body_tensor: the rank-four tensor whose index order to determine.
rtol: the relative tolerance used during the comparison.
atol: the absolute tolerance used during the comparison.
Returns:
The index order of the provided rank-four tensor.
"""
from .symmetric_two_body import SymmetricTwoBodyIntegrals
if isinstance(two_body_tensor, SymmetricTwoBodyIntegrals):
return IndexType.CHEMIST
if _check_two_body_symmetries(two_body_tensor, rtol=rtol, atol=atol):
return IndexType.CHEMIST
permuted_tensor = _phys_to_chem(two_body_tensor)
if _check_two_body_symmetries(permuted_tensor, rtol=rtol, atol=atol):
return IndexType.PHYSICIST
permuted_tensor = _phys_to_chem(permuted_tensor)
if _check_two_body_symmetries(permuted_tensor, rtol=rtol, atol=atol):
return IndexType.INTERMEDIATE
else:
return IndexType.UNKNOWN
class _ChemIndexPermutations(Enum):
"""This ``Enum`` defines the permutation symmetries satisfied by a rank-4 tensor of real
two-body integrals in chemists' index order, naming each permutation in order of appearance
in Molecular Electronic Structure Theory by Helgaker, Jørgensen, Olsen (HJO)."""
PERM_1 = ((0, 1), (2, 3)) # HJO (1.4.17)
PERM_2_AB = ((0,), (1,)) # HJO (1.4.38)
PERM_2_AC = ((2,), (3,)) # HJO (1.4.38)
PERM_2_AD = ((0, 2), (1, 3)) # HJO (1.4.38)
PERM_3 = ((0, 1), (3, 2)) # PERM_2_AB and PERM_1
PERM_4 = ((0, 1, 2), (2, 3, 1)) # PERM_2_AC and PERM_1
PERM_5 = ((0, 1, 2), (3, 2, 1)) # PERM_2_AD and PERM_1
[docs]class IndexType(Enum):
"""This ``Enum`` names the different permutation index orders that could be encountered."""
CHEMIST = "chemist"
PHYSICIST = "physicist"
INTERMEDIATE = "intermediate"
UNKNOWN = "unknown"