qml.transforms.split_non_commuting

split_non_commuting(tape)[source]

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

Parameters

tape (QNode or QuantumTape or Callable) – A circuit that contains a list of non-commuting observables to measure.

Returns

The transformed circuit as described in qml.transform.

Return type

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

Example

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

@qml.transforms.split_non_commuting
@qml.qnode(dev)
def circuit(x):
    qml.RX(x,wires=0)
    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:

@qml.qnode(dev)
def circuit(x):
    qml.RX(x,wires=0)
    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:

@qml.transforms.split_non_commuting
@qml.qnode(dev)
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.expval(qml.PauliY(1)),
            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])
[0.7071067811865475, 0.49999999999999994, 0.0, 0.49999999999999994]

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

measurements = [qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliY(0))]
tape = qml.tape.QuantumTape(measurements=measurements)

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:

measurements = [
    qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)),
    qml.expval(qml.PauliX(0) @ qml.PauliX(1)),
    qml.expval(qml.PauliZ(0)),
    qml.expval(qml.PauliX(0))
]
tape = qml.tape.QuantumTape(measurements=measurements)

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([t.measurements for t in tapes])
(expval(PauliZ(wires=[0]) @ PauliZ(wires=[1])),
expval(PauliX(wires=[0]) @ PauliX(wires=[1])),
expval(PauliZ(wires=[0])),
expval(PauliX(wires=[0])))