From b02c8591c5f1a3bda006af8d75065db0775a3ed2 Mon Sep 17 00:00:00 2001 From: Aleks Kissinger Date: Mon, 30 Dec 2024 17:00:10 +0000 Subject: [PATCH] extended pauli webs to represent errors and corrections --- demos/PauliWebs.ipynb | 9979 +++++++++++++++++++++++++++++++-- demos/PauliWebsRGB.ipynb | 11044 ++++++++++++++++++++++++++++++++++++- pyzx/gflow.py | 2 +- pyzx/pauliweb.py | 96 +- pyzx/simplify.py | 14 + 5 files changed, 20551 insertions(+), 584 deletions(-) diff --git a/demos/PauliWebs.ipynb b/demos/PauliWebs.ipynb index ba6738c8..10f0ea2f 100644 --- a/demos/PauliWebs.ipynb +++ b/demos/PauliWebs.ipynb @@ -7,10 +7,12 @@ "outputs": [], "source": [ "import sys, os, random\n", + "from fractions import Fraction\n", "sys.path.insert(0,os.path.expanduser('~/git/pyzx')) # git version\n", "sys.path.insert(0,'/workspaces/pyzx')\n", "import pyzx as zx\n", "from pyzx.pauliweb import PauliWeb, compute_pauli_webs\n", + "from pyzx import VertexType\n", "zx.settings.colors = zx.original_colors" ] }, @@ -54,7 +56,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -451,7 +453,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -830,7 +832,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -1209,7 +1211,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -1588,7 +1590,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -1981,7 +1983,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -2360,7 +2362,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -2739,7 +2741,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -3118,7 +3120,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -3515,7 +3517,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -3917,7 +3919,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -4296,7 +4298,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -4675,7 +4677,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -5054,7 +5056,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -5433,7 +5435,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -5812,7 +5814,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -6207,7 +6209,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -6599,7 +6601,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -6978,7 +6980,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -7357,7 +7359,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -7736,7 +7738,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -8115,7 +8117,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -8494,7 +8496,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -8898,7 +8900,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -9277,7 +9279,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -9680,7 +9682,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -10059,7 +10061,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -10463,7 +10465,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -10870,7 +10872,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -11249,7 +11251,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -11628,7 +11630,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -12007,7 +12009,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -12386,7 +12388,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -12765,7 +12767,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -13161,7 +13163,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -13540,7 +13542,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -13938,7 +13940,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -14317,7 +14319,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -14696,7 +14698,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -15095,7 +15097,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -15474,7 +15476,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -15853,7 +15855,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -16256,20 +16258,24 @@ "source": [ "# Errors and corrections\n", "\n", - "Before moving on to Clifford+T examples, we'll show how to introduce errors into ZX-diagrams, correct them, and prove correctness.\n", + "Before moving on to Clifford+T examples, we'll show how to introduce errors into ZX-diagrams, correct them, and check the corrected diagram is equal to the original, error-free diagram, up to a global phase.\n", "\n", - "We can use the `PauliWeb` class to represent arbitrary Pauli errors on half edges. Note this will _not_ usually be a valid, closed Pauli web, but we can still use the `PauliWeb` class to hold this data." + "We can use the `PauliWeb` class to represent arbitrary Pauli errors on half edges. Note this will _not_ usually be a valid, closed Pauli web, but we can still use the `PauliWeb` class to hold this data. At some point, it might be worth adding an `is_valid` method to `PauliWeb` to check the Pauli web conditions are satisfied.\n", + "\n", + "The kinds of errors we are interested in here are logical flips coming from lattice surgery operations. Normally, these will non-deterministically introduce X errors next to Z nodes and Z errors next to X nodes.\n", + "\n", + "Since we know at runtime where these errors are, they are always correctable. Hence, the error rate and which subset of errors _actually_ occur is irrelevant. To demonstrate the corrections for arbitrary errors, we will call `PauliWeb.random` with its default arguments, which introduces a uniformly random Pauli X, Y, or Z with 30% probability on every half-edge." ] }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -16648,7 +16654,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" ], @@ -17031,83 +17037,19 @@ "zx.draw(g, labels=True)\n", "\n", "# introduce random Pauli errors on edges and draw them\n", - "errors = PauliWeb(g)\n", - "for e in g.edges():\n", - " s,t = g.edge_st(e)\n", - " if random.random() > 0.75:\n", - " errors.add_half_edge((s,t), random.choice(['X','Y','Z']))\n", - " if random.random() > 0.75:\n", - " errors.add_half_edge((t,s), random.choice(['X','Y','Z']))\n", + "errors = PauliWeb.random(g)\n", "zx.draw(g, pauli_web=errors)" ] }, { "cell_type": "code", - "execution_count": 32, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Z-web 20: commutes\n", - "X-web 20: anti-commutes, corrected\n", - "Z-web 21: commutes\n", - "X-web 21: commutes\n", - "Z-web 22: anti-commutes, corrected\n", - "X-web 22: commutes\n", - "Z-web 23: commutes\n", - "X-web 23: commutes\n" - ] - } - ], - "source": [ - "# To find the appropriate corrections, we should introduce Pauli X and Z gates at the outputs\n", - "# to make all 8 output webs commute with `errors`.\n", - "\n", - "order, zwebs, xwebs = compute_pauli_webs(g)\n", - "output_es = [(o, next(iter(g.neighbors(o)))) for o in g.outputs()]\n", - "\n", - "\n", - "errors1 = errors.copy()\n", - "\n", - "# for each of the outputs, if the errors anti-commute with the associated Z-web, introduce\n", - "# an X-error at that output to make it commute, and do similar for X-webs.\n", - "for o,n in output_es:\n", - " print(f'Z-web {o}: ', end='')\n", - " if zwebs[o].commutes_with(errors):\n", - " print('commutes')\n", - " else:\n", - " print('anti-commutes', end='')\n", - " errors1.add_half_edge((o,n), 'X')\n", - " if zwebs[o].commutes_with(errors1):\n", - " print(', corrected')\n", - " print(f'X-web {o}: ', end='')\n", - " if xwebs[o].commutes_with(errors):\n", - " print('commutes')\n", - " else:\n", - " print('anti-commutes', end='')\n", - " errors1.add_half_edge((o,n), 'Z')\n", - " if xwebs[o].commutes_with(errors1):\n", - " print(', corrected')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Clifford+T Examples" - ] - }, - { - "cell_type": "code", - "execution_count": 18, + "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "
\n", + "
\n", "" ], "text/plain": [ @@ -17485,11 +17427,60 @@ } ], "source": [ - "# Generate a random CNOT, H, T circuit\n", - "random.seed(1330)\n", - "c = zx.generate.CNOT_HAD_PHASE_circuit(qubits=3, depth=40)\n", - "# for g in c.gates: print(g)\n", - "zx.draw(c)" + "# The `PauliWeb.graph_with_errors()` method returns a copy of the original graph, where\n", + "# Pauli errors are inserted as pi-spiders on the edges colored by the web.\n", + "zx.draw(errors.graph_with_errors())" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Z-web for output 20: commutes\n", + "X-web for output 20: anti-commutes, corrected\n", + "Z-web for output 21: anti-commutes, corrected\n", + "X-web for output 21: anti-commutes, corrected\n", + "Z-web for output 22: commutes\n", + "X-web for output 22: commutes\n", + "Z-web for output 23: anti-commutes, corrected\n", + "X-web for output 23: anti-commutes, corrected\n" + ] + } + ], + "source": [ + "# To find the appropriate corrections, we should introduce Pauli X and Z gates at the outputs\n", + "# to make all 8 output webs commute with `errors`.\n", + "\n", + "order, zwebs, xwebs = compute_pauli_webs(g)\n", + "output_es = [(o, next(iter(g.neighbors(o)))) for o in g.outputs()]\n", + "\n", + "\n", + "errors1 = errors.copy()\n", + "\n", + "# for each of the outputs, if the errors anti-commute with the associated Z-web, introduce\n", + "# an X-error at that output to make it commute, and do similar for X-webs.\n", + "for o,n in output_es:\n", + " print(f'Z-web for output {o}: ', end='')\n", + " if zwebs[o].commutes_with(errors1):\n", + " print('commutes')\n", + " else:\n", + " print('anti-commutes', end='')\n", + " errors1.add_half_edge((o,n), 'X')\n", + " if zwebs[o].commutes_with(errors1):\n", + " print(', corrected')\n", + " print(f'X-web for output {o}: ', end='')\n", + " if xwebs[o].commutes_with(errors1):\n", + " print('commutes')\n", + " else:\n", + " print('anti-commutes', end='')\n", + " errors1.add_half_edge((o,n), 'Z')\n", + " if xwebs[o].commutes_with(errors1):\n", + " print(', corrected')" ] }, { @@ -17500,7 +17491,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" ], "text/plain": [ @@ -17875,84 +17866,11 @@ }, "metadata": {}, "output_type": "display_data" - } - ], - "source": [ - "# Convert to a ZX diagram and call the full_reduce procedure on it (PyZX's main ZX diagram optimisation pass)\n", - "g = c.to_graph()\n", - "zx.full_reduce(g)\n", - "zx.to_rg(g)\n", - "\n", - "# Normalise compacts the circuit visually and ensures every input/output is connected to a Z spider\n", - "g.normalize()\n", - "\n", - "# Compute the time-ordering on nodes (which is only important for the non-Clifford nodes) and compute the Pauli\n", - "# webs for every node.\n", - "order, zwebs, xwebs = compute_pauli_webs(g)\n", - "\n", - "# Draw the simplified ZX diagram. Note blue edges correspond to edges with Hadamard gates\n", - "zx.draw(g, labels=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{(3, 1): 'Y',\n", - " (1, 3): 'Y',\n", - " (3, 43): 'Z',\n", - " (43, 3): 'Z',\n", - " (3, 5): 'Y',\n", - " (5, 3): 'Y',\n", - " (3, 58): 'Z',\n", - " (58, 3): 'X',\n", - " (3, 54): 'Z',\n", - " (54, 3): 'Z',\n", - " (3, 14): 'Y',\n", - " (14, 3): 'Y',\n", - " (5, 16): 'Y',\n", - " (16, 5): 'Y',\n", - " (5, 2): 'Y',\n", - " (2, 5): 'Y',\n", - " (5, 43): 'X',\n", - " (43, 5): 'Z',\n", - " (5, 58): 'X',\n", - " (58, 5): 'X',\n", - " (5, 54): 'X',\n", - " (54, 5): 'Z',\n", - " (14, 58): 'X',\n", - " (58, 14): 'X',\n", - " (14, 16): 'Y',\n", - " (16, 14): 'Y',\n", - " (16, 43): 'Z',\n", - " (43, 16): 'Z',\n", - " (16, 58): 'Z',\n", - " (58, 16): 'X'}" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "pw = zwebs[43]\n", - "pw.half_edges()" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ + }, { "data": { "text/html": [ - "
\n", + "
\n", "" ], "text/plain": [ @@ -18330,29 +18248,45 @@ } ], "source": [ - "# Once the Pauli webs have been computed, a specific web can be highlighted by `zx.draw` by passing it in as\n", - "# an optional argument. Note that webs change color when they cross Hadamard edges.\n", - "zx.draw(g, labels=True, pauli_web=zwebs[43])" + "# All the errors have now been corrected, so the graph with Pauli errors given by `errors1` should\n", + "# be equal, up to a global phase to the origin graph\n", + "ge = errors1.graph_with_errors()\n", + "zx.draw(ge)\n", + "\n", + "# ...so if we compose `ge` with the adjoint of `g`, we should get the identity\n", + "h = g.adjoint() * ge\n", + "zx.full_reduce(h)\n", + "h.pack_circuit_rows()\n", + "zx.draw(h)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Clifford+T Examples" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We now show how this works in some simpler cases. The first is a single T gate.\n", + "Now, we'll move to some Clifford+T examples. By definition, spiders with phases that are not multiples of $\\pi/2$ must necessarily be on the boundary of a Pauli web.\n", + "\n", + "For our purposes, we will treat non-Clifford spiders as additional outputs that have been measured in the basis $\\{|T\\rangle, Z|T\\rangle \\}$, where $|T\\rangle := T|{+}\\rangle$ is a $T$ magic state.\n", "\n", - "The T gate becomes a single, 1-legged phase gadget, connected to the input. This can be implemented by Z-merging a T magic state, then doing either an X or a Y measurement, depending on the parity of the Pauli web." + "There are many equivalent ways to implement this fault-tolerantly in lattice surgery. One way to do this is to first perform a Z-merge between a data qubit and a $|T\\rangle$ magic state, then depending on the outcome of the merge, measure the remaining qubit in the X or Y basis." ] }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "
\n", + "
\n", "" ], "text/plain": [ @@ -18731,7 +18665,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" ], "text/plain": [ @@ -19109,36 +19043,29 @@ } ], "source": [ - "c = zx.qasm(\"\"\"\n", - "qreg q[1];\n", - "t q[0];\n", - "\"\"\")\n", - "zx.draw(c)\n", + "# Merge with parity 0, followed by an X measurement\n", + "g = zx.Graph()\n", + "g.add_vertex(qubit=0, row=0)\n", + "g.add_vertex(VertexType.Z, qubit=2, row=0, phase=Fraction(1/4))\n", + "g.add_vertex(VertexType.Z, qubit=1, row=1)\n", + "g.add_vertex(VertexType.Z, qubit=1, row=2)\n", + "for e in [(0,2), (1,2), (2,3)]: g.add_edge(e)\n", + "zx.draw(g, labels=True)\n", "\n", - "g = c.to_graph()\n", + "# ...is equivalent to a |T> measurement\n", "zx.full_reduce(g)\n", - "order, zwebs, xwebs = compute_pauli_webs(g)\n", - "\n", - "# highlight the web associated to the T spider\n", - "zx.draw(g, labels=True, pauli_web=zwebs[1])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The next example is a circuit with some CNOT gates and a T gate, which can be simplified to a single phase gadget. I'm doing this manually here, since the automated simplifier comes up with a different answer (which is equivalent to this one, up to local Cliffords, but less clear what is going on)." + "zx.draw(g)" ] }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "
\n", + "
\n", "" ], "text/plain": [ @@ -19517,7 +19444,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" ], "text/plain": [ @@ -19892,11 +19819,42 @@ }, "metadata": {}, "output_type": "display_data" - }, + } + ], + "source": [ + "# Merge with parity 1, followed by an Y measurement\n", + "h = zx.Graph()\n", + "h.add_vertex(qubit=0, row=0)\n", + "h.add_vertex(VertexType.Z, qubit=2, row=0, phase=Fraction(1/4))\n", + "h.add_vertex(VertexType.Z, qubit=1, row=1)\n", + "h.add_vertex(VertexType.Z, qubit=1, row=2, phase=Fraction(1/2))\n", + "h.add_vertex(VertexType.X, qubit=1.5, row=0.5, phase=1)\n", + "for e in [(0,2), (1,4), (4,2), (2,3)]: h.add_edge(e)\n", + "zx.draw(h, labels=True)\n", + "\n", + "# ...is also equivalent to a |T> measurement\n", + "zx.full_reduce(h)\n", + "zx.draw(h)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In both cases above, we show the +1 outcome of the X/Y measurement. Getting the other outcome yields the same effect, up to a Pauli Z error, which can be corrected later.\n", + "\n", + "Using this primitive, we now look at implementing a single T gate." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ { "data": { "text/html": [ - "
\n", + "
\n", "" ], "text/plain": [ @@ -20271,37 +20229,9408 @@ }, "metadata": {}, "output_type": "display_data" - } - ], - "source": [ - "c = zx.qasm(\"\"\"\n", - "qreg q[3];\n", - "cx q[0], q[1];\n", - "cx q[1], q[2];\n", - "t q[2];\n", - "cx q[1], q[2];\n", - "cx q[0], q[1];\n", - "\"\"\")\n", - "zx.draw(c)\n", - "\n", - "# manual ZX simplification to get a single phase gadget\n", - "g = c.to_graph()\n", - "zx.simplify.gadgetize(g, graphlike=False)\n", - "zx.basicrules.strong_comp(g, 5, 7)\n", - "zx.simplify.spider_simp(g, quiet=True)\n", - "zx.basicrules.strong_comp(g, 3, 6)\n", - "zx.simplify.spider_simp(g, quiet=True)\n", - "zx.simplify.id_simp(g, quiet=True)\n", - "\n", - "zx.draw(g, labels=True)\n", - "\n", - "d = dict()\n", - "\n", - "# pauli web calculation\n", - "order, zwebs, xwebs = compute_pauli_webs(g, debug=d)\n", - "\n", - "# highlight the web associated to the T spider\n", - "zx.draw(g, labels=True, pauli_web=zwebs[15])" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "c = zx.qasm(\"\"\"\n", + "qreg q[1];\n", + "t q[0];\n", + "\"\"\")\n", + "g = c.to_graph()\n", + "zx.draw(g)\n", + "\n", + "# Unfuse the non-Clifford spiders, so they look like outputs measured in {|T>, Z|T>}\n", + "zx.simplify.unfuse_non_cliffords(g)\n", + "zx.draw(g, labels=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# The relevant Pauli webs for this example are the Z and X webs of the output 2\n", + "# as well as the Z-web of the non-Clifford spider 3.\n", + "\n", + "order, zwebs, xwebs = compute_pauli_webs(g)\n", + "zx.draw(g, labels=True, pauli_web=zwebs[2])\n", + "zx.draw(g, labels=True, pauli_web=xwebs[2])\n", + "zx.draw(g, labels=True, pauli_web=zwebs[3])" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# As we did in the Clifford examples, we can introduce random Pauli errors on all the edges\n", + "random.seed(1330)\n", + "errors = PauliWeb.random(g)\n", + "zx.draw(g, pauli_web=errors)\n", + "\n", + "errors1 = errors.copy()\n", + "\n", + "# To implement the T-measurement at node 3, we need to look at the associated Z-web of 3\n", + "# If it anti-commutes with the errors, we should apply an X correction before we measure, or equivalently,\n", + "# flip whether we are measuring X or Y in the fault-tolerant gadget that injects a magic state.\n", + "if not zwebs[3].commutes_with(errors):\n", + " errors1.add_half_edge((3,1), 'X')\n", + "\n", + "# a single Y error indeed anti-commutes with zweb[3], so a correction appears\n", + "zx.draw(g, pauli_web=errors1)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "errors2 = errors1.copy()\n", + "\n", + "# The T-measurement itself will either apply 0.5:\n", + " errors2.add_half_edge((v, n), 'Z')\n", + "\n", + "# Finally, we apply corrections on the output just like we did in the Clifford case\n", + "if not zwebs[2].commutes_with(errors2): errors2.add_half_edge((2,1), 'X')\n", + "if not xwebs[2].commutes_with(errors2): errors2.add_half_edge((2,1), 'Z')\n", + "\n", + "zx.draw(g, pauli_web=errors2)\n", + "\n", + "# The result, after we have applied corrections, should indeed be a single T gate\n", + "ge = errors2.graph_with_errors()\n", + "zx.full_reduce(ge)\n", + "zx.draw(ge)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The next example is a circuit with some CNOT gates and a T gate, which can be simplified to a single phase gadget. I'm doing this manually here, since the automated simplifier comes up with a different answer (which is equivalent to this one, up to local Cliffords, but less clear what is going on)." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "c = zx.qasm(\"\"\"\n", + "qreg q[3];\n", + "cx q[0], q[1];\n", + "cx q[1], q[2];\n", + "t q[2];\n", + "cx q[1], q[2];\n", + "cx q[0], q[1];\n", + "\"\"\")\n", + "zx.draw(c)\n", + "\n", + "# manual ZX simplification to get a single phase gadget\n", + "g = c.to_graph()\n", + "zx.simplify.unfuse_non_cliffords(g)\n", + "zx.basicrules.strong_comp(g, 5, 7)\n", + "zx.simplify.spider_simp(g, quiet=True)\n", + "zx.basicrules.strong_comp(g, 3, 6)\n", + "zx.simplify.spider_simp(g, quiet=True)\n", + "zx.simplify.id_simp(g, quiet=True)\n", + "\n", + "zx.draw(g, labels=True)\n", + "\n", + "d = dict()\n", + "\n", + "# pauli web calculation\n", + "order, zwebs, xwebs = compute_pauli_webs(g, debug=d)\n", + "\n", + "# highlight the web associated to the T spider\n", + "zx.draw(g, labels=True, pauli_web=zwebs[15])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This and the output Pauli webs is enough data to simulate running this computation and computing corrections. Rather than doing that explicitly, lets go straight to a random Clifford+T circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Generate a random CNOT, H, T circuit\n", + "random.seed(1330)\n", + "c = zx.generate.CNOT_HAD_PHASE_circuit(qubits=3, depth=30)\n", + "g = c.to_graph(compress_rows=False)\n", + "zx.draw(g)\n", + "\n", + "# Unfuse the non-Clifford spiders, so they look like outputs measured in {|T>, Z|T>}\n", + "zx.simplify.unfuse_non_cliffords(g)\n", + "zx.draw(g, labels=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# as before, we can introduce random Pauli errors into the graph\n", + "random.seed(1337)\n", + "errors = PauliWeb.random(g)\n", + "zx.draw(g, pauli_web=errors)" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{51: 1, 52: 1, 53: 1, 54: 1, 55: 2, 56: 2, 57: 2, 58: 2}\n" + ] + } + ], + "source": [ + "# however, the correction strategy is more elaborate, since we have internal non-Clifford\n", + "# gadgets to handle. First, compute Pauli webs:\n", + "\n", + "order, zwebs, xwebs = compute_pauli_webs(g)\n", + "\n", + "# `order` tells us the order in which we should measure non-Clifford spiders. For Clifford diagrams,\n", + "# it was empty, since there were no non-Clifford spiders. In this example, it shows that we need to\n", + "# split the non-Clifford spiders into 2 time steps, where the last 4 measurements might depend on the\n", + "# outcomes of the first 4.\n", + "print(order)" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# For example, if we look at the Z-web of spider 55, it touches 51 and 52, which means we need\n", + "# to be done with those spiders first.\n", + "zx.draw(g, labels=True, pauli_web=zwebs[55])" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "errors1 = errors.copy()\n", + "\n", + "# traverse through the T spiders in the order given by `order`\n", + "for v in sorted(order.keys(), key=lambda v: order[v]):\n", + " n = next(iter(g.neighbors(v)))\n", + "\n", + " # if the Z-web associated with that spider anti-commutes with previous errors,\n", + " # introduce an X error before the measurement. This has the same effect, up to Paulis\n", + " # as choosing to measure Y rather than X in the fault-tolerant gadget\n", + " if not zwebs[v].commutes_with(errors1):\n", + " errors1.add_half_edge((v, n), 'X')\n", + " \n", + " # \"do\" the measurement, i.e. plug in |T> or Z|T> with uniform probability\n", + " if random.random() > 0.5:\n", + " errors1.add_half_edge((v, n), 'Z')\n", + "\n", + "# finally, use the output webs to correct outputs, as before\n", + "for o in g.outputs():\n", + " n = next(iter(g.neighbors(o)))\n", + " if not zwebs[o].commutes_with(errors1):\n", + " errors1.add_half_edge((o,n), 'X')\n", + " if not xwebs[o].commutes_with(errors1):\n", + " errors1.add_half_edge((o,n), 'Z')\n", + "\n", + "# here is the original web\n", + "zx.draw(g, pauli_web=errors)\n", + "\n", + "# here is the corrected web\n", + "zx.draw(g, pauli_web=errors1)\n", + "\n", + "# n.b. we can multiply these two to just show the corrections\n", + "zx.draw(g, pauli_web=errors*errors1)" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "h = g.adjoint() * errors1.graph_with_errors()\n", + "zx.full_reduce(h)\n", + "h.pack_circuit_rows()\n", + "zx.draw(h)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# The same procedure works with a reduced ZX-diagram, rather than a circuit, as input\n", + "random.seed(1337)\n", + "g = zx.generate.CNOT_HAD_PHASE_circuit(qubits=3, depth=40).to_graph()\n", + "zx.draw(g)\n", + "zx.full_reduce(g)\n", + "g.normalize()\n", + "\n", + "# for simplicity, we'll make sure all the non-Clifford spiders are Z\n", + "zx.to_rg(g, init_z=set(v for v in g.vertices() if not zx.utils.phase_is_clifford(g.phase(v))))\n", + "\n", + "zx.simplify.unfuse_non_cliffords(g)\n", + "zx.draw(g, labels=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "random.seed(1337)\n", + "order, zwebs, xwebs = compute_pauli_webs(g)\n", + "errors = PauliWeb.random(g)\n", + "errors1 = errors.copy()\n", + "\n", + "# \"simulate\" the diagram and compute corrections, as before\n", + "for v in sorted(order.keys(), key=lambda v: order[v]):\n", + " n = next(iter(g.neighbors(v)))\n", + " if not zwebs[v].commutes_with(errors1): errors1.add_half_edge((v, n), 'X')\n", + " if random.random() > 0.5: errors1.add_half_edge((v, n), 'Z')\n", + "for o in g.outputs():\n", + " n = next(iter(g.neighbors(o)))\n", + " if not zwebs[o].commutes_with(errors1): errors1.add_half_edge((o,n), 'X')\n", + " if not xwebs[o].commutes_with(errors1): errors1.add_half_edge((o,n), 'Z')\n", + "\n", + "zx.draw(g, pauli_web=errors)\n", + "zx.draw(g, pauli_web=errors*errors1)\n", + "\n", + "h = g.adjoint() * errors1.graph_with_errors()\n", + "zx.full_reduce(h)\n", + "h.pack_circuit_rows()\n", + "zx.draw(h)" ] }, { diff --git a/demos/PauliWebsRGB.ipynb b/demos/PauliWebsRGB.ipynb index bd9f8831..a9f9cfd4 100644 --- a/demos/PauliWebsRGB.ipynb +++ b/demos/PauliWebsRGB.ipynb @@ -7,10 +7,12 @@ "outputs": [], "source": [ "import sys, os, random\n", + "from fractions import Fraction\n", "sys.path.insert(0,os.path.expanduser('~/git/pyzx')) # git version\n", "sys.path.insert(0,'/workspaces/pyzx')\n", "import pyzx as zx\n", "from pyzx.pauliweb import PauliWeb, compute_pauli_webs\n", + "from pyzx import VertexType\n", "zx.settings.colors = zx.rgb_colors" ] }, @@ -18,21 +20,23 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This notebook demonstrates using PyZX's methods for automatically computing and drawing (bounded) Pauli webs. A Pauli web (a.k.a. _correlation surface_) is a coloring of the edges of ZX-diagram with the following properties for every Z spider `v`:\n", + "This notebook demonstrates using PyZX's methods for automatically computing and drawing bounded Pauli webs. A Pauli web (a.k.a. _correlation surface_) is a labelling of the edges of ZX-diagram from the set $\\{I, X, Y, Z\\}$ with certain properties. To explain these properties, it is useful to make some definitions.\n", "\n", - "- _all-or-nothing_: either no incident edges or all incident edges of `v` are labelled X or Y\n", - "- _parity_: an even number of incident edges of `v` are labelled Y or Z\n", + "To account for Hadmard edges, we label \"half-edges\" of a diagram. For a pair of connected vertices `v`, `w`, we represent the half-edge closest to `v` as `(v,w)` and the half-edge closest to `w` as `(w,v)`.\n", "\n", - "All X spiders satisfy analogous conditions, swapping the role of X and Z. A _bounded Pauli web_ is a Pauli web with a chosen set of spiders, called the _boundary_ where the parity condition is allowed to be violated.\n", + "We say a spider `v` is _stabilised_ by a Pauli web if `v` (considered as a state) is a +1 eigenstate of the Pauli string labelling its adjacent half-edges and we say `v` is _anti-stabilised_ by a Pauli web if it is a -1 eigenstate of its adjacent half-edges.\n", "\n", - "We say a spider `anti-commutes` with a Pauli web if it is touching an edge of a different colour.\n", + "A Pauli web is said to be _closed_ if:\n", + "1. every spider `v` in the ZX-diagram is stabilised or anti-stabilised\n", + "2. for every half-edge `(v,w)` labelled by a Pauli $P$, the other half-edge `(w,v)` is labelled by $P$ for a simple edge and $HPH$ for a Hadamard edge\n", + "\n", + "Otherwise, we say a Pauli web is _open_. The spiders and edges violating conditions 1 and 2 above are called the _boundary_ of a Pauli web.\n", "\n", "The function `compute_pauli_webs` automitically associates each non-input vertex `v` in the diagram to an integer `order[v]` giving a time-ordering, and one or two bounded Pauli webs, with the following properties:\n", - "1. the boundary of the web consists of only `v` itself and inputs\n", + "1. the boundary of the web consists of only `v` itself, earlier non-Clifford spiders `w` (`order[v] < order[w]>`), and inputs\n", "2. Z spiders have a web `zweb[v]` with a Z-colored edge incident to `v`\n", "3. X spiders have a web `xweb[v]` with a X-colored edge incident to `v`\n", "4. output vertices have two webs `zweb[v]` and `xweb[v]` corresponding to both colors at `v`\n", - "5. for non-Pauli spiders `v, w`, if `v` anti-commutes with the Pauli web of `w` then `order[v] < order[w]`\n", "\n", "Under the hood, this is using PyZX's gflow-finding algorithm to compute a focussed Pauli flow and translate this data into Pauli webs." ] @@ -52,7 +56,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -449,7 +453,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -828,7 +832,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -1207,7 +1211,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -1586,7 +1590,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -1979,7 +1983,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -2358,7 +2362,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -2737,7 +2741,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -3116,7 +3120,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -3513,7 +3517,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -3915,7 +3919,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -4294,7 +4298,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -4673,7 +4677,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -5052,7 +5056,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -5431,7 +5435,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -5810,7 +5814,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -6205,7 +6209,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -6597,7 +6601,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -6976,7 +6980,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -7355,7 +7359,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -7734,7 +7738,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -8113,7 +8117,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -8492,7 +8496,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -8896,7 +8900,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -9275,7 +9279,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -9678,7 +9682,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -10057,7 +10061,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -10461,7 +10465,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -10868,7 +10872,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -11247,7 +11251,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -11626,7 +11630,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -12005,7 +12009,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -12384,7 +12388,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -12763,7 +12767,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -13159,7 +13163,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -13538,7 +13542,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -13936,7 +13940,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -14315,7 +14319,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -14694,7 +14698,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -15093,7 +15097,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -15472,7 +15476,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" @@ -15851,7 +15855,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" ], @@ -16243,6 +16247,7 @@ "zx.draw(g1, labels=True)\n", "\n", "order, zwebs, xwebs = compute_pauli_webs(g1)\n", + "\n", "zx.draw(g1, pauli_web=zwebs[41])\n", "zx.draw(g1, pauli_web=xwebs[41])" ] @@ -16251,7 +16256,15 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Clifford+T Examples" + "# Errors and corrections\n", + "\n", + "Before moving on to Clifford+T examples, we'll show how to introduce errors into ZX-diagrams, correct them, and check the corrected diagram is equal to the original, error-free diagram, up to a global phase.\n", + "\n", + "We can use the `PauliWeb` class to represent arbitrary Pauli errors on half edges. Note this will _not_ usually be a valid, closed Pauli web, but we can still use the `PauliWeb` class to hold this data. At some point, it might be worth adding an `is_valid` method to `PauliWeb` to check the Pauli web conditions are satisfied.\n", + "\n", + "The kinds of errors we are interested in here are logical flips coming from lattice surgery operations. Normally, these will non-deterministically introduce X errors next to Z nodes and Z errors next to X nodes.\n", + "\n", + "Since we know at runtime where these errors are, they are always correctable. Hence, the error rate and which subset of errors _actually_ occur is irrelevant. To demonstrate the corrections for arbitrary errors, we will call `PauliWeb.random` with its default arguments, which introduces a uniformly random Pauli X, Y, or Z with 30% probability on every half-edge." ] }, { @@ -16262,7 +16275,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" ], "text/plain": [ @@ -16637,25 +16650,11 @@ }, "metadata": {}, "output_type": "display_data" - } - ], - "source": [ - "# Generate a random CNOT, H, T circuit\n", - "random.seed(1330)\n", - "c = zx.generate.CNOT_HAD_PHASE_circuit(qubits=3, depth=40)\n", - "# for g in c.gates: print(g)\n", - "zx.draw(c)" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ + }, { "data": { "text/html": [ - "
\n", + "
\n", "" ], "text/plain": [ @@ -17033,81 +17032,24 @@ } ], "source": [ - "# Convert to a ZX diagram and call the full_reduce procedure on it (PyZX's main ZX diagram optimisation pass)\n", - "g = c.to_graph()\n", - "zx.full_reduce(g)\n", - "zx.to_rg(g)\n", - "\n", - "# Normalise compacts the circuit visually and ensures every input/output is connected to a Z spider\n", - "g.normalize()\n", - "\n", - "# Compute the time-ordering on nodes (which is only important for the non-Clifford nodes) and compute the Pauli\n", - "# webs for every node.\n", - "order, zwebs, xwebs = compute_pauli_webs(g)\n", + "random.seed(1337)\n", + "g = zx.generate.CNOT_HAD_PHASE_circuit(qubits=4, depth=10, clifford=True).to_graph(compress_rows=False)\n", + "zx.draw(g, labels=True)\n", "\n", - "# Draw the simplified ZX diagram. Note blue edges correspond to edges with Hadamard gates\n", - "zx.draw(g, labels=True)" + "# introduce random Pauli errors on edges and draw them\n", + "errors = PauliWeb.random(g)\n", + "zx.draw(g, pauli_web=errors)" ] }, { "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{(3, 1): 'Y',\n", - " (1, 3): 'Y',\n", - " (3, 43): 'Z',\n", - " (43, 3): 'Z',\n", - " (3, 5): 'Y',\n", - " (5, 3): 'Y',\n", - " (3, 58): 'Z',\n", - " (58, 3): 'X',\n", - " (3, 54): 'Z',\n", - " (54, 3): 'Z',\n", - " (3, 14): 'Y',\n", - " (14, 3): 'Y',\n", - " (5, 16): 'Y',\n", - " (16, 5): 'Y',\n", - " (5, 2): 'Y',\n", - " (2, 5): 'Y',\n", - " (5, 43): 'X',\n", - " (43, 5): 'Z',\n", - " (5, 58): 'X',\n", - " (58, 5): 'X',\n", - " (5, 54): 'X',\n", - " (54, 5): 'Z',\n", - " (14, 58): 'X',\n", - " (58, 14): 'X',\n", - " (14, 16): 'Y',\n", - " (16, 14): 'Y',\n", - " (16, 43): 'Z',\n", - " (43, 16): 'Z',\n", - " (16, 58): 'Z',\n", - " (58, 16): 'X'}" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "pw = zwebs[43]\n", - "pw.half_edges()" - ] - }, - { - "cell_type": "code", - "execution_count": 19, + "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "
\n", + "
\n", "" ], "text/plain": [ @@ -17485,29 +17427,71 @@ } ], "source": [ - "# Once the Pauli webs have been computed, a specific web can be highlighted by `zx.draw` by passing it in as\n", - "# an optional argument. Note that webs change color when they cross Hadamard edges.\n", - "zx.draw(g, labels=True, pauli_web=zwebs[43])" + "# The `PauliWeb.graph_with_errors()` method returns a copy of the original graph, where\n", + "# Pauli errors are inserted as pi-spiders on the edges colored by the web.\n", + "zx.draw(errors.graph_with_errors())" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": 18, "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Z-web for output 20: commutes\n", + "X-web for output 20: anti-commutes, corrected\n", + "Z-web for output 21: anti-commutes, corrected\n", + "X-web for output 21: anti-commutes, corrected\n", + "Z-web for output 22: commutes\n", + "X-web for output 22: commutes\n", + "Z-web for output 23: anti-commutes, corrected\n", + "X-web for output 23: anti-commutes, corrected\n" + ] + } + ], "source": [ - "We now show how this works in some simpler cases. The first is a single T gate.\n", + "# To find the appropriate corrections, we should introduce Pauli X and Z gates at the outputs\n", + "# to make all 8 output webs commute with `errors`.\n", + "\n", + "order, zwebs, xwebs = compute_pauli_webs(g)\n", + "output_es = [(o, next(iter(g.neighbors(o)))) for o in g.outputs()]\n", + "\n", "\n", - "The T gate becomes a single, 1-legged phase gadget, connected to the input. This can be implemented by Z-merging a T magic state, then doing either an X or a Y measurement, depending on the parity of the Pauli web." + "errors1 = errors.copy()\n", + "\n", + "# for each of the outputs, if the errors anti-commute with the associated Z-web, introduce\n", + "# an X-error at that output to make it commute, and do similar for X-webs.\n", + "for o,n in output_es:\n", + " print(f'Z-web for output {o}: ', end='')\n", + " if zwebs[o].commutes_with(errors1):\n", + " print('commutes')\n", + " else:\n", + " print('anti-commutes', end='')\n", + " errors1.add_half_edge((o,n), 'X')\n", + " if zwebs[o].commutes_with(errors1):\n", + " print(', corrected')\n", + " print(f'X-web for output {o}: ', end='')\n", + " if xwebs[o].commutes_with(errors1):\n", + " print('commutes')\n", + " else:\n", + " print('anti-commutes', end='')\n", + " errors1.add_half_edge((o,n), 'Z')\n", + " if xwebs[o].commutes_with(errors1):\n", + " print(', corrected')" ] }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "
\n", + "
\n", "" ], @@ -17886,7 +17870,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" ], "text/plain": [ @@ -18264,36 +18248,45 @@ } ], "source": [ - "c = zx.qasm(\"\"\"\n", - "qreg q[1];\n", - "t q[0];\n", - "\"\"\")\n", - "zx.draw(c)\n", - "\n", - "g = c.to_graph()\n", - "zx.full_reduce(g)\n", - "order, zwebs, xwebs = compute_pauli_webs(g)\n", + "# All the errors have now been corrected, so the graph with Pauli errors given by `errors1` should\n", + "# be equal, up to a global phase to the origin graph\n", + "ge = errors1.graph_with_errors()\n", + "zx.draw(ge)\n", "\n", - "# highlight the web associated to the T spider\n", - "zx.draw(g, labels=True, pauli_web=zwebs[1])" + "# ...so if we compose `ge` with the adjoint of `g`, we should get the identity\n", + "h = g.adjoint() * ge\n", + "zx.full_reduce(h)\n", + "h.pack_circuit_rows()\n", + "zx.draw(h)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The next example is a circuit with some CNOT gates and a T gate, which can be simplified to a single phase gadget. I'm doing this manually here, since the automated simplifier comes up with a different answer (which is equivalent to this one, up to local Cliffords, but less clear what is going on)." + "# Clifford+T Examples" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, we'll move to some Clifford+T examples. By definition, spiders with phases that are not multiples of $\\pi/2$ must necessarily be on the boundary of a Pauli web.\n", + "\n", + "For our purposes, we will treat non-Clifford spiders as additional outputs that have been measured in the basis $\\{|T\\rangle, Z|T\\rangle \\}$, where $|T\\rangle := T|{+}\\rangle$ is a $T$ magic state.\n", + "\n", + "There are many equivalent ways to implement this fault-tolerantly in lattice surgery. One way to do this is to first perform a Z-merge between a data qubit and a $|T\\rangle$ magic state, then depending on the outcome of the merge, measure the remaining qubit in the X or Y basis." ] }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "
\n", + "
\n", "" ], "text/plain": [ @@ -18672,7 +18665,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", "" ], "text/plain": [ @@ -19050,38 +19043,10595 @@ } ], "source": [ - "c = zx.qasm(\"\"\"\n", - "qreg q[3];\n", - "cx q[0], q[1];\n", - "cx q[1], q[2];\n", - "t q[2];\n", - "cx q[1], q[2];\n", - "cx q[0], q[1];\n", - "\"\"\")\n", - "zx.draw(c)\n", - "\n", - "# manual ZX simplification to get a single phase gadget\n", - "g = c.to_graph()\n", - "zx.simplify.gadgetize(g, graphlike=False)\n", - "zx.basicrules.strong_comp(g, 5, 7)\n", - "zx.simplify.spider_simp(g, quiet=True)\n", - "zx.basicrules.strong_comp(g, 3, 6)\n", - "zx.simplify.spider_simp(g, quiet=True)\n", - "zx.simplify.id_simp(g, quiet=True)\n", - "\n", - "# pauli web calculation\n", - "order, zwebs, xwebs = compute_pauli_webs(g)\n", + "# Merge with parity 0, followed by an X measurement\n", + "g = zx.Graph()\n", + "g.add_vertex(qubit=0, row=0)\n", + "g.add_vertex(VertexType.Z, qubit=2, row=0, phase=Fraction(1/4))\n", + "g.add_vertex(VertexType.Z, qubit=1, row=1)\n", + "g.add_vertex(VertexType.Z, qubit=1, row=2)\n", + "for e in [(0,2), (1,2), (2,3)]: g.add_edge(e)\n", + "zx.draw(g, labels=True)\n", "\n", - "# highlight the web associated to the T spider\n", - "zx.draw(g, labels=True, pauli_web=zwebs[15])" + "# ...is equivalent to a |T> measurement\n", + "zx.full_reduce(g)\n", + "zx.draw(g)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "metadata": {}, - "outputs": [], - "source": [] + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Merge with parity 1, followed by an Y measurement\n", + "h = zx.Graph()\n", + "h.add_vertex(qubit=0, row=0)\n", + "h.add_vertex(VertexType.Z, qubit=2, row=0, phase=Fraction(1/4))\n", + "h.add_vertex(VertexType.Z, qubit=1, row=1)\n", + "h.add_vertex(VertexType.Z, qubit=1, row=2, phase=Fraction(1/2))\n", + "h.add_vertex(VertexType.X, qubit=1.5, row=0.5, phase=1)\n", + "for e in [(0,2), (1,4), (4,2), (2,3)]: h.add_edge(e)\n", + "zx.draw(h, labels=True)\n", + "\n", + "# ...is also equivalent to a |T> measurement\n", + "zx.full_reduce(h)\n", + "zx.draw(h)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In both cases above, we show the +1 outcome of the X/Y measurement. Getting the other outcome yields the same effect, up to a Pauli Z error, which can be corrected later.\n", + "\n", + "Using this primitive, we now look at implementing a single T gate." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "c = zx.qasm(\"\"\"\n", + "qreg q[1];\n", + "t q[0];\n", + "\"\"\")\n", + "g = c.to_graph()\n", + "zx.draw(g)\n", + "\n", + "# Unfuse the non-Clifford spiders, so they look like outputs measured in {|T>, Z|T>}\n", + "zx.simplify.unfuse_non_cliffords(g)\n", + "zx.draw(g, labels=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# The relevant Pauli webs for this example are the Z and X webs of the output 2\n", + "# as well as the Z-web of the non-Clifford spider 3.\n", + "\n", + "order, zwebs, xwebs = compute_pauli_webs(g)\n", + "zx.draw(g, labels=True, pauli_web=zwebs[2])\n", + "zx.draw(g, labels=True, pauli_web=xwebs[2])\n", + "zx.draw(g, labels=True, pauli_web=zwebs[3])" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# As we did in the Clifford examples, we can introduce random Pauli errors on all the edges\n", + "random.seed(1330)\n", + "errors = PauliWeb.random(g)\n", + "zx.draw(g, pauli_web=errors)\n", + "\n", + "errors1 = errors.copy()\n", + "\n", + "# To implement the T-measurement at node 3, we need to look at the associated Z-web of 3\n", + "# If it anti-commutes with the errors, we should apply an X correction before we measure, or equivalently,\n", + "# flip whether we are measuring X or Y in the fault-tolerant gadget that injects a magic state.\n", + "if not zwebs[3].commutes_with(errors):\n", + " errors1.add_half_edge((3,1), 'X')\n", + "\n", + "# a single Y error indeed anti-commutes with zweb[3], so a correction appears\n", + "zx.draw(g, pauli_web=errors1)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "errors2 = errors1.copy()\n", + "\n", + "# The T-measurement itself will either apply 0.5:\n", + " errors2.add_half_edge((v, n), 'Z')\n", + "\n", + "# Finally, we apply corrections on the output just like we did in the Clifford case\n", + "if not zwebs[2].commutes_with(errors2): errors2.add_half_edge((2,1), 'X')\n", + "if not xwebs[2].commutes_with(errors2): errors2.add_half_edge((2,1), 'Z')\n", + "\n", + "zx.draw(g, pauli_web=errors2)\n", + "\n", + "# The result, after we have applied corrections, should indeed be a single T gate\n", + "ge = errors2.graph_with_errors()\n", + "zx.full_reduce(ge)\n", + "zx.draw(ge)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The next example is a circuit with some CNOT gates and a T gate, which can be simplified to a single phase gadget. I'm doing this manually here, since the automated simplifier comes up with a different answer (which is equivalent to this one, up to local Cliffords, but less clear what is going on)." + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "c = zx.qasm(\"\"\"\n", + "qreg q[3];\n", + "cx q[0], q[1];\n", + "cx q[1], q[2];\n", + "t q[2];\n", + "cx q[1], q[2];\n", + "cx q[0], q[1];\n", + "\"\"\")\n", + "zx.draw(c)\n", + "\n", + "# manual ZX simplification to get a single phase gadget\n", + "g = c.to_graph()\n", + "zx.simplify.unfuse_non_cliffords(g)\n", + "zx.basicrules.strong_comp(g, 5, 7)\n", + "zx.simplify.spider_simp(g, quiet=True)\n", + "zx.basicrules.strong_comp(g, 3, 6)\n", + "zx.simplify.spider_simp(g, quiet=True)\n", + "zx.simplify.id_simp(g, quiet=True)\n", + "\n", + "zx.draw(g, labels=True)\n", + "\n", + "d = dict()\n", + "\n", + "# pauli web calculation\n", + "order, zwebs, xwebs = compute_pauli_webs(g, debug=d)\n", + "\n", + "# highlight the web associated to the T spider\n", + "zx.draw(g, labels=True, pauli_web=zwebs[15])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This and the output Pauli webs is enough data to simulate running this computation and computing corrections. Rather than doing that explicitly, lets go straight to a random Clifford+T circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Generate a random CNOT, H, T circuit\n", + "random.seed(1330)\n", + "c = zx.generate.CNOT_HAD_PHASE_circuit(qubits=3, depth=30)\n", + "g = c.to_graph(compress_rows=False)\n", + "zx.draw(g)\n", + "\n", + "# Unfuse the non-Clifford spiders, so they look like outputs measured in {|T>, Z|T>}\n", + "zx.simplify.unfuse_non_cliffords(g)\n", + "zx.draw(g, labels=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# as before, we can introduce random Pauli errors into the graph\n", + "random.seed(1337)\n", + "errors = PauliWeb.random(g)\n", + "zx.draw(g, pauli_web=errors)" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{51: 1, 52: 1, 53: 1, 54: 1, 55: 2, 56: 2, 57: 2, 58: 2}\n" + ] + } + ], + "source": [ + "# however, the correction strategy is more elaborate, since we have internal non-Clifford\n", + "# gadgets to handle. First, compute Pauli webs:\n", + "\n", + "order, zwebs, xwebs = compute_pauli_webs(g)\n", + "\n", + "# `order` tells us the order in which we should measure non-Clifford spiders. For Clifford diagrams,\n", + "# it was empty, since there were no non-Clifford spiders. In this example, it shows that we need to\n", + "# split the non-Clifford spiders into 2 time steps, where the last 4 measurements might depend on the\n", + "# outcomes of the first 4.\n", + "print(order)" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# For example, if we look at the Z-web of spider 55, it touches 51 and 52, which means we need\n", + "# to be done with those spiders first.\n", + "zx.draw(g, labels=True, pauli_web=zwebs[55])" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "errors1 = errors.copy()\n", + "\n", + "# traverse through the T spiders in the order given by `order`\n", + "for v in sorted(order.keys(), key=lambda v: order[v]):\n", + " n = next(iter(g.neighbors(v)))\n", + "\n", + " # if the Z-web associated with that spider anti-commutes with previous errors,\n", + " # introduce an X error before the measurement. This has the same effect, up to Paulis\n", + " # as choosing to measure Y rather than X in the fault-tolerant gadget\n", + " if not zwebs[v].commutes_with(errors1):\n", + " errors1.add_half_edge((v, n), 'X')\n", + " \n", + " # \"do\" the measurement, i.e. plug in |T> or Z|T> with uniform probability\n", + " if random.random() > 0.5:\n", + " errors1.add_half_edge((v, n), 'Z')\n", + "\n", + "# finally, use the output webs to correct outputs, as before\n", + "for o in g.outputs():\n", + " n = next(iter(g.neighbors(o)))\n", + " if not zwebs[o].commutes_with(errors1):\n", + " errors1.add_half_edge((o,n), 'X')\n", + " if not xwebs[o].commutes_with(errors1):\n", + " errors1.add_half_edge((o,n), 'Z')\n", + "\n", + "# here is the original web\n", + "zx.draw(g, pauli_web=errors)\n", + "\n", + "# here is the corrected web\n", + "zx.draw(g, pauli_web=errors1)\n", + "\n", + "# n.b. we can multiply these two to just show the corrections\n", + "zx.draw(g, pauli_web=errors*errors1)" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "h = g.adjoint() * errors1.graph_with_errors()\n", + "zx.full_reduce(h)\n", + "h.pack_circuit_rows()\n", + "zx.draw(h)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# The same procedure works with a reduced ZX-diagram, rather than a circuit, as input\n", + "random.seed(1337)\n", + "g = zx.generate.CNOT_HAD_PHASE_circuit(qubits=3, depth=40).to_graph()\n", + "zx.draw(g)\n", + "zx.full_reduce(g)\n", + "g.normalize()\n", + "\n", + "# for simplicity, we'll make sure all the non-Clifford spiders are Z\n", + "zx.to_rg(g, init_z=set(v for v in g.vertices() if not zx.utils.phase_is_clifford(g.phase(v))))\n", + "\n", + "zx.simplify.unfuse_non_cliffords(g)\n", + "zx.draw(g, labels=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "random.seed(1337)\n", + "order, zwebs, xwebs = compute_pauli_webs(g)\n", + "errors = PauliWeb.random(g)\n", + "errors1 = errors.copy()\n", + "\n", + "# \"simulate\" the diagram and compute corrections, as before\n", + "for v in sorted(order.keys(), key=lambda v: order[v]):\n", + " n = next(iter(g.neighbors(v)))\n", + " if not zwebs[v].commutes_with(errors1): errors1.add_half_edge((v, n), 'X')\n", + " if random.random() > 0.5: errors1.add_half_edge((v, n), 'Z')\n", + "for o in g.outputs():\n", + " n = next(iter(g.neighbors(o)))\n", + " if not zwebs[o].commutes_with(errors1): errors1.add_half_edge((o,n), 'X')\n", + " if not xwebs[o].commutes_with(errors1): errors1.add_half_edge((o,n), 'Z')\n", + "\n", + "zx.draw(g, pauli_web=errors)\n", + "zx.draw(g, pauli_web=errors*errors1)\n", + "\n", + "h = g.adjoint() * errors1.graph_with_errors()\n", + "zx.full_reduce(h)\n", + "h.pack_circuit_rows()\n", + "zx.draw(h)" + ] }, { "cell_type": "code", @@ -19093,7 +29643,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -19107,7 +29657,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.12.0" } }, "nbformat": 4, diff --git a/pyzx/gflow.py b/pyzx/gflow.py index bf432cff..b106021c 100644 --- a/pyzx/gflow.py +++ b/pyzx/gflow.py @@ -137,7 +137,7 @@ def gflow( if not correct: if len(vertices) == len(processed): - return {v: k - i - 1 for v,i in l.items()}, gflow + return {v: i if reverse else k - i - 1 for v,i in l.items()}, gflow return None else: processed.update(correct) diff --git a/pyzx/pauliweb.py b/pyzx/pauliweb.py index ba865e27..dcae3f48 100644 --- a/pyzx/pauliweb.py +++ b/pyzx/pauliweb.py @@ -17,10 +17,11 @@ from __future__ import annotations from .gflow import gflow -from .utils import EdgeType, VertexType, vertex_is_zx +from .utils import EdgeType, VertexType, vertex_is_zx, phase_is_clifford from .graph.base import BaseGraph, VT, ET from typing import Any, Optional, Set, Dict, Tuple, Generic +import random def multiply_paulis(p1: str, p2: str) -> str: if p1 == 'I': return p2 @@ -57,6 +58,22 @@ def __init__(self, g: BaseGraph[VT,ET]): self.g = g self.es: Dict[Tuple[VT,VT], str] = dict() + @staticmethod + def random(g: BaseGraph[VT,ET], pX=0.1, pY=0.1, pZ=0.1): + w = PauliWeb(g) + for e in g.edges(): + s,t = g.edge_st(e) + for _ in range(2): + p = random.random() + if p < pX: + w.add_half_edge((s,t), 'X') + elif p < pX + pY: + w.add_half_edge((s,t), 'Y') + elif p < pX + pY + pZ: + w.add_half_edge((s,t), 'Z') + s,t = (t,s) + return w + def copy(self) -> PauliWeb: pw = PauliWeb(self.g) pw.es = self.es.copy() @@ -98,15 +115,73 @@ def commutes_with(self, other: PauliWeb): if p1 != 'I' and p2 != 'I' and p1 != p2: comm = not comm return comm + + def graph_with_errors(self) -> BaseGraph[VT,ET]: + g = self.g.clone() + edges = set((s,t) if s < t else (t,s) for s,t in self.es) + for s,t in edges: + p0 = self.es.get((s,t), 'I') + p1 = self.es.get((t,s), 'I') + + e = g.edge(s, t) + et = g.edge_type(e) + g.remove_edge(e) + + spots = 1 + for p in [p0,p1]: + if p == 'Y': spots += 2 + elif p == 'X' or p == 'Z': spots += 1 + + q, r = (g.qubit(s), g.row(s)) + dq, dr = ((g.qubit(t) - q)/spots, (g.row(t) - r)/spots) + + v0 = s + v1 = t + if p0 == 'X': + q += dq; r += dr + v0 = g.add_vertex(VertexType.X, q, r, phase=1) + g.add_edge((s, v0)) + elif p0 == 'Y': + q += dq; r += dr + v = g.add_vertex(VertexType.X, q, r, phase=1) + q += dq; r += dr + v0 = g.add_vertex(VertexType.Z, q, r, phase=1) + g.add_edge((s, v)) + g.add_edge((v, v0)) + elif p0 == 'Z': + q += dq; r += dr + v0 = g.add_vertex(VertexType.Z, q, r, phase=1) + g.add_edge((s, v0)) + if p1 == 'X': + q += dq; r += dr + v1 = g.add_vertex(VertexType.X, q, r, phase=1) + g.add_edge((t, v1)) + elif p1 == 'Y': + q += dq; r += dr + v1 = g.add_vertex(VertexType.Z, q, r, phase=1) + q += dq; r += dr + v = g.add_vertex(VertexType.X, q, r, phase=1) + g.add_edge((v1, v)) + g.add_edge((v, t)) + elif p1 == 'Z': + q += dq; r += dr + v1 = g.add_vertex(VertexType.Z, q, r, phase=1) + g.add_edge((t, v1)) + + g.add_edge((v0,v1), et) + return g -def transpose_corrections(c: Dict[VT, Set[VT]]) -> Dict[VT, Set[VT]]: - ct: Dict[VT, Set[VT]] = dict() - for k,s in c.items(): - for v in s: - if v not in ct: ct[v] = set() - ct[v].add(k) - return ct + + + +# def transpose_corrections(c: Dict[VT, Set[VT]]) -> Dict[VT, Set[VT]]: +# ct: Dict[VT, Set[VT]] = dict() +# for k,s in c.items(): +# for v in s: +# if v not in ct: ct[v] = set() +# ct[v].add(k) +# return ct def compute_pauli_webs(g: BaseGraph[VT,ET], backwards:bool=True, debug:Optional[Dict[str,Any]]=None) -> Tuple[Dict[VT, int], Dict[VT, PauliWeb[VT,ET]], Dict[VT, PauliWeb[VT,ET]]]: g1 = g.clone() @@ -155,12 +230,11 @@ def compute_pauli_webs(g: BaseGraph[VT,ET], backwards:bool=True, debug:Optional[ if not gf: raise ValueError("Graph must have gFlow") order, corr = gf + vset = g.vertex_set() + order = { v: i for v,i in order.items() if v in vset and not phase_is_clifford(g.phase(v)) } zwebs: Dict[VT, PauliWeb[VT,ET]] = dict() xwebs: Dict[VT, PauliWeb[VT,ET]] = dict() - vset = g.vertex_set() - # corr_t = transpose_corrections(corr) - for v,c in corr.items(): pw = PauliWeb(g) for v1 in c: diff --git a/pyzx/simplify.py b/pyzx/simplify.py index 1d4cdbe7..9823d77c 100644 --- a/pyzx/simplify.py +++ b/pyzx/simplify.py @@ -599,6 +599,20 @@ def to_graph_like(g: BaseGraph[VT,ET]) -> None: assert(is_graph_like(g,strict=True)) +def unfuse_non_cliffords(g: BaseGraph[VT,ET]) -> None: + """Unfuses any non-Clifford spider into a magic state connected to a phase-free spider + + Replaces any spider with a non-Clifford phase p and degree n > 1 with a degree 1 spider with phase p connected to + a degree n+1 spider with phase 0. + """ + for v in list(g.vertices()): + ty = g.type(v) + p = g.phase(v) + if vertex_is_zx(ty) and not phase_is_clifford(p) and g.vertex_degree(v) > 1: + v1 = g.add_vertex(ty, qubit=-1, row=g.row(v), phase=p) + g.set_phase(v, 0) + g.add_edge((v, v1)) + def to_clifford_normal_form_graph(g: BaseGraph[VT,ET]) -> None: """Converts a graph that is Clifford into the form described by the right-hand side of eq. (11) of *Graph-theoretic Simplification of Quantum Circuits with the ZX-calculus* (https://arxiv.org/abs/1902.03178).