Core concepts

Most code in the library is built from four ideas: quantum registers, functions, higher-order functions, and structs.

Quantum registers with qarray

A quantum register is usually an array of qubits. The qarray helper allocates that quantum register inside a Guppy function:

\[ \operatorname{qarray}(n) \longrightarrow |0\rangle^{\otimes n}. \]
from guppylang import guppy
from guppylang.std.quantum import discard_array

from guppyalgos.utils import qarray


@guppy
def allocate_register() -> None:
    qreg = qarray(4)
    discard_array(qreg)
  • qarray(4) returns an array[qubit, 4] containing four fresh qubits.

  • The size is known at compile time and is part of the quantum register’s type.

  • Qubits are linear resources: the program must use, measure, return, or discard every allocated qubit.

Functions on quantum registers

Algorithm primitives are Guppy functions whose signatures state which quantum registers they accept. A generic size allows one definition to work for any quantum-register width:

\[ |0\rangle^{\otimes n} \xrightarrow{\mathtt{prepare\_plus}} H^{\otimes n}|0\rangle^{\otimes n} =|+\rangle^{\otimes n}. \]
from guppylang.std.builtins import array, bool, nat, owned
from guppylang.std.quantum import collect_measurements, cx, h, measure_array, qubit, rz

from guppyalgos.utils import transversal


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


@guppy
def measure_register[n: nat](
    qreg: array[qubit, n] @ owned,
) -> array[bool, n]:
    return collect_measurements(measure_array(qreg))
  • n is inferred from the quantum register supplied by the caller.

  • Guppy checks the quantum-register shape and qubit ownership at compile time.

  • Function arguments are borrowed by default, as in prepare_plus.

  • measure_register uses @ owned because measurement consumes the qubits; the measured quantum register cannot be used afterward.

  • Repository primitives use this style for operations such as state preparation, arithmetic, and Hamiltonian simulation.

Higher-order functions

A higher-order function accepts another function as a value. Function records the required Guppy signature, allowing an implementation to be replaced without changing the surrounding algorithm:

from guppylang.std.builtins import Function


@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)
  • The injected operation receives both complete quantum registers.

  • It decides how the two quantum registers interact; the wrapper only defines the required signature and forwards them.

  • An operation with an incompatible argument or return type is rejected at compile time.

  • Use Guppy’s Function, rather than typing.Callable, for function values inside Guppy code.

Generic quantum-register types

A generic quantum-register type stands for a complete quantum-register shape. The same function can then work with arrays, tuples, or structs:

@guppy
def apply_register_operation[Regs](
    operation: Function[[Regs], None],
    qregs: Regs,
) -> None:
    operation(qregs)

Regs can be a single qubit array:

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

It can also be a fixed tuple. The repository uses this shape, for example, for two-qubit Givens-rotation targets:

@guppy
def tuple_operation(qregs: tuple[qubit, qubit]) -> None:
    h(qregs[0])
    cx(qregs[0], qregs[1])

Starting from \(|00\rangle\), this tuple operation prepares a Bell pair: