Getting started with guppy-algorithms

Download Notebook - getting_started.ipynb

This notebook introduces guppy techniques used throughout this repository: circuit building, generic registers, higher-order functions, structs, and protocols. We build a small quantum program, then turn its operations into reusable components. You only need basic Python and familiarity with qubits and gates.

By the end, you will be able to:

  • Build, compile, and simulate a circuit.

  • Write functions for quantum registers of different sizes.

  • Pass operations into higher-order functions.

  • Use structs to bundle registers or store component functions.

  • Use a protocol to accept interchangeable structs.

  • Choose a library implementation in Python and check its output.

Run the cells from top to bottom. The examples use small registers so you can focus on how the code fits together.

1. Set up your environment

From a source checkout, install the notebook and simulation dependencies:

uv sync --extra dev-dependencies

Open this notebook and select the checkout’s Python environment. The project requires Python 3.12 or newer.

Python assembles and runs experiments; functions decorated with @guppy describe the quantum program that will be compiled.

import numpy as np

from guppylang import guppy
from guppylang.std.builtins import Function, array, bool, nat, output, owned
from guppylang.std.debug import state_output
from guppylang.std.quantum import (
    collect_measurements, cx, discard_array, h, measure_array, qubit,
)

from guppyalgos.utils import qarray, transversal

2. Build your first circuit

Prepare a Bell pair:

\[ |00\rangle \xrightarrow{H\otimes I} \frac{|00\rangle+|10\rangle}{\sqrt{2}} \xrightarrow{\mathrm{CX}} \frac{|00\rangle+|11\rangle}{\sqrt{2}}. \]
  • qarray(2) allocates two qubits in \(|00\rangle\).

  • h creates a superposition; cx correlates the second qubit with the first.

  • Measurement consumes the qubits and produces classical results.

  • output records those results under a name we can inspect in Python.

@guppy
def bell_experiment() -> None:
    qreg = qarray(2)
    h(qreg[0])
    cx(qreg[0], qreg[1])
    output("bits", collect_measurements(measure_array(qreg)))


bell_experiment.check()
package = bell_experiment.compile()
print("Circuit checked and compiled.")

Compile, then simulate

check() validates types and qubit ownership. compile() produces a HUGR package. emulator(...) builds an executable simulation of the program.

Each shot is one execution. Bell-pair measurements should contain only 00 and 11; their counts fluctuate around an equal split. A seed makes the experiment reproducible.

bell_results = (
    bell_experiment.emulator(n_qubits=2)
    .with_seed(42)
    .with_shots(100)
    .run()
)
print(bell_results.collated_counts())

3. Reuse functions across register sizes

A primitive usually borrows its registers, applies gates, and leaves them available to its caller. A helper that measures a register takes ownership because the qubits are consumed.

  • n: nat is a compile-time register size, inferred from the argument.

  • array[qubit, n] makes that size part of the type.

  • @ owned explicitly transfers ownership to the measuring helper.

  • transversal applies a gate to each qubit, or to corresponding pairs of qubits.

@guppy
def prepare_plus[n: nat](qreg: array[qubit, n]) -> None:
    transversal(h, qreg)


@guppy
def entangle_pairs[n: nat](
    left_qreg: array[qubit, n], right_qreg: array[qubit, n],
) -> None:
    transversal(cx, left_qreg, right_qreg)


@guppy
def read_register[n: nat](
    qreg: array[qubit, n] @ owned,
) -> array[bool, n]:
    return collect_measurements(measure_array(qreg))


@guppy
def register_experiment() -> None:
    left_qreg = qarray(2)
    right_qreg = qarray(2)
    prepare_plus(left_qreg)
    entangle_pairs(left_qreg, right_qreg)
    output("left", read_register(left_qreg))
    output("right", read_register(right_qreg))


print(register_experiment.emulator(4).with_seed(42).with_shots(10).run().collated_counts())

The two registers now share \(n\) Bell pairs:

\[ |0\rangle_L^{\otimes n}|0\rangle_R^{\otimes n} \longmapsto \frac{1}{\sqrt{2^n}}\sum_{x=0}^{2^n-1}|x\rangle_L|x\rangle_R. \]

Their measured bitstrings should agree shot by shot. Change both allocations to qarray(3) and the emulator capacity to six to try a different width.

4. Pass an operation into a function

A higher-order function accepts another function as an argument. This lets the surrounding algorithm specify where an operation runs while the supplied function specifies what it does.

Use guppy’s Function to describe the required signature.

@guppy
def apply_between_registers[n: nat](
    operation: Function[[array[qubit, n], array[qubit, n]], None],
    left_qreg: array[qubit, n],
    right_qreg: array[qubit, n],
) -> None:
    operation(left_qreg, right_qreg)


@guppy
def injected_experiment() -> None:
    left_qreg = qarray(2)
    right_qreg = qarray(2)
    prepare_plus(left_qreg)
    apply_between_registers(entangle_pairs[2], left_qreg, right_qreg)
    output("left", read_register(left_qreg))
    output("right", read_register(right_qreg))


injected_experiment.check()
print("Injected operation has the required signature.")

The explicit [2] specializes the function for two-qubit registers. guppy checks that the operation and both arguments agree on their types.

This is the same pattern used to choose arithmetic implementations, controlled operations, and state-preparation routines.

5. Use structs in two ways

Store the pieces of an algorithm

A struct can also store component functions. Its compose method defines the order in which those components act.

Here the first component prepares the left register and the second couples it to the right register. The repository uses this pattern in larger constructions such as LCU block encodings and QSVT.

@guppy.struct
class PairPreparation[n: nat]:
    prepare: Function[[array[qubit, n]], None]
    couple: Function[[array[qubit, n], array[qubit, n]], None]

    @guppy
    def compose(
        self, left_qreg: array[qubit, n], right_qreg: array[qubit, n],
    ) -> None:
        self.prepare(left_qreg)
        self.couple(left_qreg, right_qreg)


@guppy
def composed_experiment() -> None:
    left_qreg = qarray(2)
    right_qreg = qarray(2)
    preparation = PairPreparation[2](prepare_plus[2], entangle_pairs[2])
    preparation.compose(left_qreg, right_qreg)
    output("left", read_register(left_qreg))
    output("right", read_register(right_qreg))


composed_experiment.check()
print("Composed preparation checked.")

6. Accept interchangeable structs with a protocol

A function signature describes one callable. A protocol describes the methods a component must provide.

Why use a protocol?

As an algorithm grows, you may want to swap a component without rewriting the algorithm that uses it. For example, two arithmetic implementations might offer the same operation but trade circuit depth for extra work qubits.

  • Keep the algorithm independent of a particular struct: it asks for an apply method with a specified signature, rather than naming one implementation.

  • Let each implementation carry its own configuration: a struct can store component functions or parameters behind the same interface.

  • Check compatibility before execution: guppy checks that the supplied struct provides the required method and register types when the program is compiled.

Passing a Function is enough when you only need to replace one callable. A protocol becomes useful when the replaceable component is a struct with its own fields or several related methods.

A protocol checks the interface, not the mathematics: two implementations can have matching signatures but different behavior. Tests are still needed to check that each implements the intended operation.

A small example

The protocol below requires an apply method acting on two equal-sized registers. Two different structs satisfy it: one stores a function; the other implements the gates directly. Neither needs to inherit from the protocol.

@guppy.protocol
class TwoRegisterOperation[n: nat]:
    @guppy.require
    def apply(
        self, left_qreg: array[qubit, n], right_qreg: array[qubit, n],
    ) -> None:
        ...


@guppy.struct
class FunctionLayer[n: nat]:
    operation: Function[[array[qubit, n], array[qubit, n]], None]

    @guppy
    def apply(
        self, left_qreg: array[qubit, n], right_qreg: array[qubit, n],
    ) -> None:
        self.operation(left_qreg, right_qreg)


@guppy.struct
class DirectLayer[n: nat]:
    @guppy
    def apply(
        self, left_qreg: array[qubit, n], right_qreg: array[qubit, n],
    ) -> None:
        for i in range(n):
            cx(left_qreg[i], right_qreg[i])


@guppy.struct
class RegisterAlgorithm[n: nat, Operation: TwoRegisterOperation[n]]:
    operation: Operation

    @guppy
    def compose(
        self, left_qreg: array[qubit, n], right_qreg: array[qubit, n],
    ) -> None:
        prepare_plus(left_qreg)
        self.operation.apply(left_qreg, right_qreg)

The algorithm depends on the protocol’s interface. It accepts either concrete implementation without changing its compose method.

The two experiments below should produce the same Bell-pair statistics.

@guppy
def function_layer_experiment() -> None:
    left_qreg = qarray(2)
    right_qreg = qarray(2)
    algorithm = RegisterAlgorithm(FunctionLayer[2](entangle_pairs[2]))
    algorithm.compose(left_qreg, right_qreg)
    output("left", read_register(left_qreg))
    output("right", read_register(right_qreg))


@guppy
def direct_layer_experiment() -> None:
    left_qreg = qarray(2)
    right_qreg = qarray(2)
    algorithm = RegisterAlgorithm(DirectLayer[2]())
    algorithm.compose(left_qreg, right_qreg)
    output("left", read_register(left_qreg))
    output("right", read_register(right_qreg))


for experiment in (function_layer_experiment, direct_layer_experiment):
    result = experiment.emulator(4).with_seed(42).with_shots(10).run()
    print(result.collated_counts())

7. Choose a real library component in Python

Library factories run in Python and return guppy functions that can be used in a compiled program. Here, uniform_state(4) prepares a uniform superposition over four basis states:

\[ |\psi\rangle=\frac{1}{2}(|00\rangle+|01\rangle+|10\rangle+|11\rangle). \]

The factory argument counts basis states, so this example needs two qubits. The distinction is useful throughout the repository: Python chooses an implementation and its parameters; guppy describes its use on quantum registers.

from guppyalgos.primitives.state_preparation import uniform_state

uniform = uniform_state(4)


@guppy
def library_experiment() -> None:
    qreg = qarray(2)
    uniform(qreg)
    state_output("result_state", qreg)
    discard_array(qreg)


library_experiment.check()
print("Library component checked.")

8. Check amplitudes, not just measured bits

During simulation, state_output records a state snapshot before the qubits are discarded. The repository’s get_statevector helper runs the program and returns the amplitudes recorded under "result_state" as a NumPy array. Unlike measurements, this lets you inspect relative phases.

Compare the prepared state against its expected amplitudes, allowing one overall global phase. A relative sign error would still fail this check.

from guppyalgos.tests.helpers import get_statevector

actual = get_statevector(library_experiment, n_qubits=2)
expected = np.ones(4, dtype=complex) / 2

# Remove only an overall phase; preserve amplitudes and relative phases.
phase = actual[0] / abs(actual[0])
np.testing.assert_allclose(actual / phase, expected, atol=1e-8)
print("Uniform-state amplitudes match.")

Where to go next

  • Explore generic register bundles and multi-box wiring in abstract composition.

  • Check complete operations, post-selection, and retries in statevector testing.

  • Read the user guide for library structure and applications such as arithmetic, phase estimation, and Hamiltonian simulation.

Try extending one piece at a time: change the register width, replace a supplied function, or add another struct that satisfies the protocol. Keep the small statevector checks alongside your experiments.