diff --git a/doc/code/qml_bose.rst b/doc/code/qml_bose.rst new file mode 100644 index 00000000000..365ce889a1d --- /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 + + 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..9b7d9edafc5 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -63,9 +63,13 @@ * 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 `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 diff --git a/pennylane/__init__.py b/pennylane/__init__.py index fec9cc004d2..aef87b46810 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -39,7 +39,12 @@ parity_transform, bravyi_kitaev, ) -from pennylane.bose import BoseSentence, BoseWord, binary_mapping +from pennylane.bose import ( + BoseSentence, + BoseWord, + binary_mapping, + unary_mapping, +) from pennylane.qchem import ( taper, symmetry_generators, diff --git a/pennylane/bose/__init__.py b/pennylane/bose/__init__.py index 6817810cd34..c21faeaef4a 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 unary_mapping, binary_mapping diff --git a/pennylane/bose/bosonic_mapping.py b/pennylane/bose/bosonic_mapping.py index 4f489a4933f..7abab61f48d 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,137 @@ 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 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]))