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 RZ and PhaseShift gates are not supported, and the pass raises an error if it encounters them. Decompose them into single-qubit rotations first, for example with graph_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 to 1e-15 are supported. With "deterministic", each rotation has operator norm error at most epsilon, and therefore diamond norm error at most 2 * epsilon. With "mixed" and "mixed-fallback", the channel averaged over samples has diamond norm error at most 2 * 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:

QNode

Note

The circuits generated from this pass with ppr_basis=True are executable on devices that support PauliRot, and PauliMeasure for method="mixed-fallback", such as lightning.qubit, lightning.kokkos, and lightning.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" or method="mixed-fallback", the accuracy guarantee holds on average over samples, and each sampled sequence has an error of order \(\sqrt{\epsilon}\). By default, and with mcm_method="single-branch-statistics", a call samples one sequence per rotation, and one ancilla measurement outcome for "mixed-fallback", shared by all its shots. With mcm_method="one-shot", every shot samples its own. Samples are drawn from the runtime random number generator seeded by the seed argument of qjit(), 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, as pennylane.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
    . . .
}