{ "cells": [ { "cell_type": "markdown", "id": "getting-started-0", "metadata": {}, "source": [ "# Getting started with guppy-algorithms\n", "\n", "**Download Notebook** - {nb-download}`getting_started.ipynb`\n", "\n", "This notebook introduces [guppy](https://docs.quantinuum.com/guppy/language_guide/language_guide_index.html) 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.\n", "\n", "By the end, you will be able to:\n", "\n", "- Build, compile, and simulate a circuit.\n", "- Write functions for quantum registers of different sizes.\n", "- Pass operations into higher-order functions.\n", "- Use structs to bundle registers or store component functions.\n", "- Use a protocol to accept interchangeable structs.\n", "- Choose a library implementation in Python and check its output.\n", "\n", "Run the cells from top to bottom. The examples use small registers so you can focus on how the code fits together.\n", "\n", "## 1. Set up your environment\n", "\n", "From a source checkout, install the notebook and simulation dependencies:\n", "\n", "```console\n", "uv sync --extra dev-dependencies\n", "```\n", "\n", "Open this notebook and select the checkout's Python environment. The project requires Python 3.12 or newer.\n", "\n", "Python assembles and runs experiments; functions decorated with `@guppy` describe the quantum program that will be compiled." ] }, { "cell_type": "code", "execution_count": null, "id": "getting-started-1", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "\n", "from guppylang import guppy\n", "from guppylang.std.builtins import Function, array, bool, nat, output, owned\n", "from guppylang.std.debug import state_output\n", "from guppylang.std.quantum import (\n", " collect_measurements, cx, discard_array, h, measure_array, qubit,\n", ")\n", "\n", "from guppyalgos.utils import qarray, transversal" ] }, { "cell_type": "markdown", "id": "getting-started-2", "metadata": {}, "source": [ "## 2. Build your first circuit\n", "\n", "Prepare a Bell pair:\n", "\n", "$$\n", "|00\\rangle\n", "\\xrightarrow{H\\otimes I}\n", "\\frac{|00\\rangle+|10\\rangle}{\\sqrt{2}}\n", "\\xrightarrow{\\mathrm{CX}}\n", "\\frac{|00\\rangle+|11\\rangle}{\\sqrt{2}}.\n", "$$\n", "\n", "- `qarray(2)` allocates two qubits in $|00\\rangle$.\n", "- `h` creates a superposition; `cx` correlates the second qubit with the first.\n", "- Measurement consumes the qubits and produces classical results.\n", "- `output` records those results under a name we can inspect in Python." ] }, { "cell_type": "code", "execution_count": null, "id": "getting-started-3", "metadata": {}, "outputs": [], "source": [ "@guppy\n", "def bell_experiment() -> None:\n", " qreg = qarray(2)\n", " h(qreg[0])\n", " cx(qreg[0], qreg[1])\n", " output(\"bits\", collect_measurements(measure_array(qreg)))\n", "\n", "\n", "bell_experiment.check()\n", "package = bell_experiment.compile()\n", "print(\"Circuit checked and compiled.\")" ] }, { "cell_type": "markdown", "id": "getting-started-4", "metadata": {}, "source": [ "### Compile, then simulate\n", "\n", "`check()` validates types and qubit ownership. `compile()` produces a HUGR package. `emulator(...)` builds an executable simulation of the program.\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": null, "id": "getting-started-5", "metadata": {}, "outputs": [], "source": [ "bell_results = (\n", " bell_experiment.emulator(n_qubits=2)\n", " .with_seed(42)\n", " .with_shots(100)\n", " .run()\n", ")\n", "print(bell_results.collated_counts())" ] }, { "cell_type": "markdown", "id": "getting-started-6", "metadata": {}, "source": [ "## 3. Reuse functions across register sizes\n", "\n", "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", "\n", "- `n: nat` is a compile-time register size, inferred from the argument.\n", "- `array[qubit, n]` makes that size part of the type.\n", "- `@ owned` explicitly transfers ownership to the measuring helper.\n", "- `transversal` applies a gate to each qubit, or to corresponding pairs of qubits." ] }, { "cell_type": "code", "execution_count": null, "id": "getting-started-7", "metadata": {}, "outputs": [], "source": [ "@guppy\n", "def prepare_plus[n: nat](qreg: array[qubit, n]) -> None:\n", " transversal(h, qreg)\n", "\n", "\n", "@guppy\n", "def entangle_pairs[n: nat](\n", " left_qreg: array[qubit, n], right_qreg: array[qubit, n],\n", ") -> None:\n", " transversal(cx, left_qreg, right_qreg)\n", "\n", "\n", "@guppy\n", "def read_register[n: nat](\n", " qreg: array[qubit, n] @ owned,\n", ") -> array[bool, n]:\n", " return collect_measurements(measure_array(qreg))\n", "\n", "\n", "@guppy\n", "def register_experiment() -> None:\n", " left_qreg = qarray(2)\n", " right_qreg = qarray(2)\n", " prepare_plus(left_qreg)\n", " entangle_pairs(left_qreg, right_qreg)\n", " output(\"left\", read_register(left_qreg))\n", " output(\"right\", read_register(right_qreg))\n", "\n", "\n", "print(register_experiment.emulator(4).with_seed(42).with_shots(10).run().collated_counts())" ] }, { "cell_type": "markdown", "id": "getting-started-8", "metadata": {}, "source": [ "The two registers now share $n$ Bell pairs:\n", "\n", "$$\n", "|0\\rangle_L^{\\otimes n}|0\\rangle_R^{\\otimes n}\n", "\\longmapsto\n", "\\frac{1}{\\sqrt{2^n}}\\sum_{x=0}^{2^n-1}|x\\rangle_L|x\\rangle_R.\n", "$$\n", "\n", "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.\n", "\n", "## 4. Pass an operation into a function\n", "\n", "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.\n", "\n", "Use guppy's `Function` to describe the required signature." ] }, { "cell_type": "code", "execution_count": null, "id": "getting-started-9", "metadata": {}, "outputs": [], "source": [ "@guppy\n", "def apply_between_registers[n: nat](\n", " operation: Function[[array[qubit, n], array[qubit, n]], None],\n", " left_qreg: array[qubit, n],\n", " right_qreg: array[qubit, n],\n", ") -> None:\n", " operation(left_qreg, right_qreg)\n", "\n", "\n", "@guppy\n", "def injected_experiment() -> None:\n", " left_qreg = qarray(2)\n", " right_qreg = qarray(2)\n", " prepare_plus(left_qreg)\n", " apply_between_registers(entangle_pairs[2], left_qreg, right_qreg)\n", " output(\"left\", read_register(left_qreg))\n", " output(\"right\", read_register(right_qreg))\n", "\n", "\n", "injected_experiment.check()\n", "print(\"Injected operation has the required signature.\")" ] }, { "cell_type": "markdown", "id": "getting-started-10", "metadata": {}, "source": [ "The explicit `[2]` specializes the function for two-qubit registers. guppy checks that the operation and both arguments agree on their types.\n", "\n", "This is the same pattern used to choose arithmetic implementations, controlled operations, and state-preparation routines.\n", "\n", "## 5. Use structs in two ways\n", "\n", "### Bundle related registers\n", "\n", "A struct gives names to registers that belong together. It does not allocate qubits by itself: the constructor below receives arrays allocated with `qarray`.\n", "\n", "A generic type such as `Registers` can represent the whole bundle, allowing a wrapper to accept arrays, tuples, or structs." ] }, { "cell_type": "code", "execution_count": null, "id": "getting-started-11", "metadata": {}, "outputs": [], "source": [ "@guppy.struct\n", "class RegisterPair[n: nat]:\n", " left_qreg: array[qubit, n]\n", " right_qreg: array[qubit, n]\n", "\n", "\n", "@guppy\n", "def prepare_pair[n: nat](registers: RegisterPair[n]) -> None:\n", " prepare_plus(registers.left_qreg)\n", " entangle_pairs(registers.left_qreg, registers.right_qreg)\n", "\n", "\n", "@guppy\n", "def apply_to_registers[Registers](\n", " operation: Function[[Registers], None],\n", " registers: Registers,\n", ") -> None:\n", " operation(registers)\n", "\n", "\n", "@guppy\n", "def bundled_experiment() -> None:\n", " registers = RegisterPair(qarray(2), qarray(2))\n", " apply_to_registers(prepare_pair[2], registers)\n", " output(\"left\", read_register(registers.left_qreg))\n", " output(\"right\", read_register(registers.right_qreg))\n", "\n", "\n", "bundled_experiment.check()\n", "print(\"Register bundle checked.\")" ] }, { "cell_type": "markdown", "id": "getting-started-12", "metadata": {}, "source": [ "### Store the pieces of an algorithm\n", "\n", "A struct can also store component functions. Its `compose` method defines the order in which those components act.\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": null, "id": "getting-started-13", "metadata": {}, "outputs": [], "source": [ "@guppy.struct\n", "class PairPreparation[n: nat]:\n", " prepare: Function[[array[qubit, n]], None]\n", " couple: Function[[array[qubit, n], array[qubit, n]], None]\n", "\n", " @guppy\n", " def compose(\n", " self, left_qreg: array[qubit, n], right_qreg: array[qubit, n],\n", " ) -> None:\n", " self.prepare(left_qreg)\n", " self.couple(left_qreg, right_qreg)\n", "\n", "\n", "@guppy\n", "def composed_experiment() -> None:\n", " left_qreg = qarray(2)\n", " right_qreg = qarray(2)\n", " preparation = PairPreparation[2](prepare_plus[2], entangle_pairs[2])\n", " preparation.compose(left_qreg, right_qreg)\n", " output(\"left\", read_register(left_qreg))\n", " output(\"right\", read_register(right_qreg))\n", "\n", "\n", "composed_experiment.check()\n", "print(\"Composed preparation checked.\")" ] }, { "cell_type": "markdown", "id": "getting-started-14", "metadata": {}, "source": [ "## 6. Accept interchangeable structs with a protocol\n", "\n", "A function signature describes one callable. A protocol describes the methods a component must provide.\n", "\n", "### Why use a protocol?\n", "\n", "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.\n", "\n", "- **Keep the algorithm independent of a particular struct:** it asks for an `apply` method with a specified signature, rather than naming one implementation.\n", "- **Let each implementation carry its own configuration:** a struct can store component functions or parameters behind the same interface.\n", "- **Check compatibility before execution:** guppy checks that the supplied struct provides the required method and register types when the program is compiled.\n", "\n", "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.\n", "\n", "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.\n", "\n", "### A small example\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": null, "id": "getting-started-15", "metadata": {}, "outputs": [], "source": [ "@guppy.protocol\n", "class TwoRegisterOperation[n: nat]:\n", " @guppy.require\n", " def apply(\n", " self, left_qreg: array[qubit, n], right_qreg: array[qubit, n],\n", " ) -> None:\n", " ...\n", "\n", "\n", "@guppy.struct\n", "class FunctionLayer[n: nat]:\n", " operation: Function[[array[qubit, n], array[qubit, n]], None]\n", "\n", " @guppy\n", " def apply(\n", " self, left_qreg: array[qubit, n], right_qreg: array[qubit, n],\n", " ) -> None:\n", " self.operation(left_qreg, right_qreg)\n", "\n", "\n", "@guppy.struct\n", "class DirectLayer[n: nat]:\n", " @guppy\n", " def apply(\n", " self, left_qreg: array[qubit, n], right_qreg: array[qubit, n],\n", " ) -> None:\n", " for i in range(n):\n", " cx(left_qreg[i], right_qreg[i])\n", "\n", "\n", "@guppy.struct\n", "class RegisterAlgorithm[n: nat, Operation: TwoRegisterOperation[n]]:\n", " operation: Operation\n", "\n", " @guppy\n", " def compose(\n", " self, left_qreg: array[qubit, n], right_qreg: array[qubit, n],\n", " ) -> None:\n", " prepare_plus(left_qreg)\n", " self.operation.apply(left_qreg, right_qreg)" ] }, { "cell_type": "markdown", "id": "getting-started-16", "metadata": {}, "source": [ "The algorithm depends on the protocol's interface. It accepts either concrete implementation without changing its `compose` method.\n", "\n", "The two experiments below should produce the same Bell-pair statistics." ] }, { "cell_type": "code", "execution_count": null, "id": "getting-started-17", "metadata": {}, "outputs": [], "source": [ "@guppy\n", "def function_layer_experiment() -> None:\n", " left_qreg = qarray(2)\n", " right_qreg = qarray(2)\n", " algorithm = RegisterAlgorithm(FunctionLayer[2](entangle_pairs[2]))\n", " algorithm.compose(left_qreg, right_qreg)\n", " output(\"left\", read_register(left_qreg))\n", " output(\"right\", read_register(right_qreg))\n", "\n", "\n", "@guppy\n", "def direct_layer_experiment() -> None:\n", " left_qreg = qarray(2)\n", " right_qreg = qarray(2)\n", " algorithm = RegisterAlgorithm(DirectLayer[2]())\n", " algorithm.compose(left_qreg, right_qreg)\n", " output(\"left\", read_register(left_qreg))\n", " output(\"right\", read_register(right_qreg))\n", "\n", "\n", "for experiment in (function_layer_experiment, direct_layer_experiment):\n", " result = experiment.emulator(4).with_seed(42).with_shots(10).run()\n", " print(result.collated_counts())" ] }, { "cell_type": "markdown", "id": "getting-started-18", "metadata": {}, "source": [ "## 7. Choose a real library component in Python\n", "\n", "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:\n", "\n", "$$\n", "|\\psi\\rangle=\\frac{1}{2}(|00\\rangle+|01\\rangle+|10\\rangle+|11\\rangle).\n", "$$\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": null, "id": "getting-started-19", "metadata": {}, "outputs": [], "source": [ "from guppyalgos.primitives.state_preparation import uniform_state\n", "\n", "uniform = uniform_state(4)\n", "\n", "\n", "@guppy\n", "def library_experiment() -> None:\n", " qreg = qarray(2)\n", " uniform(qreg)\n", " state_output(\"result_state\", qreg)\n", " discard_array(qreg)\n", "\n", "\n", "library_experiment.check()\n", "print(\"Library component checked.\")" ] }, { "cell_type": "markdown", "id": "getting-started-20", "metadata": {}, "source": [ "## 8. Check amplitudes, not just measured bits\n", "\n", "During simulation, `state_output` records a state snapshot before the qubits are discarded. The repository\u2019s `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.\n", "\n", "Compare the prepared state against its expected amplitudes, allowing one overall global phase. A relative sign error would still fail this check." ] }, { "cell_type": "code", "execution_count": null, "id": "getting-started-21", "metadata": {}, "outputs": [], "source": [ "from guppyalgos.tests.helpers import get_statevector\n", "\n", "actual = get_statevector(library_experiment, n_qubits=2)\n", "expected = np.ones(4, dtype=complex) / 2\n", "\n", "# Remove only an overall phase; preserve amplitudes and relative phases.\n", "phase = actual[0] / abs(actual[0])\n", "np.testing.assert_allclose(actual / phase, expected, atol=1e-8)\n", "print(\"Uniform-state amplitudes match.\")" ] }, { "cell_type": "markdown", "id": "getting-started-22", "metadata": {}, "source": [ "## Where to go next\n", "\n", "- Explore generic register bundles and multi-box wiring in [abstract composition](https://github.com/Quantinuum/guppy-algorithms/blob/main/examples/core_concepts/abstract_construction.ipynb).\n", "- Check complete operations, post-selection, and retries in [statevector testing](https://github.com/Quantinuum/guppy-algorithms/blob/main/examples/core_concepts/statevector_testing.ipynb).\n", "- Read the user guide for library structure and applications such as arithmetic, phase estimation, and Hamiltonian simulation.\n", "\n", "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." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.12" } }, "nbformat": 4, "nbformat_minor": 5 }