Source code for pennylane.ftqc.decomposition
# Copyright 2025 Xanadu Quantum Technologies Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Contains functions to convert a PennyLane tape to the textbook MBQC formalism
"""
from functools import partial, singledispatch
import networkx as nx
from pennylane import math
from pennylane.decomposition import enabled_graph, register_resources
from pennylane.devices.preprocess import null_postprocessing
from pennylane.measurements import SampleMP, sample
from pennylane.ops import CNOT, CZ, RZ, GlobalPhase, H, Identity, Rot, S, X, Y, Z, cond
from pennylane.queuing import AnnotatedQueue
from pennylane.tape import QuantumScript
from pennylane.transforms import decompose, transform
from .conditional_measure import cond_measure
from .graph_state_preparation import make_graph_state
from .operations import RotXZX
from .parametric_midmeasure import measure_arbitrary_basis, measure_x, measure_y
from .utils import QubitMgr, parity
mbqc_gate_set = frozenset({CNOT, H, S, RotXZX, RZ, X, Y, Z, Identity, GlobalPhase})
@register_resources({RotXZX: 1})
def _rot_to_xzx(phi, theta, omega, wires, **__):
mat = Rot.compute_matrix(phi, theta, omega)
lam, theta, phi = math.decomposition.xzx_rotation_angles(mat)
RotXZX(lam, theta, phi, wires)
[docs]
@transform
def convert_to_mbqc_gateset(tape):
"""Converts a circuit expressed in arbitrary gates to the limited gate set that we can
convert to the textbook MBQC formalism"""
if not enabled_graph():
raise RuntimeError(
"Using `convert_to_mbqc_gateset` requires the graph-based decomposition"
" method. This can be toggled by calling `qml.decomposition.enable_graph()`"
)
tapes, fn = decompose(tape, gate_set=mbqc_gate_set, alt_decomps={Rot: [_rot_to_xzx]})
return tapes, fn
[docs]
@transform
def convert_to_mbqc_formalism(tape):
"""Convert a circuit to the textbook MBQC formalism based on the procedures outlined in
Raussendorf et al. 2003, https://doi.org/10.1103/PhysRevA.68.022312. The circuit must
be decomposed to the gate set {CNOT, H, S, RotXZX, RZ, X, Y, Z, Identity, GlobalPhase}
before applying the transform.
Note that this transform leaves all Paulis and Identities as physical gates, and applies
all byproduct operations online immediately after their respective measurement procedures."""
if len(tape.measurements) != 1 or not isinstance(tape.measurements[0], (SampleMP)):
raise NotImplementedError(
"Transforming to the MBQC formalism is not implemented for circuits where the "
"final measurements have not been converted to a single samples measurement"
)
mp = tape.measurements[0]
meas_wires = mp.wires if mp.wires else tape.wires
# we include 13 auxillary wires - the largest number needed is 13 (for CNOT)
num_qubits = len(tape.wires) + 13
q_mgr = QubitMgr(num_qubits=num_qubits, start_idx=0)
wire_map = {w: q_mgr.acquire_qubit() for w in tape.wires}
with AnnotatedQueue() as q:
for op in tape.operations:
if isinstance(op, GlobalPhase): # no wires
GlobalPhase(*op.data)
elif isinstance(op, CNOT): # two wires
ctrl, tgt = op.wires[0], op.wires[1]
wire_map[ctrl], wire_map[tgt], measurements = queue_cnot(
q_mgr, wire_map[ctrl], wire_map[tgt]
)
cnot_corrections(measurements)(wire_map[ctrl], wire_map[tgt])
else: # one wire
# pylint: disable=isinstance-second-argument-not-valid-type
if isinstance(op, (X, Y, Z, Identity)):
# else branch because Identity may not have wires
wire = wire_map[op.wires[0]] if op.wires else ()
op.__class__(wire)
else:
w = op.wires[0]
wire_map[w], measurements = queue_single_qubit_gate(
q_mgr, op, in_wire=wire_map[w]
)
queue_corrections(op, measurements)(wire_map[w])
temp_tape = QuantumScript.from_queue(q)
new_wires = [wire_map[w] for w in meas_wires]
new_tape = tape.copy(operations=temp_tape.operations, measurements=[sample(wires=new_wires)])
return (new_tape,), null_postprocessing
def queue_single_qubit_gate(q_mgr, op, in_wire):
"""Queue the resource state preparation, measurements and byproducts
to execute the operation in the MBQC formalism. This implementation
follows the procedures defined in Raussendorf et al. 2003,
https://doi.org/10.1103/PhysRevA.68.022312, see Fig. 2"""
graph_wires = q_mgr.acquire_qubits(4)
wires = [in_wire] + graph_wires
make_graph_state(nx.grid_graph((4,)), wires=graph_wires)
CZ([wires[0], wires[1]])
measurements = queue_measurements(op, wires)
# release input qubit and intermediate graph qubits
q_mgr.release_qubits(wires[0:-1])
return wires[-1], measurements
@singledispatch
def queue_measurements(op, wires):
"""Queue the measurements needed to execute the operation in the MBQC formalism"""
raise NotImplementedError(f"Received unsupported gate of type {op}")
@queue_measurements.register(RotXZX)
def _rot_measurements(op: RotXZX, wires):
"""Queue the measurements needed to execute RotXZX in the MBQC formalism"""
phi, theta, omega = op.data
m1 = measure_x(wires[0], reset=True)
m2 = cond_measure(
m1,
partial(measure_arbitrary_basis, angle=phi),
partial(measure_arbitrary_basis, angle=-phi),
)(plane="XY", wires=wires[1], reset=True)
m3 = cond_measure(
m2,
partial(measure_arbitrary_basis, angle=theta),
partial(measure_arbitrary_basis, angle=-theta),
)(plane="XY", wires=wires[2], reset=True)
m4 = cond_measure(
m1 ^ m3,
partial(measure_arbitrary_basis, angle=omega),
partial(measure_arbitrary_basis, angle=-omega),
)(plane="XY", wires=wires[3], reset=True)
return [m1, m2, m3, m4]
@queue_measurements.register(RZ)
def _rz_measurements(op: RZ, wires):
"""Queue the measurements needed to execute RZ in the MBQC formalism"""
angle = op.parameters[0]
m1 = measure_x(wires[0], reset=True)
m2 = measure_x(wires[1], reset=True)
m3 = cond_measure(
m2,
partial(measure_arbitrary_basis, angle=angle, reset=True),
partial(measure_arbitrary_basis, angle=-angle, reset=True),
)(plane="XY", wires=wires[2])
m4 = measure_x(wires[3], reset=True)
return [m1, m2, m3, m4]
@queue_measurements.register(H)
def _hadamard_measurements(op: H, wires):
"""Queue the measurements needed to execute Hadamard in the MBQC formalism"""
m1 = measure_x(wires[0], reset=True)
m2 = measure_y(wires[1], reset=True)
m3 = measure_y(wires[2], reset=True)
m4 = measure_y(wires[3], reset=True)
return [m1, m2, m3, m4]
@queue_measurements.register(S)
def _s_measurements(op: S, wires):
"""Queue the measurements needed to execute S in the MBQC formalism"""
m1 = measure_x(wires[0], reset=True)
m2 = measure_x(wires[1], reset=True)
m3 = measure_y(wires[2], reset=True)
m4 = measure_x(wires[3], reset=True)
return [m1, m2, m3, m4]
def queue_corrections(op, measurements):
"""Queue the byproduct corrections associated with the operation in the
MBQC formalism, based on the operation and the measurement results"""
x_corr, z_corr = _single_xz_corrections(op, *measurements)
def corrections_func(wire):
cond(z_corr, Z)(wire)
cond(x_corr, X)(wire)
return corrections_func
@singledispatch
def _single_xz_corrections(op, m1, m2, m3, m4):
"""Get the xz corrections based on the measurements. Returns a tuple with
two boolean elements, indicating the need for PauliX and PauliZ
corrections respectively."""
raise NotImplementedError(f"Received unsupported gate of type {op}")
@_single_xz_corrections.register(RotXZX)
@_single_xz_corrections.register(RZ)
def _rotation_corrections(op, m1, m2, m3, m4):
"""Get the xz corrections based on the measurements. Returns a tuple with
two boolean elements, indicating the need for PauliX and PauliZ
corrections respectively. Note that these corrections also apply in the
more specific rotation case, RZ = RotXZX(0, Z, 0)"""
return m2 ^ m4, m1 ^ m3
@_single_xz_corrections.register(H)
def _hadamard_corrections(op, m1, m2, m3, m4):
"""Get the xz corrections based on the measurements. Returns a tuple with
two boolean elements, indicating the need for PauliX and PauliZ
corrections respectively."""
return parity(m1, m3, m4), m2 ^ m3
@_single_xz_corrections.register(S)
def _s_corrections(op, m1, m2, m3, m4):
"""Get the xz corrections based on the measurements. Returns a tuple with
two boolean elements, indicating the need for PauliX and PauliZ
corrections respectively."""
return m2 ^ m4, parity(m1, m2, m3, 1)
def queue_cnot(q_mgr, ctrl_idx, target_idx):
"""Queue the resource state preparation, measurements and byproducts to execute
the operation in the MBQC formalism. This is the 15-qubit procedure from
Raussendorf et al. 2003, https://doi.org/10.1103/PhysRevA.68.022312, Fig. 2"""
graph_wires = q_mgr.acquire_qubits(13)
# Denote the index for the final output state
output_ctrl_idx = graph_wires[5]
output_target_idx = graph_wires[12]
# Prepare the state
make_graph_state(_generate_cnot_graph(), wires=graph_wires)
# entangle input and graph using first qubit
CZ([ctrl_idx, graph_wires[0]])
CZ([target_idx, graph_wires[7]])
measurements = cnot_measurements((ctrl_idx, target_idx, graph_wires))
q_mgr.release_qubit(ctrl_idx)
q_mgr.release_qubit(target_idx)
# We can now free all but the last qubit, which has become the new input_idx
q_mgr.release_qubits(graph_wires[0:5] + graph_wires[6:-1])
return output_ctrl_idx, output_target_idx, measurements
def cnot_measurements(wires):
"""Queue the measurements needed to execute CNOT in the MBQC formalism.
Numbering convention follows the procedure in Raussendorf et al. 2003,
https://doi.org/10.1103/PhysRevA.68.022312, see Fig. 2"""
ctrl_idx, target_idx, graph_wires = wires
m1 = measure_x(ctrl_idx, reset=True)
m2 = measure_y(graph_wires[0], reset=True)
m3 = measure_y(graph_wires[1], reset=True)
m4 = measure_y(graph_wires[2], reset=True)
m5 = measure_y(graph_wires[3], reset=True)
m6 = measure_y(graph_wires[4], reset=True)
m8 = measure_y(graph_wires[6], reset=True)
m9 = measure_x(target_idx, reset=True)
m10 = measure_x(graph_wires[7], reset=True)
m11 = measure_x(graph_wires[8], reset=True)
m12 = measure_y(graph_wires[9], reset=True)
m13 = measure_x(graph_wires[10], reset=True)
m14 = measure_x(graph_wires[11], reset=True)
return [m1, m2, m3, m4, m5, m6, m8, m9, m10, m11, m12, m13, m14]
def cnot_corrections(measurements):
"""Queue the byproduct corrections associated with the CNOT gate in
the MBQC formalism, based on measurement results"""
(x_cor_ctrl, z_cor_ctrl), (x_cor_tgt, z_cor_tgt) = _cnot_xz_corrections(measurements)
def correction_func(ctrl_wire, target_wire):
cond(z_cor_ctrl, Z)(ctrl_wire)
cond(x_cor_ctrl, X)(ctrl_wire)
cond(z_cor_tgt, Z)(target_wire)
cond(x_cor_tgt, X)(target_wire)
return correction_func
def _cnot_xz_corrections(measurements):
"""Get the xz corrections for the control and target wire based on the measurements.
Returns a list of two tuples indicating corrections for the control and target wires
respectively. For each tuple, the first element is a boolean indicating whether an
PauliX correction is needed, and the second element indicates whether a PauliZ
correction is needed."""
# Numbering convention follows the procedure in Raussendorf et al. 2003,
# https://doi.org/10.1103/PhysRevA.68.022312, Fig 2
m1, m2, m3, m4, m5, m6, m8, m9, m10, m11, m12, m13, m14 = measurements
# corrections on control
x_cor_ctrl = parity(m2, m3, m5, m6)
z_cor_ctrl = parity(m1, m3, m4, m5, m8, m9, m11, 1)
# corrections on target
x_cor_tgt = parity(m2, m3, m8, m10, m12, m14)
z_cor_tgt = parity(m9, m11, m13)
return [(x_cor_ctrl, z_cor_ctrl), (x_cor_tgt, z_cor_tgt)]
def _generate_cnot_graph():
"""Generate a graph for creating the resource state for a
CNOT gate. Raussendorf et al. 2003, Fig. 2a.
https://doi.org/10.1103/PhysRevA.68.022312"""
wires = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
g = nx.Graph()
g.add_nodes_from(wires)
g.add_edges_from(
[
(0, 1),
(1, 2),
(2, 3),
(3, 4),
(4, 5),
(2, 6),
(6, 9),
(7, 8),
(8, 9),
(9, 10),
(10, 11),
(11, 12),
]
)
return g
_modules/pennylane/ftqc/decomposition
Download Python script
Download Notebook
View on GitHub