Splits a qnode measuring non-commuting observables into groups of commuting observables.


qnode (pennylane.QNode or QuantumTape) – quantum tape or QNode that contains a list of non-commuting observables to measure.


If a QNode is passed, it returns a QNode capable of handling non-commuting groups. If a tape is passed, returns a tuple containing a list of quantum tapes to be evaluated, and a function to be applied to these tape executions to restore the ordering of the inputs.

Return type

qnode (pennylane.QNode) or tuple[List[QuantumTape], function]


This transform allows us to transform a QNode that measures non-commuting observables to multiple circuit executions with qubit-wise commuting groups:

dev = qml.device("default.qubit", wires=1)

def circuit(x):
    return [qml.expval(qml.PauliX(0)), qml.expval(qml.PauliZ(0))]

Instead of decorating the QNode, we can also create a new function that yields the same result in the following way:

def circuit(x):
    return [qml.expval(qml.PauliX(0)), qml.expval(qml.PauliZ(0))]

circuit = qml.transforms.split_non_commuting(circuit)

Internally, the QNode is split into groups of commuting observables when executed:

>>> print(qml.draw(circuit)(0.5))
0: ──RX(0.50)─┤  <X>
0: ──RX(0.50)─┤  <Z>

Note that while internally multiple QNodes are created, the end result has the same ordering as the user provides in the return statement. Here is a more involved example where we can see the different ordering at the execution level but restoring the original ordering in the output:

def circuit0(x):
    qml.RY(x[0], wires=0)
    qml.RX(x[1], wires=0)
    return [qml.expval(qml.PauliX(0)),
            qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)),

Drawing this QNode unveils the separate executions in the background

>>> print(qml.draw(circuit0)([np.pi/4, np.pi/4]))
0: ──RY(0.79)──RX(0.79)─┤  <X>
1: ─────────────────────┤  <Y>
0: ──RY(0.79)──RX(0.79)─┤  <Z> ╭<Z@Z>
1: ─────────────────────┤      ╰<Z@Z>

Yet, executing it returns the original ordering of the expectation values. The outputs correspond to \((\langle \sigma_x^0 \rangle, \langle \sigma_z^0 \rangle, \langle \sigma_y^1 \rangle, \langle \sigma_z^0\sigma_z^1 \rangle)\).

>>> circuit0([np.pi/4, np.pi/4])
tensor([0.70710678, 0.5       , 0.        , 0.5       ], requires_grad=True)

Internally, this function works with tapes. We can create a tape with non-commuting observables:

with qml.tape.QuantumTape() as tape:

tapes, processing_fn = qml.transforms.split_non_commuting(tape)

Now tapes is a list of two tapes, each for one of the non-commuting terms:

>>> [t.observables for t in tapes]
[[expval(PauliZ(wires=[0]))], [expval(PauliY(wires=[0]))]]

The processing function becomes important when creating the commuting groups as the order of the inputs has been modified:

with qml.tape.QuantumTape() as tape:
    qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
    qml.expval(qml.PauliX(0) @ qml.PauliX(1))

tapes, processing_fn = qml.transforms.split_non_commuting(tape)

In this example, the groupings are group_coeffs = [[0,2], [1,3]] and processing_fn makes sure that the final output is of the same shape and ordering:

>>> processing_fn(tapes)
tensor([tensor(expval(PauliZ(wires=[0]) @ PauliZ(wires=[1])), dtype=object, requires_grad=True),
    tensor(expval(PauliX(wires=[0]) @ PauliX(wires=[1])), dtype=object, requires_grad=True),
    tensor(expval(PauliZ(wires=[0])), dtype=object, requires_grad=True),
    tensor(expval(PauliX(wires=[0])), dtype=object, requires_grad=True)],
dtype=object, requires_grad=True)