Skip to content

Commit

Permalink
Implement of.QubitOperator <-> qml.ops.LinearCombination. (#5773)
Browse files Browse the repository at this point in the history
**Context:**
This PR adds the functionality to convert a ``QubitOperator`` in
OpenFermion to a ``LinearCombination`` in PennyLane and vice versa.

**Description of the Change:**
Added `convert_openfermion` module to `qchem` where users are able to
convert a `QubitOperator` object from OpenFermion into a
`LinearCombination` object in PennyLane using `from_openfermion`.
Conversion of a `LinearCombination` object to `QubitOperator` object is
implemented in `to_openfermion`.

**Benefits:**
Directly convert the operator in terms of a ``LinearCombination`` object
instead of the current functionality where a list of coefficients and
operators is needed (see `_pennylane_to_openfermion` and
`_openfermion_to_pennylane` methods in
[convert.py](https://github.com/PennyLaneAI/pennylane/blob/master/pennylane/qchem/convert.py)).

**Possible Drawbacks:**
If we provide a custom wire mapping where we e.g. swap the order of the
wires, the `LinearCombination` object is still given in the order of the
unswapped wires. However, the order of the operators should only matter
for printing anyway.
```
>>> q_op = openfermion.QubitOperator("X0", 1.2) + openfermion.QubitOperator("Z1", 2.4) + openfermion.QubitOperator("Y2", 0.1)
>>> q_op
1.2 [X0] +
2.4 [Z1] +
0.1 [Y2]
>>> pl_linear_combination = qml.from_openfermion(q_op, wires={0: 2, 1: 1, 2: 0})
>>> pl_linear_combination
1.2 * X(2) + 2.4 * Z(1) + 0.1 * Y(0)
```

**Related GitHub Issues:**
Closes #5759.

---------

Co-authored-by: soranjh <[email protected]>
Co-authored-by: Thomas R. Bromley <[email protected]>
Co-authored-by: soranjh <[email protected]>
  • Loading branch information
4 people authored Jun 19, 2024
1 parent c874bbd commit 11d3d93
Show file tree
Hide file tree
Showing 8 changed files with 582 additions and 96 deletions.
33 changes: 19 additions & 14 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,6 @@
* The `default.tensor` device is introduced to perform tensor network simulations of quantum circuits using the `mps` (Matrix Product State) method.
[(#5699)](https://github.com/PennyLaneAI/pennylane/pull/5699)

* Added `from_openfermion` to convert openfermion `FermionOperator` objects to PennyLane `FermiWord` or
`FermiSentence` objects.
[(#5808)](https://github.com/PennyLaneAI/pennylane/pull/5808)

```python
of_op = openfermion.FermionOperator('0^ 2')
pl_op = qml.from_openfermion(of_op)

```
```pycon
>>> print(pl_op)
a⁺(0) a(2)
```

* A new `qml.noise` module which contains utililty functions for building `NoiseModels`.
[(#5674)](https://github.com/PennyLaneAI/pennylane/pull/5674)
[(#5684)](https://github.com/PennyLaneAI/pennylane/pull/5684)
Expand All @@ -77,6 +63,24 @@
}, t1 = 0.04)
```

* The ``from_openfermion`` and ``to_openfermion`` functions are added to convert between
OpenFermion and PennyLane objects.
[(#5773)](https://github.com/PennyLaneAI/pennylane/pull/5773)
[(#5808)](https://github.com/PennyLaneAI/pennylane/pull/5808)

```python
of_op = openfermion.FermionOperator('0^ 2')
pl_op = qml.from_openfermion(of_op)
of_op_new = qml.to_openfermion(pl_op)

```
```pycon
>>> print(pl_op)
a⁺(0) a(2)
>>> print(of_op_new)
1.0 [0^ 2]
```

<h3>Improvements 🛠</h3>

* Add operation and measurement specific routines in `default.tensor` to improve scalability.
Expand Down Expand Up @@ -504,6 +508,7 @@ Isaac De Vlugt,
Diksha Dhawan,
Pietropaolo Frisoni,
Emiliano Godinez,
Daria Van Hende,
Austin Huang,
David Ittah,
Soran Jahangiri,
Expand Down
1 change: 1 addition & 0 deletions pennylane/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
taper_operation,
import_operator,
from_openfermion,
to_openfermion,
)
from pennylane._device import Device, DeviceError
from pennylane._grad import grad, jacobian, vjp, jvp
Expand Down
42 changes: 42 additions & 0 deletions pennylane/fermi/fermionic.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,48 @@ def from_string(fermi_string):
return FermiWord({(i, int(s[:-1])): s[-1] for i, s in enumerate(operators)})


def _to_string(fermi_op, of=False):
r"""Return a string representation of the :class:`~.FermiWord` object.
Args:
fermi_op (FermiWord): the fermionic operator
of (bool): whether to return a string representation in the same style as OpenFermion using
the shorthand: 'q^' = a^\dagger_q 'q' = a_q. Each operator in the word is
represented by the number of the wire it operates on
Returns:
(str): a string representation of the :class:`~.FermiWord` object
**Example**
>>> w = FermiWord({(0, 0) : '+', (1, 1) : '-'})
>>> _to_string(w)
'0+ 1-'
>>> w = FermiWord({(0, 0) : '+', (1, 1) : '-'})
>>> _to_string(w, of=True)
'0^ 1'
"""
if not isinstance(fermi_op, FermiWord):
raise ValueError(f"fermi_op must be a FermiWord, got: {type(fermi_op)}")

pl_to_of_map = {"+": "^", "-": ""}

if len(fermi_op) == 0:
return "I"

op_list = ["" for _ in range(len(fermi_op))]
for loc, wire in fermi_op:
if of:
op_str = str(wire) + pl_to_of_map[fermi_op[(loc, wire)]]
else:
op_str = str(wire) + fermi_op[(loc, wire)]

op_list[loc] += op_str

return " ".join(op_list).rstrip()


# pylint: disable=too-few-public-methods
class FermiC(FermiWord):
r"""FermiC(orbital)
Expand Down
2 changes: 1 addition & 1 deletion pennylane/qchem/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from .basis_data import load_basisset
from .basis_set import BasisFunction, atom_basis_data, mol_basis_data
from .convert import import_operator, import_state
from .convert_openfermion import from_openfermion
from .convert_openfermion import from_openfermion, to_openfermion
from .dipole import dipole_integrals, dipole_moment, fermionic_dipole, molecular_dipole
from .factorization import basis_rotation, factorize
from .givens_decomposition import givens_decomposition
Expand Down
23 changes: 19 additions & 4 deletions pennylane/qchem/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def _process_wires(wires, n_wires=None):
return wires


def _openfermion_to_pennylane(qubit_operator, wires=None):
def _openfermion_to_pennylane(qubit_operator, wires=None, tol=1.0e-16):
r"""Convert OpenFermion ``QubitOperator`` to a 2-tuple of coefficients and
PennyLane Pauli observables.
Expand All @@ -131,6 +131,8 @@ def _openfermion_to_pennylane(qubit_operator, wires=None):
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): whether to keep the imaginary part of the coefficients if they are smaller
than the provided tolerance.
Returns:
tuple[array[float], Iterable[pennylane.operation.Operator]]: coefficients and their
Expand Down Expand Up @@ -177,8 +179,12 @@ def _get_op(term, wires):
*[(coef, _get_op(term, wires)) for term, coef in qubit_operator.terms.items()]
# example term: ((0,'X'), (2,'Z'), (3,'Y'))
)
coeffs = np.array(coeffs)

return np.array(coeffs).real, list(ops)
if (np.abs(coeffs.imag) < tol).all():
coeffs = coeffs.real

return coeffs, list(ops)


def _ps_to_coeff_term(ps, wire_order):
Expand All @@ -196,7 +202,8 @@ def _ps_to_coeff_term(ps, wire_order):
return coeffs, ops_str


def _pennylane_to_openfermion(coeffs, ops, wires=None):
# pylint:disable=too-many-branches
def _pennylane_to_openfermion(coeffs, ops, wires=None, tol=1.0e-16):
r"""Convert a 2-tuple of complex coefficients and PennyLane operations to
OpenFermion ``QubitOperator``.
Expand All @@ -211,6 +218,8 @@ def _pennylane_to_openfermion(coeffs, ops, wires=None):
corresponding to the qubit number equal to its index.
For type dict, only consecutive-int-valued dict (for wire-to-qubit conversion) is
accepted. If None, will map sorted wires from all `ops` to consecutive int.
tol (float): whether to keep the imaginary part of the coefficients if they are smaller
than the provided tolerance.
Returns:
QubitOperator: an instance of OpenFermion's ``QubitOperator``.
Expand Down Expand Up @@ -249,6 +258,10 @@ def _pennylane_to_openfermion(coeffs, ops, wires=None):
else:
qubit_indexed_wires = all_wires

coeffs = np.array(coeffs)
if (np.abs(coeffs.imag) < tol).all():
coeffs = coeffs.real

q_op = openfermion.QubitOperator()
for coeff, op in zip(coeffs, ops):
if isinstance(op, Tensor):
Expand Down Expand Up @@ -298,7 +311,9 @@ def _openfermion_pennylane_equivalent(
(bool): True if equivalent
"""
coeffs, ops = pennylane_qubit_operator.terms()
return openfermion_qubit_operator == _pennylane_to_openfermion(coeffs, ops, wires=wires)
return openfermion_qubit_operator == _pennylane_to_openfermion(
np.array(coeffs), ops, wires=wires
)


def import_operator(qubit_observable, format="openfermion", wires=None, tol=1e010):
Expand Down
156 changes: 153 additions & 3 deletions pennylane/qchem/convert_openfermion.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,81 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This module contains the functions for converting an openfermion fermionic operator to Pennylane
FermiWord or FermiSentence operators.
This module contains the functions for converting between OpenFermion and PennyLane objects.
"""

from functools import singledispatch
from typing import Union

# pylint: disable= import-outside-toplevel,no-member,unused-import
from pennylane.fermi import FermiSentence, FermiWord
import pennylane as qml
from pennylane import numpy as np
from pennylane.fermi.fermionic import FermiSentence, FermiWord
from pennylane.ops import LinearCombination, Sum
from pennylane.qchem.convert import (
_openfermion_to_pennylane,
_pennylane_to_openfermion,
_process_wires,
)
from pennylane.wires import Wires


def _import_of():
"""Import openfermion."""
try:
# pylint: disable=import-outside-toplevel, unused-import, multiple-imports
import openfermion
except ImportError as Error:
raise ImportError(
"This feature requires openfermion. "
"It can be installed with: pip install openfermion."
) from Error

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.
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
Returns:
(pennylane.ops.Sum, pennylane.ops.LinearCombination): a linear combination of Pauli words
**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
>>> of_op = 0.5 * FermionOperator('0^ 2') + FermionOperator('0 2^')
>>> pl_op = from_openfermion_qubit(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


def from_openfermion(openfermion_op, tol=1e-16):
Expand Down Expand Up @@ -65,3 +135,83 @@ def from_openfermion(openfermion_op, tol=1e-16):
pl_op.simplify(tol=tol)

return pl_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``.
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, ...).
Returns:
(QubitOperator, FermionOperator): an OpenFermion operator
**Example**
>>> w1 = qml.fermi.FermiWord({(0, 0) : '+', (1, 1) : '-'})
>>> w2 = qml.fermi.FermiWord({(0, 1) : '+', (1, 2) : '-'})
>>> s = qml.fermi.FermiSentence({w1 : 1.2, w2: 3.1})
>>> of_op = qml.to_openfermion(s)
>>> of_op
1.2 [0^ 1] +
3.1 [1^ 2]
"""
return _to_openfermion_dispatch(pennylane_op, wires=wires, tol=tol)


@singledispatch
def _to_openfermion_dispatch(pl_op, wires=None, tol=1.0e-16):
"""Dispatches to appropriate function if pl_op is a ``Sum``, ``LinearCombination, ``FermiWord`` or ``FermiSentence``."""
raise ValueError(
f"pl_op must be a Sum, LinearCombination, FermiWord or FermiSentence, got: {type(pl_op)}."
)


@_to_openfermion_dispatch.register
def _(pl_op: Sum, wires=None, tol=1.0e-16):
coeffs, ops = pl_op.terms()
return _pennylane_to_openfermion(np.array(coeffs), ops, wires=wires, tol=tol)


# pylint: disable=unused-argument, protected-access
@_to_openfermion_dispatch.register
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))


@_to_openfermion_dispatch.register
def _(pl_op: FermiSentence, wires=None, tol=1.0e-16):
openfermion = _import_of()

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)
else:
fermion_op += pl_op[fermi_word] * to_openfermion(fermi_word, wires=wires)

return fermion_op
Loading

0 comments on commit 11d3d93

Please sign in to comment.