diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index a6b39d4c984..a0ecade0fed 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -97,6 +97,7 @@ OpenFermion and PennyLane objects. [(#5773)](https://github.com/PennyLaneAI/pennylane/pull/5773) [(#5808)](https://github.com/PennyLaneAI/pennylane/pull/5808) + [(#5881)](https://github.com/PennyLaneAI/pennylane/pull/5881) ```python of_op = openfermion.FermionOperator('0^ 2') diff --git a/pennylane/qchem/convert_openfermion.py b/pennylane/qchem/convert_openfermion.py index dad1d5dcec2..ce7f3123100 100644 --- a/pennylane/qchem/convert_openfermion.py +++ b/pennylane/qchem/convert_openfermion.py @@ -45,116 +45,87 @@ def _import_of(): return openfermion -def _from_openfermion_qubit(of_op, tol=1.0e-16, **kwargs): - r"""Convert OpenFermion ``QubitOperator`` to a :class:`~.LinearCombination` object in PennyLane representing a linear combination of qubit operators. +def from_openfermion(openfermion_op, wires=None, tol=1e-16): + r"""Convert OpenFermion + `FermionOperator `__ + to PennyLane :class:`~.fermi.FermiWord` or :class:`~.fermi.FermiSentence` and + OpenFermion `QubitOperator `__ + to PennyLane :class:`~.LinearCombination`. Args: - of_op (QubitOperator): fermionic-to-qubit transformed operator in terms of - Pauli matrices - wires (Wires, list, tuple, dict): Custom wire mapping used to convert the qubit operator - to an observable terms measurable in a PennyLane ansatz. - For types Wires/list/tuple, each item in the iterable represents a wire label - corresponding to the qubit number equal to its index. - For type dict, only int-keyed dict (for qubit-to-wire conversion) is accepted. - If None, will use identity map (e.g. 0->0, 1->1, ...). - tol (float): tolerance value to decide whether the imaginary part of the coefficients is retained - return_sum (bool): flag indicating whether a ``Sum`` object is returned + openfermion_op (FermionOperator, QubitOperator): OpenFermion operator + wires (dict): Custom wire mapping used to convert the external qubit + operator to a PennyLane operator. + Only dictionaries with integer keys (for qubit-to-wire conversion) are accepted. + If ``None``, the identity map (e.g., ``0->0, 1->1, ...``) will be used. + tol (float): tolerance for discarding negligible coefficients Returns: - (pennylane.ops.Sum, pennylane.ops.LinearCombination): a linear combination of Pauli words + Union[FermiWord, FermiSentence, LinearCombination]: PennyLane operator **Example** - >>> q_op = QubitOperator('X0', 1.2) + QubitOperator('Z1', 2.4) - >>> from_openfermion_qubit(q_op) - 1.2 * X(0) + 2.4 * Z(1) - - >>> from openfermion import FermionOperator + >>> from openfermion import FermionOperator, QubitOperator >>> of_op = 0.5 * FermionOperator('0^ 2') + FermionOperator('0 2^') - >>> pl_op = from_openfermion_qubit(of_op) + >>> pl_op = from_openfermion(of_op) + >>> print(pl_op) + 0.5 * a⁺(0) a(2) + + 1.0 * a(0) a⁺(2) + + >>> of_op = QubitOperator('X0', 1.2) + QubitOperator('Z1', 2.4) + >>> pl_op = from_openfermion(of_op) >>> print(pl_op) - 0.5 * a⁺(0) a(2) - + 1.0 * a(0) a⁺(2) + 1.2 * X(0) + 2.4 * Z(1) """ - coeffs, pl_ops = _openfermion_to_pennylane(of_op, tol=tol) - pl_term = qml.ops.LinearCombination(coeffs, pl_ops) + openfermion = _import_of() - if "format" in kwargs: - if kwargs["format"] == "Sum": - return qml.dot(*pl_term.terms()) - if kwargs["format"] != "LinearCombination": - f = kwargs["format"] - raise ValueError(f"format must be a Sum or LinearCombination, got: {f}.") + if isinstance(openfermion_op, openfermion.FermionOperator): - return pl_term + if wires: + raise ValueError("Custom wire mapping is not supported for fermionic operators.") + typemap = {0: "-", 1: "+"} -def from_openfermion(openfermion_op, tol=1e-16): - r"""Convert OpenFermion - `FermionOperator `__ - object to PennyLane :class:`~.fermi.FermiWord` or :class:`~.fermi.FermiSentence` objects. + fermi_words = [] + fermi_coeffs = [] - Args: - openfermion_op (FermionOperator): OpenFermion fermionic operator - tol (float): tolerance for discarding negligible coefficients + for ops, val in openfermion_op.terms.items(): + fw_dict = {(i, op[0]): typemap[op[1]] for i, op in enumerate(ops)} + fermi_words.append(FermiWord(fw_dict)) + fermi_coeffs.append(val) - Returns: - Union[FermiWord, FermiSentence]: the fermionic operator object + if len(fermi_words) == 1 and fermi_coeffs[0] == 1.0: + return fermi_words[0] - **Example** + pl_op = FermiSentence(dict(zip(fermi_words, fermi_coeffs))) + pl_op.simplify(tol=tol) - >>> from openfermion import FermionOperator - >>> openfermion_op = 0.5 * FermionOperator('0^ 2') + FermionOperator('0 2^') - >>> pl_op = from_openfermion(openfermion_op) - >>> print(pl_op) - 0.5 * a⁺(0) a(2) - + 1.0 * a(0) a⁺(2) - """ - try: - import openfermion - except ImportError as Error: - raise ImportError( - "This feature requires openfermion. " - "It can be installed with: pip install openfermion" - ) from Error - - typemap = {0: "-", 1: "+"} + return pl_op - fermi_words = [] - fermi_coeffs = [] + coeffs, pl_ops = _openfermion_to_pennylane(openfermion_op, wires=wires, tol=tol) - for ops, val in openfermion_op.terms.items(): - fw_dict = {(i, op[0]): typemap[op[1]] for i, op in enumerate(ops)} - fermi_words.append(FermiWord(fw_dict)) - fermi_coeffs.append(val) + pennylane_op = qml.ops.LinearCombination(coeffs, pl_ops) - if len(fermi_words) == 1 and fermi_coeffs[0] == 1.0: - return fermi_words[0] - - pl_op = FermiSentence(dict(zip(fermi_words, fermi_coeffs))) - pl_op.simplify(tol=tol) - - return pl_op + return pennylane_op def to_openfermion( pennylane_op: Union[Sum, LinearCombination, FermiWord, FermiSentence], wires=None, tol=1.0e-16 ): - r"""Convert a PennyLane operator to a OpenFermion ``QubitOperator`` or ``FermionOperator``. + r"""Convert a PennyLane operator to OpenFermion + `QubitOperator `__ or + `FermionOperator `__. Args: pennylane_op (~ops.op_math.Sum, ~ops.op_math.LinearCombination, FermiWord, FermiSentence): - linear combination of operators - wires (Wires, list, tuple, dict): - Custom wire mapping used to convert the qubit operator - to an observable terms measurable in a PennyLane ansatz. - For types Wires/list/tuple, each item in the iterable represents a wire label - corresponding to the qubit number equal to its index. - For type dict, only int-keyed dict (for qubit-to-wire conversion) is accepted. - If None, will use identity map (e.g. 0->0, 1->1, ...). + PennyLane operator + wires (dict): Custom wire mapping used to convert a PennyLane qubit operator + to the external operator. + Only dictionaries with integer keys (for qubit-to-wire conversion) are accepted. + If ``None``, the identity map (e.g., ``0->0, 1->1, ...``) will be used. Returns: - (QubitOperator, FermionOperator): an OpenFermion operator + (QubitOperator, FermionOperator): OpenFermion operator **Example** @@ -166,6 +137,7 @@ def to_openfermion( 1.2 [0^ 1] + 3.1 [1^ 2] """ + return _to_openfermion_dispatch(pennylane_op, wires=wires, tol=tol) @@ -189,16 +161,7 @@ def _(ops: FermiWord, wires=None, tol=1.0e-16): openfermion = _import_of() if wires: - all_wires = Wires.all_wires(ops.wires, sort=True) - mapped_wires = _process_wires(wires) - if not set(all_wires).issubset(set(mapped_wires)): - raise ValueError("Supplied `wires` does not cover all wires defined in `ops`.") - - pl_op_mapped = {} - for loc, orbital in ops.keys(): - pl_op_mapped[(loc, mapped_wires.index(orbital))] = ops[(loc, orbital)] - - ops = FermiWord(pl_op_mapped) + raise ValueError("Custom wire mapping is not supported for fermionic operators.") return openfermion.ops.FermionOperator(qml.fermi.fermionic._to_string(ops, of=True)) @@ -207,11 +170,14 @@ def _(ops: FermiWord, wires=None, tol=1.0e-16): def _(pl_op: FermiSentence, wires=None, tol=1.0e-16): openfermion = _import_of() + if wires: + raise ValueError("Custom wire mapping is not supported for fermionic operators.") + fermion_op = openfermion.ops.FermionOperator() for fermi_word in pl_op: if np.abs(pl_op[fermi_word].imag) < tol: - fermion_op += pl_op[fermi_word].real * to_openfermion(fermi_word, wires=wires) + fermion_op += pl_op[fermi_word].real * to_openfermion(fermi_word) else: - fermion_op += pl_op[fermi_word] * to_openfermion(fermi_word, wires=wires) + fermion_op += pl_op[fermi_word] * to_openfermion(fermi_word) return fermion_op diff --git a/tests/qchem/openfermion_pyscf_tests/test_convert_openfermion.py b/tests/qchem/openfermion_pyscf_tests/test_convert_openfermion.py index 3d565bb74b8..2938ca07a29 100644 --- a/tests/qchem/openfermion_pyscf_tests/test_convert_openfermion.py +++ b/tests/qchem/openfermion_pyscf_tests/test_convert_openfermion.py @@ -23,7 +23,6 @@ import pennylane as qml from pennylane import fermi from pennylane import numpy as np -from pennylane.qchem.convert_openfermion import _from_openfermion_qubit openfermion = pytest.importorskip("openfermion") @@ -72,40 +71,57 @@ class TestFromOpenFermion: @pytest.mark.parametrize("of_op, pl_op", OPS) def test_convert_qubit(self, of_op, pl_op): """Test conversion from ``QubitOperator`` to PennyLane.""" - converted_pl_op = _from_openfermion_qubit(of_op) + converted_pl_op = qml.from_openfermion(of_op) assert converted_pl_op.compare(pl_op) + OPS_WIRES = ( + ( + (openfermion.QubitOperator("X0", 1.2) + openfermion.QubitOperator("Z1", 2.4)), + ({0: "a", 1: 2}), + (1.2 * qml.X("a") + 2.4 * qml.Z(2)), + ), + ) + + @pytest.mark.parametrize("of_op, wires, pl_op", OPS_WIRES) + def test_wires_qubit(self, of_op, wires, pl_op): + """Test conversion from ``QubitOperator`` to PennyLane with wire map.""" + converted_pl_op = qml.from_openfermion(of_op, wires=wires) + assert converted_pl_op.compare(pl_op) + + OPS_FERMI = ( + ((openfermion.FermionOperator("0^ 1")), ({0: "a", 1: 2})), + ( + (openfermion.FermionOperator("0^ 1") + openfermion.FermionOperator("3^ 4^")), + ({0: "a", 3: 2}), + ), + ) + + @pytest.mark.parametrize("of_op, wires", OPS_FERMI) + def test_wires_fermionic(self, of_op, wires): + """Test that an error is raised for mapping wires in fermionic operators.""" + with pytest.raises( + ValueError, + match="Custom wire mapping is not supported for fermionic operators.", + ): + qml.from_openfermion(of_op, wires=wires) + def test_tol_qubit(self): """Test with complex coefficients.""" q_op = openfermion.QubitOperator("X0", complex(1.0, 1e-8)) + openfermion.QubitOperator( "Z1", complex(1.3, 1e-8) ) - pl_op = _from_openfermion_qubit(q_op, tol=1e-6) + pl_op = qml.from_openfermion(q_op, tol=1e-6) assert not np.any(pl_op.coeffs.imag) - pl_op = _from_openfermion_qubit(q_op, tol=1e-10) + pl_op = qml.from_openfermion(q_op, tol=1e-10) assert np.any(pl_op.coeffs.imag) - def test_sum_qubit(self): - """Test that the from_openfermion_qubit method yields a :class:`~.Sum` object if requested.""" + def test_type_qubit(self): + """Test that from_openfermion yields a ``LinearCombination`` object.""" q_op = openfermion.QubitOperator("X0 X1", 0.25) + openfermion.QubitOperator("Z1 Z0", 0.75) - assert isinstance(_from_openfermion_qubit(q_op), qml.ops.LinearCombination) - assert isinstance( - _from_openfermion_qubit(q_op, format="LinearCombination"), qml.ops.LinearCombination - ) - assert isinstance(_from_openfermion_qubit(q_op, format="Sum"), qml.ops.Sum) - - def test_invalid_format_qubit(self): - """Test if error is raised if format is invalid.""" - q_op = openfermion.QubitOperator("X0") - - with pytest.raises( - ValueError, - match="format must be a Sum or LinearCombination, got: invalid_format", - ): - _from_openfermion_qubit(q_op, format="invalid_format") + assert isinstance(qml.from_openfermion(q_op), qml.ops.LinearCombination) # PennyLane operators were obtained from openfermion operators manually @pytest.mark.parametrize( @@ -281,26 +297,9 @@ def test_tol(self, pl_op): MAPPED_OPS = ( ( - (qml.fermi.FermiWord({(0, 0): "+", (1, 1): "-"})), - (openfermion.FermionOperator("1^ 0")), - ({0: 1, 1: 0}), - ), - ( - ( - qml.fermi.FermiSentence( - { - qml.fermi.FermiWord( - {(0, 0): "+", (1, 1): "-", (2, 3): "+", (3, 2): "-"} - ): 0.25, - qml.fermi.FermiWord({(1, 0): "+", (0, 1): "-"}): 0.1, - } - ) - ), - ( - 0.1 * openfermion.FermionOperator("1 0^") - + 0.25 * openfermion.FermionOperator("0^ 1 2^ 3") - ), - ({0: 0, 1: 1, 2: 3, 3: 2}), + (1.2 * qml.X("a") + 2.4 * qml.Z(2)), + (openfermion.QubitOperator("X0", 1.2) + openfermion.QubitOperator("Z1", 2.4)), + ({"a": 0, 2: 1}), ), ) @@ -327,40 +326,31 @@ def test_not_xyz(self, op): with pytest.raises(ValueError, match=_match): qml.to_openfermion(qml.to_openfermion(pl_op)) - INVALID_OPS_WIRES = ( - ( - qml.ops.LinearCombination( - np.array([0.1, 0.2]), - [ - qml.operation.Tensor(qml.PauliX(wires=["w0"])), - qml.operation.Tensor(qml.PauliY(wires=["w0"]), qml.PauliZ(wires=["w1"])), - ], - ) - ), - ((qml.fermi.FermiWord({(0, 0): "+", (1, 1): "-"}))), - ) - - @pytest.mark.parametrize("pl_op", INVALID_OPS_WIRES) - def test_wires_not_covered(self, pl_op): - r"""Test if the conversion complains about supplied wires not covering ops wires.""" + def test_invalid_op(self): + r"""Test if to_openfermion throws an error if the wrong type of operator is given.""" + pl_op = "Wrong type." with pytest.raises( ValueError, - match="Supplied `wires` does not cover all wires defined in `ops`.", + match=f"pl_op must be a Sum, LinearCombination, FermiWord or FermiSentence, got: {type(pl_op)}.", ): qml.to_openfermion( pl_op, - wires=qml.wires.Wires(["w0", "w2"]), ) - def test_invalid_op(self): - r"""Test if to_openfermion throws an error if the wrong type of operator is given.""" - pl_op = "Wrong type." + OPS_FERMI_WIRE = ( + ((qml.fermi.FermiWord({(0, 0): "+", (1, 1): "-"})), ({0: "a", 1: 2})), + ( + (qml.fermi.FermiSentence({qml.fermi.FermiWord({(0, 0): "+", (1, 1): "-"}): 1.2})), + ({0: "a", 1: 2}), + ), + ) + @pytest.mark.parametrize("pl_op, wires", OPS_FERMI_WIRE) + def test_wires_fermionic_error(self, pl_op, wires): + """Test that an error is raised for mapping wires in fermionic operators.""" with pytest.raises( ValueError, - match=f"pl_op must be a Sum, LinearCombination, FermiWord or FermiSentence, got: {type(pl_op)}.", + match="Custom wire mapping is not supported for fermionic operators.", ): - qml.to_openfermion( - pl_op, - ) + qml.to_openfermion(pl_op, wires=wires)