Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support qubit operator in from_openfermion #5881

Merged
merged 17 commits into from
Jun 21, 2024
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
132 changes: 43 additions & 89 deletions pennylane/qchem/convert_openfermion.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,96 +45,64 @@ 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 int-keyed dictionaries (for qubit-to-wire conversion) are accepted.
soranjh marked this conversation as resolved.
Show resolved Hide resolved
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)
"""
coeffs, pl_ops = _openfermion_to_pennylane(of_op, tol=tol)
pl_term = qml.ops.LinearCombination(coeffs, pl_ops)

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}.")

return pl_term

>>> of_op = QubitOperator('X0', 1.2) + QubitOperator('Z1', 2.4)
>>> pl_op = from_openfermion(of_op)
>>> print(pl_op)
1.2 * X(0) + 2.4 * Z(1)
"""
openfermion = _import_of()

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.
soranjh marked this conversation as resolved.
Show resolved Hide resolved

Args:
openfermion_op (FermionOperator): OpenFermion fermionic operator
tol (float): tolerance for discarding negligible coefficients

Returns:
Union[FermiWord, FermiSentence]: the fermionic operator object
if isinstance(openfermion_op, openfermion.FermionOperator):
typemap = {0: "-", 1: "+"}
soranjh marked this conversation as resolved.
Show resolved Hide resolved

**Example**
fermi_words = []
fermi_coeffs = []

>>> 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
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)

typemap = {0: "-", 1: "+"}
if len(fermi_words) == 1 and fermi_coeffs[0] == 1.0:
return fermi_words[0]

fermi_words = []
fermi_coeffs = []
pl_op = FermiSentence(dict(zip(fermi_words, fermi_coeffs)))
pl_op.simplify(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)
return pl_op

if len(fermi_words) == 1 and fermi_coeffs[0] == 1.0:
return fermi_words[0]
coeffs, pl_ops = _openfermion_to_pennylane(openfermion_op, wires=wires, tol=tol)

pl_op = FermiSentence(dict(zip(fermi_words, fermi_coeffs)))
pl_op.simplify(tol=tol)
pennylane_op = qml.ops.LinearCombination(coeffs, pl_ops)

return pl_op
return pennylane_op


def to_openfermion(
Expand All @@ -145,12 +113,9 @@ def to_openfermion(
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.
wires (dict): Custom wire mapping used to convert a PennyLane qubit operator
to the external operator.
Only int-keyed dict (for qubit-to-wire conversion) is accepted.
soranjh marked this conversation as resolved.
Show resolved Hide resolved
If None, will use identity map (e.g. 0->0, 1->1, ...).

Returns:
Expand All @@ -166,6 +131,7 @@ def to_openfermion(
1.2 [0^ 1] +
3.1 [1^ 2]
"""

return _to_openfermion_dispatch(pennylane_op, wires=wires, tol=tol)


Expand All @@ -188,18 +154,6 @@ def _(pl_op: Sum, wires=None, tol=1.0e-16):
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)

return openfermion.ops.FermionOperator(qml.fermi.fermionic._to_string(ops, of=True))


Expand All @@ -210,8 +164,8 @@ def _(pl_op: FermiSentence, wires=None, tol=1.0e-16):
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
90 changes: 23 additions & 67 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,7 +71,21 @@ 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)

OPSWIRES = (
soranjh marked this conversation as resolved.
Show resolved Hide resolved
(
(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", OPSWIRES)
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)

def test_tol_qubit(self):
Expand All @@ -81,31 +94,17 @@ def test_tol_qubit(self):
"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):
ddhawan11 marked this conversation as resolved.
Show resolved Hide resolved
"""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 +280,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,32 +309,6 @@ 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):
soranjh marked this conversation as resolved.
Show resolved Hide resolved
r"""Test if the conversion complains about supplied wires not covering ops wires."""

with pytest.raises(
ValueError,
match="Supplied `wires` does not cover all wires defined in `ops`.",
):
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."
Expand Down
Loading