Source code for pennylane.bose.bosonic_mapping
# Copyright 2024 Xanadu Quantum Technologies Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This module contains functions to map bosonic operators to qubit operators."""
from collections import defaultdict
from functools import singledispatch
from typing import Union
import numpy as np
import pennylane as qml
from pennylane.pauli import PauliSentence, PauliWord
from .bosonic import BoseSentence, BoseWord
# pylint: disable=too-many-branches
def _get_pauli_op(i, j, qub_id):
r"""Returns expression to convert qubit-local term ::math::``\ket{x_i}\bra{x_j}``
to qubit operators as given in :math:`Eq. (6-9)` in `arXiv.1909.12847 <https://arxiv.org/abs/1909.12847>_`.
"""
c1, c2 = 0.5, -0.5 if i == 1 else 0.5
if i != j:
return PauliSentence({PauliWord({qub_id: "X"}): c1, PauliWord({qub_id: "Y"}): c2 * 1j})
return PauliSentence({PauliWord({}): c1, PauliWord({qub_id: "Z"}): c2})
[docs]def binary_mapping(
bose_operator: Union[BoseWord, BoseSentence],
n_states: int = 2,
ps: bool = False,
wire_map: dict = None,
tol: float = None,
):
r"""Convert a bosonic operator to a qubit operator using the standard-binary mapping.
The mapping procedure is described in equations :math:`27-29` in `arXiv:1507.03271 <https://arxiv.org/pdf/1507.03271>`_.
Args:
bose_operator (BoseWord, BoseSentence): the bosonic operator
n_states (int): Maximum number of allowed bosonic states. Defaults to ``2``.
ps (bool): Whether to return the result as a ``PauliSentence`` instead of an
operator. Defaults to ``False``.
wire_map (dict): A dictionary defining how to map the states of
the Bose operator to qubit wires. If ``None``, integers used to
label the bosonic states will be used as wire labels. Defaults to ``None``.
tol (float): tolerance for discarding the imaginary part of the coefficients
Returns:
Union[PauliSentence, Operator]: a linear combination of qubit operators
**Example**
>>> w = qml.BoseWord({(0, 0): "+"})
>>> qml.binary_mapping(w, n_states=4)
(
0.6830127018922193 * X(0)
+ -0.1830127018922193 * X(0) @ Z(1)
+ -0.6830127018922193j * Y(0)
+ 0.1830127018922193j * Y(0) @ Z(1)
+ 0.3535533905932738 * X(0) @ X(1)
+ -0.3535533905932738j * X(0) @ Y(1)
+ 0.3535533905932738j * Y(0) @ X(1)
+ (0.3535533905932738+0j) * Y(0) @ Y(1)
)
"""
qubit_operator = _binary_mapping_dispatch(bose_operator, n_states, tol=tol)
wires = list(bose_operator.wires) or [0]
identity_wire = wires[0]
if not ps:
qubit_operator = qubit_operator.operation(wire_order=[identity_wire])
if wire_map:
return qubit_operator.map_wires(wire_map)
return qubit_operator
@singledispatch
def _binary_mapping_dispatch(bose_operator, n_states, tol):
"""Dispatches to appropriate function if bose_operator is a BoseWord or BoseSentence."""
raise TypeError(f"bose_operator must be a BoseWord or BoseSentence, got: {bose_operator}")
@_binary_mapping_dispatch.register
def _(bose_operator: BoseWord, n_states, tol=None):
if n_states < 2:
raise ValueError(
f"Number of allowed bosonic states cannot be less than 2, provided {n_states}."
)
nqub_per_boson = int(np.ceil(np.log2(n_states)))
creation = np.zeros((n_states, n_states))
for s in range(n_states - 1):
creation[s + 1, s] = np.sqrt(s + 1.0)
coeff_mat = {"+": creation, "-": creation.T}
qubit_operator = PauliSentence({PauliWord({}): 1.0})
for (_, b_idx), sign in bose_operator.items():
op = PauliSentence()
sparse_coeffmat = np.nonzero(coeff_mat[sign])
for i, j in zip(*sparse_coeffmat):
coeff = coeff_mat[sign][i][j]
binary_row = list(map(int, bin(i)[2:]))[::-1]
if nqub_per_boson > len(binary_row):
binary_row += [0] * (nqub_per_boson - len(binary_row))
binary_col = list(map(int, bin(j)[2:]))[::-1]
if nqub_per_boson > len(binary_col):
binary_col += [0] * (nqub_per_boson - len(binary_col))
pauliOp = PauliSentence({PauliWord({}): 1.0})
for n in range(nqub_per_boson):
pauliOp @= _get_pauli_op(binary_row[n], binary_col[n], n + b_idx * nqub_per_boson)
op += coeff * pauliOp
qubit_operator @= op
for pw in qubit_operator:
if tol is not None and abs(qml.math.imag(qubit_operator[pw])) <= tol:
qubit_operator[pw] = qml.math.real(qubit_operator[pw])
qubit_operator.simplify(tol=1e-16)
return qubit_operator
@_binary_mapping_dispatch.register
def _(bose_operator: BoseSentence, n_states, tol=None):
qubit_operator = PauliSentence()
for bw, coeff in bose_operator.items():
bose_word_as_ps = binary_mapping(bw, n_states=n_states, ps=True)
for pw in bose_word_as_ps:
qubit_operator[pw] = qubit_operator[pw] + bose_word_as_ps[pw] * coeff
if tol is not None and abs(qml.math.imag(qubit_operator[pw])) <= tol:
qubit_operator[pw] = qml.math.real(qubit_operator[pw])
qubit_operator.simplify(tol=1e-16)
return qubit_operator
[docs]def unary_mapping(
bose_operator: Union[BoseWord, BoseSentence],
n_states: int = 2,
ps: bool = False,
wire_map: dict = None,
tol: float = None,
):
r"""Convert a bosonic operator to a qubit operator using the unary mapping.
The mapping procedure is described in `arXiv.1909.12847 <https://arxiv.org/abs/1909.12847>`_.
Args:
bose_operator(BoseWord, BoseSentence): the bosonic operator
n_states(int): Maximum number of allowed bosonic states. Defaults to ``2``.
ps (bool): Whether to return the result as a ``PauliSentence`` instead of an
operator. Defaults to ``False``.
wire_map (dict): A dictionary defining how to map the states of
the Bose operator to qubit wires. If ``None``, integers used to
label the bosonic states will be used as wire labels. Defaults to ``None``.
tol (float): tolerance for discarding the imaginary part of the coefficients
Returns:
Union[PauliSentence, Operator]: a linear combination of qubit operators
**Example**
>>> w = qml.BoseWord({(0, 0): "+"})
>>> qml.unary_mapping(w, n_states=4)
(
0.25 * X(0) @ X(1)
+ -0.25j * X(0) @ Y(1)
+ 0.25j * Y(0) @ X(1)
+ (0.25+0j) * Y(0) @ Y(1)
+ 0.3535533905932738 * X(1) @ X(2)
+ -0.3535533905932738j * X(1) @ Y(2)
+ 0.3535533905932738j * Y(1) @ X(2)
+ (0.3535533905932738+0j) * Y(1) @ Y(2)
+ 0.4330127018922193 * X(2) @ X(3)
+ -0.4330127018922193j * X(2) @ Y(3)
+ 0.4330127018922193j * Y(2) @ X(3)
+ (0.4330127018922193+0j) * Y(2) @ Y(3)
)
"""
qubit_operator = _unary_mapping_dispatch(bose_operator, n_states, tol=tol)
wires = list(bose_operator.wires) or [0]
identity_wire = wires[0]
if not ps:
qubit_operator = qubit_operator.operation(wire_order=[identity_wire])
if wire_map:
return qubit_operator.map_wires(wire_map)
return qubit_operator
@singledispatch
def _unary_mapping_dispatch(bose_operator, n_states, ps=False, wires_map=None, tol=None):
"""Dispatches to appropriate function if bose_operator is a BoseWord or BoseSentence."""
raise TypeError(f"bose_operator must be a BoseWord or BoseSentence, got: {bose_operator}")
@_unary_mapping_dispatch.register
def _(bose_operator: BoseWord, n_states, tol=None):
if n_states < 2:
raise ValueError(
f"Number of allowed bosonic states cannot be less than 2, provided {n_states}."
)
creation = np.zeros((n_states, n_states))
for i in range(n_states - 1):
creation[i + 1, i] = np.sqrt(i + 1.0)
coeff_mat = {"+": creation, "-": creation.T}
qubit_operator = PauliSentence({PauliWord({}): 1.0})
ops_per_idx = defaultdict(list)
# Avoiding superfluous terms by taking the product of
# coefficient matrices.
for (_, b_idx), sign in bose_operator.items():
ops_per_idx[b_idx].append(sign)
for b_idx, signs in ops_per_idx.items():
coeff_mat_prod = np.eye(n_states)
for sign in signs:
coeff_mat_prod = np.dot(coeff_mat_prod, coeff_mat[sign])
op = PauliSentence()
sparse_coeffmat = np.nonzero(coeff_mat_prod)
for i, j in zip(*sparse_coeffmat):
coeff = coeff_mat_prod[i][j]
row = np.zeros(n_states)
row[i] = 1
col = np.zeros(n_states)
col[j] = 1
pauliOp = PauliSentence({PauliWord({}): 1.0})
for n in range(n_states):
if row[n] == 1 or col[n] == 1:
pauliOp @= _get_pauli_op(row[n], col[n], n + b_idx * n_states)
op += coeff * pauliOp
qubit_operator @= op
for pw in qubit_operator:
if tol is not None and abs(qml.math.imag(qubit_operator[pw])) <= tol:
qubit_operator[pw] = qml.math.real(qubit_operator[pw])
qubit_operator.simplify(tol=1e-16)
return qubit_operator
@_unary_mapping_dispatch.register
def _(bose_operator: BoseSentence, n_states, tol=None):
qubit_operator = PauliSentence()
for bw, coeff in bose_operator.items():
bose_word_as_ps = unary_mapping(bw, n_states=n_states, ps=True)
for pw in bose_word_as_ps:
qubit_operator[pw] = qubit_operator[pw] + bose_word_as_ps[pw] * coeff
if tol is not None and abs(qml.math.imag(qubit_operator[pw])) <= tol:
qubit_operator[pw] = qml.math.real(qubit_operator[pw])
qubit_operator.simplify(tol=1e-16)
return qubit_operator
[docs]def christiansen_mapping(
bose_operator: Union[BoseWord, BoseSentence],
ps: bool = False,
wire_map: dict = None,
tol: float = None,
):
r"""Convert a bosonic operator to a qubit operator using the Christiansen mapping.
This mapping assumes that the maximum number of allowed bosonic states is 2 and works only for
Christiansen bosons defined in `J. Chem. Phys. 120, 2140 (2004)
<https://pubs.aip.org/aip/jcp/article-abstract/120/5/2140/534128/A-second-quantization-formulation-of-multimode?redirectedFrom=fulltext>`_.
The bosonic creation and annihilation operators are mapped to the Pauli operators as
.. math::
b^{\dagger}_0 = \left (\frac{X_0 - iY_0}{2} \right ), \:\: \text{...,} \:\:
b^{\dagger}_n = \frac{X_n - iY_n}{2},
and
.. math::
b_0 = \left (\frac{X_0 + iY_0}{2} \right ), \:\: \text{...,} \:\:
b_n = \frac{X_n + iY_n}{2},
where :math:`X`, :math:`Y`, and :math:`Z` are the Pauli operators.
Args:
bose_operator(BoseWord, BoseSentence): the bosonic operator
ps (bool): Whether to return the result as a ``PauliSentence`` instead of an
operator. Defaults to ``False``.
wire_map (dict): A dictionary defining how to map the states of
the Bose operator to qubit wires. If ``None``, integers used to
label the bosonic states will be used as wire labels. Defaults to ``None``.
tol (float): tolerance for discarding the imaginary part of the coefficients
Returns:
Union[PauliSentence, Operator]: A linear combination of qubit operators.
**Example**
>>> w = qml.bose.BoseWord({(0,0):"+", (1,1): "-"})
>>> qml.christiansen_mapping(w)
(
0.25 * (X(0) @ X(1))
+ 0.25j * (X(0) @ Y(1))
+ -0.25j * (Y(0) @ X(1))
+ (0.25+0j) * (Y(0) @ Y(1))
)
"""
qubit_operator = _christiansen_mapping_dispatch(bose_operator, tol)
wires = list(bose_operator.wires) or [0]
identity_wire = wires[0]
if not ps:
qubit_operator = qubit_operator.operation(wire_order=[identity_wire])
if wire_map:
return qubit_operator.map_wires(wire_map)
return qubit_operator
@singledispatch
def _christiansen_mapping_dispatch(bose_operator, tol):
"""Dispatches to appropriate function if bose_operator is a BoseWord or BoseSentence."""
raise TypeError(f"bose_operator must be a BoseWord or BoseSentence, got: {bose_operator}")
@_christiansen_mapping_dispatch.register
def _(bose_operator: BoseWord, tol=None):
qubit_operator = PauliSentence({PauliWord({}): 1.0})
coeffs = {"+": -0.5j, "-": 0.5j}
for (_, b_idx), sign in bose_operator.items():
qubit_operator @= PauliSentence(
{
PauliWord({**{b_idx: "X"}}): 0.5,
PauliWord({**{b_idx: "Y"}}): coeffs[sign],
}
)
for pw in qubit_operator:
if tol is not None and abs(qml.math.imag(qubit_operator[pw])) <= tol:
qubit_operator[pw] = qml.math.real(qubit_operator[pw])
qubit_operator.simplify(tol=1e-16)
return qubit_operator
@_christiansen_mapping_dispatch.register
def _(bose_operator: BoseSentence, tol=None):
qubit_operator = PauliSentence()
for bw, coeff in bose_operator.items():
bose_word_as_ps = christiansen_mapping(bw, ps=True)
for pw in bose_word_as_ps:
qubit_operator[pw] = qubit_operator[pw] + bose_word_as_ps[pw] * coeff
if tol is not None and abs(qml.math.imag(qubit_operator[pw])) <= tol:
qubit_operator[pw] = qml.math.real(qubit_operator[pw])
qubit_operator.simplify(tol=1e-16)
return qubit_operator
_modules/pennylane/bose/bosonic_mapping
Download Python script
Download Notebook
View on GitHub