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:
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 anarray[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:
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))
nis 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_registeruses@ ownedbecause 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 thantyping.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:
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
Regstype 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)
PrepRegsis the type of the quantum registers used to select an operation.TargetRegsis 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
selectaccepts the same quantum-register shapes supplied toapply_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:
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
Register1in both signatures defines their shared, type-checked boundary.A replacement box is compatible when its
Functionsignature 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, andRegister2are all inferred asarray[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)
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()
Register1is now inferred asMiddleBundle[3]; the outer composer is unchanged.Both boxes can access
middle.upperandmiddle.lower.Passing a box that expects one array alongside a box that expects
MiddleBundle[3]fails at compile time because theirRegister1types 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:
@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)
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.
composedefines their order and quantum-register wiring once, and can be called wherever the initialized oracle is needed.Reusing
PrepRegsacross 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)
TwoRegisterOperationenforces the component’s two-registerapplyinterface.TwoRegisterLayersatisfies 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_registersappliescxpair by pair.A different struct can be passed to
RegisterAlgorithmif it provides the sameapplymethod.The elementary example uses explicit array types. The advanced register-composition example shows how generic register bundles extend this pattern.