diff --git a/doc/code/qml_bose.rst b/doc/code/qml_bose.rst new file mode 100644 index 00000000000..9ce4c80d590 --- /dev/null +++ b/doc/code/qml_bose.rst @@ -0,0 +1,32 @@ +qml.bose +========= + +Overview +-------- + +This module contains functions and classes for creating and manipulating bosonic operators. + + +BoseWord and BoseSentence +--------------------------- + +.. currentmodule:: pennylane.bose + +.. autosummary:: + :toctree: api + + ~BoseWord + ~BoseSentence + +Mapping to qubit operators +-------------------------- + +.. currentmodule:: pennylane.bose + +.. autosummary:: + :toctree: api + + ~binary_mapping + ~unary_mapping + ~christiansen_mapping + diff --git a/doc/index.rst b/doc/index.rst index 4398685fdf5..85f2e57c732 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -193,6 +193,7 @@ PennyLane is **free** and **open source**, released under the Apache License, Ve :hidden: code/qml + code/qml_bose code/qml_compiler code/qml_data code/qml_debugging diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 50ac0259fa1..6f369935041 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -63,9 +63,16 @@ * Added a second class `DefaultMixedNewAPI` to the `qml.devices.qubit_mixed` module, which is to be the replacement of legacy `DefaultMixed` which for now to hold the implementations of `preprocess` and `execute` methods. [(#6607)](https://github.com/PennyLaneAI/pennylane/pull/6507) +* Added `christiansen_mapping()` function to map `BoseWord` and `BoseSentence` to qubit operators, using christiansen mapping. + [(#6623)](https://github.com/PennyLaneAI/pennylane/pull/6623) + +* Added `unary_mapping()` function to map `BoseWord` and `BoseSentence` to qubit operators, using unary mapping. + [(#6576)](https://github.com/PennyLaneAI/pennylane/pull/6576) + * Added `binary_mapping()` function to map `BoseWord` and `BoseSentence` to qubit operators, using standard-binary mapping. [(#6564)](https://github.com/PennyLaneAI/pennylane/pull/6564) +

Improvements 🛠

* Raises a comprehensive error when using `qml.fourier.qnode_spectrum` with standard numpy @@ -282,6 +289,7 @@ This release contains contributions from (in alphabetical order): Shiwen An, Astral Cai, Yushao Chen, +Diksha Dhawan, Pietropaolo Frisoni, Austin Huang, Korbinian Kottmann, diff --git a/pennylane/__init__.py b/pennylane/__init__.py index fec9cc004d2..a5477f6ef7a 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -39,7 +39,13 @@ parity_transform, bravyi_kitaev, ) -from pennylane.bose import BoseSentence, BoseWord, binary_mapping +from pennylane.bose import ( + BoseSentence, + BoseWord, + binary_mapping, + unary_mapping, + christiansen_mapping, +) from pennylane.qchem import ( taper, symmetry_generators, diff --git a/pennylane/_version.py b/pennylane/_version.py index 838af0398bf..629332369dc 100644 --- a/pennylane/_version.py +++ b/pennylane/_version.py @@ -16,4 +16,4 @@ Version number (major.minor.patch[-label]) """ -__version__ = "0.40.0-dev22" +__version__ = "0.40.0-dev23" diff --git a/pennylane/bose/__init__.py b/pennylane/bose/__init__.py index 6817810cd34..ca7993ff92c 100644 --- a/pennylane/bose/__init__.py +++ b/pennylane/bose/__init__.py @@ -14,4 +14,4 @@ """A module containing utility functions and mappings for working with bosonic operators. """ from .bosonic import BoseSentence, BoseWord -from .bosonic_mapping import binary_mapping +from .bosonic_mapping import binary_mapping, christiansen_mapping, unary_mapping diff --git a/pennylane/bose/bosonic_mapping.py b/pennylane/bose/bosonic_mapping.py index 4f489a4933f..559ded8b68d 100644 --- a/pennylane/bose/bosonic_mapping.py +++ b/pennylane/bose/bosonic_mapping.py @@ -13,6 +13,7 @@ # 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 @@ -159,3 +160,240 @@ def _(bose_operator: BoseSentence, n_states, tol=None): qubit_operator.simplify(tol=1e-16) return qubit_operator + + +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 `_. + + Args: + bose_operator(BoseWord, BoseSentence): the bosonic operator + n_states(int): maximum number of allowed bosonic states + 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): "+"}) + >>> 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 ValueError(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 + + +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) + `_. + 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. + """ + + 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 ValueError(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 diff --git a/tests/bose/test_christiansen_mapping.py b/tests/bose/test_christiansen_mapping.py new file mode 100644 index 00000000000..121d679bfe7 --- /dev/null +++ b/tests/bose/test_christiansen_mapping.py @@ -0,0 +1,392 @@ +# Copyright 2018-2023 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. +"""Unit testing of christiansen mapping for Bose operators.""" +import pytest + +import pennylane as qml +from pennylane import I, X, Y, Z +from pennylane.bose import BoseSentence, BoseWord, christiansen_mapping +from pennylane.ops import SProd +from pennylane.pauli import PauliSentence, PauliWord +from pennylane.pauli.conversion import pauli_sentence + +BOSE_WORDS_AND_OPS = [ + ( + BoseWord({(0, 0): "+"}), + # trivial case of a creation operator, 0^ -> (X_0 - iY_0) / 2 + ([0.5, -0.5j], [X(0), Y(0)]), + ), + ( + BoseWord({(0, 0): "-"}), + # trivial case of an annihilation operator, 0 -> (X_0 + iY_0) / 2 + ([(0.5 + 0j), (0.0 + 0.5j)], [X(0), Y(0)]), + ), + ( + BoseWord({(0, 0): "+", (1, 0): "-"}), + ([(0.5 + 0j), (-0.5 + 0j)], [I(0), Z(0)]), + ), + ( + BoseWord({(0, 0): "-", (1, 0): "+"}), + ([(0.5 + 0j), (0.5 + 0j)], [I(0), Z(0)]), + ), + ( + BoseWord({(0, 0): "-", (1, 1): "+"}), + ( + [(0.25 + 0j), -0.25j, 0.25j, (0.25 + 0j)], + [ + X(0) @ X(1), + X(0) @ Y(1), + Y(0) @ X(1), + Y(0) @ Y(1), + ], + ), + ), + ( + BoseWord({(0, 1): "-", (1, 0): "+"}), + ( + [(0.25 + 0j), -0.25j, 0.25j, (0.25 + 0j)], + [ + X(1) @ X(0), + X(1) @ Y(0), + Y(1) @ X(0), + Y(1) @ Y(0), + ], + ), + ), + ( + BoseWord({(0, 3): "+", (1, 0): "-"}), + ( + [(0.25 + 0j), -0.25j, 0.25j, (0.25 + 0j)], + [ + X(0) @ X(3), + X(0) @ Y(3), + Y(0) @ X(3), + Y(0) @ Y(3), + ], + ), + ), + ( + BoseWord({(0, 1): "+", (1, 4): "-"}), + ( + [(0.25 + 0j), 0.25j, -0.25j, (0.25 + 0j)], + [ + X(1) @ X(4), + X(1) @ Y(4), + Y(1) @ X(4), + Y(1) @ Y(4), + ], + ), + ), + ( + BoseWord({(0, 3): "+", (1, 1): "+", (2, 3): "-", (3, 1): "-"}), + ( + [(0.25 + 0j), (-0.25 + 0j), (0.25 + 0j), (-0.25 + 0j)], + [I(0), Z(1), Z(3) @ Z(1), Z(3)], + ), + ), +] + +WIRE_MAP_FOR_BOSE_WORDS = [ + ( + {0: 3, 1: 2}, + [ + qml.s_prod(-0.25j, qml.prod(Y(3), X(2))), + qml.s_prod(-0.25 + 0j, qml.prod(Y(3), Y(2))), + qml.s_prod(0.25 + 0j, qml.prod(X(3), X(2))), + qml.s_prod(-0.25j, qml.prod(X(3), Y(2))), + ], + ), + ( + {0: "b", 1: "a"}, + [ + qml.s_prod(-0.25j, qml.prod(Y("b"), X("a"))), + qml.s_prod(-0.25 + 0j, qml.prod(Y("b"), Y("a"))), + qml.s_prod(0.25 + 0j, qml.prod(X("b"), X("a"))), + qml.s_prod(-0.25j, qml.prod(X("b"), Y("a"))), + ], + ), +] + + +class TestBoseWordMapping: + """Tests for mapping BoseWords""" + + @pytest.mark.parametrize("bosonic_op, result", BOSE_WORDS_AND_OPS) + def test_christiansen_mapping_bose_word_ps(self, bosonic_op, result): + """Test that the christiansen_mapping function returns the correct qubit operator.""" + + qubit_op = christiansen_mapping(bosonic_op, ps=True) + qubit_op.simplify() + + expected_op = pauli_sentence(qml.Hamiltonian(result[0], result[1])) + expected_op.simplify() + + assert qubit_op == expected_op + + @pytest.mark.parametrize("bosonic_op, result", BOSE_WORDS_AND_OPS) + def test_christiansen_mapping_bose_word_operation(self, bosonic_op, result): + r"""Test that the christiansen_mapping function returns the correct operator for + return type ps=False.""" + wires = bosonic_op.wires or [0] + + qubit_op = christiansen_mapping(bosonic_op) + + expected_op = pauli_sentence(qml.Hamiltonian(result[0], result[1])) + expected_op = expected_op.operation(wires) + + qml.assert_equal(qubit_op.simplify(), expected_op.simplify()) + + def test_christiansen_mapping_for_identity(self): + """Test that the christiansen_mapping function returns the correct qubit operator for Identity.""" + qml.assert_equal(christiansen_mapping(BoseWord({})), I(0)) + + def test_christiansen_mapping_for_identity_ps(self): + """Test that the christiansen_mapping function returns the correct PauliSentence for Identity when ps=True.""" + assert christiansen_mapping(BoseWord({}), ps=True) == PauliSentence( + {PauliWord({0: "I"}): 1.0 + 0.0j} + ) + + @pytest.mark.parametrize("wire_map, ops", WIRE_MAP_FOR_BOSE_WORDS) + def test_providing_wire_map_bose_word_to_operation(self, wire_map, ops): + r"""Test that the christiansen_mapping function returns the correct operator + for a given wiremap.""" + + w = BoseWord({(0, 0): "+", (1, 1): "+"}) + + op = christiansen_mapping(w, wire_map=wire_map) + result = qml.sum(*ops) + + op.simplify() + + assert pauli_sentence(op) == pauli_sentence(result) + + @pytest.mark.parametrize("wire_map, ops", WIRE_MAP_FOR_BOSE_WORDS) + def test_providing_wire_map_bose_word_to_ps(self, wire_map, ops): + r"""Test that the christiansen_mapping function returns the correct PauliSentence + for a given wiremap.""" + w = BoseWord({(0, 0): "+", (1, 1): "+"}) + + op = christiansen_mapping(w, wire_map=wire_map, ps=True) + result_op = qml.sum(*ops) + ps = pauli_sentence(result_op) + + ps.simplify() + op.simplify() + + assert ps == op + + +bw1 = BoseWord({(0, 0): "+", (1, 1): "-"}) +bw2 = BoseWord({(0, 0): "+", (1, 0): "-"}) +bw3 = BoseWord({(0, 0): "+", (1, 3): "-", (2, 0): "+", (3, 4): "-"}) +bw4 = BoseWord({}) +bw5 = BoseWord({(0, 3): "+", (1, 2): "-"}) +bw6 = BoseWord({(0, 1): "+", (1, 4): "-"}) + + +BOSE_AND_PAULI_SENTENCES = [ + (BoseSentence({bw4: 0, bw2: 0}), PauliSentence({})), + ( + BoseSentence({bw2: 2}), + PauliSentence({PauliWord({}): (1 + 0j), PauliWord({0: "Z"}): (-1 + 0j)}), + ), + ( + BoseSentence({bw1: 1, bw2: 1}), + PauliSentence( + { + PauliWord({0: "Y", 1: "X"}): -0.25j, + PauliWord({0: "Y", 1: "Y"}): (0.25 + 0j), + PauliWord({0: "X", 1: "X"}): (0.25 + 0j), + PauliWord({0: "X", 1: "Y"}): 0.25j, + PauliWord({}): (0.5 + 0j), + PauliWord({0: "Z"}): (-0.5 + 0j), + } + ), + ), + ( + BoseSentence({bw1: 1j, bw2: -2}), + PauliSentence( + { + PauliWord({0: "Y", 1: "X"}): (0.25 + 0j), + PauliWord({0: "Y", 1: "Y"}): 0.25j, + PauliWord({0: "X", 1: "X"}): 0.25j, + PauliWord({0: "X", 1: "Y"}): (-0.25 + 0j), + PauliWord({}): (-1 + 0j), + PauliWord({0: "Z"}): (1 + 0j), + } + ), + ), + ( + BoseSentence({bw1: -2, bw5: 1j}), + PauliSentence( + { + PauliWord({0: "X", 1: "X"}): -0.5, + PauliWord({0: "X", 1: "Y"}): -0.5j, + PauliWord({0: "Y", 1: "X"}): 0.5j, + PauliWord({0: "Y", 1: "Y"}): -0.5, + PauliWord({2: "X", 3: "X"}): 0.25j, + PauliWord({2: "X", 3: "Y"}): 0.25, + PauliWord({2: "Y", 3: "X"}): -0.25, + PauliWord({2: "Y", 3: "Y"}): 0.25j, + } + ), + ), + ( + BoseSentence({bw6: 1, bw2: 2}), + PauliSentence( + { + PauliWord({0: "I"}): 1.0, + PauliWord({0: "Z"}): -1.0, + PauliWord({1: "X", 4: "X"}): 0.25, + PauliWord({1: "X", 4: "Y"}): 0.25j, + PauliWord({1: "Y", 4: "X"}): -0.25j, + PauliWord({1: "Y", 4: "Y"}): 0.25, + } + ), + ), +] + +WIRE_MAP_FOR_BOSE_SENTENCE = [ + ( + {0: 3, 1: 2}, + [ + qml.s_prod(-0.25j, qml.prod(Y(3), X(2))), + qml.s_prod((0.25 + 0j), qml.prod(Y(3), Y(2))), + qml.s_prod((0.25 + 0j), qml.prod(X(3), X(2))), + qml.s_prod(0.25j, qml.prod(X(3), Y(2))), + qml.s_prod((0.5 + 0j), I(3)), + qml.s_prod((-0.5 + 0j), Z(3)), + ], + ), + ( + {0: "b", 1: "a"}, + [ + qml.s_prod(-0.25j, qml.prod(Y("b"), X("a"))), + qml.s_prod((0.25 + 0j), qml.prod(Y("b"), Y("a"))), + qml.s_prod((0.25 + 0j), qml.prod(X("b"), X("a"))), + qml.s_prod(0.25j, qml.prod(X("b"), Y("a"))), + qml.s_prod((0.5 + 0j), I("b")), + qml.s_prod((-0.5 + 0j), Z("b")), + ], + ), +] + + +class TestBoseSentencesMapping: + """Tests for mapping BoseSentences""" + + def test_empty_bose_sentence(self): + """Test that an empty BoseSentence (bose null operator) is + converted to an empty PauliSentence or the null operator""" + op = BoseSentence({}) + + ps_op = christiansen_mapping(op, ps=True) + ps_op.simplify() + assert ps_op == PauliSentence({}) + + op = christiansen_mapping(op).simplify() + assert isinstance(op, SProd) + assert isinstance(op.base, I) + assert op.scalar == 0 + + def test_bose_sentence_identity(self): + """Test that a BoseSentence composed of a single Identity operator + converts to PauliSentence and operation as expected""" + op = BoseSentence({bw4: 1}) + ps = PauliSentence({PauliWord({}): 1}) + + ps_op = christiansen_mapping(op, ps=True) + qubit_op = christiansen_mapping(op) + + assert ps_op == ps + + result = ps.operation(wire_order=[0]) + qml.assert_equal(qubit_op.simplify(), result.simplify()) + + @pytest.mark.parametrize("bosonic_op, result", BOSE_AND_PAULI_SENTENCES) + def test_christiansen_mapping_for_bose_sentence_ps(self, bosonic_op, result): + r"""Test that christiansen_mapping function returns the correct PauliSentence.""" + qubit_op = christiansen_mapping(bosonic_op, ps=True) + qubit_op.simplify() + + assert qubit_op == result + + @pytest.mark.parametrize("bosonic_op, result", BOSE_AND_PAULI_SENTENCES) + def test_christiansen_mapping_for_bose_sentence_operation(self, bosonic_op, result): + r"""Test that christiansen_mapping function returns the correct qubit operator.""" + wires = bosonic_op.wires or [0] + + qubit_op = christiansen_mapping(bosonic_op) + result = result.operation(wires) + + qml.assert_equal(qubit_op.simplify(), result.simplify()) + + @pytest.mark.parametrize("wire_map, ops", WIRE_MAP_FOR_BOSE_SENTENCE) + def test_providing_wire_map_bose_sentence_to_operation(self, wire_map, ops): + r"""Test that the christiansen_mapping function returns the correct operator + for a given wiremap.""" + bs = BoseSentence( + {BoseWord({(0, 0): "+", (1, 1): "-"}): 1, BoseWord({(0, 0): "+", (1, 0): "-"}): 1} + ) + + op = christiansen_mapping(bs, wire_map=wire_map) + result = qml.sum(*ops) + + assert op.wires == result.wires + + assert pauli_sentence(op) == pauli_sentence(result) + + @pytest.mark.parametrize("wire_map, ops", WIRE_MAP_FOR_BOSE_SENTENCE) + def test_providing_wire_map_bose_sentence_to_ps(self, wire_map, ops): + r"""Test that the christiansen_mapping function returns the correct PauliSentence + for a given wiremap.""" + bs = BoseSentence( + {BoseWord({(0, 0): "+", (1, 1): "-"}): 1, BoseWord({(0, 0): "+", (1, 0): "-"}): 1} + ) + + op = christiansen_mapping(bs, wire_map=wire_map, ps=True) + result_op = qml.sum(*ops) + ps = pauli_sentence(result_op) + + ps.simplify() + op.simplify() + + assert ps == op + + +bs1 = BoseSentence({bw1: 1}) + + +@pytest.mark.parametrize( + "bose_op, qubit_op_data, tol", + ( + (bw1, (0.25, 0.25j, -0.25j, (0.25 + 0j)), None), + (bw1, (0.25, 0.25j, -0.25j, 0.25), 0.0), + (bw1, (0.25, 0.25j, -0.25j, 0.25), 1.0e-12), + (bs1, (0.25, 0.25j, -0.25j, (0.25 + 0j)), None), + (bs1, (0.25, 0.25j, -0.25j, 0.25), 0.0), + (bs1, (0.25, 0.25j, -0.25j, 0.25), 1.0e-12), + ), +) +def test_christiansen_mapping_tolerance(bose_op, qubit_op_data, tol): + """Test that christiansen_mapping properly removes negligible imaginary components""" + op = christiansen_mapping(bose_op, tol=tol) + assert isinstance(op.data[1], type(qubit_op_data[1])) + + +def test_error_is_raised_for_incompatible_type(): + """Test that an error is raised if the input is not a BoseWord or BoseSentence""" + + with pytest.raises(ValueError, match="bose_operator must be a BoseWord or BoseSentence"): + christiansen_mapping(X(0)) diff --git a/tests/bose/test_unary_mapping.py b/tests/bose/test_unary_mapping.py new file mode 100644 index 00000000000..edebef362ad --- /dev/null +++ b/tests/bose/test_unary_mapping.py @@ -0,0 +1,637 @@ +# 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 tests for unary_mapping of bosonic operators.""" +import pytest + +import pennylane as qml +from pennylane import I, X, Y, Z +from pennylane.bose import BoseSentence, BoseWord, unary_mapping +from pennylane.pauli import PauliSentence, PauliWord +from pennylane.pauli.conversion import pauli_sentence + +# Expected results were generated manually +BOSE_WORDS_AND_OPS = [ + ( + BoseWord({(0, 0): "+"}), + # trivial case of a creation operator with 2 allowed bosonic states, 0^ -> (X_0 - iY_0) / 2 + 2, + ([0.25, -0.25j, 0.25j, (0.25 + 0j)], [X(0) @ X(1), X(0) @ Y(1), Y(0) @ X(1), Y(0) @ Y(1)]), + ), + ( + BoseWord({(0, 0): "-"}), + # trivial case of an annihilation operator with 2 allowed bosonic states, 0 -> (X_0 + iY_0) / 2 + 2, + ([0.25, 0.25j, -0.25j, (0.25 + 0j)], [X(0) @ X(1), X(0) @ Y(1), Y(0) @ X(1), Y(0) @ Y(1)]), + ), + ( + BoseWord({(0, 0): "+"}), + # creation operator with 4 allowed bosonic states + 4, + ( + [ + 0.25, + -0.25j, + 0.25j, + (0.25 + 0j), + 0.3535533905932738, + -0.3535533905932738j, + 0.3535533905932738j, + (0.3535533905932738 + 0j), + 0.4330127018922193, + -0.4330127018922193j, + 0.4330127018922193j, + (0.4330127018922193 + 0j), + ], + [ + X(0) @ X(1), + X(0) @ Y(1), + Y(0) @ X(1), + Y(0) @ Y(1), + X(1) @ X(2), + X(1) @ Y(2), + Y(1) @ X(2), + Y(1) @ Y(2), + X(2) @ X(3), + X(2) @ Y(3), + Y(2) @ X(3), + Y(2) @ Y(3), + ], + ), + ), + ( + BoseWord({(0, 0): "-"}), + # annihilation operator with 4 allowed bosonic states + 4, + ( + [ + 0.25, + 0.25j, + -0.25j, + (0.25 + 0j), + 0.3535533905932738, + 0.3535533905932738j, + -0.3535533905932738j, + (0.3535533905932738 + 0j), + 0.4330127018922193, + 0.4330127018922193j, + -0.4330127018922193j, + (0.4330127018922193 + 0j), + ], + [ + X(0) @ X(1), + X(0) @ Y(1), + Y(0) @ X(1), + Y(0) @ Y(1), + X(1) @ X(2), + X(1) @ Y(2), + Y(1) @ X(2), + Y(1) @ Y(2), + X(2) @ X(3), + X(2) @ Y(3), + Y(2) @ X(3), + Y(2) @ Y(3), + ], + ), + ), + ( + BoseWord({(0, 0): "+", (1, 0): "-"}), + 4, + ([3.0, -0.5, -1.0000000000000002, -1.4999999999999998], [I(), Z(1), Z(2), Z(3)]), + ), + ( + BoseWord({(0, 0): "-", (1, 0): "+", (2, 0): "-"}), + 4, + ( + [ + 0.25, + 0.25j, + -0.25j, + (0.25 + 0j), + 0.7071067811865477, + 0.7071067811865477j, + -0.7071067811865477j, + (0.7071067811865477 + 0j), + 1.2990381056766578, + 1.2990381056766578j, + -1.2990381056766578j, + (1.2990381056766578 + 0j), + ], + [ + X(0) @ X(1), + X(0) @ Y(1), + Y(0) @ X(1), + Y(0) @ Y(1), + X(1) @ X(2), + X(1) @ Y(2), + Y(1) @ X(2), + Y(1) @ Y(2), + X(2) @ X(3), + X(2) @ Y(3), + Y(2) @ X(3), + Y(2) @ Y(3), + ], + ), + ), + ( + BoseWord({(0, 0): "-", (1, 1): "-", (2, 0): "+"}), + 2, + ( + [0.125, 0.125j, -0.125j, (0.125 + 0j), -0.125, -0.125j, 0.125j, (-0.125 + 0j)], + [ + X(2) @ X(3), + X(2) @ Y(3), + Y(2) @ X(3), + Y(2) @ Y(3), + Z(0) @ X(2) @ X(3), + Z(0) @ X(2) @ Y(3), + Z(0) @ Y(2) @ X(3), + Z(0) @ Y(2) @ Y(3), + ], + ), + ), + ( + BoseWord({(0, 1): "+", (1, 1): "-", (2, 0): "+", (3, 0): "-"}), + 4, + ( + [ + 9.0, + -1.5, + -3.000000000000001, + -4.499999999999999, + -1.5, + 0.25, + 0.5000000000000001, + 0.7499999999999999, + -3.000000000000001, + 0.5000000000000001, + 1.0000000000000004, + 1.5, + -4.499999999999999, + 0.7499999999999999, + 1.5, + 2.2499999999999996, + ], + [ + I(), + Z(5), + Z(6), + Z(7), + Z(1), + Z(1) @ Z(5), + Z(1) @ Z(6), + Z(1) @ Z(7), + Z(2), + Z(2) @ Z(5), + Z(2) @ Z(6), + Z(2) @ Z(7), + Z(3), + Z(3) @ Z(5), + Z(3) @ Z(6), + Z(3) @ Z(7), + ], + ), + ), +] + + +class TestBoseWordMapping: + """Tests for mapping BoseWords.""" + + @pytest.mark.parametrize("bose_op, n_states, result", BOSE_WORDS_AND_OPS) + def test_unary_mapping_boseword(self, bose_op, n_states, result): + """Test that the unary_mapping function returns the correct qubit operator.""" + qubit_op = unary_mapping(bose_op, n_states=n_states, ps=True) + qubit_op.simplify(tol=1e-8) + + expected_op = pauli_sentence(qml.Hamiltonian(result[0], result[1])) + expected_op.simplify(tol=1e-8) + assert qubit_op == expected_op + + @pytest.mark.parametrize("bosonic_op, n_states, result", BOSE_WORDS_AND_OPS) + def test_unary_mapping_bose_word_operation(self, bosonic_op, n_states, result): + r"""Test that the unary_mapping function returns the correct operator for + return type ps=False.""" + wires = bosonic_op.wires or [0] + + qubit_op = unary_mapping(bosonic_op, n_states=n_states, ps=False) + + expected_op = pauli_sentence(qml.Hamiltonian(result[0], result[1])) + expected_op = expected_op.operation(wires) + + qml.assert_equal(qubit_op.simplify(), expected_op.simplify()) + + def test_unary_mapping_for_identity(self): + """Test that the unary_mapping function returns the correct qubit operator for Identity.""" + qml.assert_equal(unary_mapping(BoseWord({})), I(0)) + + def test_unary_mapping_for_identity_ps(self): + """Test that the unary_mapping function returns the correct PauliSentence for Identity when ps=True.""" + assert unary_mapping(BoseWord({}), ps=True) == PauliSentence( + {PauliWord({0: "I"}): 1.0 + 0.0j} + ) + + +bw1 = BoseWord({(0, 0): "+"}) +bw2 = BoseWord({(0, 0): "+", (1, 1): "-", (2, 0): "-"}) +bw3 = BoseWord({(0, 0): "+", (1, 0): "-"}) + +BOSE_SEN_AND_OPS = [ + ( + BoseSentence({bw1: 1.0, bw3: -1.0}), + 4, + ( + [ + 0.25, + -0.25j, + 0.25j, + (0.25 + 0j), + 0.3535533905932738, + -0.3535533905932738j, + 0.3535533905932738j, + (0.3535533905932738 + 0j), + 0.4330127018922193, + -0.4330127018922193j, + 0.4330127018922193j, + (0.4330127018922193 + 0j), + -3.0, + 0.5, + 1.0000000000000002, + 1.4999999999999998, + ], + [ + X(0) @ X(1), + X(0) @ Y(1), + Y(0) @ X(1), + Y(0) @ Y(1), + X(1) @ X(2), + X(1) @ Y(2), + Y(1) @ X(2), + Y(1) @ Y(2), + X(2) @ X(3), + X(2) @ Y(3), + Y(2) @ X(3), + Y(2) @ Y(3), + I(), + Z(1), + Z(2), + Z(3), + ], + ), + ), + ( + BoseSentence({bw2: 1.0, bw3: -0.5}), + 4, + ( + [ + 0.75, + 0.75j, + -0.75j, + (0.75 + 0j), + 1.0606601717798214, + 1.0606601717798214j, + -1.0606601717798214j, + (1.0606601717798214 + 0j), + 1.299038105676658, + 1.299038105676658j, + -1.299038105676658j, + (1.299038105676658 + 0j), + -0.125, + -0.125j, + 0.125j, + (-0.125 + 0j), + -0.1767766952966369, + -0.1767766952966369j, + 0.1767766952966369j, + (-0.1767766952966369 + 0j), + -0.21650635094610965, + -0.21650635094610965j, + 0.21650635094610965j, + (-0.21650635094610965 + 0j), + -0.25000000000000006, + -0.25000000000000006j, + 0.25000000000000006j, + (-0.25000000000000006 + 0j), + -0.35355339059327384, + -0.35355339059327384j, + 0.35355339059327384j, + (-0.35355339059327384 + 0j), + -0.4330127018922194, + -0.4330127018922194j, + 0.4330127018922194j, + (-0.4330127018922194 + 0j), + -0.37499999999999994, + -0.37499999999999994j, + 0.37499999999999994j, + (-0.37499999999999994 + 0j), + -0.5303300858899106, + -0.5303300858899106j, + 0.5303300858899106j, + (-0.5303300858899106 + 0j), + -0.6495190528383289, + -0.6495190528383289j, + 0.6495190528383289j, + (-0.6495190528383289 + 0j), + -1.5, + 0.25, + 0.5000000000000001, + 0.7499999999999999, + ], + [ + X(4) @ X(5), + X(4) @ Y(5), + Y(4) @ X(5), + Y(4) @ Y(5), + X(5) @ X(6), + X(5) @ Y(6), + Y(5) @ X(6), + Y(5) @ Y(6), + X(6) @ X(7), + X(6) @ Y(7), + Y(6) @ X(7), + Y(6) @ Y(7), + Z(1) @ X(4) @ X(5), + Z(1) @ X(4) @ Y(5), + Z(1) @ Y(4) @ X(5), + Z(1) @ Y(4) @ Y(5), + Z(1) @ X(5) @ X(6), + Z(1) @ X(5) @ Y(6), + Z(1) @ Y(5) @ X(6), + Z(1) @ Y(5) @ Y(6), + Z(1) @ X(6) @ X(7), + Z(1) @ X(6) @ Y(7), + Z(1) @ Y(6) @ X(7), + Z(1) @ Y(6) @ Y(7), + Z(2) @ X(4) @ X(5), + Z(2) @ X(4) @ Y(5), + Z(2) @ Y(4) @ X(5), + Z(2) @ Y(4) @ Y(5), + Z(2) @ X(5) @ X(6), + Z(2) @ X(5) @ Y(6), + Z(2) @ Y(5) @ X(6), + Z(2) @ Y(5) @ Y(6), + Z(2) @ X(6) @ X(7), + Z(2) @ X(6) @ Y(7), + Z(2) @ Y(6) @ X(7), + Z(2) @ Y(6) @ Y(7), + Z(3) @ X(4) @ X(5), + Z(3) @ X(4) @ Y(5), + Z(3) @ Y(4) @ X(5), + Z(3) @ Y(4) @ Y(5), + Z(3) @ X(5) @ X(6), + Z(3) @ X(5) @ Y(6), + Z(3) @ Y(5) @ X(6), + Z(3) @ Y(5) @ Y(6), + Z(3) @ X(6) @ X(7), + Z(3) @ X(6) @ Y(7), + Z(3) @ Y(6) @ X(7), + Z(3) @ Y(6) @ Y(7), + I(), + Z(1), + Z(2), + Z(3), + ], + ), + ), +] + + +class TestBoseSentenceMapping: + """Tests for mapping BoseSentences""" + + def test_empty_bose_sentence(self): + """Test that an empty BoseSentence (bose null operator) is + converted to an empty PauliSentence or the null operator""" + op = BoseSentence({}) + + ps_op = unary_mapping(op, ps=True) + ps_op.simplify() + assert ps_op == PauliSentence({}) + + op = unary_mapping(op).simplify() + assert isinstance(op, qml.ops.SProd) + assert isinstance(op.base, I) + assert op.scalar == 0 + + @pytest.mark.parametrize("bose_op, n_states, result", BOSE_SEN_AND_OPS) + def test_unary_mapping_bosesentence(self, bose_op, n_states, result): + """Test that the unary_mapping function returns the correct qubit operator.""" + + qubit_op = unary_mapping(bose_op, n_states=n_states, ps=True) + qubit_op.simplify(tol=1e-8) + + expected_op = pauli_sentence(qml.Hamiltonian(result[0], result[1])) + expected_op.simplify(tol=1e-8) + assert qubit_op == expected_op + + +@pytest.mark.parametrize( + "bose_op", + [ + BoseWord({(0, 0): "-"}), + BoseSentence({BoseWord({(0, 0): "-"}): 1.0, BoseWord({(0, 1): "-"}): 1.0}), + ], +) +def test_return_unary_mapping_sum(bose_op): + """Test that the correct type is returned for unary mapping + when ps is set to False.""" + + qubit_op = unary_mapping(bose_op, ps=False) + assert isinstance(qubit_op, qml.ops.Sum) + + +@pytest.mark.parametrize( + "bose_op", + [ + BoseWord({(0, 0): "-"}), + BoseSentence({BoseWord({(0, 0): "-"}): 1.0, BoseWord({(0, 1): "-"}): 1.0}), + ], +) +def test_return_unary_mapping_ps(bose_op): + """Test that the correct type is returned for unary mapping + when ps is set to False.""" + + qubit_op = unary_mapping(bose_op, ps=True) + assert isinstance(qubit_op, qml.pauli.PauliSentence) + + +@pytest.mark.parametrize( + ("bose_op, wire_map, result"), + [ + ( + BoseWord({(0, 0): "+"}), + {0: 1, 1: 2}, + ( + [0.25, -0.25j, 0.25j, (0.25 + 0j)], + [X(1) @ X(2), X(1) @ Y(2), Y(1) @ X(2), Y(1) @ Y(2)], + ), + ), + ( + BoseSentence({BoseWord({(0, 0): "-"}): 1.0, BoseWord({(0, 1): "+"}): 1.0}), + {0: "a", 1: "b", 2: "c", 3: "d"}, + ( + [0.25, 0.25j, -0.25j, (0.25 + 0j), 0.25, -0.25j, 0.25j, (0.25 + 0j)], + [ + X("a") @ X("b"), + X("a") @ Y("b"), + Y("a") @ X("b"), + Y("a") @ Y("b"), + X("c") @ X("d"), + X("c") @ Y("d"), + Y("c") @ X("d"), + Y("c") @ Y("d"), + ], + ), + ), + ], +) +def test_unary_mapping_wiremap(bose_op, wire_map, result): + """Test that the unary_mapping function returns the correct qubit operator.""" + qubit_op = unary_mapping(bose_op, n_states=2, wire_map=wire_map, ps=True) + qubit_op.simplify(tol=1e-8) + + expected_op = pauli_sentence(qml.Hamiltonian(result[0], result[1])) + expected_op.simplify(tol=1e-8) + assert qubit_op == expected_op + + +def test_n_states_error_unary(): + """Test that an error is raised if invalid number of states is provided.""" + bw = BoseWord({(0, 0): "-"}) + with pytest.raises( + ValueError, match="Number of allowed bosonic states cannot be less than 2, provided 0." + ): + unary_mapping(bw, n_states=0) + + +def test_error_is_raised_for_incompatible_type(): + """Test that an error is raised if the input is not a BoseWord or BoseSentence""" + + with pytest.raises(ValueError, match="bose_operator must be a BoseWord or BoseSentence"): + unary_mapping(X(0)) + + +bs1 = BoseSentence({bw1: 1}) + + +@pytest.mark.parametrize( + "bose_op, qubit_op_data, tol", + ( + ( + bw1, + ( + 0.25, + -0.25j, + 0.25j, + (0.25 + 0j), + 0.3535533905932738, + -0.3535533905932738j, + 0.3535533905932738j, + (0.3535533905932738 + 0j), + 0.4330127018922193, + -0.4330127018922193j, + 0.4330127018922193j, + (0.4330127018922193 + 0j), + ), + None, + ), + ( + bw1, + ( + 0.25, + -0.25j, + 0.25j, + 0.25, + 0.3535533905932738, + -0.3535533905932738j, + 0.3535533905932738j, + 0.3535533905932738, + 0.4330127018922193, + -0.4330127018922193j, + 0.4330127018922193j, + 0.4330127018922193, + ), + 0.0, + ), + ( + bw1, + ( + 0.25, + 0.25, + 0.3535533905932738, + -0.3535533905932738j, + 0.3535533905932738j, + 0.3535533905932738, + 0.4330127018922193, + -0.4330127018922193j, + 0.4330127018922193j, + 0.4330127018922193, + ), + 0.3, + ), + ( + bs1, + ( + 0.25, + -0.25j, + 0.25j, + (0.25 + 0j), + 0.3535533905932738, + -0.3535533905932738j, + 0.3535533905932738j, + (0.3535533905932738 + 0j), + 0.4330127018922193, + -0.4330127018922193j, + 0.4330127018922193j, + (0.4330127018922193 + 0j), + ), + None, + ), + ( + bs1, + ( + 0.25, + -0.25j, + 0.25j, + 0.25, + 0.3535533905932738, + -0.3535533905932738j, + 0.3535533905932738j, + 0.3535533905932738, + 0.4330127018922193, + -0.4330127018922193j, + 0.4330127018922193j, + 0.4330127018922193, + ), + 0.0, + ), + ( + bs1, + ( + 0.25, + 0.25, + 0.3535533905932738, + 0.3535533905932738, + 0.4330127018922193, + -0.4330127018922193j, + 0.4330127018922193j, + 0.4330127018922193, + ), + 0.4, + ), + ), +) +def test_unary_mapping_tolerance(bose_op, qubit_op_data, tol): + """Test that unary_mapping properly removes negligible imaginary components""" + op = unary_mapping(bose_op, n_states=4, tol=tol) + assert isinstance(op.data[1], type(qubit_op_data[1]))