Source code for qiskit_nature.second_q.mappers.logarithmic_mapper

# 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