catalyst.passes.gridsynth¶
- gridsynth(qnode, epsilon=0.0001, ppr_basis=False, method='deterministic')¶
A quantum compilation pass to discretize single-qubit RZ and PhaseShift gates into the Clifford+T basis or the PPR basis using the Ross-Selinger Gridsynth algorithm. Reference: https://arxiv.org/abs/1403.2975
Note
The actual discretization is only performed during execution time.
Note
Controlled
RZandPhaseShiftgates are not supported, and the pass raises an error if it encounters them. Decompose them into single-qubit rotations first, for example withgraph_decomposition().- Parameters:
qnode (QNode) – the QNode to apply the gridsynth compiler pass to
epsilon (float) – The accuracy per rotation gate. Defaults to
1e-4. Values down to1e-15are supported. With"deterministic", each rotation has operator norm error at mostepsilon, and therefore diamond norm error at most2 * epsilon. With"mixed"and"mixed-fallback", the channel averaged over samples has diamond norm error at most2 * epsilon.ppr_basis (bool) – If true, decompose directly to Pauli Product Rotations (PPRs) in PBC dialect. Defaults to
Falsemethod (str) – The synthesis method.
"deterministic"(default) applies a single Ross-Selinger gate sequence per rotation."mixed"applies, per execution, one of two twirled sequences that under- and over-rotate the target, which roughly halves the T-count at the same accuracy. This is the mixed diagonal approximation of Kliuchnikov et al., Section 3.4 (Proposition 3.13), in arXiv:2203.10064."mixed-fallback"is the mixed fallback approximation of Section 3.5 (Proposition 3.18), which roughly halves the T-count of"mixed"at the cost of one temporary ancilla qubit per rotation.
- Returns:
Note
The circuit generated from this pass with
ppr_basis=Trueare currently only executable on thelightning.qubitdevice with program enabled.Note
With
method="mixed", each rotation applies the under- or the over-rotating sequence, conjugated as \(\sigma U \sigma^\dagger\) with \(\sigma\) drawn uniformly from \(\{I, S, Z, S^\dagger\}\). This gives eight possible sequences per rotation. The twirl leaves the target rotation unchanged, since it commutes with \(\sigma\).With
method="mixed"ormethod="mixed-fallback", the accuracy guarantee holds on average over samples, and each sampled sequence has an error of order \(\sqrt{\epsilon}\). By default, and withmcm_method="single-branch-statistics", a call samples one sequence per rotation, and one ancilla measurement outcome for"mixed-fallback", shared by all its shots. Withmcm_method="one-shot", every shot samples its own. Samples are drawn from the runtime random number generator seeded by theseedargument ofqjit(), so with a fixed seed every call uses the same gate sequences.Note
With
method="mixed-fallback", each rotation uses a temporary ancilla qubit and a mid-circuit measurement, aspennylane.transforms.ppr_to_ppm()does. Resource estimates count every allocation, so they report one additional wire per rotation.Example
In this example the RZ gate will be converted into a new function, which calls the discretization at execution time.
import pennylane as qp from catalyst import qjit from catalyst.passes import gridsynth pipe = [("pipe", ["quantum-compilation-stage"])] @qjit(pipelines=pipe, target="mlir") @gridsynth @qp.qnode(qp.device("null.qubit", wires=1)) def circuit(): qp.RZ(x, wires=0) return qp.probs() >>> print(circuit.mlir_opt)
Example MLIR Representation:
. . . func.func private @rs_decomposition_get_phase(f64, f64, i1) -> f64 func.func private @rs_decomposition_get_gates(memref<?xindex>, f64, f64, i1) func.func private @rs_decomposition_get_size(f64, f64, i1) -> index func.func private @__catalyst_decompose_RZ_0(%arg0: !quantum.bit, %arg1: f64) -> !quantum.bit { . . . %2 = scf.for %arg2 = %c0 to %0 step %c1 iter_args(%arg3 = %arg0) -> (!quantum.bit) { %3 = memref.load %alloc[%arg2] : memref<?xindex> %4 = scf.index_switch %3 -> !quantum.bit case 0 { %out_qubits = quantum.custom "T"() %arg3 : !quantum.bit scf.yield %out_qubits : !quantum.bit } case 1 { %out_qubits = quantum.custom "Hadamard"() %arg3 : !quantum.bit %out_qubits_0 = quantum.custom "T"() %out_qubits : !quantum.bit scf.yield %out_qubits_0 : !quantum.bit } case 2 { %out_qubits = quantum.custom "S"() %arg3 : !quantum.bit %out_qubits_0 = quantum.custom "Hadamard"() %out_qubits : !quantum.bit %out_qubits_1 = quantum.custom "T"() %out_qubits_0 : !quantum.bit scf.yield %out_qubits_1 : !quantum.bit } . . . } } func.func public @circuit_0(%arg0: tensor<f64>) -> tensor<f64> attributes {diff_method = "adjoint", llvm.linkage = #llvm.linkage<internal>, qnode} { . . . %2 = call @__catalyst_decompose_RZ_0(%1, %extracted) : (!quantum.bit, f64) -> !quantum.bit . . . }