Skip to content

Commit

Permalink
Support qubit operator in from_openfermion (#5881)
Browse files Browse the repository at this point in the history
**Context:**
The `from_openfermion` function is updated to supports the conversion of
qubit operators in addition to fermionic operators.

**Description of the Change:**

**Benefits:**

**Possible Drawbacks:**

**Related GitHub Issues:**
  • Loading branch information
soranjh authored and mudit2812 committed Jul 2, 2024
1 parent 463e8f5 commit 656fec2
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 157 deletions.
1 change: 1 addition & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
148 changes: 57 additions & 91 deletions pennylane/qchem/convert_openfermion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://quantumai.google/reference/python/openfermion/ops/FermionOperator>`__
to PennyLane :class:`~.fermi.FermiWord` or :class:`~.fermi.FermiSentence` and
OpenFermion `QubitOperator <https://quantumai.google/reference/python/openfermion/ops/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 <https://quantumai.google/reference/python/openfermion/ops/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 <https://quantumai.google/reference/python/openfermion/ops/QubitOperator>`__ or
`FermionOperator <https://quantumai.google/reference/python/openfermion/ops/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**
Expand All @@ -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)


Expand All @@ -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))

Expand All @@ -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
122 changes: 56 additions & 66 deletions tests/qchem/openfermion_pyscf_tests/test_convert_openfermion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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}),
),
)

Expand All @@ -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)

0 comments on commit 656fec2

Please sign in to comment.