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.X(0)), qml.expval(qml.Z(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.X(0)), qml.expval(qml.Z(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.X(0)), qml.expval(qml.Z(0)), qml.expval(qml.Y(1)), qml.expval(qml.Z(0) @ qml.Z(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]
Usage Details
Internally, this function works with tapes. We can create a tape with non-commuting observables:
measurements = [qml.expval(qml.Z(0)), qml.expval(qml.Y(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(Z(0))], [expval(Y(0))]]
The processing function becomes important when creating the commuting groups as the order of the inputs has been modified:
measurements = [ qml.expval(qml.Z(0) @ qml.Z(1)), qml.expval(qml.X(0) @ qml.X(1)), qml.expval(qml.Z(0)), qml.expval(qml.X(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]]
andprocessing_fn
makes sure that the final output is of the same shape and ordering:>>> processing_fn([t.measurements for t in tapes]) (expval(Z(0) @ Z(1)), expval(X(0) @ X(1)), expval(Z(0)), expval(X(0)))
Measurements that accept both observables and
wires
so that e.g.qml.counts
,qml.probs
andqml.sample
can also be used. When initialized using onlywires
, these measurements are interpreted as measuring with respect to the observableqml.Z(wires[0])@qml.Z(wires[1])@...@qml.Z(wires[len(wires)-1])
measurements = [ qml.expval(qml.X(0)), qml.probs(wires=[1]), qml.probs(wires=[0, 1]) ] tape = qml.tape.QuantumTape(measurements=measurements) tapes, processing_fn = qml.transforms.split_non_commuting(tape)
This results in two tapes, each with commuting measurements:
>>> [t.measurements for t in tapes] [[expval(X(0)), probs(wires=[1])], [probs(wires=[0, 1])]]