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 the PBC dialect, with one \(\pi/8\) PPR per T gate.
method (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 circuits generated from this pass with
ppr_basis=Trueare executable on devices that supportPauliRot, andPauliMeasureformethod="mixed-fallback", such aslightning.qubit,lightning.kokkos, andlightning.gpu.Note
With
ppr_basis=True, the Clifford gates of each Clifford+T sequence are moved past its T gates. Each T gate thereby becomes a \(\pi/8\) PPR \(\exp(\mp i\frac{\pi}{8}P)\) about a Pauli \(P \in \{X, Y, Z\}\), so a sequence has as many \(\pi/8\) PPRs as its Clifford+T counterpart has T gates, and neighboring \(\pi/8\) PPRs rotate about different Paulis. The signs of the \(\pi/8\) PPRs are chosen such that no Clifford gates remain, except in short sequences with few T gates, which may end with up to two \(\pi/4\) or \(\pi/2\) PPRs. About a third of the \(\pi/8\) PPRs become \(Y\)-basis PPRs in this process.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 . . . }