Transversal gates

Download Notebook - transversal.ipynb

transversal applies an operation across a quantum register, or between corresponding qubits in two registers. It keeps repeated gate application concise and makes the intended register wiring clear.

This is a higher-order function: you supply the gate itself, such as x or cx, as an argument. Run the cells in order to explore both forms.

from guppylang import guppy
from guppylang.std.builtins import array, output
from guppylang.std.quantum import collect_measurements, cx, measure_array, x

from guppyalgos.utils import transversal, qarray

n_qubits = 5

Apply a one-qubit gate

For a one-qubit operation \(U\), applying it to every qubit of an \(n\)-qubit register gives

\[ U_{\mathrm{all}}=U^{\otimes n}. \]

The call transversal(x, qreg) applies X to each qubit. Starting from zero, every qubit becomes one:

\[ |0\rangle^{\otimes n}\xrightarrow{X^{\otimes n}}|1\rangle^{\otimes n}. \]

The measured result below is therefore always 11111.

@guppy
def single_qubit_example() -> None:
    qreg = qarray(n_qubits)
    transversal(x, qreg)
    output("bits", collect_measurements(measure_array(qreg)))


print(single_qubit_example.emulator(n_qubits).stabilizer_sim().run().collated_counts())

Apply a two-qubit gate

With two registers, transversal applies the gate to matching indices:

\[ V_{\mathrm{pairs}}=\prod_{i=0}^{n-1}V_{c_i,t_i}. \]
  • Both registers must have the same size.

  • For cx, the first register supplies the controls and the second supplies the targets.

  • Each control interacts with its matching target, rather than every target.

On computational-basis inputs, the action is

\[ |c\rangle_C|t\rangle_T \longmapsto |c\rangle_C|t\oplus c\rangle_T, \]

where \(\oplus\) is bitwise XOR. Here the controls are prepared as all ones, so the initially zero targets also become all ones.

@guppy
def two_qubit_example() -> None:
    control_qreg = qarray(n_qubits)
    target_qreg = qarray(n_qubits)
    transversal(x, control_qreg)
    transversal(cx, control_qreg, target_qreg)
    output("controls", collect_measurements(measure_array(control_qreg)))
    output("targets", collect_measurements(measure_array(target_qreg)))


print(two_qubit_example.emulator(2 * n_qubits).stabilizer_sim().run().collated_counts())

One name, six overloads

The library registers six implementations with @guppy.overload. At compile time, guppy resolves a call from the gate’s signature and the supplied arguments.

Scope

One-qubit gate

Two-qubit gate

Every index

transversal(x, qreg)

transversal(cx, control_qreg, target_qreg)

Exclude indices

transversal(x, qreg, array(1, 3))

transversal(cx, control_qreg, target_qreg, array(1, 3))

Index range

transversal(x, qreg, 2, 4)

transversal(cx, control_qreg, target_qreg, 2, 4)

All indices are zero-based. The register arguments are borrowed, so they remain available for subsequent gates or measurement.

Exclude selected indices

The array argument contains indices to skip. For a five-qubit register, array(1, 3) applies the gate at indices 0, 2, and 4. For a two-qubit gate, it skips the corresponding pairs.

Apply only within a range

The range includes its start and excludes its end: 2, 4 applies only at indices 2 and 3. Keep the bounds within the register width.

The complete example below uses every overload in sequence. Each operation acts on the state left by the preceding operation.

@guppy
def main() -> None:
    control_qreg = qarray(n_qubits)
    target_qreg = qarray(n_qubits)

    # Apply at every index.
    transversal(x, control_qreg)
    transversal(cx, control_qreg, target_qreg)

    # Skip indices 1 and 3; swap control and target roles for this CX layer.
    transversal(x, target_qreg, array(1, 3))
    transversal(cx, target_qreg, control_qreg, array(1, 3))

    # Apply only at indices 2 and 3.
    transversal(x, control_qreg, 2, 4)
    transversal(cx, control_qreg, target_qreg, 2, 4)

    output("controls", collect_measurements(measure_array(control_qreg)))
    output("targets", collect_measurements(measure_array(target_qreg)))


print(main.emulator(2 * n_qubits).stabilizer_sim().with_seed(2).run().collated_counts())

Read the result

All gates in this example act deterministically on computational-basis states. After the final layer:

  • The control qubits at indices 0, 1, and 4 are one; indices 2 and 3 are zero.

  • Target indices 1 and 3 are one; the other target qubits are zero.

The explicit output calls record the measured bitstrings for inspection. The stabilizer simulator is sufficient here because X and CX are Clifford gates.

Try replacing X with H in the first example to prepare a superposition. Applying CX from that register to a zero register then prepares one Bell pair per matching index.