Repository: PennyLaneAI/pennylane
Pull request #4639 (2023-10-02 19:08:27)

Return qchem Hamiltonians with real dtype coeffs
**Context:**
The qchem Hamiltonians built with the new `qml.jordan_wigner` function have complex coefficients with zero imaginary components. This PR removes the imaginary part if it is less than a pre-defined tolerance.

**Description of the Change:**
The `qml.jordan_wigner` function is modified to optionally remove the imaginary components of the computed qubit operator. 

**Benefits:**

**Possible Drawbacks:**

**Related GitHub Issues:**


github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.
soranjh: [sc 44462]
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4639?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
All modified lines are covered by tests :white_check_mark:
> Comparison is base [(`da79370`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/da79370db01834671bcd4d1edce408131f920ab2?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.63% compared to head [(`700bda6`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4639?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.63%.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #4639   +/-   ##
=======================================
  Coverage   99.63%   99.63%           
=======================================
  Files         376      376           
  Lines       33666    33672    +6     
=======================================
+ Hits        33542    33548    +6     
  Misses        124      124           
```


| [Files](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4639?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/fermi/conversion.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4639?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2Zlcm1pL2NvbnZlcnNpb24ucHk=) | `100.00% <100.00%> (ø)` | |
| [pennylane/qchem/observable\_hf.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4639?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3FjaGVtL29ic2VydmFibGVfaGYucHk=) | `100.00% <100.00%> (ø)` | |


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4639?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

timmysilv: I second Lillian's sentiment - keeping the default to `None` but changing `if tol` to `if tol is not None` should meet the requirements you're describing (preferring opt-in over opt-out to this behaviour), and that would work for me
soranjh: > Right now `tol=0` behaves the same as `tol=None`; I would expect `tol=0` to remove complex terms that were exactly 0, so I think this should be updated.
> 
> I guess it's being indirectly tested through the `observable_hf` test file, but could we add a small test explicitly testing just this for tol of `None`, `0`, and something small? Something that would catch the current behaviour where 0 is treated as None.
> 
> Also needs a changelog entry :)

Added the test and the changelog. 

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index e0ce840e7..3d7a8d7fc 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -191,6 +191,10 @@
   still a `_qfunc_output` property on `QNode` instances.
   [(#4651)](https://github.com/PennyLaneAI/pennylane/pull/4651)
 
+* The `qml.jordan_wigner` function has been modified to optionally remove the imaginary components
+  of the computed qubit operator, if imaginary components are smaller than a threshold. 
+  [(#4639)](https://github.com/PennyLaneAI/pennylane/pull/4639)
+
 <h3>Breaking changes 💔</h3>
 
 * The device test suite now converts device kwargs to integers or floats if they can be converted to integers or floats.
diff --git a/pennylane/fermi/conversion.py b/pennylane/fermi/conversion.py
index 45b109fd2..7a7fb6aea 100644
--- a/pennylane/fermi/conversion.py
+++ b/pennylane/fermi/conversion.py
@@ -15,14 +15,20 @@
 
 from functools import singledispatch
 from typing import Union
+
+import pennylane as qml
 from pennylane.operation import Operator
-from pennylane.pauli import PauliWord, PauliSentence
-from .fermionic import FermiWord, FermiSentence
+from pennylane.pauli import PauliSentence, PauliWord
+
+from .fermionic import FermiSentence, FermiWord
 
 
 # pylint: disable=unexpected-keyword-arg
 def jordan_wigner(
-    fermi_operator: (Union[FermiWord, FermiSentence]), ps: bool = False, wire_map: dict = None
+    fermi_operator: (Union[FermiWord, FermiSentence]),
+    ps: bool = False,
+    wire_map: dict = None,
+    tol: float = None,
 ) -> Union[Operator, PauliSentence]:
     r"""Convert a fermionic operator to a qubit operator using the Jordan-Wigner mapping.
 
@@ -49,6 +55,7 @@ def jordan_wigner(
         wire_map (dict): a dictionary defining how to map the oribitals of
             the Fermi operator to qubit wires. If None, the integers used to
             order the orbitals 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
@@ -72,17 +79,17 @@ def jordan_wigner(
     + (0.25+0j) * X(2) @ X(3)
     + 0.25j * X(2) @ Y(3)
     """
-    return _jordan_wigner_dispatch(fermi_operator, ps, wire_map)
+    return _jordan_wigner_dispatch(fermi_operator, ps, wire_map, tol)
 
 
 @singledispatch
-def _jordan_wigner_dispatch(fermi_operator, ps, wire_map):
+def _jordan_wigner_dispatch(fermi_operator, ps, wire_map, tol):
     """Dispatches to appropriate function if fermi_operator is a FermiWord or FermiSentence."""
     raise ValueError(f"fermi_operator must be a FermiWord or FermiSentence, got: {fermi_operator}")
 
 
 @_jordan_wigner_dispatch.register
-def _(fermi_operator: FermiWord, ps=False, wire_map=None):
+def _(fermi_operator: FermiWord, ps=False, wire_map=None, tol=None):
     wires = list(fermi_operator.wires) or [0]
     identity_wire = wires[0]
 
@@ -104,6 +111,10 @@ def _(fermi_operator: FermiWord, ps=False, wire_map=None):
                 }
             )
 
+    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])
+
     if not ps:
         # wire_order specifies wires to use for Identity (PauliWord({}))
         qubit_operator = qubit_operator.operation(wire_order=[identity_wire])
@@ -115,7 +126,7 @@ def _(fermi_operator: FermiWord, ps=False, wire_map=None):
 
 
 @_jordan_wigner_dispatch.register
-def _(fermi_operator: FermiSentence, ps=False, wire_map=None):
+def _(fermi_operator: FermiSentence, ps=False, wire_map=None, tol=None):
     wires = list(fermi_operator.wires) or [0]
     identity_wire = wires[0]
 
@@ -127,6 +138,9 @@ def _(fermi_operator: FermiSentence, ps=False, wire_map=None):
         for pw in fermi_word_as_ps:
             qubit_operator[pw] = qubit_operator[pw] + fermi_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])
+
     if not ps:
         qubit_operator = qubit_operator.operation(wire_order=[identity_wire])
 
diff --git a/pennylane/qchem/dipole.py b/pennylane/qchem/dipole.py
index 909869b8f..1f1fcaa78 100644
--- a/pennylane/qchem/dipole.py
+++ b/pennylane/qchem/dipole.py
@@ -228,7 +228,7 @@ def fermionic_dipole(mol, cutoff=1.0e-18, core=None, active=None):
     return _fermionic_dipole
 
 
-def dipole_moment(mol, cutoff=1.0e-18, core=None, active=None):
+def dipole_moment(mol, cutoff=1.0e-16, core=None, active=None):
     r"""Return a function that computes the qubit dipole moment observable.
 
     The dipole operator in the second-quantized form is
diff --git a/pennylane/qchem/observable_hf.py b/pennylane/qchem/observable_hf.py
index 7005928a0..5cb96b52f 100644
--- a/pennylane/qchem/observable_hf.py
+++ b/pennylane/qchem/observable_hf.py
@@ -134,7 +134,7 @@ def qubit_observable(o_ferm, cutoff=1.0e-12):
     + ((0.775+0j)*(PauliX(wires=[0]) @ PauliX(wires=[1])))
     + (0.775j*(PauliX(wires=[0]) @ PauliY(wires=[1])))
     """
-    h = qml.jordan_wigner(o_ferm, ps=True)
+    h = qml.jordan_wigner(o_ferm, ps=True, tol=cutoff)
     h.simplify(tol=cutoff)
 
     if active_new_opmath():


Test patch:
diff --git a/tests/fermi/test_fermi_mapping.py b/tests/fermi/test_fermi_mapping.py
index 21f218eb4..ddf12ece5 100644
--- a/tests/fermi/test_fermi_mapping.py
+++ b/tests/fermi/test_fermi_mapping.py
@@ -654,3 +654,23 @@ def test_providing_wire_map_fermi_word_to_ps(wire_map, ops):
     op.simplify()
 
     assert ps == op
+
+
+fs1 = FermiSentence({fw1: 1})
+
+
+@pytest.mark.parametrize(
+    "fermi_op, qubit_op_data, tol",
+    (
+        (fw1, (-0.25j, (0.25 + 0j), (0.25 + 0j), 0.25j), None),
+        (fw1, (-0.25j, 0.25, 0.25, 0.25j), 0.0),
+        (fw1, (-0.25j, 0.25, 0.25, 0.25j), 1.0e-12),
+        (fs1, (-0.25j, (0.25 + 0j), (0.25 + 0j), 0.25j), None),
+        (fs1, (-0.25j, 0.25, 0.25, 0.25j), 0.0),
+        (fs1, (-0.25j, 0.25, 0.25, 0.25j), 1.0e-12),
+    ),
+)
+def test_jordan_wigner_tolerance(fermi_op, qubit_op_data, tol):
+    """Test that jordan_wigner properly removes negligible imaginary components"""
+    op = jordan_wigner(fermi_op, tol=tol)
+    assert isinstance(op.data[1], type(qubit_op_data[1]))
diff --git a/tests/qchem/test_dipole.py b/tests/qchem/test_dipole.py
index 23554cf35..e19db8fe3 100644
--- a/tests/qchem/test_dipole.py
+++ b/tests/qchem/test_dipole.py
@@ -285,9 +285,7 @@ def test_gradient_expvalD():
 
     mol = qchem.Molecule(symbols, geometry, charge=1, alpha=alpha)
     args = [mol.alpha]
-    # TODO: `d_qubit[0]` has coeff dtype complex, but is actually a real-valued Hamiltonian
-    # default.qubit.legacy casts Hamiltonian expectations to real, but default.qubit does not
-    dev = qml.device("default.qubit.legacy", wires=6)
+    dev = qml.device("default.qubit", wires=6)
 
     def dipole(mol):
         @qml.qnode(dev)
diff --git a/tests/qchem/test_hamiltonians.py b/tests/qchem/test_hamiltonians.py
index 78caa5bf6..c6c9f3c09 100644
--- a/tests/qchem/test_hamiltonians.py
+++ b/tests/qchem/test_hamiltonians.py
@@ -284,7 +284,7 @@ def test_gradient_expvalH():
 
     mol = qchem.Molecule(symbols, geometry, alpha=alpha)
     args = [alpha]
-    dev = qml.device("default.qubit.legacy", wires=4)
+    dev = qml.device("default.qubit", wires=4)
 
     def energy(mol):
         @qml.qnode(dev)
@@ -336,7 +336,7 @@ class TestJax:
 
         mol = qchem.Molecule(symbols, geometry, alpha=alpha)
         args = [jax.numpy.array(alpha)]
-        dev = qml.device("default.qubit.legacy", wires=4)
+        dev = qml.device("default.qubit", wires=4)
 
         def energy(mol):
             @qml.qnode(dev, interface="jax")