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:

A Hadamard on the first qubit followed by a controlled X to the second prepares a Bell pair from two zero qubits.

For a named bundle of several quantum registers, Regs can be a Guppy struct:

@guppy.struct
class WorkAndTarget[n: nat]:
    work_qreg: array[qubit, n]
    target_qreg: array[qubit, n]


@guppy
def struct_operation[n: nat](qregs: WorkAndTarget[n]) -> None:
    transversal(h, qregs.work_qreg)
    transversal(cx, qregs.work_qreg, qregs.target_qreg)
  • The generic function does not need separate implementations for arrays, tuples, and structs.

  • Its Regs type is inferred from the concrete operation and argument at the call site.

  • Guppy checks that both agree on the entire shape.

Composed algorithms often use more than one generic quantum-register type. For example, an algorithm can keep preparation and target quantum registers distinct without fixing their sizes or layouts:

@guppy
def apply_select[PrepRegs, TargetRegs](
    select: Function[[PrepRegs, TargetRegs], None],
    prep_qreg: PrepRegs,
    target_qreg: TargetRegs,
) -> None:
    select(prep_qreg, target_qreg)
  • PrepRegs is the type of the quantum registers used to select an operation.

  • TargetRegs is the type acted on by that selected operation.

  • The caller’s concrete arguments determine both types.

  • Either type can be a qubit array, tuple, or struct containing several quantum registers.

  • Reusing the type names in the function signature guarantees that select accepts the same quantum-register shapes supplied to apply_select.

This separation lets one algorithm work with different quantum-register layouts and oracle implementations. The advanced register-composition example compares a single-register type with a two-register bundle.

Advanced register composition with generic types

Generic quantum-register types let a Guppy function connect component functions without fixing the shape of every quantum-register bundle. A struct can store those components and define their wiring once in a compose method, as LCU does.

Start with one qubit array in each quantum-register bundle. The first box shares R_1 with the second, so it is both an output of box_0 and an input of box_1:

Three quantum-register bundles, marked by slashes on their wires, pass through two boxes. Box 0 acts on quantum registers 0 and 1; box 1 acts on quantum registers 1 and 2.

from guppylang import guppy
from guppylang.std.angles import angle
from guppylang.std.builtins import Function, array, nat
from guppylang.std.quantum import qubit


@guppy.struct
class BoxComposition[Register0, Register1, Register2]:
    box_0: Function[[Register0, Register1], None]
    box_1: Function[[Register1, Register2, angle], None]

    @guppy
    def compose(
        self,
        qreg_0: Register0,
        qreg_1: Register1,
        qreg_2: Register2,
        theta: angle,
    ) -> None:
        self.box_0(qreg_0, qreg_1)
        self.box_1(qreg_1, qreg_2, theta)
  • The struct stores two component boxes with different function signatures.

  • Reusing Register1 in both signatures defines their shared, type-checked boundary.

  • A replacement box is compatible when its Function signature matches its position in the struct.

One quantum register in every bundle

In this first specialization, every generic type is one qubit array:

@guppy
def bell_transversal[n: nat](
    left_qreg: array[qubit, n], shared: array[qubit, n]
) -> None:
    transversal(h, left_qreg)
    transversal(cx, left_qreg, shared)


@guppy
def rz_cx_transversal[n: nat](
    shared: array[qubit, n], right_qreg: array[qubit, n], theta: angle
) -> None:
    for i in range(len(shared)):
        rz(shared[i], theta)
    transversal(cx, shared, right_qreg)


@guppy
def compose_arrays(
    qreg_0: array[qubit, 3], qreg_1: array[qubit, 3],
    qreg_2: array[qubit, 3], theta: angle,
) -> None:
    composition = BoxComposition(bell_transversal[3], rz_cx_transversal[3])
    composition.compose(qreg_0, qreg_1, qreg_2, theta)


compose_arrays.check()
  • Here, Register0, Register1, and Register2 are all inferred as array[qubit, 3].

  • The explicit [3] specializes each generic box before it is stored as a function value.

Two registers in the middle bundle

A generic type can instead be a struct containing several quantum registers. The middle bundle can therefore contain two arrays without changing BoxComposition:

@guppy.struct
class MiddleBundle[n: nat]:
    upper_qreg: array[qubit, n]
    lower_qreg: array[qubit, n]


@guppy
def bundled_box_0[n: nat](
    left_qreg: array[qubit, n], middle: MiddleBundle[n]
) -> None:
    transversal(h, left_qreg)
    transversal(cx, left_qreg, middle.upper_qreg)
    transversal(cx, left_qreg, middle.lower_qreg)


@guppy
def bundled_box_1[n: nat](
    middle: MiddleBundle[n], right_qreg: array[qubit, n], theta: angle
) -> None:
    for i in range(n):
        rz(middle.upper_qreg[i], theta)
        rz(middle.lower_qreg[i], theta)
    transversal(cx, middle.upper_qreg, right_qreg)
    transversal(cx, middle.lower_qreg, right_qreg)

Quantum-register wires are marked by slashes. Register 1 is a bundle of two quantum registers. Box 0 acts on quantum register 0 and both quantum registers in the middle bundle; box 1 acts on both quantum registers in the middle bundle and quantum register 2.

The struct is instantiated with boxes that accept the complete middle bundle:

@guppy
def compose_bundles(
    qreg_0: array[qubit, 3], middle: MiddleBundle[3],
    qreg_2: array[qubit, 3], theta: angle,
) -> None:
    composition = BoxComposition(bundled_box_0[3], bundled_box_1[3])
    composition.compose(qreg_0, middle, qreg_2, theta)


compose_bundles.check()
  • Register1 is now inferred as MiddleBundle[3]; the outer composer is unchanged.

  • Both boxes can access middle.upper and middle.lower.

  • Passing a box that expects one array alongside a box that expects MiddleBundle[3] fails at compile time because their Register1 types do not match.

  • Guppy does not support higher-rank polymorphic function values, so the boxes are specialized before being stored.

See the abstract construction notebook for both complete executable specializations.

Structs for composed algorithms

A higher-order function works well for one small injected operation. Use a Guppy struct when several oracles must be stored together, their quantum-register wiring is more involved, or construction should be separate from use.

For example, the repository’s LCU struct stores its three oracles when it is initialized, then exposes the block encoding through compose:

\[ U_A = \mathrm{PREPARE}^{\dagger}\, \mathrm{SELECT}\,\mathrm{PREPARE}, \qquad (\langle 0^a|\otimes I)U_A(|0^a\rangle\otimes I)=\frac{A}{\alpha}. \]
@guppy.struct
class LCU[PrepRegs, TargetRegs]:
    prepare: Function[[PrepRegs], None]
    select: Function[[PrepRegs, TargetRegs], None]
    unprepare: Function[[PrepRegs], None]

    @guppy
    def compose(self, prep_qreg: PrepRegs, target_qreg: TargetRegs) -> None:
        self.prepare(prep_qreg)
        self.select(prep_qreg, target_qreg)
        self.unprepare(prep_qreg)

An LCU block encoding. PREPARE creates a coefficient state on the preparation register, SELECT applies an operation to the target register controlled by that state, and PREPARE dagger uncomputes the preparation register. Slashes mark register bundles rather than individual qubits.

Construct the composed oracle once, then call it where it is needed:

@guppy
def apply_lcu[PrepRegs, TargetRegs](
    prepare: Function[[PrepRegs], None],
    select: Function[[PrepRegs, TargetRegs], None],
    unprepare: Function[[PrepRegs], None],
    prep_qreg: PrepRegs, target_qreg: TargetRegs,
) -> None:
    lcu = LCU(prepare, select, unprepare)
    lcu.compose(prep_qreg, target_qreg)
  • The struct stores the interchangeable component boxes.

  • compose defines their order and quantum-register wiring once, and can be called wherever the initialized oracle is needed.

  • Reusing PrepRegs across all three signatures guarantees that they agree on the preparation-register shape.

  • A generic quantum-register type may be one qubit array or a struct containing several quantum registers.

The repository uses this pattern for compositions including LCU block encodings, qubitization, QSVT, reflections, and rotation strategies.

Protocols for interchangeable structs

A protocol defines the methods a component must provide without requiring a particular struct. Here, a compatible component must apply an operation between two equal-sized quantum registers:

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

A concrete struct satisfies the protocol by providing apply with the same signature. Here it stores a function that acts on both complete quantum registers:

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


@guppy.struct
class TwoRegisterLayer[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)

The component is passed into the algorithm when its struct is initialized. The algorithm can then expose its own compose method without requiring that name from every component:

@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:
        self.operation.apply(left_qreg, right_qreg)


@guppy
def apply_cx_layer() -> None:
    left_qreg = qarray(4)
    right_qreg = qarray(4)

    algorithm = RegisterAlgorithm(
        TwoRegisterLayer[4](entangle_registers[4])
    )
    algorithm.compose(left_qreg, right_qreg)

    discard_array(left_qreg)
    discard_array(right_qreg)
  • TwoRegisterOperation enforces the component’s two-register apply interface.

  • TwoRegisterLayer satisfies the protocol structurally; it does not need to inherit from it.

  • The stored operation receives both complete quantum registers and defines how they interact. Here, entangle_registers applies cx pair by pair.

  • A different struct can be passed to RegisterAlgorithm if it provides the same apply method.

  • The elementary example uses explicit array types. The advanced register-composition example shows how generic register bundles extend this pattern.