{ "cells": [ { "cell_type": "markdown", "id": "071b43d2", "metadata": {}, "source": [ "#### Qurt\n", "\n", "This package implements some of the functionality of Qiskits circuits. The focus has been on implementing fundamental structures and functions, eg to represent a circuit as a DAG and to add and remove nodes. This required a bit of work, since there is no efficient labeled (or weighted) digraph implemented in Julia (AFAIK).\n", "\n", "This document is a demo, but mostly explains some of the current design. I want to expose it to criticism as early as possible." ] }, { "cell_type": "code", "execution_count": 1, "id": "89140a18", "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "\u001b[32m\u001b[1m Activating\u001b[22m\u001b[39m project at `~/myrepos/quantum_repos/Qurt`\n" ] } ], "source": [ "# This \"activates\" the project environment of the package `Qurt`. Normally you would use `Qurt`\n", "# from an environment external to the package.\n", "import Pkg\n", "Pkg.activate(\".\");" ] }, { "cell_type": "code", "execution_count": 2, "id": "64abb6cc", "metadata": {}, "outputs": [], "source": [ "# Now `Qurt` should be visible.\n", "# `import` imports the package, but no other symbols.\n", "import Qurt" ] }, { "cell_type": "code", "execution_count": 3, "id": "b3f1c38f", "metadata": {}, "outputs": [], "source": [ "# Names of objects that are not imported are printed fully qualified, which is verbose.\n", "# So import more only so names are not printed fully qualified.\n", "import Graphs.SimpleGraphs.SimpleDiGraph\n", "import Qurt.NodeStructs.Node" ] }, { "cell_type": "code", "execution_count": 4, "id": "7f08968b", "metadata": {}, "outputs": [], "source": [ "using BenchmarkTools # This provides tools like Python %timeit" ] }, { "cell_type": "markdown", "id": "dcda1a41", "metadata": {}, "source": [ "### Creating a quantum circuit" ] }, { "cell_type": "code", "execution_count": 5, "id": "723c9f19", "metadata": {}, "outputs": [], "source": [ "# `using` is similar to `import`. But this invocation imports all of the symbols on the export list of\n", "# `Qurt.Circuits`\n", "using Qurt.Circuits\n", "\n", "using Qurt.Interface # many symbols go here as a catch all. They may live elsewhere in the future." ] }, { "cell_type": "markdown", "id": "d76c9b3f", "metadata": {}, "source": [ "Create a circuit with 2 quantum wires and two classical wires. Inputs and outputs will be created and connected" ] }, { "cell_type": "code", "execution_count": 6, "id": "43c6f780", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "circuit {nq=2, ncl=2, nv=8, ne=4} SimpleDiGraph{Int64} Node{Int64}" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "qc = Circuit(2, 2)" ] }, { "cell_type": "code", "execution_count": 7, "id": "e850f682", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " 6 => 8 ClInput((), (4,)) => ClOutput((), (4,))\n", " 5 => 7 ClInput((), (3,)) => ClOutput((), (3,))\n", " 2 => 4 Input(2) => Output(2)\n", " 1 => 3 Input(1) => Output(1)\n" ] } ], "source": [ "using Qurt.IOQDAGs\n", "print_edges(qc)" ] }, { "cell_type": "markdown", "id": "286b8a89", "metadata": {}, "source": [ "The circuit is a represented in part as a digraph from the package `Graphs.jl`. But this structure carries no payloads on vertices or edges. So information on wire connections is carried in a parallel structure. Everything else that lives on a node is also in this structure.\n", "\n", "The `Circuit` is an immutable struct with fixed fields." ] }, { "cell_type": "code", "execution_count": 8, "id": "3a2dfc75", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(:graph, :nodes, :param_table, :wires, :global_phase)" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "propertynames(qc)" ] }, { "cell_type": "code", "execution_count": 9, "id": "634ec87d", "metadata": { "scrolled": true }, "outputs": [ { "data": { "text/plain": [ "SimpleDiGraph{Int64}" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "typeof(qc.graph) # digraph from `Graphs.jl`" ] }, { "cell_type": "markdown", "id": "cb0fc974", "metadata": {}, "source": [ "The vertices of a `SimpleDiGraph` are the integers from `1` to $|V|$. We refer these with the words `vertex` and `vertices` rather than something like \"vertex index\". When possible `vertex` refers to this integer and `node` refers to the `vertex` together all information on the circuit element applied there.\n", "\n", "The information on the nodes is stored in several arrays indexed by `vertex`. We can also get a view that collects the element from each of these arrays for a single `vertex`." ] }, { "cell_type": "code", "execution_count": 10, "id": "32426c95", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "8-element StructArray(::Vector{Qurt.Elements.Element}, ::Vector{Tuple{Int64, Vararg{Int64}}}, ::Vector{Int32}, ::Vector{Vector{Int64}}, ::Vector{Vector{Int64}}, ::Vector{Tuple}) with eltype Node{Int64}:\n", " Node{Int64}(el=Input, wires=(1,), nq=1, in=Int64[], out=[3], params=())\n", " Node{Int64}(el=Input, wires=(2,), nq=1, in=Int64[], out=[4], params=())\n", " Node{Int64}(el=Output, wires=(1,), nq=1, in=[1], out=Int64[], params=())\n", " Node{Int64}(el=Output, wires=(2,), nq=1, in=[2], out=Int64[], params=())\n", " Node{Int64}(el=ClInput, wires=(3,), nq=0, in=Int64[], out=[7], params=())\n", " Node{Int64}(el=ClInput, wires=(4,), nq=0, in=Int64[], out=[8], params=())\n", " Node{Int64}(el=ClOutput, wires=(3,), nq=0, in=[5], out=Int64[], params=())\n", " Node{Int64}(el=ClOutput, wires=(4,), nq=0, in=[6], out=Int64[], params=())" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "qc.nodes" ] }, { "cell_type": "markdown", "id": "f82b2b72", "metadata": {}, "source": [ "Add two gates and save the vertices (integers) that they occupy." ] }, { "cell_type": "code", "execution_count": 11, "id": "6a8269e8", "metadata": {}, "outputs": [], "source": [ "using Qurt.Builders\n", "import .Qurt.Elements: H, CX, X, Y, RX\n", "(nH, nCX) = @build qc H(1) CX(1,2);" ] }, { "cell_type": "code", "execution_count": 12, "id": "d1521cf4", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " 6 => 8 ClInput((), (4,)) => ClOutput((), (4,))\n", " 5 => 7 ClInput((), (3,)) => ClOutput((), (3,))\n", " 2 => 10 Input(2) => CX(1,2)\n", " 1 => 9 Input(1) => H(1)\n", " 9 => 10 H(1) => CX(1,2)\n", " 10 => 3 CX(1,2) => Output(1)\n", " 10 => 4 CX(1,2) => Output(2)\n" ] } ], "source": [ "print_edges(qc)" ] }, { "cell_type": "markdown", "id": "76d118d1", "metadata": {}, "source": [ "You can index into `qc` with a vertex (again an integer) to get information on the operation (or element) at the vertex." ] }, { "cell_type": "code", "execution_count": 13, "id": "5bdce4da", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Node{Int64}(el=CX, wires=(1, 2), nq=2, in=[9, 2], out=[3, 4], params=())" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "qc[nCX]" ] }, { "cell_type": "markdown", "id": "14fd84f7", "metadata": {}, "source": [ "### Data structure for circuit\n", "\n", "The return value was a `struct`. But for performance the data is actually stored as a *struct of arrays*. For example, the element ids are instances of a modified enum. The array of elements is essentially an array integers (say 32 or 64 bits).\n", "\n", "Neither the internal nor external API access these arrays directly. But this is what one of them looks like." ] }, { "cell_type": "code", "execution_count": 14, "id": "72f7ae75", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "10-element Vector{Qurt.Elements.Element}:\n", " Input::Element\n", " Input::Element\n", " Output::Element\n", " Output::Element\n", " ClInput::Element\n", " ClInput::Element\n", " ClOutput::Element\n", " ClOutput::Element\n", " H::Element\n", " CX::Element" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "qc.nodes.element" ] }, { "cell_type": "code", "execution_count": 15, "id": "22d1c570", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "14003" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "Integer(qc.nodes.element[1])" ] }, { "cell_type": "code", "execution_count": 16, "id": "8307c25b", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Input::Element" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import .Interface.getelement\n", "getelement(qc, 1) # This is currently a way to access the element" ] }, { "cell_type": "markdown", "id": "1f137cc2", "metadata": {}, "source": [ "Julia is compiled. In particular, `getelement` is a zero-cost abstraction" ] }, { "cell_type": "code", "execution_count": 17, "id": "1ad81731", "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " 2.197 ns (0 allocations: 0 bytes)\n" ] }, { "data": { "text/plain": [ "Input::Element" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "@btime getelement($qc, 1) # Dollar sign is a detail of how @btime works" ] }, { "cell_type": "code", "execution_count": 18, "id": "91c05cea", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " 1.981 ns (0 allocations: 0 bytes)\n" ] }, { "data": { "text/plain": [ "Input::Element" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "@btime $qc.nodes.element[1] # This is no more or less efficient" ] }, { "cell_type": "markdown", "id": "b55bd570", "metadata": {}, "source": [ "### Builders\n", "Making builder interfaces is easy with macros. I added a couple of simple macros to make development easier. These are handwritten, but you might use tools to develop them further." ] }, { "cell_type": "code", "execution_count": 19, "id": "12422955", "metadata": {}, "outputs": [], "source": [ "qc = Circuit(2)\n", "@build qc X(1) Y(2)\n", "\n", "@build qc begin\n", " X(1)\n", " Y(2)\n", " CX(1,2)\n", " RX{1.5}(1)\n", "end;" ] }, { "cell_type": "markdown", "id": "5d77bd42", "metadata": {}, "source": [ "We use `RX{1.5}(1)` rather than `RX(1.5, 1)` because we don't want to require understanding what this gate means in order to insert it into a circuit. This pushes off the error if you do for example `X{1.5, 2.0}(1,2,3)`. We will need to add the option to validate somewhere. Especially for user input." ] }, { "cell_type": "markdown", "id": "93dedf6f", "metadata": {}, "source": [ "Because we want maniupulating storage to be as efficient as possible, there is no gate object per se. You collect different attributes of a gate for different purposes." ] }, { "cell_type": "code", "execution_count": 20, "id": "8fc6a6bb", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "RX::Element" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "g1 = @gate RX" ] }, { "cell_type": "code", "execution_count": 21, "id": "afc710ae", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "RX{1.5}" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "g2 = @gate RX{1.5} # circuit element identity and parameters" ] }, { "cell_type": "code", "execution_count": 22, "id": "b28f71ef", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "RX{1.5}(2)" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "g3 = @gate RX{1.5}(2) # Include wires" ] }, { "cell_type": "markdown", "id": "1d127b28", "metadata": {}, "source": [ "These can be used like this, with information not included in the \"gate\" included when adding it to the circuit." ] }, { "cell_type": "code", "execution_count": 23, "id": "2bfd2cc1", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "11" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "add_node!(qc, (g1, 1.5), (2,))" ] }, { "cell_type": "code", "execution_count": 24, "id": "ba780693", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "12" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "add_node!(qc, g2, (2,))" ] }, { "cell_type": "code", "execution_count": 25, "id": "2121b7f3", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "13" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "add_node!(qc, g3)" ] }, { "cell_type": "code", "execution_count": 26, "id": "826deafd", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "3-element StructArray(view(::Vector{Qurt.Elements.Element}, 11:13), view(::Vector{Tuple{Int64, Vararg{Int64}}}, 11:13), view(::Vector{Int32}, 11:13), view(::Vector{Vector{Int64}}, 11:13), view(::Vector{Vector{Int64}}, 11:13), view(::Vector{Tuple}, 11:13)) with eltype Node{Int64}:\n", " Node{Int64}(el=RX, wires=(2,), nq=1, in=[9], out=[12], params=(1.5,))\n", " Node{Int64}(el=RX, wires=(2,), nq=1, in=[11], out=[13], params=(1.5,))\n", " Node{Int64}(el=RX, wires=(2,), nq=1, in=[12], out=[4], params=(1.5,))" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "(qc[11:13])" ] }, { "cell_type": "markdown", "id": "c30c8ce5", "metadata": {}, "source": [ "Julia constructs elaborate types. We need to define the function (like Python `repr`) that prints abbreviated type information above." ] }, { "cell_type": "code", "execution_count": null, "id": "cdf5bdd0", "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Julia 1.9.0-rc2", "language": "julia", "name": "julia-1.9" }, "language_info": { "file_extension": ".jl", "mimetype": "application/julia", "name": "julia", "version": "1.9.0" } }, "nbformat": 4, "nbformat_minor": 5 }