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 PBC dialect. Defaults to False

  • 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 circuit generated from this pass with ppr_basis=True are currently only executable on the lightning.qubit device 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" 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
    . . .
}