# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2021, 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 Logarithmic Mapper."""
from __future__ import annotations
import operator
from collections import defaultdict
from fractions import Fraction
from functools import reduce
import numpy as np
from qiskit.quantum_info import SparsePauliOp
from qiskit.quantum_info.operators import Operator
from qiskit_nature.second_q.operators import SpinOp
from .spin_mapper import SpinMapper
[docs]class LogarithmicMapper(SpinMapper):
r"""A mapper for Logarithmic spin-to-qubit mapping.
In this local encoding transformation, each individual spin S system is represented via
the lowest lying :math:`2S+1` states in a qubit system with the minimal number of qubits needed to
represent :math:`>= 2S+1` distinct states [1].
References:
[1] S. V. Mathis, G. Mazzola and I. Tavernelli.
Toward scalable simulations of lattice gauge theories on quantum computers.
Phys. Rev. D, 102 (9), 094501 (2020). https://doi.org/10.1103/PhysRevD.102.094501
"""
def __init__(self, *, padding: float = 1, embed_upper: bool = True) -> None:
r"""
Args:
padding:
When embedding a matrix into the upper/lower diagonal block of a
:math:`2^n` by :math:`2^n` matrix ,where :math:`n` is the number of qubits, pads
the diagonal of the block matrix with the value of ``padding``.
embed_upper:
This parameter sets whether the given matrix is embedded in the upper left hand
corner or the lower right hand corner of the larger matrix.
I.e. using ``embed_upper`` = `True` returns the matrix:
.. math::
\begin{pmatrix}
\text{matrix} & 0 \\
0 & \text{padding} * I
\end{pmatrix}
Using `embed_upper` = `False` returns the matrix:
.. math::
\begin{pmatrix}
\text{padding} * I & 0 \\
0 & \text{matrix}
\end{pmatrix}
"""
self._padding = padding
self._embed_upper = embed_upper
def _map_single(
self, second_q_op: SpinOp, *, register_length: int | None = None
) -> SparsePauliOp:
"""Map spins to qubits using the Logarithmic encoding.
Args:
second_q_op: Spins mapped to qubits.
Returns:
Qubit operators generated by the Logarithmic encoding
"""
if register_length is None:
register_length = second_q_op.register_length
qubit_ops_list: list[SparsePauliOp] = []
# get logarithmic encoding of the general spin matrices.
spinx, spiny, spinz, identity = self._logarithmic_encoding(second_q_op.spin)
ordered_op = second_q_op.index_order()
char_map = {"X": spinx, "Y": spiny, "Z": spinz}
for terms, coeff in ordered_op.terms():
mat = defaultdict(tuple) # type: dict[int, tuple]
for op, idx in terms:
mat[idx] = mat[idx] @ char_map[op] if idx in mat else char_map[op]
operatorlist = [mat[i] if i in mat else identity for i in range(register_length)]
# Now, we can tensor all operators in this list
qubit_ops_list.append(coeff * reduce(operator.xor, reversed(operatorlist)))
qubit_op = reduce(operator.add, qubit_ops_list)
return qubit_op
def _logarithmic_encoding(
self, spin: Fraction | int
) -> tuple[SparsePauliOp, SparsePauliOp, SparsePauliOp, SparsePauliOp]:
"""The logarithmic encoding.
Args:
spin: Positive half-integer (integer or half-odd-integer) that represents spin.
Returns:
A tuple containing four SparsePauliOp.
"""
spin_op_encoding: list[SparsePauliOp] = []
dspin = int(2 * spin + 1)
num_qubits = int(np.ceil(np.log2(dspin)))
# Get the spin matrices
spin_matrices = [
SpinOp.x(spin).to_matrix(),
SpinOp.y(spin).to_matrix(),
SpinOp.z(spin).to_matrix(),
np.eye(dspin),
]
# Embed the spin matrices in a larger matrix of size 2**num_qubits x 2**num_qubits
embedded_spin_matrices = [
self._embed_matrix(matrix, num_qubits) for matrix in spin_matrices
]
# Generate operators from these embedded spin matrices
embedded_operators = [Operator(matrix) for matrix in embedded_spin_matrices]
for op in embedded_operators:
op = SparsePauliOp.from_operator(op)
op.chop()
spin_op_encoding.append(op)
return tuple(spin_op_encoding)
def _embed_matrix(
self,
matrix: np.ndarray,
num_qubits: int,
) -> np.ndarray:
r"""
Embeds `matrix` into the upper/lower diagonal block of a :math:`2^\text{num_qubits}`
by :math:`2^\text{num_qubits}` matrix and pads the diagonal of the upper left block matrix
with the value of `padding`. Whether the upper/lower diagonal block is used depends on
`embed_upper`. I.e. using `embed_upper` = `True` returns the matrix:
.. math::
\begin{pmatrix}
\text{matrix} & 0 \\
0 & \text{padding} * I
\end{pmatrix}
Using `embed_upper` = `False` returns the matrix:
.. math::
\begin{pmatrix}
\text{padding} * I & 0 \\
0 & \text{matrix}
\end{pmatrix}
Args:
matrix: The matrix (2D-array) to embed.
num_qubits: The number of qubits on which the embedded matrix should act on.
Returns:
If `matrix` is of size :math: `2^\text{num_qubits}`, returns `matrix`.
Else it returns the block matrix (:math: `I` = identity)
Raises:
ValueError: If the passed matrix does not fit into the space spanned by num_qubits.
"""
full_dim = 1 << num_qubits
subs_dim = matrix.shape[0]
dim_diff = full_dim - subs_dim
if dim_diff == 0:
full_matrix = matrix
elif dim_diff > 0:
if self._embed_upper:
full_matrix = np.block(
[
[matrix, np.zeros((subs_dim, dim_diff), dtype=complex)],
[
np.zeros((dim_diff, subs_dim), dtype=complex),
np.eye(dim_diff) * self._padding,
],
]
)
else:
full_matrix = np.block(
[
[
np.eye(dim_diff) * self._padding,
np.zeros((dim_diff, subs_dim), dtype=complex),
],
[np.zeros((subs_dim, dim_diff), dtype=complex), matrix],
]
)
else:
raise ValueError(
f"The given matrix does not fit into the space spanned by {num_qubits} qubits."
)
return full_matrix