Optimizing state overlap / multi-qubit qml.expval.Hermitian()

Hi @bencbartlett! Warning, this is a bit of a long post :slight_smile:

Since PennyLane treats hardware as a first class citizen when creating QNodes, we depend on the targeted device or plugin implementing the requested operations, and as such we’re a bit limited in the expectation values we can return.

We can be creative though if we want — for example, the PennyLane-Forest plugin allows for any one-qubit expectation value (even though pyQuil only supports Pauli-Z measurements!) through some nifty change of basis tricks.

In this case, there is no reason why we can’t support multi-mode Hermitian observables — to test it out, I made the following local changes to my PennyLane installation:

diff --git a/pennylane/expval/qubit.py b/pennylane/expval/qubit.py
index a47dc1e..a84029b 100644
--- a/pennylane/expval/qubit.py
+++ b/pennylane/expval/qubit.py
@@ -156,7 +156,7 @@ class Hermitian(Expectation):
         A (array): square hermitian matrix.
         wires (Sequence[int] or int): the wire the operation acts on
     """
-    num_wires = 1
+    num_wires = 0
     num_params = 1
     par_domain = 'A'
     grad_method = 'F'
diff --git a/pennylane/plugins/default_qubit.py b/pennylane/plugins/default_qubit.py
index 993e918..81803e7 100644
--- a/pennylane/plugins/default_qubit.py
+++ b/pennylane/plugins/default_qubit.py
@@ -367,10 +367,13 @@ class DefaultQubit(Device):
         Returns:
           float: expectation value :math:`\expect{A} = \bra{\psi}A\ket{\psi}`
         """
-        if A.shape != (2, 2):
-            raise ValueError('2x2 matrix required.')
+        if A.shape == (2, 2):
+            A = self.expand_one(A, wires)
+        elif A.shape == (4, 4):
+            A = self.expand_two(A, wires)
+        else:
+            raise ValueError('Only 1 or 2 wire expectation values supported.')
 
-        A = self.expand_one(A, wires)
         expectation = np.vdot(self._state, A @ self._state)
 
         if np.abs(expectation.imag) > tolerance:

That is:

  1. Modified pennylane/expval/qubit.py so that the Hermitian observable can act on any number of wires, and
  2. Modified the default.qubit simulator to implement two-mode observables.

And everything works! I tried your example (slightly modified):

import pennylane as qml
from pennylane import numpy as np

dev = qml.device('default.qubit', wires=2)

@qml.qnode(dev)
def circuit(x, target_observable=None):
    qml.RX(x[0], wires=0)
    qml.RY(x[1], wires=0)
    qml.RZ(x[2], wires=0)
    qml.CNOT(wires=[0, 1])
    return qml.expval.Hermitian(target_observable, wires=[0, 1])

target_state = 1/np.sqrt(2) * np.array([1,0,0,1])
target_herm_op = np.outer(target_state.conj(), target_state)
weights = np.array([0.5, 0.1, 0.2])

>>> circuit(weights, target_observable=target_herm_op)
0.5905564040875388

Note that QNodes are a restricted subset of Python functions, in that they must only consist of quantum operations, one per line, finishing with an expectation value. This mimics the behaviour of quantum hardware, and requires that all classical processing be done outside the QNode. In order to pass training data to the QNode, we can instead use keyword arguments, which are never treated as differentiable by PennyLane.


One final note: the default.qubit simulator is more of a reference plugin, and provided simply as a guide for plugin developers. As a result, it is not optimized for high performance, and only supports limited features (i.e., maximum of two-qubit operations/expectations, no mixed state simulations, etc.).

So, I would suggest using either PennyLane-Forest or PennyLane-ProjectQ for production code.

2 Likes