From c0627e2c90dd9598c6ebaf13212786e27c339962 Mon Sep 17 00:00:00 2001 From: Craig Gidney Date: Fri, 26 Jul 2024 18:03:01 -0700 Subject: [PATCH] Add lassynth to glue directory (#803) Supporting code for https://arxiv.org/abs/2404.18369 Author: Daniel Tan --- glue/lattice_surgery/README.md | 39 + glue/lattice_surgery/docs/demo.ipynb | 645 ++++ glue/lattice_surgery/lassynth/__init__.py | 2 + .../lassynth/lattice_surgery_synthesis.py | 590 ++++ .../lassynth/rewrite_passes/__init__.py | 0 .../lassynth/rewrite_passes/attach_fixups.py | 86 + .../lassynth/rewrite_passes/color_z.py | 210 ++ .../rewrite_passes/remove_unconnected.py | 152 + .../lassynth/sat_synthesis/__init__.py | 0 .../sat_synthesis/lattice_surgery_sat.py | 1842 ++++++++++++ .../lassynth/tools/__init__.py | 0 .../lassynth/tools/verify_stabilizers.py | 38 + .../lassynth/translators/__init__.py | 1 + .../lassynth/translators/gltf_generator.py | 2622 +++++++++++++++++ .../translators/networkx_generator.py | 53 + .../lassynth/translators/textfig_generator.py | 217 ++ .../lassynth/translators/zx_grid_graph.py | 292 ++ glue/lattice_surgery/setup.py | 28 + glue/lattice_surgery/stimzx/__init__.py | 14 + .../stimzx/_external_stabilizer.py | 90 + .../stimzx/_external_stabilizer_test.py | 7 + .../stimzx/_text_diagram_parsing.py | 178 ++ .../stimzx/_text_diagram_parsing_test.py | 149 + .../stimzx/_zx_graph_solver.py | 196 ++ .../stimzx/_zx_graph_solver_test.py | 137 + 25 files changed, 7588 insertions(+) create mode 100644 glue/lattice_surgery/README.md create mode 100644 glue/lattice_surgery/docs/demo.ipynb create mode 100644 glue/lattice_surgery/lassynth/__init__.py create mode 100644 glue/lattice_surgery/lassynth/lattice_surgery_synthesis.py create mode 100644 glue/lattice_surgery/lassynth/rewrite_passes/__init__.py create mode 100644 glue/lattice_surgery/lassynth/rewrite_passes/attach_fixups.py create mode 100644 glue/lattice_surgery/lassynth/rewrite_passes/color_z.py create mode 100644 glue/lattice_surgery/lassynth/rewrite_passes/remove_unconnected.py create mode 100644 glue/lattice_surgery/lassynth/sat_synthesis/__init__.py create mode 100644 glue/lattice_surgery/lassynth/sat_synthesis/lattice_surgery_sat.py create mode 100644 glue/lattice_surgery/lassynth/tools/__init__.py create mode 100644 glue/lattice_surgery/lassynth/tools/verify_stabilizers.py create mode 100644 glue/lattice_surgery/lassynth/translators/__init__.py create mode 100644 glue/lattice_surgery/lassynth/translators/gltf_generator.py create mode 100644 glue/lattice_surgery/lassynth/translators/networkx_generator.py create mode 100644 glue/lattice_surgery/lassynth/translators/textfig_generator.py create mode 100644 glue/lattice_surgery/lassynth/translators/zx_grid_graph.py create mode 100644 glue/lattice_surgery/setup.py create mode 100644 glue/lattice_surgery/stimzx/__init__.py create mode 100644 glue/lattice_surgery/stimzx/_external_stabilizer.py create mode 100644 glue/lattice_surgery/stimzx/_external_stabilizer_test.py create mode 100644 glue/lattice_surgery/stimzx/_text_diagram_parsing.py create mode 100644 glue/lattice_surgery/stimzx/_text_diagram_parsing_test.py create mode 100644 glue/lattice_surgery/stimzx/_zx_graph_solver.py create mode 100644 glue/lattice_surgery/stimzx/_zx_graph_solver_test.py diff --git a/glue/lattice_surgery/README.md b/glue/lattice_surgery/README.md new file mode 100644 index 000000000..613674808 --- /dev/null +++ b/glue/lattice_surgery/README.md @@ -0,0 +1,39 @@ +# Lattice Surgery Subroutine Synthesizer (LaSsynth) +A lattice surgery subroutine (LaS) is a confined volume with a set of ports. +Within this volume, lattice surgery merges and splits are performed. +The function of a LaS is characterized by a set of stabilizers on these ports. + +The lattice surgery subroutine synthesizer (LaSsynth) uses SAT/SMT solvers to synthesize LaS given the volume, the ports, and the stabilizers. +LaSsynth outputs a textual representation of LaS (LaSRe) which is a JSON file with filename extension `.lasre`. +LaSsynth can also generate 3D modelling files in the [GLTF](https://www.khronos.org/gltf/) format from LaSRe files. + +The main ideas of this project is provided in the paper [A SAT Scalpel for Lattice Surgery](http://arxiv.org/abs/2404.18369) by Tan, Niu, and Gidney. +For files specific to the paper, please refer to [its Zenodo archive](https://zenodo.org/doi/10.5281/zenodo.11051465). + +## Installation +It is recommended to create a virtual Python environment. Once inside the environment, in this directory, `pip install .` +Apart from LaSsynth, this will install a few packages that we need: + - `z3-solver` version `4.12.1.0`, from pip + - `networkx` default version, from pip + - `stim` default version, from pip + - `stimzx` from files included in sirectory `./stimzx/`. We copied these files from [here](https://github.com/quantumlib/Stim/tree/0fdddef863cfe777f3f2086a092ba99785725c07/glue/zx). + - `ipykernel` default version, from pip, to view the demo Jupyter notebook. + +We have a dependency [kissat](https://github.com/arminbiere/kissat) which is a SAT solver, not a Python package. +It is recommended to install it and find out the directory of the executable `kissat` because we will need it later. +LaSsynth can be used without Kissat, in which case it just uses `z3-solver`, but on certain cases Kissat can offer big runtime improvements. + +## How to use +See the [demo notebook in the docs directory](docs/demo.ipynb) + +## Cite this work +```bibtex +@inproceedings{tan-niu-gidney_lattice_surgery, + author = {Tan, Daniel Bochen and Niu, Murphy Yuezhen and Gidney, Craig}, + title = {A {SAT} Scalpel for Lattice Surgery: Representation and Synthesis of Subroutines for Surface-Code Fault-Tolerant Quantum Computing}, + shorttitle = {A {SAT} Scalpel for Lattice Surgery}, + booktitle = {2024 ACM/IEEE 51st Annual International Symposium on Computer Architecture ({ISCA})}, + year = {2024}, + url = {http://arxiv.org/abs/2404.18369}, +} +``` \ No newline at end of file diff --git a/glue/lattice_surgery/docs/demo.ipynb b/glue/lattice_surgery/docs/demo.ipynb new file mode 100644 index 000000000..36dfe2185 --- /dev/null +++ b/glue/lattice_surgery/docs/demo.ipynb @@ -0,0 +1,645 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Introduction to the LaSSynth, Lattice Surgery Subroutine Synthesizer" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisites" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "This Jupyter notebook aims at giving a minimal demo on how to use this software.\n", + "The reader needs to know how lattice surgery works to fully understand this notebook.\n", + "The most direct reference is [our paper](http://arxiv.org/abs/2404.18369) which, in itself, also provides more pointers to background knowledge references.\n", + "There are two we would like to mention here.\n", + "- [arXiv:1704.08670](https://arxiv.org/abs/1704.08670) links merging and spliting operations in lattice surgery to ZX calculus.\n", + "We leverage this connection a lot in our software.\n", + "- [arXiv:1808.02892](https://arxiv.org/abs/1808.02892) is helpful because it works through some examples of composing lattice surgery operations to perform quantum computation." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Introduction" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "In what follows, we are assuming a surface code quantum memory with nearest-neighbor connectivity among the qubits (in both the physical and the logical sense).\n", + "We perform fault-tolerant quantum computing with lattice surgery between patches of physical qubits.\n", + "Some of these patches correspond to logical qubits whereas others can be temporary ancilla during computation.\n", + "\n", + "Since the logical qubits are in a 2D grid, and there is the time dimension, the compilation problem is laying out operations in a 3D grid to realize certain computation.\n", + "We consider only a bounded spacetime and what *can* be realized within the bounds is called a *lattice surgery subroutine* (LaS), because it should be considered as a subroutine in the whole quantum algorithm.\n", + "\n", + "Because of the connection between lattice surgery and ZX calculus, a LaS can be seen as a ZX diagram with nodes at points in a 3D grid and edges only between nearest neighbors, as seen in the figure below.\n", + "If you have worked with ZX calculus, you would know that this ZX diagram is a CNOT.\n", + "However, it seems that there are two \"unnecessary\" identity nodes in the middle.\n", + "This is because there are other constraints when it comes to realizing the CNOT in a surface code memory.\n", + "Our representation of a LaS, the \"pipe diagram\" below, does account for these extra constraints." + ] + }, + { + "attachments": { + "e1d30228-b8e9-4608-942a-e09fe765c155.png": { + "image/png": "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" + } + }, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Screenshot from 2023-09-14 05-14-00.png](attachment:e1d30228-b8e9-4608-942a-e09fe765c155.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Pipe diagrams" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "\n", + "We use a 3D coordinate system with basis I, J, and K.\n", + "We avoid using X, Y, and Z because these letters are also used in ZX calculus for different purposes.\n", + "K is the time dimension while I and J are the space dimensions.\n", + "\n", + "If we want the pipe diagram to look like exactly what happens on chip, we need to draw them in scale, e.g, shown on the right below.\n", + "There are four patches of surface codes, and only three are used in the computation.\n", + "These three are identified by their coordinates in the I-J plane: (1,0), (1,1), and (0,1) from left to right in the picture.\n", + "Between the patches, there are some \"gaps\" which are lines on physical qubits.\n", + "We can perform merging and splitting of patches with these gaps.\n", + "Since the gap is very narrow compared to the patches, but what happens there are really what decides the computation.\n", + "Thus, to see these merging and splitting more clearly, we often stretch the gaps in the pipe diagram, resulting in something like the picture on the left below.\n", + "\n", + "The unit in all three dimension is the code distance.\n", + "So, a patch going through a full QEC cycle will become a cube.\n", + "These cubes are sitting at integer points in the I-J-K grid.\n", + "Nontrivial logical operations are done by connecting these cubes with pipes.\n", + "For example, two cubes connected in the I-J plane correspond to performing merging and splitting of two patches; a cube that has a vertical connection below but not above is a logical measurement; etc.\n", + "At this point, we see that the problem of compiling LaS is laying out these cubes and pipes in a limited spacetime." + ] + }, + { + "attachments": { + "dac9ce3a-5960-445f-a5d9-f13e0ac44c80.png": { + "image/png": "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" + } + }, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Screenshot from 2023-09-14 05-55-57.png](attachment:dac9ce3a-5960-445f-a5d9-f13e0ac44c80.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## LaS Specification" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are some constraints in the construction of these diagrams, e.g., matching colors at intersection of two pipes, but we are not going to introduce them here.\n", + "After all, the purpose of a synthesizer is to let a computer consider those constraints instead of humans.\n", + "The reader can refer to our paper, or even to the code in this repo for these constraints later on.\n", + "What we are going to detail now is how to specify a problem to the compiler, so that the reader can start using the software." + ] + }, + { + "attachments": { + "4fef384b-1f6b-4712-8dcf-8e3f7b973a2f.png": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAi4AAAENCAYAAAAomu7aAAAABHNCSVQICAgIfAhkiAAAABl0RVh0U29mdHdhcmUAZ25vbWUtc2NyZWVuc2hvdO8Dvz4AACAASURBVHic7N15XJTl+vjxzwwM+yqLIKKIokQaejQXcEPE3MqFVHAJ9ZhLR7E0S+249cPMY6VmVu65ZKZgrpngvuZSiimCGyggIiAIyDYw8/uD7zw5Acq+6P1+vXgFM/fzPNeMzcw193LdMrVarUYQBEEQBKEOkJfnIJHrCMKLS7y+BUGozcqVuBw6dIjp06fz+PHjyo5HEIQqsmnTJrp06ULnzp0ZM2ZMie1iYmKYMGECUVFR1RidIAhC6cjKO1R06NAhJk2aRFhYGE5OTpUcliAIVaFly5Zcu3YNd3d3Ll++XGK7+Ph4unbtyrfffssbb7xRjREKgiA8W7l6XAB69uyJp6cnffr0ITMzszJjEgShihgYGJSqnYODA3PmzGHQoEFcuXKliqMSBEEovecmLtnZ2SXeN378eCIjI5k9e3alBiUIQvVQq9Xk5eUVe5+fnx8KhYJ33nkHlUpVzZEJgiAUr9jE5erVqwwfPhw/Pz9cXV2xtLRk3rx5RSbtderUCQsLC7777jvi4uKqJWBBECpOrVbz+eefY2VlhaGhId7e3ty9e1erjYGBAV5eXoSHh7Njx44ailQQBEFbkcTlzp07dOrUifT0dLZt28aZM2dIS0vj008/Zc2aNVptZTIZLVq0ID8/nx9//LHaghYEoWJu3LhB48aNuXXrFt27d+fIkSP4+PiQm5ur1c7V1RWAH374oQaiFARBKKpI4nLo0CEyMzPZv38/9+7do0GDBtJ958+fL3KC+vXrA3Dy5MkqDFMQhMrUokUL/P39qVevHlOmTAHg5s2bhISEaLXTvL7PnDkjhosEQagVdP95w5AhQ/jjjz+wtLSkQYMG/PLLL9J9//w2BmBiYgLA7du3qzBMQRCqirOzs/T777//zvDhw6W/Na/v9PR0kpKSpERGEAShphTpcbG0tGTVqlV06dKFDh068ODBA+m+4lZO6+oW5j5iZZEg1E2GhobS7/+cjK95fYN4jQuCUDsUSVzUajVjxoyhf//+vP/++7z33nvPPIFSqQTAzMysaiIUBKFKPb2qqFGjRlr3aV7fIF7jgiDUDkUSl3Xr1vHDDz+go6PDiBEjnnuC9PR04O9JfIIg1H5Pz1dJTk6Wfn/rrbe02mle31ZWVtjY2FRPcIIgCM9QZI7LH3/8AUBBQQFjx45FLpejo6NDQUEBDx8+JDw8HHd3d6l9QkICAD169KimkAVBqKjExERUKhVyuZzTp08DMHz4cK3XNvz9+vby8qr2GAVBEIpTJHEZOXIkW7duJT09nfj4eFauXElBQQGbNm3i0qVLpKWlSW0LCgqIiorC2NgYf3//ag1cEISyk8lk/Pvf/+bhw4cMHz6ctm3b8tVXXzF69Gi+++67Iu2vXbsGwLhx46o7VEEQhGIVu1dRTk4O2dnZWFpaSrc9fPgQCwsL9PT0pNuOHDmCt7c3QUFBfPLJJ9UTsSAI5RYTEyPtLXbv3j3i4uJo1qwZtra2RdpmZmZSv359PDw8CAsLq+ZIBUEQilfuTRYBBg4cyOPHjwkLC9NaffAiUqvVFBQU1HQYwlM00zTk5d5xq+7Q0dFBJpNV6zWXLVvG4sWLuXjxIg4ODtV6bUEQhJKUO9tYtWoVWVlZ7Nmz54VPWqBwWCw8/M+aDkN4SkxMPrm5alq0UNR0KFXutddao1DoPb9hJblw4QIbNmzg6NGjImkRBKFWKVfGcejQIXR0dDh48GC1fwsUBKFqxcTEcPDgQU6dOoWpqWlNhyMIgqClQkNFL5P8/HzR41LLiB4XQRCEl8+LP8YjCAIAv/zyC4MHD67pMARBqEUCAwNZvnx5TYdRJi/BtEZBEAAuXrxY0yEIglDLfP3117z77rs1HUaZiB4XQXjJ7Nu3j379+tV0GIIg1LCkpCS6du3K2rVrUalUrF27tk7MWxU9LoIgCILwErKxseHEiRO4urqyfv16xo0bV+xmyrWN6HERXiixsfrs2WNDcHB9cnML8/Ju3VLp3TuZbt0Kqz7/9psVn33WhPx8Gbq6atq1S2fEiATats2oydAFQRCqnSZ56dq1K+vXrweo9T0vInERXiiOjrn85z9x3LhhxOnTFnTtmsaiRbe02nTv/oilSxvh7JzNzJkxNG6cU0PRCoIg1Ly6lryIxEV4IT14oA/AgAEPtW5PS9Nl1qxmDByYxIQJcS9F1V1BEITnKS55WbduXQ1HVTzxti28cB490uXOHUP09dV06JAu3X71qjFTprRg5MgEJk0SSYsgCMLT/jnn5b333qvpkIpV7h4XpVLJpUuXuHjxInFxcWRmZpKRkUFubm5lxlcnGRsbY2hoiKmpCc7Ozri4NMPY2Kimw3ppnD5tgVoN7ds/xsCgcEOjrVvtOH7ckiVLbmJnl1fDEQqCINROmuTFw8ND2jH+22+/reGotJU5cUlKSiI4OJjff/8dpVIp3W5mZoa1tTVGRi/3B3R+fj6PHz8mPT2d5ORkoqNjOHz4CA0bOtC5sycODg1qOsQX3qlTFgB07ZpKRoYOn37qjL19LitXRqKrW/tnzAuCINQkGxsbTp48SZcuXWpl8lLqxCUjI4Pt27dz+PBhAJydnWnVqhWvvPIKjo6OVRZgXfbo0SMiIyO5fv06f/75J9u2badx40Z0794Va2vrmg7vhZSfD+fOmSOTgZWVkgkTXuHdd+Px8kqt6dAEQRDqDDs7u1qbvJQqcYmJiWHJkiWkpqZibW3N22+/TatWrao6tjqvXr16eHh44OHhwZtvvslPP/3EjRs32LLlJ7y9vWjVqmVNh/jCCQ83JTNTB2trJbNmuaCjo8bV9UlNhyUIglDn1Nbk5bnTE48cOcLs2bNJTU2lS5cuLFiwQCQt5WBra8vUqVMJCAigoKCA0NBD/PbbwZoO64WjGSby9n7EoEEPycqS89lnTWo4KqEy5ebmkpycXNNh1GlxcXFER0ejUqlqOhShltMkL82aNeO7776rFRN2n5m4nD9/XlrL/fbbb+Pn51ddcb2w2rdvz4cffoihoSHXrl3n119/q+mQXiinTxcmLl26pPHee7HY2+dx9qw5+/eLobkXxejRo2ncuDE5OVVff2f37t3IZLJifywtLSt07pSUFLKysiop0tI5c+YMbm5uODo64uzsTKNGjQgJCanWGIS6p7YlLyUmLtHR0axcuRKAd955By8vr2oL6kXXpEkTZsyYgb6+PtevR/Lnn5dqOqQXwv37ety5Y4iRkYq2bdMxMlIxa1Y0AF991YhHj0TZohfBuHHjmD9/PgYGBlV+rddff53g4GCCg4MZOnQoABs3biQ4OJiNGzdW6Nx2dnbMmzevMsIslfv379OnTx+cnZ25fPkyUVFR9O/fn6FDh3L+/Plqi0Oom2pT8lJs4pKfn8/XX3+NUqnkjTfeoH379tUd1wuvfv36TJw4EZlMxrFjJ0hNTavpkOo8zTBRhw6PUSgKVw95eDymT58UHj/W5X//c6rB6Gq/goICsrOzpb+f/r041d1boOHt7c2MGTOe206pVLJjxw7u3LlT7ms1aNAAX19ffH19cXNzA2DAgAH4+vry1ltvlXicWq2uttIQpd1bZvXq1SiVSjZu3Ii7uzvNmzdn5cqVODk5sXTp0iqOUngR1JbkpdjE5eDBgyQmJtKyZUvefPPN6o7ppdG8eXP8/f1Rq9UcPnykpsOp8zTDRJ07ayeB06ffxcIin0OH6nH8uEVNhFar2dnZMWnSJOzs7DA2NiYwMJCePXtiZGSEq6sr8fHxUtvMzEymTZuGra0txsbGmJubM27cOFJT/161FRERgYODQ5HkYs+ePdjY2LB69eoyxzhz5kxsbGykHzs7u+ces3r1aoYOHcrAgQPLfL2ySk9Px8bGhp9//pmpU6diamqKkZER7dq1488//5Ta7d27V3oM+fn5rFy5Uvq7adOm5br2wIEDmTNnjta/U3HOnj1L8+bNsbKykm5Tq9U4ODhw5syZcl1bePnUhuSlSOKSmZlJSEgICoWCgICAWrtXwYvC09OTpk2bcvfuPaKjo2s6nDorO1vOxYvmAHTqpJ24WFjkM2lSHACLFjUhLU0MGT0tJSWFa9eusX37dnr37s2KFSvo0aMHv/zyC3fv3tUq+z1+/Hg2bdrE8uXLuXLlCkuXLmXbtm2MGzdOauPm5sZHH33El19+SWhoKFA4TDF27Fjat2/Pu+++W+YY+/fvT1BQEEFBQbRu3ZqUlJTnHuPu7o6joyPe3t5lvl5ZqdVqkpOTmTx5Mmq1ml27drF582YSExPx8/MjPz8fgA4dOrBt2za2bduGXC6nf//+0t+aMutl1apVK5YvX46TkxN+fn6cPn262HbJyclaZRji4+Pp3r07J0+eFJOdhTKp6eSlyDv40aNHycnJwdvb+6UvJlddevfuzcqVK7l48RJNmogVMOVx8KAVubkyTE0LsLVVFrm/TZvCnZ+TkxXMnt2MZcui0NMTxeg0+vXrh5eXF5GRkRw4cIDAwEBMTAorP9+9e1dq5+joyJo1axg0aBBQ+KEZERHB0qVLycvLQ09PD4CpU6dy7NgxRo8eTXh4OKNGjcLQ0JBNmzaV68tQ586d6dy5MwC3bt3i2LFjpTrm3r17Zb5WRQwbNoyvv/5a+jsjI4OJEydy69YtXF1dsbW1lRIpuVxO48aNK5xYBQUFMXv2bHbu3MmmTZvo2rUrrVu3ZsqUKfj5+UlzgVQqlfTcHzlyBH9/fzp06EBAQAA///xzhWIQXj41uVS6SI/LxYsXgcKeAKF6uLm5YW5uTmxsrNgyoYwiIsz56qtGfPllYwAyMnT46CMXrVVER49aMmfO393w58+b4efXipUrG4rel3+Q/98GTpoPOLlcrrVkdvHixfTr14/jx4+zefNmVq1aRUxMDCqVqsicmA0bNqCvr0+bNm04ceIE27Zt0xqmeBH984uHi4sLALGxsVV6XSMjI0aOHEloaCj37t2jefPmjBkzhsWLF2u1U6lUfPbZZ/Tt25fp06eze/fuCq+OEl5eNdXzovWunZaWxs2bN6lXrx7169evlgCEQu7u7pw4cYJbt27z6qtuNR1OneHm9phBg7KYNq3kb9ZeXqmicm4lCQ0N5Z133gGgZcuWWFhYcOPGjWLbWlhYMG/ePMaMGYOfn99L+WVIR0cHoFrqpSQlJbFlyxY2bNhAREQEgwcPZvDgwdL9crmco0ePcv36dcLCwujSpYsUm1zsOCqUkyZ56dSpU7X1vGj933r79m0A2rRpU6UXFYrSFPV78CCxhiOpPqVcDCHUEhkZGQwZMoRevXoRGxvLoUOHCA4OJiAgoNj2qampzJ8/n4YNGxIcHMzJkyerOeKXQ1hYGIMGDcLBwYGgoCD69OnD7du3CQkJ0SoWamVlhb29PZcvX5aSFoDExESxBYlQIZrkxcnJie+++44PPvigSq+nlbg8evQIgEaNGpV4QExMjDScVFskJiZy4MCBmg6jQjT7PWVmZtZwJFVv3z5rBgxwp2PH1/Hza8XFi6Y1HZJQCtHR0aSnp+Pj44NCoZBuT0hIKNJWrVYTEBBAdnY2Fy5coE+fPvj5+ZGUlFSdIRMbG0teXu3cDdzc3JzHjx9X+Dxz587l9u3brFy5kri4OBYvXkzjxo2LtOvYsSMpKSnSPCQo/He6fPkyHTt2rHAcwsutYcOGUvKybNmyKk1eik1cTE2LfpDcuXOHSZMm0a5dO3bu3FllAZXHwoULGTFiBH/99VdNh1JuJiYmQOG32hdZaGg9/vzTlBkz7jJ1aiwJCXpMnepKQoLe8w8WapSLiwuWlpYsWbKEI0eOcP78eT766COpBsjTdV2++OIL9u3bx8aNG7Gzs5NWzIwYMaJcwyZXrlxh9+7d7N69m5s3b6JSqaS/Dx4sfuuMHTt20KhRI/r27VuOR1vowYMH0nUiIyMB2L9/P7t37+bXX38t93mhsIp2SEgIBw8e5MaNGxw8eLBctXHWr1/PlStXePfddzE0NCyx3YQJE9DX18ff358rV65w8+ZNJk+ezM2bN6v8G7Lwcqiu5KXIHBcAMzOzIg2dnZ1ZsGBBjc8+VyqVREdH07x5c+m29957D1dXV6lAVF0kk8kwMzN74XtclEoZc+f+vezbyKiAoKAmXLpkir3985e4CjXH0NCQkJAQxowZI62E8fT0ZM6cOSxYsICrV69ib2/P6dOnmT17NtOmTaN3794AWFtbs2nTJnr16kVQUBBz584t07XXrFnDN998o3Wbpj5Lw4YNi538qkmQKlLS4dy5c0XqwIwYMQIonMPzdP2aslq6dCkDBw6UniNTU1POnTvHK6+8UqbzlLa9g4MDBw4cYOzYsbi7uwOFhTC3bdsmelyESqNJXrp06cKyZcsAKr3AoVbiolQWXUb6NBsbm0q9eHns3LkTmUymlbi4urri6upag1FVDl1d3Rd+VVG/ftrJiWbnZheXmqnCKmi/7idMmMCECROkv//Zi+nl5UVMTAxxcXEoFAppEv/8+fOlNp6ensW+l3h7e1NQUKB1m6GhYYl7Dk2ePJkVK1YAsGLFCun30ho2bBidO3fG1ta2TMc9bcCAAaWqTGtubl5su27dupV4fIsWLYiIiCA2NpaCggIaNWokTeatKh4eHkRGRhIbG4tSqcTJyUlMzBUqXVUnL2VaC1rTxehu377NrFmz+Pzzz2s0DqHyREQYM3z4A1xcnl1eXqhdGjZsWCnnCQ0NLXHoyMHBocLnr4xzVCWZTPbMOYVVRTOnThCqSlUmL5VWxGLz5s388ccfUgXGSZMmFVn+GBkZyapVq8jJySEhIQFfX19GjRol3R8TE8OyZcswNDQkKiqKZs2aMXv2bCwsLDh79izvvvsuaWlpbNy4kePHjzNq1CheffVVgoOD2bp1Kzt27MDC4u+S7tHR0axatYrs7Gzu3LmDh4cHgYGBGBsbA3D16lU2btyIubk57733HgsWLOD8+fN06tSJzz//XJrEplar8ff3x8zMrFzlyoXiXbpkyk8/2bFsWfHLaYUX39OrWwRBeLFUVfJSbOJS1q7D999/Hz09PSmw5cuXM2DAANauXSuND//+++8EBgaya9cuGjRowI8//siUKVNQKpWMHTuWJ0+e0K9fP/7zn//w3nvvkZSUROvWrcnLy2PZsmW8/vrrrF+/nt69ezN8+HB8fX1RKBSEhISwfv16rl27ptUNff36dfz8/Ni1axdNmjQhISGB/v37c+jQIX799VcSEhLYv38/69atw8fHhwULFtCtWzcsLCxYsWIFLi4uTJo0CSjsSj937pyU8NRmGRkZqNVgZlZ7V+rk58v48Uc7tm61IyVFwfDhLVm5MpKWLZ/UdGiCIAhCJaqK5EUrQynPUNCxY8fYtGkT06dPl26bMmUKzs7OzJw5k8zMTFQqFZMnT2bs2LE0aNAAKBzvrl+/vrQ8MjExkYSEBGmuio2NDS4uLtJMfl1dXWkJpkKhwMDAAB0dHYYOHSpNbnvatGnT6Nmzp1TJ0t7eno8//phLly6xevVqnJyc+Pjjj9HX10epVLJ06VIGDx7M/PnzsbKy4uzZs9K59PT0OH/+fLXUoSjvcFx2djZHjhxj9ep1bNiwkby8Z89Xqkm6umoCAhLYv/8SU6bE8uSJDl98UXT5pkZIiC39+rUu8jNhQlt27RJd3oIgCLVZZa820kpcSrs9+tO2bt2KmZmZVqVduVyOr68vDx8+5NSpU4SHh3Pnzh1pJjsUFqy5fv06H3/8MVC4aunatWv06NGD1NRUvv/+ex4+fPjcCcOAVl0CKJwLc+7cOZo1a6Z1++DBg9HV1dVazq2np0fDhg21Sp3Xr1+/yOoeGxubaimNXdZ/g/z8fH7//Rxr1qzj0qXLAOjr66Onp3jOkTVPVxcCAhLo2jWN69dN+L996IooKJCRmysv8pOXJy/xGEEQBKH2qMzkRWuoqDzf9m/evCntfPo0zYSzu3fvSsnH8wpBGRsbM2vWLJRKJe+++y67d+8u1Sqbf8Z98+ZNgCJx6erq0qBBA61N44p7zHK5vFxJXGUoy79BePgVzpz5Xar9oClo1ayZc1WFVyU8PdM4d86MkhZUDB2ayNChRSsKx8Tkk5urBmp/kiYIgvCy0yQvnTp1qtCwUbE9LmUpEGVpaUlWVlaRegb16tUDwNbWVtpl+sqVK0WOj4uLAwr32ejevTsuLi588cUXtGjRogwPo2hMULht+z/Vq1evQssjq1ppEqaoqBusW/cDhw4dISsrC1dXV2bNmoWTkxNAkZ6m2u7xY108PB5Tw4vWhDrk0aNHRTZ1FEovLi6O6OjoatlDSRCepkleGjZsWO6elwov4O/UqRNAkeqVSUlJ6Ovr4+XlxWuvvYaOjg7r1q3TerPJy8sjODgYgI0bNxITE8Pbb7+tdZ6nP8g1vRHP+3B/9dVXMTMzIzQ0tMh9SUlJxc6JqQtiY+PYsmUr+/b9SlpaGg4ODrz//vtMmTIFW1tbrl69ip6eHo0alX3eh1qtpqCggLy8PLKzs8nMzOTGjVvcunUbpVJZKT1QBQWwa5cNt2//Xd0zKUlBaKgVgYElb5IoCP/k6urKyJEjq/w6u3fvRiaTFftT0aHjlJSUclXKrYgzZ87g5uaGo6Mjzs7ONGrUiJCQkGqNQRCcnJwqlLyUaTm0Ztjm6SGfCRMmsG7dOr777jsGDx4szTfZv38/gYGB0vJkf39/tmzZwrBhw5g6dSr5+fmsWrWKTz75pDAQ3cJQ9uzZg7+/PyEhIURHR6Onp0dkZCR6enpSWfyoqChSUlK4dOkSPXv2lOLR/NfExIQPPviABQsWsGvXLmll06VLl8jKymLy5MlA4Yd1bm5ukSGl/Px8rSEqtVrN+PHjsbKyqpEaMsnJyRw7dlIa4rK2tmbAgAG0adNGSuYiIiIoKCjAxaVZqVaF5ebmkZGRTmbmE9LSUklOTiM5OYvY2HRiY9O4ezeD7GxdLA0j6NO3LYP8/DE3N6/Q40hNVbB8eSMyM3V4/fV0rK2VgJr//e8Gjo4vduE9oXLNmTMHFxeXKr/O66+/Ln252r59O9u3b2fjxo0YGxtr7ddUHnZ2drz//vssWbKkMkJ9rvv379OnTx+6dOnCTz/9hKGhIV999RVDhw7l7NmztG/fvlriEAT4O3nRrDYyMDBg0aJFpTq21InLw4cP+frrrwE4cOAA//rXvxg+fDhmZmbs2LGDcePG0b9/fwYOHMjFixdp0aKFNPEWYPHixWRkZLB7925OnTqFubk5X331FW3btgUKy2hv2bKFwMBAvvjiCxYtWkSvXr3YsmULGzduZNGiRajVary9vVm2bBkXLlzgxx9/5ODBg/zyyy8ALFq0iA8++IAmTZoQGBhISkoKU6dO5c8//8TExIRTp06xe/durK2tSUpK4rvvviM3N5dDhw6xefNm+vXrx08//cSNGzfQ09NjzZo1jB49GqVSya+//oq5uXm1Ji7p6emcOnWa69ejgMKS4H379sXT07NIhc3w8HAAXFyKDhPl5+eTkpJCfPx9kpKSuXfvLikpuaSnG5GVZURamjmZmZbIZA7o6TVAoahP/fqOyGU6GN3vT8Kv21iXmsa4994rdjuI0rK2VnLgwCXu3TNAJlNjb5+HiUnB8w8UqoWmx02z3012dvYz977JysqShoGr25QpU57bRqlUsmvXLtq2bYuzc/nmfTVo0ABfX1+gsO4TFFbTfV4Sr1arycvLQ19fv1zXLQu1Wl2quXGrV69GqVSyceNGrKysAFi5ciVhYWEsXbqUn376qapDFQQtTycvms/W0iQvMvVTYwDffPMNZ86c4b///S/29vZlCkCtVhMREUFWVhbNmzcv8YUdHR1NSkoKbm5uRd708vPzSUhIwMHBAblcTl5eHsnJydISao3k5ORSb8P+6NEjbt68iYWFRYXmzcTExKCnp1cklso0Z84csrKeMHbsaM6ePUd4+BVUKhX6+vr4+Pjg7e1dZAUVFM5JmjFjBnl5eUye/B4KRWE+mpyczJo167hzJ5aMDANksqbI5W7IZK+go9MIHR1TZDIDdHRMkcuNkMm0e2rUqixsE/vjo3eUYxk6PHL1YP78ORXueaksmsm5LVq8+JNzX3utNQpFxTai/OSTT/jss8/Yt28f/fr1k263s7Nj0KBBBAcHk5KSwuTJk4mIiODw4cO0aNGCw4cPSxVoMzMzmTt3Llu2bCEpKQkzMzOGDBnCkiVLpKGTiIgIfHx8GD58uFZvwp49e/j3v//NwoULGT9+fJnj7927N3/88Yf0d48ePZ65d9rKlSuZPHkyrVq1KnZ+XVktWLCA+fPnk5aWVuQ1kJ6eTtOmTaX3UM2weJs2bVi9ejX/+te/ANi7dy9jx44FCl+fhoaGUn0oMzMzbt++Xea4BgwYwGuvvcbEiROfWSn4jTfeIDExkcuXL0u35efn06NHD+7evau1aEEQqlNMTAxdunQhLi6OmTNnPjd5KbaOS3n2rpDJZLz66qu8/vrrz/xga9KkCe3atSv2m5quri6Ojo7S9UtKFEqbtEDhZNwOHTpUKGmBwsywKpMWjZycXNasWc+lS5dRqVR069aNTz/9lD59+hSbtADcunWLnJwcnJ2bSEkLwNq169i/5xQZGbOxsDiKhcUGzMxmYGraHyOj19DXb4Kenj06OiZFkpanGepAH4sCLCJOseTzxaSmptbYqiuh8qWkpHDt2jW2b99O7969WbFiBT169OCXX37h7t27rFu3Tmo7fvx4Nm3axPLly7ly5QpLly5l27ZtjBs3Tmrj5ubGRx99xJdffinNM7t//z5jx46lffv2vPvuu+WKc8KECQQFBREUFISBgQHp6enPbO/u7o6jo6O0IWRVUqvVJCcnM3nyZNRqNbt27WLz5s0kJibi5+cnDUd36NCBbdu2sW3bNuRyOf3795f+1uygXVatWrVi/O4ooAAAIABJREFU+fLlODk54efnx+nTp4tt988vfPHx8XTv3p2TJ09KFc8FoSY8Pefl888/Z9asWc9srzVUJD6MapYmYVMqlbRr14633npL6tJ9lkuXLgHQrFnTIvf1NH3IneT/ki7Twcy8LzJZ+Tdx622p5sTVE/zwrREjx0+oFZtuCpWjX79+eHl5ERkZyYEDBwgMDMTExARnZ2etb+KOjo6sWbOGQYMGAYUfmhERESxdupS8vDwpuZ46dSrHjh1j9OjRhIeHM2rUKAwNDdm0aVO5iyxqrgnw/fffP7d9586duXeveid9Dxs2TBpSh8JK1hMnTuTWrVu4urpia2srJVJyuZzGjRtXOLEKCgpi9uzZ7Ny5k02bNtG1a1dat27NlClT8PPzw8DAACjsmdU890eOHMHf358OHToQEBDwzJ4rQagOZRk2EtuC1iLdu3endevWfPLJJ4wZM6ZUSQsgdf02bVo0cWmoD/1MrqHz8EPS0vZWKD4dGXQyUqL3+37Wfr2MmJiYCp1PqH2eLsSo+fvpJbOLFy+mX79+HD9+nM2bN7Nq1SpiYmJQqVRFlidv2LABfX192rRpw4kTJ9i2bVup/5+uqzSVujU0E4hjY2Or9LpGRkaMHDmS0NBQ7t27R/PmzRkzZgyLFy/WaqdSqfjss8/o27cv06dPZ/fu3dVSWFMQSqO0PS/FTs4Va/trhpeXF15eXmU65s6dO6Snp+Po2BADg6ITAWVAEwMYJr/B6rgACgq+xdp6RLniU6shuwAUahXXTxxm1q27fP/9t5ibl3/CrlC3hIaG8s477wDQsmVLLCwsuHGj+E0yLSwsmDdvHmPGjMHPz6/IpqsvA80k+up4T01KSmLLli1s2LCBiIgIBg8ezODBg6X75XI5R48e5fr164SFhUkbXKpUqnJNDxCEquDk5MSxY8fo3r07n3/+ORYWFloLfUD0uNR5z1pNpCGXgZ0+zGiQjuLhf0hN/gGVqujyY7U6n4KCJ+Tnp5KXl0Bubgz3czL55ZE53yfaMDfekaAkD3apAnli/T1ZWfakpj6qsscm1C4ZGRkMGTKEXr16ERsby6FDhwgODiYgIKDY9qmpqcyfP5+GDRsSHBxcLXt9vYzCwsIYNGgQDg4OBAUF0adPH27fvk1ISAitWrWS2llZWWFvb8/ly5e1duVOTEws07xBQahqTZs25fDhw0BhLaV/KlMdF6H2+fPPPwFKVdPCUAfG2DzmQPpc4vJTMK03goKCVJTKh+TnJ5KX9wC1OoeCgkwKCtLIz3+EgUFfMgw/Qk+vEQ76TVEo/u7qT0ws+j+U8OKKjo4mPT0dHx8frRomCQkJRdqq1WoCAgLIzs4mPDyc8ePH4+fnx+XLl6t1blRsbCz169cvcWJ7TdJs0VFRc+fO5cmTJ6xcuZKRI0eWuIS9Y8eOnDhxQuu5UKvVXL58mY4dO1Y4DkGoTJptg4rrrRSJSx2WkJDAo0ePqF/fFhMT41IdY6mA3qax/PZ4ARHZUSgUNsjlRujoWKJQWKNQOKCrWw+Foj46OhbI5bXvDV+oGS4uLlhaWrJkyRIcHBwwMTEhODhY2mskKytLWlH4xRdfsG/fPn799Vfs7OxYv3497u7ujBgxgt9++61ahiZ27NjB0KFD8fb25tChQ+U6x4MHDzh37hyAtFP9/v37pQJ0ffv2LXd87du3JyQkBF9fX5o0aUJ0dDRdunQpc22c9evX88orrzy33YQJE1i+fDn+/v4sXrwYQ0NDli1bxs2bN/nhhx/K+SgEofqJxKUO00zKfdYwUXHqKaCTcQbx+r0xM+uGTKaPXG6ATCb+dxBKZmhoSEhICGPGjJFWwnh6ejJnzhwWLFjA1atXsbe35/Tp08yePZtp06ZJ22tYW1uzadMmevXqRVBQEHPnzq3yeDXf1Mq7igng3LlzUuVtjREjCueIWVhYFNmjrSyWLl3KwIEDpefI1NSUc+fOlSoJeVpp2zs4OHDgwAHGjh2Lu7s7APXr12fbtm2ix0WoU2rsk0qlUpGRkSH9LZPJSqzKWt7JY6WtKFna4/7ZrWtmZlahN8WKKs38lpLoykBXtx66ui/2Kg/h+TS7t0Pht/IJEyZIf//1119abb28vIiJiSEuLg6FQkH9+vUBmD9/vtTG09NT65wa3t7eFBRoV0o2NDQkJyen2LgmT57MihUrir0vJSXluR/Yw4YNo3PnzhXaVHXAgAGlKhNhbm5ebLtu3bqVeHyLFi2IiIggNjaWgoICGjVqVKQidmXz8PAgMjKS2NhYlEolTk5OYmKuUOfUWOISFRWFp6cnHTp0wMrKCoVCwYYNG7TanD17liVLltC3b1+tAlfPs3z5cs6cOYOJiQnGxsYsXLgQU1PTZx6Tn5/Pp59+ytatW8nOzqZTp04sXboUR8fCDQtzcnL4z3/+AxROZvvjjz+4fv269MZd3R4/fkxsbCyWlpbSTtyCUF0aNmxYKecJDQ0tccVNSVVgT58+TWxsrFSN9lmeVUm2NpDJZNJYfnXSvK8JQl1U42MDn3zyCZ07dy5y+08//cTOnTs5duwYffr0KfX5goKCOH36NPv370culzNv3jwCAgIICQl5Zu/IvHnzsLS05LvvvuPYsWN8//33DBs2jNOnTyOTyTAwMGDLli1A4Rj3qFGjyv5gK5Gm9LmLS9HaLYJQVzy9uuVZNJsb5uTkcPLkSdzd3aUvEoIgvFxqPHEpib+/P+7u7tKSqNKIiopi+fLlbNy4Uer+nDhxIq+++qo0Ua84ycnJ9OjRQxq39/HxITk5mR07dnD//v1a+a1NM0zUrFnZh4kEoa6xtbXF2dkZfX19AgIC8Pf3r/DuzIIg1E21NnEByjy7fvPmzRQUFNCpUyfpNnt7exwdHdmwYUOJiYu1tXWRstvu7u4cP368QuPjVSUzM5Nbt25hbGyEvb1dTYfzQlCpVOTm5pKbm0tOTg7p6RkkJaVy924i9+/H06ljK9q93q5advsViurevTvdu3ev6TAEQagFanXiUtZJY0ePHsXAwKBICeumTZty+vRpcnNzS/3BEx4ezpIlS2rlt7q/J+U+v3aLULycnBxSUlJISkomISGBtLQMkpLySU2F5GQ19+9DXp4Z+vrO5GY94v7ZRSQNH8MbffuWWCdDEARBqHq1OnEpK81k1X8yNTVFqVQSHx+Ps7PzM8+hVqtZv349Dx8+LHP5/epSkdVEL6uEhAdERkYSH3+fe/fukZSUTW6uFXl5DcnKqo9a/SpyuTW6uubo6NTD2toWubxwc7rHSdl0yP+Ws1vWkJuXxzB//xp+NIIgCC+vFyZxycvLIzMzs9jZ8ppKkVlZWc88R0JCAsuWLePHH38kKysLHx8f9u/fX6tW7eTl5REZGYmenh4NG9a+uTe1TWpqKtOmfUhcnC6Ghu0wNGyPoeFoFIomyGQ66OjoYmamC+iUOHlbBtgowIdUNq9eSYEahg8XyYsgCEJNeGEW8Ovq6iKTyYpdWpmXlwdQYp0YDXt7exYvXsxff/3FwIEDiYqKKrGORE25evUqBQUFNGvWVNRfKIVbt26RdOcmBup61Ks3DUvLf2No2Pr/elZMpMJ7panHY6ELo62VHF+/km1bt/LkyZNqeAQCFK4+XLhwYU2H8cKIi4sjOjpabKgr1EkvzCefXC7H3t6+2L0/Hj9+jI6OTqk3EtMsi7axseHixYuVHWqFlLda7svMxQh66R/nyf0PyM6+WqqCYiUx1YUR9ZREbV9P8JbNFaqcKpReaGgox44dq/br7t69G5lMVuxPccPSZZGSkvLcXuDKdubMGdzc3HB0dMTZ2ZlGjRoREhJSrTEIQkW9MIkLFO79kZKSUqQSZ1xcHG3atCnTKiV9fX06d+5cq1aR5Ofnc+XKFXR1dWnSxKmGo6lb2pkU4KV3mMz7H5CXd69C5zLSAS+9DFIO/sy3y5aVWPlVqPtef/11goODCQ4OllYlbty4keDgYDZu3Fihc9vZ2TFv3rzKCLNU7t+/T58+fXB2duby5ctERUXRv39/hg4dyvnz56stDkGoqBcqcfHz80OpVGr1kqSlpRETE4Ovr2+Zz5eWloaPj09lhlghUVFRKJVKmjRxqvLS4C8ahRzameTT3/AotyI9ycm5Wa7zPM6H39Nhbwqce5jF/gMH+f333ys52ppRUFBAdna29PfTvxenunsLykqpVLJjxw7u3LlT7nM0aNAAX19ffH19cXNzAwq3AfD19eWtt94q8Ti1Wk1ubm65r1sWpe1BXL16NUqlko0bN+Lu7k7z5s1ZuXIlTk5O0kaZglAX1OrERfPC18xReVpkZCRdu3ZlxowZ0m29evXCy8uLPXv2SLf99NNPuLi4MGbMGKDwzVbzRvTo0SMAHj58yA8//EBycrJ03PHjx8nMzGT06NFV8dDKRTNM1KyZqJZbHnIZtDJWM6tBPLl3e/M49RdUqsL/t9RqNSpVDkplEjk5t3jy5A/S04+QlLSORxnnWZf6KvMTO/J52mAOKRYTa7MHw2bXsWu4nPv3E2r4kVWMnZ0dkyZNws7ODmNjYwIDA+nZsydGRka4uroSHx8vtc3MzGTatGnY2tpibGyMubk548aN0xoyi4iIwMHBQeu1CbBnzx5sbGxYvXp1hWPOycmhf//+tGjRgtu3bxfbZvXq1QwdOrTIJolVIT09HRsbG37++WemTp2KqakpRkZGtGvXjj///FNqt3fvXmxsbLCxsSE/P5+VK1dKfzdtWr7X9cCBA5kzZ47Wv1Nxzp49S/PmzbGy+nt/MrVajYODA2fOnCnXtQWhJtTaVUWHDx9m69atAPz444+YmZkxYsQIaULqhQsXuHr1KlevXsXX11fa3XTt2rWMGzeOoKAgFAoFFy5cYOfOndLKooSEBI4ePQoUbgf/4YcfcvnyZWbMmMGcOXPw9vZGoVBgY2NDcHBwrRkqUqvVhIeHI5PJaNr02Uu6Xx4qcnKyUalMyzRR2UYBwyzucPDRhyQpE9FR2JGTc4OCgnRph2yVKge53ACFogGWVv4o9D7GXtEAHR1TZLK/r5WRcayyH1S1S0lJ4dq1a2zfvp0lS5awYsUKFi5cyOTJk/H392fdunXSbs7jx48nNDSUFStW0LJlSy5cuEBgYCCpqanSXAk3Nzc++ugjPvjgA3x8fOjVqxf3799n7NixdOjQgXfffbdC8ebk5DBgwADOnz/P4cOHS/zAd3d3x9HRsUhxyaqgVqtJTk6WnrNdu3bx8OFDPv74Y/z8/IiIiEBXV5cOHTqwbds2oPCLVv/+/aVNLXV1y/d23KpVK5YvX87nn3+Or68vU6ZMwdPTs0i75ORkrXl+8fHx0rYmZS32KQg1qdYmLt7e3nh7e7Nu3bpi7x8+fDhubm789ttvWnMMLC0tCQkJITo6Grlczscff6x1XNOmTTl58iR3797l0qVLQOEbSGRkJHFxcRgaGuLk5CQlOrXF7du3efLkCY0bN6o1yVR1y8rK5tatW9y+fZv4+PtERcWQnp7JK684MXXqlDIVhmugB31N7/Bj+npyTIZjYOD2f7tlm6OjY45cbvx/SUqtfYlUqn79+uHl5UVkZCQHDhwgMDAQExMTnJ2duXv3rtTO0dGRNWvWMGjQIKDwQzMiIoKlS5eSl5cnvW6mTp3KsWPHGD16NOHh4YwaNQpDQ0M2bdpUoR3Vc3JyGDhwIGfPniUsLOyZGy127tyZe/cqNp+prIYNG8bXX38t/Z2RkcHEiRO5desWrq6u2NraSomUXC6ncePGFU6sgoKCmD17Njt37mTTpk107dqV1q1bM2XKFPz8/DAwKKxHpFKppOf+yJEj+Pv706FDBwICAvj5558rFIMgVKc6+66so6ODu7s733zzDdOnTy9yf5MmTUo89tVXX2X79u34P1VIzMrKSqsLtbZ5GVcT3bhxk+vXrxMRcZ1Tp07z6NEjnJyccHFxwd7eDi8vL5KTH/HXX+f58MOPCAr6tNQrPWQysNMDa2M38u3er+JHUndoeq40H3ByuVxryezixYvJy8vj+PHj3Lt3j6ysLGJiYlCpVGRnZ2sl/Bs2bKBNmza0adOGxMREjh07VqHXmFKpZOjQoYSFhXHs2DE6dOhQ7nNVlX++72iqW8fGxuLq6lpl1zUyMmLkyJGMHDmS+Ph4PvzwQ8aMGcPdu3e1JgCrVCo+++wzPv30Uz799FNmzJjBtGnTqiwuQagKdTZxuX37NsHBwcybN0/6RlEa6enpbN68mW7dulXpG0ll01TLbd78xSjzn5ubR2ZmBpmZT8jMzCQtLY2kpGTi4+OJj48nLi4eAwN9GjRwwNbWhnHj/o25ubnWFgzZ2WqSkx/h4eFJeHg4X321jPHj36VhQ4cKfasXShYaGso777wDQMuWLbGwsODGjRvFtrWwsGDevHmMGTMGPz+/YocvyuLEiRPo6uqiUqk4c+ZMqXeWrkmaSfTVUS8lKSmJLVu2sGHDBiIiIhg8eDCDBw+W7pfL5Rw9epTr168TFhYmPX8qlUrUhBLqlBpLXPT19XF1dWXRokXo6emhUCjYvn17qY+3s7MrMgxUGnp6ekycOLHMq3JycnKkHprc3FxcXV2rbR+juLg4Hj16RIMG9nVynxylUklycjIPHiTy4MEDUlJSSE/PIDs7m6ysLPLz85HJZBgbm2BpaUnbtm3p2bNnmZ5fd/fXuHr1Gj/8sJFx4/4tNp+sAhkZGQwZMoQBAwawbt066d/nyy+/5MMPPyzSPjU1lfnz59OwYUOCg4N57733KpRsmJiYcODAAdasWcPs2bNp06YNvXr1Kvf5XhRhYWF8++237N+/H1NTU8aNG8fevXtp3LixVjsrKyvs7e25dOmS1uaxiYmJpa5xJQi1QY0lLs7OzhWayW5sbFyu48rSO/PP43755ZdyHVtRmt6WurKaKCMjk2vXrnHnzh1iY+O4f/8+KpWKevWsMDY2wtzcHFNTU2xsbNDX18fIyKjC83Z0dXVxc3uFW7du8cUXXzJ9+jQaNLCvpEckAERHR5Oeno6Pj49WUpmQUHRVlVqtJiAggOzsbMLDwxk/fjx+fn5cvnwZGxubcl2/Q4cOeHp60rZtW65cuYK/vz8XLlx45v5jsbGx1K9fv9bNWQMwNzcvtmBmWc2dO5cnT56wcuVKRo4cWeKXm44dO3LixAmt50KtVnP58mVpcYMg1AV1dqjoZaKZ39KiRfMajqSorKws/vzzEjdv3uLKlStcu3aNjIxMmjZtSpMmTjRo0IB27dpVyweHnp4ebm5u3LkTzYcfzuDLL7+o8mu+TFxcXLC0tGTJkiU4ODhgYmJCcHCwVAMkKysLc3NzAL744gv27dvHr7/+ip2dHevXr8fd3Z0RI0bw22+/VWhowsDAgJ07d9K2bVsGDRrE2bNni10Vs2PHDoYOHYq3tzeHDh0q17UePHjAuXPngMISDAD79+/H2NgYhUJB3759y/042rdvT0hICL6+vjRp0oTo6Gi6dOlS5hU+69ev55VXXnluuwkTJrB8+XL8/f1ZvHgxhoaGLFu2jJs3b/LDDz+U81EIQvUTiUstl5KSwv3797G2tn7uXkvVLS3tMXPmzKN+fTvs7e1xdnamY8eOmJqa1miBPGfnJujr67Nw4We0adMatar8Jf6FvxkaGhISEsKYMWOklTCenp7MmTOHBQsWcPXqVezt7Tl9+jSzZ89m2rRp9O7dGwBra2s2bdpEr169CAoKkpZXl5eTkxNbt26lb9++jB07Vlpi/DTNvJKKzHc6d+5ckTowI0aMAArn8FRky4elS5cycOBA6TkyNTXl3LlzpUpCnlba9g4ODhw4cICxY8fi7u4OQP369dm2bZvocRHqFJG41HKaJdsuLrVvmMjIyJBhw4Zhb1/7hmQaNLBHLpdx9OhxzPOVNR1OraZU/v38TJgwQaorAvDXX39ptfXy8iImJoa4uDgUCgX169cHYP78+VIbT09PrXNqeHt7U1BQoHWboaFhiVsmTJ48Wdrk9MKFC0Xuf+ONN4qc72nDhg2jc+fOWvM5ymrAgAGlqkxrbm5ebLtu3bqVeHyLFi2IiIggNjaWgoICGjVqVOUJv4eHB5GRkcTGxqJUKnFychITc4U6RyQutdzf1XJr4zJoWa1905PJZNSvX58WLZrzMPEmUPIHnFB2DRs2rJTzhIaGlrjixsHBocLnr4xzVCWZTEajRo2q/bqOjo7Vfk1BqCwicanFMjMziY6OxtTUFFvb8k1ofJnJ5XIMDQ2RIZZG11Z1YUmzIAi1S+38uiwATw8Tlb+3JUkJeVVfQkIQBEEQqoVIXGoxzTLoiiQuvyuNOJ9RO5OXUm5qKwiCIAgSMVRUS2VnZxMZGYmBgQEODg3KfZ4W7Tpy8fZNMtJi6WUJOjU8aqJWw+nT7hw92pa0NFNsbNLo2fMc7dpF1mxggiAIQp0gEpda6q+//kKtVuPi0qxCyzkLJ6i2YPfuPeSnxNPHCnQrmLwUrpJQAWXvMjl5sjW//96KTp3+IidHn7NnW/Hjj3148sSQbt0uVSwwQXiJzZo1S5rM7+/vL23NUNfExcVp7SC+efNmUdlX0CKGimqpK1euAJVTLdfOzo4BA94iyqoZu1LkZJZxgY1KlYdSmUhOzi0yM38nM3MvT578gIWFskwVb9VquHvXnunTf6Rnzwv073+KqVN/QqHIJyysoxg6El4YqampZGVlVes1L168yI0bN2jZsmWFloDXNH19fVq2bIlCoeC3334jNze3pkMSahmRuNRC+fn5XL16FV1dXRo3rpylknZ2dgwZ8jZ5r3QgLF1RYvKiVhegVCaTmXmO5OQtJCQsICXlA1Sq/2JltRQXl/W0a7eTLl3OM3BgRywsLEodw5MnBrzxxu/o6Pw94cbWNo1XX73DkycG5OfXXNE6oWalp6dXSvn7ynb37l1kMhmrVq0q03Fubm68+eablRJDWZ4bNzc3lixZIhW1e9rKlSuRyWTSj76+Pu7u7nz55ZfVkhzk5+eTkpJSbI2fp9nY2LBkyRICAgKqPCahbhJDRbXQtWvXUCqVtGjRvFILUhkaGtK5e3dOAaFXzoMpqFS5ZGVd4smTS2RlXUKtvk69emoaNrTBwcEWGxsLTEys0NNToKurQFdXBx0dnXINX5mY5GBiUrTYmLFxNlZWj1EoRK2Vl5Wvry9paWnFFpqri+bOnUuDBuWfm/a0yn5udu7cSb169cjJyeHUqVPMnTuXc+fOlWmT2/I4d+4cnTt3Zt++ffTr169KryW82ETiUgtVxmqikhgYGNC5e3d+y8klKurfGBsX4OLSkLZtnbCxscXWtm+5N6Isr7g4Wzw8rlTrNYWSPXnyBENDw+cWF8zIyMDExKTUSWxWVha6uroV3rcqLy8PuVyOru6z376USiW7du2ibdu2z9yIsSpMmjSpVO2ys7MxMDCo0Dy2svLw8JAqHr/xxhs4ODgwadIkjh8/Trdu3bTa5uXloVKpyvSekJGRgZGRUY1u+yG82MRQUS2jUqkIDw9HLpdX2ZutgYEB/fr1w9u7JY0b29KhQwfatGlDo0aONZC02JCVZUjXrn9W63WFv+3duxcbGxvWrl2Li4sLJiYmWFhY8N///rdIufqCggIWLFiAjY0NZmZmWFpa8v7775OdnV3kvD179iQgIIDQ0FBeffVVjI2NMTQ05OjRowA8evQIGxsbbGxsOH78uLRztObn6tWrWuc7ePAg//rXvzAyMsLQ0JDXX3+dsLCwEh/X6tWrGTp0aJG9hqpKly5dtOL39fUtse2yZcto1KgRRkZGmJiYMGDAAG7duiXdX9bnpiI0+05dv35dui08PBwvLy8MDAyk5/r48eNFjn3y5Ak2NjasWrWKr776Cjs7O8zMzDAxMZG2cpg5cyY2NjZSL8vw4cOlx1FXJxALNatSelxOnz5N06ZNsbOzAwqz9F9//RUfHx+MjY0r4xLldvjwYZo3b15nSlzfvHmTnJwcmjRxQqGoug4xhUKXdu3aEhUVxcmTJ+nc2bPaS4+rVDL27u1KQMBedHVLLjRz9WpT/vjDtcjtBQVqrK0v8K9/Xa7KMF94eXl5JCcn8//+3//js88+w97enu+//56FCxdiZ2fH5MmTpbYzZ87kq6++IigoiF69enH+/HmmTZtGQkICP//8s9Z5Hz9+zLVr19i9ezdDhgwhICCAtLQ0XFxcgMJNBTWbI86YMYMnT57w7bffSsc7OTlJv4eHh/Pmm2/y9ttvs2LFCrKzs1m0aBH9+/fnwoULvPbaa0Uel7u7O46OjtIHc1WbMmWKtOniZ599Rnp6erHtvv32W6ZNm0ZQUBA9e/YkJiaGmTNn0qtXLyIiIjAwMCjTc1NRmoRFM7SVkJBAt27daNasGWFhYejp6fHpp5/i4+PDhQsXpA0aNZKTk1m4cCEqlYp33nkHGxsbHj9+LH0JGjNmDD4+Pvz111988MEHfPTRR9KmjjY2oiK4UHYV+mQMDQ1l0aJFhIeHs2fPHilx2bVrFxMnTmThwoWl7jKtCvHx8QwZMoS+ffuyZcuWGoujLKpymKg4zZo1Q61Ws2/ffoYP9y/TZNuKOnDAAw+PcBwckp/Z7uFDSy5fbl7sfa+9Fg+IxKUyrF27Fh8fH6BwM8V79+6xZMkSKXGJiYlh+fLlzJw5k1mzZgHQtm1b5HI5EydOZNq0aXTo0EHrnElJSZw5c4b27dsXuZ5CoZCSCisrK3R0dEpMMo4ePYpSqeSbb76hXr16ALRr145Ro0Zx8+bNYhOXzp07c+/evXI+G2U3dOhQ6fe1a9eW2C40NJSWLVsye/ZsANq3b4+9vT3/+9//uHfvHs2bNy/Tc1NWmqQiNzeXM2fOMGXKFNq2bStN6J03bx4FBQWEhoZKz/X+/ftxcXFh5syZHDhwoMg55XI5ly5dwsrKqsh9LVq0oEWLFlIi07p162pLJoXt8GrbAAAgAElEQVQXU4USl169enHv3j3pw1bjjTfeYM6cOc/sKq0KERERuLm5SX87ODjwv//9j7Zt21ZrHBWhKfNfGcugS0NHRwdXV1fkcjmrV69m5MiR2NnZVfnmiWfPtsTCIgN391vPbdup01+89lrRdjk5apKSnn+8UDqaDyko3Pxv2LBhTJs2jZSUFKysrDh48CBKpZJRo0ZpHTdixAgmTpzIvn37iiQuHTt2LDZpKasuXbqgp6fHqFGjCAwMxNPTEwsLC/bu3Vvhc1e3nj17EhgYyLRp0xg6dCjt2rWjS5cu1bZvU4sWLaTf5XI5vr6+LFu2TJp7tHfvXvr166f1/4Oenh5Dhgxh+fLl5ObmFimDMHLkyGKTFkGoChX+dNJM8nqaubk5H3zwQbXWEsjPz2fBggVFbh83bhxt2rSptjgqIiYmhvT0dBo2dMDQ0LBar928eXP69u3LkSNHiIqKIj8/v8qu9ccfrmRnG+DpqT0h9+5du2LbGxrmYm2dVuTHyioNA4MnVRbny06zA/T9+/cBePjwIVB0x2UTExMsLS1JSkoqcg6FQlEpsbRt25ajR4+Sl5fHm2++Sb169fDx8SE0NLRSzl+dJk+ezA8//MChQ4fo1KkT1tbWjBo1iqioqGq5/qFDh7h8+TLXrl0jMzOT7du3a62AevjwYbG7ajdq1Ij8/HxpOOxplfXvLAilUeHEpaq/mZfW7NmztSa31UWanqvq6m35p8aNG+Ph4cG1a9ekAniV7cKFVzh8+HUUinxOnmzNyZOtOXbsX/z4Y2+iohpXyTWF8klMTAT+/nKi+QauSWA08vLyePz4MZaWllUaj4eHB2FhYTx69IidO3eiUCjo3bs3Bw8erNLrVoV33nmHK1eucP/+fZYvX84ff/yBp6cnDx48qPJrt2zZEnd3d9zc3Ir9glSvXr0i/8ZQOOwnl8sxNzev8hgF4VmKzTqelYzExsYSGBjIlClTGDNmDCdOnNC6v6CggAMHDjBs2DAOHToEFC6D3Lp1K3369CEuLo7//ve/dOnShXPnzgGQm5vLV199xfjx4+nVqxfTp08vktVHRkbywQcfMGnSJAYOHMjmzZuBwp6WwMBA1q5dS3JyMv/5z3+YM2cOUDjsMnXqVBYvXqx1rszMTD7//HOmT5/OwIEDCQwM1BoLz8rKYsuWLfTu3ZvExET27t3LG2+8gY+PDydPntQ6l+Zbk+axVERERARQffNbimNtbY2Hhwdnz/7OsWNFVxFUxB9/uPLTT71JSLBm504v6Wf37m5cvPgKbduK/Ypq0j+LkO3duxd7e3up57RHjx7I5XJCQkK02u3ZsweVSkXPnj3LfW1zc/NnFlkLDAxk9OjRQGEPT//+/dm3bx8mJibs27evxONiY2PJy8srd1yVLT8/nx49erBs2TIA7O3tCQgIYOPGjaSkpHD69OkixzzvualsPXv25LfffitS+XfXrl14eHiUuzdYk/DUxkKDQt1Spjkud+/epXfv3nz//fd069aNzMxM+vTpo9Xm9OnTbN++nbCwMIYPHw78f/bOOyyqa+vD7zTKUEURqQYBBbsCYiMWUBM7FmxEo/EmMSqxRMWo8SbXaOzdGE2uRk2isUSj2HuMDY0lFqJGRRQVkI6UGWa+P7icz5Ghd3Le55lH5px99llnOzNnnb3X+q3sYLQNGzZw+fJlvv76a5RKJY8fP+b+/ft4e3szfPhwxowZw6RJk3j8+DFdu3blwoULnDhxAoVCwfnz5wkODmb37t3Y2dnxww8/MH78eFQqFaNGjWLBggU8e/aMe/fusXjxYiQSCXfv3iU0NJTNmzczevRowb6XL1/SvXt3Jk2aREhICJmZmYwcOZLOnTtz8OBBXF1dOXToEP/973+5evUq69atw8jIiDFjxvDll1/y3nvvcePGDWE9+NatW/z111/cuXMn1/p+UXn58iWGhoaYm5uXqJ+SIJFIMDMzo1+/AHbu3IVMJsXHx6fE2hsAnp7hFeKcVMLC2JWS4OBgVq1ahZ2dHWvWrOHw4cMsXLhQ2O/h4cF7773Hv//9b2rWrEmnTp34448/+PDDD+natWuJAi5btWrFzp07Wb16NV27duXZs2c4OjoK2TONGjXiww8/xMHBgZEjR6LVatm+fTvJycl5fu+2b99OYGAgfn5+wkNUcbl+/TqhoaG5tvv4+Ah1dI4fP05ycjKQLfmfmZnJnj17gOxlN09PT+RyOTY2Nnz22WfCGMbFxbFo0SKMjIxyZewUZmxKmzlz5rB7924CAgKYO3cuBgYGLFiwgFu3buV6UC0Kbm5uWFpasmzZMiFY99mzZ2KgrkiR0eu4aDT6f+qnTJlCq1atBJEiU1NTgoKChAwDgDfffBONRiN8YQH69u1LVFQUly9fxtvbm759+/Lpp58ikUjYuXMnCoWCjh07Atlf8CFDhrB06VIOHjxIjx49GDduHKNHjxbWYf38/LCxsRHW1I2MjJBKpUgkEiFyPScCfsmSJTrXsHTpUjIzMwVtBwMDA+bNm4eXlxczZsxg27ZtBAQEEBERwdWrV/H396dNmzZA9lTptGnTuHv3Lo0aNQKyUyC7d++Oq2vFzZKUBaampgQE9OW3384QFnYJb2+vUnFeyhszMzNiMeRxhhqHwpdV+kfi5eVF9+7diY+PRyaTMXHiRCZNmqTTZs2aNZibm/PRRx+RmZmJTCYjKCiI1atXl+jcwcHBnDp1SshgksvlrF+/Xphl+eCDD8jMzOTLL7/kyy+/BLKzbRYtWkRQUJDePnN+x0pD3G3NmjU66cg5HD16VLjxfvzxx7n0VXJ+Z4KCgoRZ4m+//ZaQkBA++OADQf/G3d2dX375Re/vSEFjU9q4uLhw5swZgoKC8PLyArJ/lw8ePEi7du2K3a+hoSEbN25k5MiRQr+urq7cvXu3VOwW+edQ6BmXyMhIjh49yr///W+d7TkBfK+ir/BeTvBWy5Ytgf//MTl06BCPHz8WlncA0tPTGTJkCEZGRly7do379+/rPInUqVNHRywpL+Ryea5lrx9//DFXsK6TkxOtW7fmyJEjJCUlYW5uLtykX9U2yUn3TklJEbZJJJJq57TkYGZmRrt2bQkLu8TJkyfp2rVrRZtUZKysrGjo25k9pw/TyywDp/LV16tS/Otf/2LFihVERkZiZWWlN2ZFLpezaNEiPv/8c548eYKNjU2eMQ9Fkag3MjIiNDSUmJgY4uPjcXJyyiWGOH78eMaPH8+zZ89Qq9XY2dnlu6w9aNAg2rdvX6Ikgbp16+YS4cuLP//8s1DtTExMWLlyJcuXLycyMhITE5N8qx8XZmwKw9ixYxk7dmyh2np6enL79m0iIyNRq9XUrVtX71ibmJgUenwA+vTpw/Pnz4mIiECpVJZaWQSRfxaFdlxyAkcLo/NRlCecJ0+e4OzszH/+8x+9+3PSHYu7Tv2qLcnJyTx9+pTGjRvnapfjoERGRtKoUSO915DzxS3KF7UqI5FIsLCwoFOnjpw8eYqtW7fRr19AlZt5cXd3x8DAgAMnD9JXmopN1TK/XDEwMMDFpeDgcBMTE+rX16+tUxJyFFXzI+cBojDoy46pLEilUurWLXxAemHGJiwsjICAAAYOHCgs1ZeEshDuVCgU+T7sPXnyhHHjxvHkyZNSP7dI9aDQKUE50645qZGlhZGREVeuXNGbfhsdHY1SqQTQm+Xy+PHjIp3LxMQEQ0NDvV+InKdLUclRF4lEglwux9/fD1NTU3bv3kNCQkKVct4kEgnOzs7Ubd+FPZlWPM0ETQnM12q1aDRpqNUvUKtzp4aKiFQEffr0YeDAgTg4OFRonFxJkcvlODg44OPjw7hx4ypcfV2k8lHoGZccYbfQ0FCmTZuWa0Yir7iYgmjevDknTpxg/fr1Oiq7YWFhxMbG4uXlhUwm47vvvmP06NFCRHtmZiY7duxgwoQJQPbNqaCbqVSaHWj622+/ERkZqfM0ERsbi6enZ7lqz1Q12rdvx+XLlzl27Bh+fv5YWladtEipVEqDBvUxNjZm79mTeKU8pZlp4Y7VaNJRq2PJzIxCpXqCSvUUuTwGI6NEpNLzQMmCsiua5s2bs2rVqnIv+SBSurxamqEqY2Njw8qVKyvaDJFKTKFnXFxdXfHz8+PmzZssWrQIyH7yPHfuHJCdcZQTaJaTVqlSqYTjs7KyAN34EIB3330XU1NTZs2axbRp0zh06BDffPMN8+bNw9/fH2tra4YMGcKjR48YNGgQx44d49ChQwwePFhHadLU1JTo6Gji4+PZtWsXKpUKtVpNVlaWzjJTTlDwihUrhG0pKSmcOnWKmTNnCttyCoS9OhOU8/eraaOnTp3irbfeKjPdk8qETCajWbNmgmJpamrVE39zcnKkhf/bXDJ7g/NJkKXH11WpoklKOkl09NdERATz6NEQVKqp1Kq1isaN9/Lmm2F07fqMt9+W0rhx1XHe8sLFxYWxY8eKs40iIiJVgiKlQ69du5YRI0Ywb948fvzxR6ytrfHx8UEqlXLw4EEaNGiAoaEhX3/9NQDfffcdDg4OJCQksGnTJgBmzpzJtGnThBRGR0dHfvjhBz788EPWr1/P+vXrcXNz48cffxQCeufPn09ycjJ79uzhzJkzWFhYsGTJEh0p/+HDhxMaGkqzZs1YvHgxsbGxQhbAoUOH2LVrF/369aNVq1Z88803fPLJJyQkJNC0aVNOnjzJf/7zHyFb6tixY/z888/CuT/66CMSEhJYt24dAKtXr8bCwoLmzZtz/vx5Ll68yJUrV/TWS6luGBoa0qpVK/744w+++WYdI0e+W+bCY6VNrVq1aNO1O8cPHOBFQgo1kk6SnHyGlJQzvHx5DTOzDBo2bIijowOOjo7Y2uYtxf7w4cPyM1xEREREBIn2lfWVVatWcfbsWWbOnImtrW2eB4WHh5OcnEzTpk1JTk4mPT1db3ZRUVCpVFy/fh2pVErTpk2RyWS52jx48IAXL17QsGFDIfblVV6+fIlWqy3Ummh6ejo3btxAoVDg7u6uNxOqKHa3bNmyxGmXs2bNIjU1lXHjSqcw5VdfzcfAwBB399yVlUuDGzdu8PjxY9q0aVNghkdZkJam5eHDCMzMFMjluT8vGo2GzMxMMjIyyMjIID09nbS0NFJTU0lOTiExMRGNRoOjoyNWVlbY2tpSq1bNIq2pnz17FicnR4YOHVKal5aLpk2bo1CULKp4xowZzJ07l3379tGjR49SskxERESk9ElPT8fY2BgfHx/Onz+vs69YRRZfvREW94b/OgqFosBiiM7Ozjg7O+e5X58zkxdGRkaClkBJKIzd1ZUcB/LMmd9p0aK5TvG28kar1ZKenk5ycjKJiUkkJyeTkZGOVpu9TyaTYWRkhFJpjL29AzVqWGJpaYmJiUmlKVshIiIiIlIwJaoOLfLPJiedU6Ew4MSJE6SlpdO8eW7lz7JAq9Xy4kU09+7dIS0tgfj4eNTqLGrWtMLc3AIrqxrUqGGFiYkSAwMDjI2NMTY21juTJ1L6WFtbExsbK7yvU6cObdq04bPPPqN58+Zlfv60tDRevnwpViwWEamGiI6LSImQyWQ4OjrQs2dPNm/egkwmpUmTJqV+ntjYWCIiInj8+AkPHjwgOvo5Vla1sbe3p359F+rVq1cojSGR8qN3795MmjQJrVZLVFQUa9asoXXr1pw9e1YQoiwrlixZwsyZM0lJSRHTaUVEqhmi4yJSKlhYWDBw4MD/FYmT0KhRw0IvwajVatLT08nMzCQtLY3k5GTS0tKIi4snPj6e+Pg45HIF9vb2uLq60rFjB6ysrNBqDXn8+AnW1qbI5eJHubJhZ2cnBLwD9OvXDw8PDyZPnsyJEydytU9NTcXQ0LDQ/5cajYbU1FTMzMxKbOuxY8dQKpVCeQ8REZHKi/hrL1Jq1K5tTZs2bbh+/TpZWWq9SwIajYa0tDRiY2NJSkoiLi7ulfRyCVKpBIlEioWFOfXqOWNh0RxLS0u9N6e0tKojgieSHVfWtm1bjh07prN9x44dzJgxgzt37iCXy+nZsyfLli3LpSp7+PBhhg0bxr59+9iwYQObNm0iLS2Njh07Co6Qr68v4eHhQmXjunXrCkHzc+bM4YMPPshl182bN/H390cmkxEVFSVqOYmIVHJEx0Wk1JBIJNSpY4NS6cOpU6d5+vQZHTq8yfPnz3nyJIq4uBfExsb+zzGxoHbt2lhbW2NpaYlSqcTQ0BClUikUzRSpfoSHh+vUp9m3bx+BgYEMHTqUTZs2ER0dTXBwMB06dOD27duC4CRkZ/DFxsYycOBAbGxsmDp1KkZGRjp1fhYuXEhqaipbtmxh48aNbNy4UegjrxIFtra2NGnSBEtLy1x1l06fPs3s2bPzvJ7Dhw8Lsg0iIiLlg+i4iJQqOfWNevfuxYEDB5g/fwFvvFGXevXq0bx5C5ycHMWYg38IGRkZJCYmotVqefbsGUuXLuWPP/5g+/btQHaA9eTJk2nbti1btmwRjnN1daVRo0asWLGCadOm5eq3RYsW/PLLL3qd29atWwMI6ZOdOnUq8PNmZWWVp4Ckubm53tpmOZRG5WkREZGiITouImVGhw4d8Pb2zveHX6T6smHDBjZs2CC8t7Oz4/vvv2fAgAEA3Llzhzt37jBx4kSd4zw8PGjZsiX79u3T67iMHz++3GbkmjdvLsrPi4hUMkTHRUREpEwYMGCAUEajVq1auSo1R0dHA/orODs5OXHr1i29/Zbn0syTJ09yiV+9SkBAgLisKSJSzlRbxyUqKooffviB48ePA9lPTu+88w4NGzbk3r17hISEkJqaio2NDf369aN3794VbLGISPWiVq1aNGuWt66PlZUV8P8OzKvExMRUilIS58+fF2aI9JGenl5qIpwiIiKFo9o+KtjZ2TFmzBguXLjAixcv+OKLL4QK1w4ODjx58oQOHTqwdu1a0WkREakA3N3dcXBwYOfOnTrbo6KiuHjxIv7+/sXuOyfINjExsVDtY2Nj9bbt378/Wq02z5fotIiIlD/V1nGB/y+AFxAQIEwvP3v2jPfff5+vvvqKkJAQjIyMKtBCEZF/LjKZjPnz53PgwAGmTJnCX3/9xZkzZ+jVqxeWlpZ88sknxe67VatWQHZR1/DwcC5fvszly5f1tn306BGOjo44OzuTlJRU7HOKiIiUD9XacclZJnr77bcBOHnyJNOnT2f+/Pk6wlgiIiIVw9ChQ9m8eTPfffcd7u7u+Pr6YmhoyLlz53KlJheFVq1aMWvWLDZt2oSHhwdeXl5C1frXyakzK2YIiYhUDaptjAtkayzY2dnRuHFj5s+fj0ql4ttvvxXr1YiIlDExMTGFbhsUFMTgwYOJiIjAxMSEOnXq6G3Xo0cPXilmXyBffPEFU6dO5cmTJ9SuXTvPmJm6devy5MkTFApFqajwioiIlC3V1nFJSkri4sWLdOvWjYEDBzJmzBi6du1a0WaJiIjoQS6X4+LiUur9mpqaFqpqeU6gsIiISOWn2i4VHT9+HLVaTWJiIqdOnSIqKqqiTRIREREREREpIVXWcVGpVKSnp+u8Xp1GPnz4MDKZjA0bNuDl5cXs2bNF50VERERERKSKU2UdlyFDhmBnZ6fzunv3LpBdyO/YsWO0atWKmjVrsmLFCtLT00uUpSAiIiIiIiJS8VTZGJcxY8bQp08fnW05QX1//PEHMTExjB07FsjWi5gwYQILFy5k586d9O/fv9ztFRERERERESk5VdZx8fPzy3PfkSNHAHSCcSdPnsyePXuYPn06HTp00KkoKyIiIiJSPG7evKkzmx0aGiqWQaiGDB06lPj4eACmTJlC586dK8yWavnp2rNnDzVr1sTd3V3YZmBgwPvvv09sbCxjx44lKyurAi0UEREpChcuXGDIkCE6r7/++quizSox8fHxvHz5sqLNKBEJCQkcPHiQWrVqiQVVqzHu7u7Uq1ePgwcP8vTp0wq1pVo5Lr/99htjxozhzp07JCYmMn/+fCIiIgAICwtj69atQPaMzIABA9ixY0dFmisiIlJItFotarUatVpNZGQkW7duJTY2tqLN0suLFy8K7Yw0bNiQXr16lbFF5UPOcvzrsy2JiYlIJJI8X9euXSv2OZOSkgpd1iE/Jk6cmKd9AQEBxe5XrVbz4sULVCpVieyrDGP42Wef8cUXXxT7PKVJlV0q0oevry++vr56FTK9vb05fPhwBVglIiJSUlq3bs327duB7KWInj17VrBFeVOnTh3hJl4Qn332GXZ2duVgVcWhVCqFh8SbN28ye/Zspk6dKpRleOONN4rdd//+/UlISCAsLKxENr777ru0b98egNmzZ5OamsqiRYsASvT/c+HCBdq3b8++ffvo0aNHsfupCmNYnlQrx0VEREQkh8zMTDQaTYH1yLRaLS9fvsTExKTAdunp6RgbG5eajWPGjClUu9TUVAwNDZHLC/7JTktLK9BGlUrF7t278fT0pF69eoWyobgoFAohISIntrBt27a5kiteR6vVkpmZWS6FLJs1ayZUMl+1apWOzflRmLEuDarCGJYn1WqpSEREROTatWt06tQJIyMjjI2N8fb25tSpU7naqdVqpk6dioWFBaamptjZ2fHFF1+QmZmp0+7+/fv069cPc3NzlEolDg4OzJ07V6fN3r17sba2xtraGrVazerVq4X3rysC+/r6Cvusra3zvUHu2LGDBg0aYGpqirGxMQEBAcLydw6dOnVi2rRprF27Fnt7e5RKJU5OTvz000959rtu3ToCAwPp27dvnm3Km6SkJKytrdm2bRsff/wxZmZmKJVKvLy8+OOPP4R2cXFxwtidOnWKq1ev6oznjRs3yszGmTNn0r59e06dOkWLFi1QKpVYWFgQEhKioyMWEhKCtbW1MMsydOhQwb7hw4eXmX1VYQxLA3HGpZKhQVPRJoiIVFmePn1Khw4dcHV15ciRIxgYGPDFF1/QpUsXwsLChKdqgGHDhrF3716WLVtGixYtOH36NDNmzCAhIYElS5YAkJ6eTpcuXbC0tGTXrl3CTWHGjBmYmZkxfvx4AHx8fIQYuq5du9KzZ08++OADgFyzJOPHjxeyM+bOnZtnRep9+/YRGBjI0KFD2bRpE9HR0QQHB9OhQwdu374tPOknJCTw3Xff4enpydq1a5HJZMybN48RI0bg7e2Nq6trrr6bNWuGo6NjvtmZ5Y1WqyU2NpZx48YxZMgQdu/eTXR0NNOmTWPw4MHcunULuVyOmZmZMNZTpkwhNTWVNWvWCP2UZNmkIFJTU7ly5QoTJkxg4sSJODg4sHXrVubPn4+HhwcjRowAYOTIkXTp0oU///yTiRMnMnXqVFq3bg2AtbV1mdlXFcawNBAdl0qGFCnJmmTStenUktYSK9aKiBSB2bNnk5WVxeHDh4X6Q6Ghobi5uRESEsKBAweA7GD9n3/+mXXr1vGvf/0LyI6De/HiBYsXL2bu3LkYGRkRHR1N7969GTVqFE2aNAGgefPmHDlyhG3btgmOS+3atQUnQCqVUrdu3TydgsDAQOHvb7/9Vm8brVbL5MmTadu2LVu2bBG2u7q60qhRI1asWMG0adOE7fb29hw4cEAIjHVxccHd3Z2jR4/qdVzat2/Po0ePCjGi5c+gQYNYsWKF8D45OZkPP/yQe/fu4e7ujkKhEMa2Zs2ayGSycnXAVCoV+/fvx9bWFoCOHTuyf/9+Dhw4IDguDRo0oEGDBsIyZfPmzcvVxso+hiVFXCqqhJhJzTDAADXqijZFRKRKsXfvXnr06KFTNNHAwICBAwdy9OhRMjIygOwMRMj+gX+V6dOnc/36dRQKBQBOTk4sXboUe3t7Dh06xH//+1+++eYbMjMzSU1NLbPruHPnDnfu3CEoKEhnu4eHBy1btmTfvn06252cnHSyedzc3ACIjIwsMxvLCmdnZ533le1aDA0NBacFsh3VevXqVRr7oPKPYUkRZ1wqKUqpEhky4rPiqSGrUdHmiIhUCaKjo7G3t8+13cnJCbVaTXx8PHXq1CEyMhJjY2PMzc112pmZmeWqJv3VV1/x+eef4+joiKurKyYmJsTExAhK3WV1HUCe13Lr1q18j89xYjSaqr/0LJPJgMp9LTKZrMQpz2VJVRjDoiA6LpUUhST7ic9UasoD1QOcFc4FHCEiImJlZSXc9F8lJiYGqVSKhYUFALa2tqSlpfHy5UuUSmWe/Z06dYrp06ezdu1aIWYFoFevXjx+/Lj0L+B/5MwY5XUtNWqIDzMi/1zEpaJKjkKi4A35G2RqM9Foq4e3LCJSVvj7+3Pw4MFcAnC7d++mbdu2QkCrj48PAL/++qtOu82bN9OyZUvh+Bxhr+7duwtttFotz549y9MGCwuLEouiubu74+DgwM6dO3W2R0VFcfHiRfz9/UvUP2QvG7yeQVXVKI2xLktyHOXKbmNltk8f4oxLFUCDhpealxhKDDGWlL1mgIhIZSMqKkoQyMr59/Tp04J6rr+/PyYmJsyZM4fdu3cTEBDA3LlzMTAwYMGCBdy6dYvTp08L/XXo0IFu3boxduxYTExMaNGiBefOnWPixIn07NlTmIXJEfiaPHkyU6ZMITk5mRUrVnDp0iXq16+v19ZWrVoJxVydnZ158OABvr6+Qp/Hjx8nOTkZyJb8z8zMZM+ePQA4ODjg6emJTCZj/vz5DBs2jClTpjB69GhiYmL4+OOPsbS0LHGl++3btxMYGIifnx9Hjx4tUV8FoVarCQ0NBRDSbC9cuCDs79SpU64lu8KSM9arV6+ma9euPHv2DEdHx1xZMXPmzGHPnj2cPn1ar+7KjRs3+PvvvwGIjY0lNTVV+D+xtbUVPgdFxc3NDUtLS5YtWyYE6z579ixXIGx0dDQ9evQgKCiIjz/+OFc/lWEMKxOi41IFkElkWMosydJmEZsVi6HEEDOpWUWbVSpoNCDWYxMpiHPnzjFgwACdbeq59oAAACAASURBVJ9++qnw971793BxccHFxYUzZ84QFBSEl5cXkO0MHDx4kHbt2ukcv3PnTsaMGUNAQABZWVkYGxszZswYHY2W1q1bs3z5cmbMmMH27duRy+VCraQdO3aQmpqaS7hu6dKl9O3bl7feegvIjpu5cOECHh4eAHz88ce5dDJy9FSCgoLYvHkzkK39odFoCA4OFlRc27Rpw7lz54Qn+eKSE+tQHlmLqampufRi5s2bJ/x99epVnTT1ohAcHMypU6cYN24ckJ16vn79et59912ddg8ePODSpUs6Wiuv8t1337Fs2TKdbTk29+3bl19++aVY9hkaGrJx40ZGjhwpfB5dXV25e/euTruMjAwuXbpEp06d9PZTGcawMiE6LlUImURGLVn1qGodE2PGgQMtyMyUM3r08Yo2R6SS079//zxvOq/j6enJ7du3iYyMRK1WU7duXb3Vik1MTNi0aRMrV64kNjaWunXr6lWmDQ4O5qOPPuLRo0fUqlVLeLL98ccf9Z6/QYMG3Lp1i8jISLKysnBychKCIwH+/PPPQl0HZDsygwcPJiIiAhMTE70BwVeuXNF7bH7jNWjQINq3b0/t2rULbUtxsbCwKNT/XV7tOnTokOfxRkZGhIaGEhMTQ3x8PE5OTnqVkp8+fYq1tXWe8UxLly5l6dKlBdqYV7sTJ07keUyfPn14/vw5ERERKJVKvSUEcooW5jXLURnGsDIhOi5VkHRNOmrUmEhMqqTOS3i4HX/84cyZM+60bPmgos0RqaY4OjoWqp2FhUWBMxhyubxI0vgSiQQnJ6dCty/o3K+r75YG+jKWSsLkyZOpUaMGO3fu1OsoliU5iq/6SE5O5uzZs4JeT0WgUCj06unkcPDgQUxMTApVZqCsyG8MIVtU7/nz5+VoUd6IjksVxEhqRJomDS1a4rMSsKpi6dLu7lG4u0cRFpb3F1lERKRqYGtrKywzVEZSUlIYNmwYn332WUWbkieOjo6sX78eGxubijYlT2xtbTE1NWXcuHF5xneVF6LjUkUxllb9IF1Dw8qreyAiIlI46tWrx8qVKyvajDyxtbVl9erVFW1GvowcObKiTSiQ1+tzVSRiWGQVx0xqylN13qmZlZkquMolIiIiIlLBiI5LFUchUWArr0OyJpk0TVpFmyMiIiIiIlKmiI5LNcFMakaSRn+VWRERERERkeqCGONSjbCSWZGlzSJNm4ap1LSizSk1njyxIiIid7R7ZqYWmQysrauW6qOIyD+dzMxMrl69Wmxht7IgLi6OqKgoGjduXKTjwsLCaNq0KYaGhmVkWcVz48YN7OzsdIqXViTlNuOSnp6eq6JpWZCZmcnu3bvLtHLr77//riP5XR7nLAwKiQKZRIZaqyZFk0IhZS8qPTduOLJxY4dcrx9/7Eh4eNF+ZESqFxMmTMglHJaDVqslKiqqnC0qGzIyMgSV4KpORkYG3bt3Z/369RVtig6PHz/mzTff1FFYLoj9+/fTpUuXalN1OS/Onj2Lj49PvqUuypNycVxWrlxJ48aNyyWPfvfu3YwaNYpNmzaVet+HDx+mU6dO9OrVi3v37pXLOYuDpcwSU6kpiZrqMRPh4fGEIUN+z/UaMOB33NxuV7R5IhVIaGgov//+u95969atw97evkg3ovLkxYsXuWoq5cW7775L3bp1SU9PL2Oryp5JkyaRmJjIqlWrSqU/b2/vUskaatq0KQsWLCAgIKBQeiUPHjwgMDCQ7777LpdGy549e5BIJHpfeRXIDA0NzVdHRR/Lly9HIpEQERGRa19qaioSiYSbN2/qPbZJkya8/fbbOtuePXuGvb09TZs21XkQf//99/Hx8WHgwIGFFoIsS8plqWj8+PGcPXuWkydPlvm5unXrxqxZs8pEyKdr1648evRIKLxWHucsCQqJArU2q6LNKDFOTrE4OeV+2kxL0/L48ROg+iyLiZQe/v7+zJgxg6ZNm1a0KXqpU6cOEyZMYOHChQW2HT16NC1btqz0iqYFcenSJdauXcv58+cr5dLKe++9x7p165gyZUqBD6Iff/wxHTt21Pu77+3tzY4dOwD4+eef+fnnn/n+++8xMTFBoVCUie0lRaVSMWDAANLS0ti9e3euUhaLFy/G1dWVDRs2MGrUqAqyMptyWyoqL2EdCwsLJk6cWGZS1vquo6zPWVxMpEpkEilZ2qxKWVlarZaiVssKbigi8gqpqalkZRXskLu4uDBnzhwsLS0L3XdycnKefWu1WqE4YmFsLE38/PyYMmVKoc6rVqsL1WdaWsFZiCqViu3bt3P//v1C9VkQixYtok2bNnh7e+fb7vbt29SoUYNdu3aVynkLi0QiYcKECfz000/5Lv/cvn2bffv2MWHCBL377ezs6N+/P/3796dhw4ZAtvR///796d27d5nYXlI+/vhjzp07x9atW/WqRNvY2DBkyBAWLVpU4bMu5ea4lLcEdFlR1a5DggSZRIYESYV/2HKIjLRizx4vkpKU3Lljy7FjjUlI0F9DREQkh3PnztG0aVNMTU0xNzdn1qxZuW7Se/fuFaTLc16vVtF9ldTUVKytrfnmm29YsmQJderUwdzcHFNTU50lGZVKxYwZM6hZsybm5uZYW1uzfPlyvX1u2rQJR0dHTE1NsbS0ZPjw4TpxAa/ap1arWb16tfD+dVn/kJAQnevQV6cohx07dtCgQQNMTU0xNjYmICAg1/LBzJkzad++PadOnaJFixYolUosLCwICQnJ87dh3bp1BAYG5irwVxxynuQDAwMLbKvRaEhISEClKn+Ryr59+6JQKIQZE3389NNP1KxZk86dO5ejZWXHd999x9dff828efPo2rVrnu2GDh3K7du3uXr1ajlal5siLRWNHz9eqCr6OtOmTStUbY6MjAzmzZvHoUOHsLe3Z8aMGbRo0ULY//DhQ5YtW4axsTF//fUXrq6ufPrpp8JTk0qlYunSpaSmpgpPDF5eXgQGBpKVlcXhw4fZuHEj//rXv/D39y/wmIKIjIxk4cKFaLVaUlJScs2q6Dvny5cv2b17N5s3b2b9+vWsXbuWU6dOsWjRInx8fFi4cCGHDh3i559/Lrco7UwyUaBAQsWrvjk6xuHoGEefPpcq2hSRKkJsbCxvv/02tWvXZteuXSiVShYuXMjDhw+FqrsAjRo1Ys6cOQBcv36dNWvW5DsDERsby5dffolGo2H48OFYW1uTmJiosyQTFBTEgQMHmDt3Lp6enhw+fJjJkyeTnp7OtGnThHZr165lzJgxTJo0icGDB/P3338zdepUevfuzdmzZ5HL5fj4+LB161Yge+m5Z8+efPDBBwC5Cjz27NkTZ2dnINsxyWupfd++fQQGBjJ06FA2bdpEdHQ0wcHBdOjQgdu3b2NsnK2ynZqaypUrV5gwYQITJ07EwcGBrVu3Mn/+fDw8PBgxYkSuvps1a4ajoyN+fn55jmFh+f3338nIyMDHx6fEfZUlSqWS5s2bc/z4cSZOnKi3zfHjx/Hy8qpyD7L6uHDhAmPHjiUwMJCpU6fm29bHxwepVMrx48d17tvlTaEdF7Vazd9//83GjRuF6qhnzpxh8ODB9OnTp1BOi1arZeLEidjY2ODt7c1PP/3E+fPnOXPmDG+88Qapqan06NGDsWPH8tFHHxETE0Pz5s3JzMwUMgeWLFlCjRo1hAHOCfSC7C/Gzz//zJEjRxg6dKhw3vyOyY+IiAjeeust1q5dS4cOHUhJSckVzKTvnIcPH2bDhg1cvnyZr7/+GqVSyePHj7l//z4+Pj6EhYVx7do1YmNjy81xMZQYotFq0Gq1aNEilVT9L5zIP4fly5eTnJzM5cuXhZkJX19f4caeQ7169QRHIDQ0lDVr1hTYt1Qq5cqVK9SsWTPXvjNnzvDzzz+zceNG4cbepk0bYmNjWbBgAZMmTUKhUJCWlsasWbMYOnQoixcvBrLjHKysrOjWrRunT5+mc+fO1K5dW3ACpFIpdevWzdMpaN++Pe3btwfg3r17eh0XrVbL5MmTadu2LVu2bBG2u7q60qhRI1asWKHjXKlUKvbv34+trS0AHTt2ZP/+/Rw4cECv49K+fXsePXpU4BgWhvDwcIA869ycO3eOixcvAv9fLXn//v3CjJWzs3O5LbPUq1dPsEUf4eHhDBs2rFxsKUueP39Ov379yMjIYPTo0QW2NzY2xtbWlr/++qscrMubQjsuWVlZvP/++8KMQ3R0NOPHj8fZ2TnPdMTX0Wg0zJw5Uyjr3a5dO8aMGcOCBQtYs2YNz58/5+nTp7i7uwPZ1Srd3NyEDzzAyZMnad26tfB+6tSp7N+/H4A333wTjUbDnj17dM6b3zH5MWXKFFq1akWHDh0AMDU1JSgoiOnTpwtt9J2zb9++REVFcfnyZby9venbty+ffvqpUMl548aNxMXF4eDgUKhxKy2kEilarbZSzLqIiBSFCxcu4OnpqbOcolQqhYeokhAUFKTXaYHs2QyJRIK5uTmHDh0StteoUYO4uDgePXqEi4sLt2/fJjY2lkGDBukc7+fnR3h4uPCbV9rcuXOHO3fu5JoZ8PDwoGXLluzbt0/HcTE0NBScFsh2nurVq1cu6bzR0dFIJJI8K3Hv379fmC3LYdOmTUKQbM+ePYvsuKxfv15vjR0XFxeOHj2a53FWVlZ5Zhap1Wri4uLyzA6qSly5cgV7e3vq1avH2LFjuXbtmjBDlxf5jU15UWjHxdDQUFjn1Gg0fPjhhyQmJrJ9+3bMzMyAbOfm9TVJhUKBTJYdgCmTyXS+wIGBgcyfP59Ll7KXDOrVq8fNmzextbUlPj6ebdu2ER0drfNFc3NzY/ny5Tx+/Jhp06bh6urKO++8o2Pn6+R3TF42R0VFcfToUf7973/r7NPnbOg7Z07keMuWLQEEpwWyf3CVyoqJ6ZBIJKi1arK0WRhgoGOXiEhl5fHjx2VWkTa/LI9nz56h1Wr1LivL5XKioqJwcXERbvyvx6HIZDIaNGhQuga/QnR0NAD29va59jk5OXHr1q0C+5DJZOUSS5KRkYFMJstzeWX69OlCsOvt27fx9fVl3bp19OvXD8j//ykv2rZtm+s3HMjTecrBwMAgz9RzlUqFVqvNtbRXFTE3N+fAgQO8ePGCzp07M2vWLBYtWpTvMfmNTXlRrJFfuHAhJ0+eZPny5ToqgytXruSLL77Qabt48eI8K19KJBLc3Nx0gudMTEyYPn06KpWKf/3rX+zZs4eMjAxh/5w5c0hMTGTnzp388ssvDBs2jC+//FL4UOu7Eed3TF425+TTFyYjoard/OUSOXLkqLQqFFTO1DwRkVextbUlJiam3M9ramqKoaEhycnJ+d44cx6uXrx4UV6mAQhLzTkOzKvExMRUqlmBnIDktLQ0vU/1rz7Q5dhtbm6e52xYYWjUqBGNGjUq8nGJiYl5ZokaGxtjYmJS6Ayz0iA0NJQHDx4wbtw4ne052WsGBgbF6rdt27Y0adIEyNZqWbp0Kf3796dNmzZ5HpOYmIiHh0exzldaFDnQ4bfffmPhwoUEBgbqzHQAdOnSheXLl+u82rVrl29/KpVK+NLHxMTQsWNH3NzcWLRokd4nFXNzc77//nt2795N48aN2bx5M++9916+58jvmLxszglCri7Km/rI1nkpXOpkZUOlUpGUFE9CQlyFe/8iZY+XlxdhYWFC7ANkK1anpKSU6Xn9/PzIyMhg586dOtu1Wq3OdLmHhwdmZma5lqlv375NixYtOHPmTK6+LSwsChVrlx/u7u44ODjksi8qKoqLFy8KCQolITIykszMzBL34+joCMCTJ09K3FdZExkZme9SvqOjY7lex+3btwkODubhw4c6248ePYqNjU2pyI0sWLAAOzs7Ro0aledvqkajISoqqtzDHF6nSDMu0dHRvP/++7i5uQkBaJAdqGRvb19k71aj0XDnzh1hGvb777/n4cOHDBgwQKfdq6l6a9as4aOPPuLNN9/k6NGjjBo1in379pGenp6nOFN+x+Rlc850ZmhoKNOmTcs1q5JXdlVVQ4aMTG0mMmTIJJVTUyUrK4uUlFRevHhBYmIicXFxqFQqjIxMyczMJDr6MV5enrkEk0SqDxMnTmT16tX06dOH+fPnY2VlxZw5c3JJkEdFRREWFgYg/Hv69GlBLt/f379In5OAgAD8/f15//33iY+Pp1u3biQkJPD5559z8+ZNwsPDkcvlmJmZ8emnnzJjxgzc3NwYNGgQDx8+JDg4mIyMDDw9PXP13apVK3bu3En//v1xdnbmwYMH+Pr6CrMO169f58GDBwDcvXtXJ5bOyMiIbt26IZPJmD9/PsOGDWPKlCmMHj2amJgYPv74YywtLfnkk0+KONK6bN++ncDAQPz8/PKNCSkMb775JpAdV/G60uzrGBkZ4eXlVWEzRlevXuXdd9/Nc/+bb76p1xnN4dmzZ8JKQk6MZmhoqCBA17179yLZM2LECBYvXkzv3r1ZvHgxtWrVYsOGDZw4cYINGzaUSnaTubk533zzDT169GD27NnMnz8/V5u//vqLly9fCnGfFUWhHZecuJbk5GR++eUXnS//r7/+ypgxYwrVR2xsLLVq1QJg69atGBoaEhwcnG3M/9YMf/31V4YMGcLOnTt58OABBgYGhIeHY2BgwJEjR+jUqRMeHh7I5XK6d+/O9evXBaclZ1np1TXbgo7Rh6urK35+fhw7doxFixYxZcoUtFot586dA7IzjnKmPPWdM0fESt8T4apVqzh58iT//e9/SyW4sCRIJBIMMCBDk4FMIssO3i3HpS+tVotWqyUrKwuNRoNGoyEjI4OkpCSSkpKIi4snJSUFpdIYCwsLzMzMsLOzxdTUlLQ0LWp1Fn//fYNLly7TsGFData0qhYpiiK61KlThyNHjjBgwAA6d+6MRCJh8ODBuRyCc+fO5Xrw+fTTT4W/7927l0svpSB2797NtGnTmDBhgjDz0KZNG3bt2qUT5xASEoJcLmf27Nl88sknSKVSevXqxcqVK/UujSxdupS+ffvy1ltvAWBmZsaFCxeEafj169fnksXPiTN0cHAQ4mqGDh2KRqMhODhYiE9o06YN586dKzCWoyByHtBK4zfB1tYWLy8vQkNDGThwYL5tXVxcBMezvLl8+TLPnz+nV69eebbp3bs369at4/79+3rF2i5cuJBL+yYnC8nS0pL4+Pgi2WRtbc2RI0cYPXq0oLNSo0YN1q1bl6+DVVS6d+9OUFAQixcvpn///rmKYO7fvx8LCwvBCa0oCu24rF27lpMnT+Li4sLKlSuF7fHx8Rw5coRJkyble3xQUBA3b97E19eXwYMHk56ezv379/n111+FOJJhw4axZcsW4QuYI4azZcsWvv/+e+bNm4dGo2HgwIF8+OGH2NjY8NNPP/HNN98AcO3aNb7++msgW1DHwcGB1q1b53tMQdc8YsQI5s2bx48//oi1tbWQx37w4EEaNGiAoaFhrnMmJCQIkfAzZ85k2rRpOtoFhw4d4uzZszx79qzCHZccDKXZAcYqVBhQvPXSwqDRaEhPTycjI4OMjAzS09NRqVSkp6cLgdJabfZ6t7GxMfXr18LExCRPZ0Qmk9K8eXPu37/P33//jUqVSZ06dUTnpRrSpk0bIiIiePjwIVZWVnqlBPr3719ooUUTE5NCtTUxMWHVqlUsWLCAJ0+eYGVllWfcxSeffML48eOJjIzEzs4u3yD8Bg0acOvWLSIjI8nKysLJyUlIZIDsmMFXf2vzIygoiMGDBxMREYGJiYlesbqlS5eydOnSXNtPnDiRZ7+DBg2iffv2paYKHhwczEcffcSSJUsqTaXh19mwYQOenp75hjm8/fbbuLq68u233+rNWurTp0+pC342btyY8+fPk5SURHR0NG+88UaJAoT//PNPvds3b97M5s2bc23XarVs3LiR0aNHV1hySQ6FvuqPPvqIjz76qNgnatmyJYcOHSIyMlJYP3xd+8Xa2prz58/z9OlT7O3tkUql+Pn5ERISImQj/fDDD8jlcsLDw9FoNGzbtk2YOWnWrJkg7vQq+R2THzVr1mTfvn2Eh4eTnJxM06ZNSU5O5oMPPtBZ49N3zpynKH1s2rSJ58+fl1mWREmQIs0O2pWUPGhXq9UKVW1TUlJITk4RZqfkcjkymRSFQoFCocDS0hIDAwMMDQ2Ry+VFfsKrW/cN5HIFERGP0Gg0Fb4GK1I2yOXyApcZygqlUombm1uB7QwNDQtto0QiKZQGVmGQy+VFnk0qDPoylorL0KFDWblyJV9++aVOuEFl4e7du2zYsKFAuQypVMrixYt55513GDt2bKmOUUGYm5tXyAPvli1biI6OJiQkpNzP/Trlns/l6OgoBGnpQy6X6+w3MDDQSaE2Nc0uqNe8efNCn7M4x7xKjq4M6E99Lio1atSoVNH+ryIj+4mvOM6LRqMhPj6B+Ph4EhISePHiBXK5HLVajZmZGRYW5hgZGSGTZTstUmn2qzSmoWUyKQ4O9kgkEu7c+QutVpvv50xERKT8kclkbNmyhXbt2uHt7c3gwYMr2iSBmJgY+vXrx+TJkwsVw9G7d2+GDx/OgAEDOHToUKWZPS8LLl68yIQJE/jxxx+FUI+KpOonoouUKjlOhAIFWdosIWA3Ow5Fg1qtJitLLaQ1pqSkkJKSSkJCAhkZGZiammBpaYm1dS1sbeug1YKHh3t+pyw1pFIpjo4OSCQS7t27J8y8vDr9LiIiUrHUr1+fI0eO6Aj6lYQVK1aUyoxHdHQ0Q4YM0YmJKojly5czffp0kpOTS+y4tGrVim3btpWoj1cxMjLi4MGD1K1bt8R93b17l82bN9OtW7dSsKzkiI6LiF7UajUJLxPQZmhJS0sjPT2DrKws1GrV//7NQiqVYGJiipVVDezt7TE0NNBxEnKOK28cHLJtuX//AVlZGpycHKuFWJSISHWhefPmxZ4Bf538NEeKQnE0X6RSqd7sm+JgbW1dqkUbZTJZqTkala28gfhrLkJWVhZpaekkJyeRlJRMcnIy6elqtFoFL9PiMTYxwNbGFhMjJQqFAgMDAx1F5MpIrVq1kMvl3Llzh9TUFBo2bFip7RURERERKRyi4/IPJDMzkxcv4oiLiyMhIYH4+ES0WjnGxrWRy80wNHTFyMgUCVoUmmukJj3DwMGpRAqW5Y1EIsHS0pImTZoQFnaJK1eu0rRpk2IrTIqIiIiIVA5Ex6Wak5SUTGTkE5KTU0lJSSMxMYmMDA0GBpYYGlqhUNhSp44FMlnuQFytNgtTQwU1ZYbcvnkV5wYNsK1th0xaNWYuJBIJSqUSH59W/PHHFcLD/6J+fTcMDQ2rXJkGkdLl0qVLLF68mP/85z8VlqUE2XIShoaGFZpeOmHCBN544w2hTtCraLVanj59WmZFIsuTjIwMkpOTK0VwqUjJEMUuqjm//36eK1ciiYlRolY7YWnpg51dR6ytW2Ju/gbGxrX0Oi2vYmZogJuFKTF/PyAy8nGpyH+XJ0ZGRjRp0pjMzEzu3bunU/tK5J/JkydP2Lp1a7nXFnqdhg0b5it0Vh6Ehoby+++/6923bt067O3tOX36dDlbVThevHjBy5cvC9X23XffpW7dulWyREhiYiISiSTP17Vr13Idk5qaikQi4ebNm4U+z7Vr15BIJHz//fd693t7e7N69Wq9+8aPH59LaDErK4suXbpgZmZWqIKfhUWccanm1FXHkxL7jFq1GmNsXHwVTRMDBU4yKU+jInmYno6Lq2uVihkxNTXF3b0Bd+7c4fr1P2natEmhtHxERMqSzz77rFLPZvj7+zNjxgyaNm1a0abopU6dOkyYMIGFCxcW2Hb06NG0bNmySn7vlUolO3bsAODmzZvMnj2bqVOnCsq2b7zxRgValzfTp0/n2LFj7Ny5k4YNG5Zav6LjUs3xNski+eVVzl1LpZn3kAJnV/JDIZVib2pIRMxT/tSk0dyjdLICygOJRIKpqSlNmjThzz9vcPHiRZo1a1ZiSXSR6kFqaipKpTLfJUStVkt6erpe+f7XSUtLw8jIqMAlycKUSoHsuDSpVFpq2XGpqan/01TK/+HDxcWFOXPmFKnv5ORklEql3r61Wi0pKSmYmZkVysbSrD/m5+eHn59fge0yMzPRaDSFcnDyqnT9OseOHUOpVBY7A0qhUNC/f38AYamrbdu29OnTp1j9lQfbtm1j4cKFzJw5k4CAgFLtu9ovFe3du5d33nmHt99+m969e7N69Wqh4No/AbkEOlpoeUt7h/Dz3xEf/xittngFIjVa0Gi12JgYkfD4Oddv/Vmlik1KJBIMDAzw9GxJ7dq1uX07nLi4uIo2S6QQTJw4EWtra72vwt789XHs2DHq16+Pqakp5ubmTJ8+Pddn+v79+/Tr1w9zc3OUSiUODg56Zd4Bli1bhpOTE0qlElNTU/r06cO9e/d02vj6+urYn3ND0sehQ4do2bKlUALD29ubI0eOFPt6z507R9OmTYXrnTVrFmq1boX4vXv35hrjnIKBr5Oamoq1tTXffPMNS5YsoU6dOpibm2NqaqqzJKNSqZgxYwY1a9bE3Nwca2trli9frrfPTZs24ejoiKmpKZaWlgwfPlynmOar9qnValavXi28f105OCQkROc69JVCyOHatWt06tQJIyMjYaxPnTql02bmzJm0b9+eU6dO0aJFC5RKJRYWFoSEhOQp8X/z5k38/f3x9fUlOjo6z/NXJ65fv857771Hjx49+Pzzz0u9/2o/49KrVy9iYmL45JNPmDlzJmPHjq1okyqEFmZg9PIxx2/uQlW/O7VqOSPNJ8hWo8lCk5XGy4x0YjTppGdlkZklQSsxJktihIGyBmnJqWRkZmBoaIhUUrV8YDc3Nx4+jODOnbu4ublWqYypfyJDhw6ldevWOtvWrFnD6dOn8fX1LXa/S5YsYe7cubi5ubFr1y6++uorjI2N+eyzzwBIT0+nS5cuJKodOwAAIABJREFUWFpasmvXLqytrdm2bRszZszAzMyM8ePH69gzadIk5syZg7+/Pw8fPiQkJISuXbty69Yt4Ql+/PjxQpG9uXPnkpSUpNe2a9eu0atXLwYMGMDKlStJS0tj3rx59OzZk7CwsCIv38TGxvL2229Tu3Ztdu3ahVKpZOHChTx8+BAvLy+hXaNGjYRZluvXr7NmzZpczs3r/X755ZdoNBqGDx+OtbU1iYmJOjMWQUFBHDhwgLlz5+Lp6cnhw4eZPHky6enpTJs2TWi3du1axowZw6RJkxg8eDB///03U6dOpXfv3pw9exa5XI6Pj49QZqVr16707NmTDz74ACDXjFTPnj1xdnYGYMeOHZw8eVLvNTx9+pQOHTrg6urKkSNHMDAw4IsvvqBLly6EhYXRrFkzINtRu3LlChMmTGDixIk4ODiwdetW5s+fj4eHByNGjMjVt62tLU2aNMHS0vIfMcMbFxdHQEAAdnZ2/PDDD2VSN67aOy4AN27cQCKRVDoRnfLGQwlG0mj2/rUfiaQX1tbOwr6srAwyM5PIyEggIyMRtToVqVSOQq4gTe6E1NAcmcwImdQAA6kcU5kBKtVN0P6vPACKKuW8yOVy6tZ1QqPRcP36nzRu3Ahra+uKNkskD7y9vfH29hbe//rrr/z2229MmTKFoUOHFrvftWvXCtWkO3XqREREBCtWrGDmzJlIpVKio6Pp3bs3o0aNokmTJkC2eNqRI0fYtm2bjuNy+PBhGjduLCivtmrVCltbWxYsWMCjR4+E2mSBgYHCMd9++22etp04cQKVSsWqVauEgoReXl6888473L17t8iOy/Lly0lOTuby5cvCzISvr69wY8+hXr16giMQGhrKmjVrCuxbKpVy5coVvQ8AZ86c4eeff2bjxo3Cjb1NmzbExsayYMECJk2ahEKhIC0tjVmzZjF06FChjpG3tzdWVlZ069aN06dP07lzZ2rXri0s+UilUurWrZvnElD79u1p3749kF0ZPC/HZfbs2WRlZXH48GFhrENDQ3FzcyMkJIQDBw4IbVUqFfv378fW1haAjh07sn//fg4cOKDXcbGysuL69esFjmF1QKPRMHjwYO7fv8+nn35aZo7aP8JxOXr0KC1atMDGxqaiTalwnI3gLU00W69uQ9usJ1lZCaSnx6HVajEyssLAwAIzMyeMjWsikRQUfJu9fp9TWTpNk4axtOD13sqCQqGgfn03FAo5f/75J02aNBGdlyrAzZs3CQoKolu3bsybN69Efb1ez6pv377s3buXe/fuUb9+fZycnFi6dClxcXEcOnSIJ0+eoFKpyMzMJCsrS+dYf39/goODmTRpEoGBgXh5eeHr61vsGSFfX18MDAx45513CA4Opl27dlhaWrJ3795i9XfhwgU8PT11llOUSmWp1NgJCgrKc9Zy3759SCQSzM3NdWT+a9SoQVxcHI8ePcLFxYXbt28TGxvLoEGDdI738/MjPDy8TIOY9+7dS48ePXQqVhsYGDBw4ECWL19ORkaGUKfO0NBQcFog23mqV68ekZGRZWZfVSEzM5MTJ07QunVrFi9ezLBhw0o1KDeHau+43L59m8ePH//jZ1tepb4SemalsOnyb3h4NMXevjMKhRJJCWdMjCRGaLSaKjXzAuDs7IxEIuXvv++jVquxsbEpk+lNkZITFxdHnz59sLW15aeffir1zLach5tXY5+++uorPv/8cxwdHXF1dcXE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" + } + }, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "![Screenshot from 2023-09-14 06-26-00.png](attachment:4fef384b-1f6b-4712-8dcf-8e3f7b973a2f.png)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The LaS specification is shown in part b) of the figure above.\n", + "`max_i`, `max_j` and `max_k` are the bounds of spacetime.\n", + "In our example, they are 2, 2, and 3, which means all the cubes and pipes are within 2x2x3 volume." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "input_dict = {\n", + " \"max_i\": 2, \"max_j\": 2, \"max_k\": 3\n", + "}" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are certain *ports* that connects the LaS to the outside (which makes sense since a subroutine in classical computing always has some arguements and some returns).\n", + "In this example, there are two ports on the bottom floor corresponding to the two qubits before the CNOT; then, there is some manipulation of these two qubits implmented with the pipes in the gray box; on the top floor, the two ports are the qubits after going through the CNOT.\n", + "\n", + "We need to provide three things to specify each port.\n", + "Let us look at the port for the output of control qubit in the CNOT indicated in the callout in part a) of the figure above.\n", + "In the code block below, it is the third port in `input_dict[\"ports\"]`.\n", + "- Its `location` is `[1,0,3]` because that is where the information is going out of the LSS.\n", + "- In general, the pipe connecting a port can also be in I, J or K direction.\n", + "Additionally, we need another character (`-` or `+`) to indicate the direction from the port to the other parts of the LaS.\n", + "In this example, the pipe is in the K direction, and we need to go downward from `[1, 0, 3]` to everything else, so the `direction` of the port is `-K`.\n", + "- Finally, surface code patches have a space orientation of the X and Z boundaries indicated by red and blue above.\n", + "We provide which one of I, J, and K is orthogonal to the face of Z boundary (blue).\n", + "In this example, it is J that is orthogonal to the blue faces, so the `z_basis_direction` of this port is `J`." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "input_dict[\"ports\"] = [\n", + " {\"location\": [1, 0, 0], \"direction\": \"+K\", \"z_basis_direction\": \"J\"},\n", + " {\"location\": [0, 1, 0], \"direction\": \"+K\", \"z_basis_direction\": \"J\"},\n", + " {\"location\": [1, 0, 3], \"direction\": \"-K\", \"z_basis_direction\": \"J\"},\n", + " {\"location\": [0, 1, 3], \"direction\": \"-K\", \"z_basis_direction\": \"J\"},\n", + "]" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we need to provide the stabilizer constraints on the ports to ensure that the LaS indeed realizes the logical operations we want to perform.\n", + "Although intuitively there are input and output ports for the CNOT, in a LaS, there is no inherent distinction between inputs and outputs.\n", + "What matters is that the given stabilizers have to match the ordering of the ports.\n", + "Our ordering is (control qubit input, target qubit input, control qubit output, target qubit output), so the correct stabilizers are ZIZI, IZZZ, XIXX, and IXIX.\n", + "If we change the ordering of the `\"ports\"` list above, we also need to change the stabilizers." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "input_dict[\"stabilizers\"] = [\"Z.Z.\", \".ZZZ\", \"X.XX\", \".X.X\"]\n", + "# Note that we use a . for an identity in a stabilizer" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Solving LaS" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "By now we have finished preparing the specification of the LaS.\n", + "We can use our software package `lassynth`, specifically the class `LatticeSurgerySynthesizer` to solve the problem.\n", + "When we invoke `solve` method, the synthesizer gives us a solution with respect to a `specification`." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from lassynth import LatticeSurgerySynthesizer\n", + "\n", + "las_synth = LatticeSurgerySynthesizer()\n", + "result = las_synth.solve(specification=input_dict)\n", + "result" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As you have noticed, the return value is of a class `LatticeSurgerySolution`.\n", + "We implement a few methods for this class to help us further manipulate the solution.\n", + "To see the \"raw\" solution, i.e., LaSRe (lattice surgery subroutine representation) in the paper, you can access the `lasre` of this result.\n", + "Due to technical reasons, the `ports` here is another encoding compared to the `ports` in the specification.\n", + "Intersted readers can refer to comments in the code to understand this encoding, but it is not too important in this notebook." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'n_i': 2,\n", + " 'n_j': 2,\n", + " 'n_k': 3,\n", + " 'n_p': 4,\n", + " 'n_s': 4,\n", + " 'ports': [{'i': 1, 'j': 0, 'k': 0, 'd': 'K', 'e': '-', 'c': 1},\n", + " {'i': 0, 'j': 1, 'k': 0, 'd': 'K', 'e': '-', 'c': 1},\n", + " {'i': 1, 'j': 0, 'k': 2, 'd': 'K', 'e': '+', 'c': 1},\n", + " {'i': 0, 'j': 1, 'k': 2, 'd': 'K', 'e': '+', 'c': 1}],\n", + " 'stabs': [[{'KI': 0, 'KJ': 1},\n", + " {'KI': 0, 'KJ': 0},\n", + " {'KI': 0, 'KJ': 1},\n", + " {'KI': 0, 'KJ': 0}],\n", + " [{'KI': 0, 'KJ': 0},\n", + " {'KI': 0, 'KJ': 1},\n", + " {'KI': 0, 'KJ': 1},\n", + " {'KI': 0, 'KJ': 1}],\n", + " [{'KI': 1, 'KJ': 0},\n", + " {'KI': 0, 'KJ': 0},\n", + " {'KI': 1, 'KJ': 0},\n", + " {'KI': 1, 'KJ': 0}],\n", + " [{'KI': 0, 'KJ': 0},\n", + " {'KI': 1, 'KJ': 0},\n", + " {'KI': 0, 'KJ': 0},\n", + " {'KI': 1, 'KJ': 0}]],\n", + " 'port_cubes': [(1, 0, 0), (0, 1, 0), (1, 0, 3), (0, 1, 3)],\n", + " 'optional': {},\n", + " 'ExistI': [[[0, 1, 0], [0, 1, 0]], [[0, 0, 0], [0, 0, 0]]],\n", + " 'ExistJ': [[[0, 0, 1], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", + " 'ExistK': [[[0, 1, 0], [1, 1, 1]], [[1, 1, 1], [1, 1, 0]]],\n", + " 'ColorI': [[[0, 1, 1], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", + " 'ColorJ': [[[0, 0, 0], [0, 0, 0]], [[0, 0, 1], [0, 0, 0]]],\n", + " 'NodeY': [[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [1, 0, 1]]],\n", + " 'CorrIJ': [[[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", + " [[[0, 1, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", + " [[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", + " [[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]]],\n", + " 'CorrIK': [[[[0, 0, 0], [0, 0, 1]], [[0, 0, 0], [0, 0, 0]]],\n", + " [[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", + " [[[0, 1, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", + " [[[0, 0, 0], [0, 1, 0]], [[0, 0, 0], [0, 0, 0]]]],\n", + " 'CorrJK': [[[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", + " [[[0, 0, 1], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", + " [[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", + " [[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]]],\n", + " 'CorrJI': [[[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", + " [[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", + " [[[0, 0, 1], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", + " [[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]]],\n", + " 'CorrKI': [[[[1, 0, 1], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", + " [[[1, 0, 1], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", + " [[[1, 1, 1], [0, 0, 1]], [[1, 1, 1], [0, 0, 0]]],\n", + " [[[0, 0, 0], [1, 1, 1]], [[0, 0, 0], [1, 1, 0]]]],\n", + " 'CorrKJ': [[[[0, 0, 1], [0, 0, 0]], [[1, 1, 1], [0, 0, 0]]],\n", + " [[[1, 1, 1], [1, 1, 1]], [[0, 1, 1], [0, 0, 0]]],\n", + " [[[1, 0, 1], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]],\n", + " [[[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [1, 1, 0]]]]}" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result.lasre" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Post-process and Output LaS" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We provide a few rewrite passes to remove valid but unnecessary structures in the solution, and also color the K-pipes.\n", + "These can be applied with the follow call. " + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "result = result.after_default_optimizations()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can export the result to a few formats.\n", + "The most direct one is to save the LaSRe, which is now just a dictionary" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "result.save_lasre(\"cnot.lasre.json\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also create a 3D modelling file in the [GLTF](https://www.khronos.org/gltf/) format.\n", + "This can be opened in many software, a lot of them are also web-based.\n", + "The `attach_axes` flag attaches I (red), J (green), and K (blue) axis to the GLTF." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "result.to_3d_model_gltf(\"cnot.gltf\", attach_axes=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Like mentioned previously, the generated LaS can be easily mapped to a ZX-diagram.\n", + "We can use this connection to verify our result.\n", + "Internally, we construct the ZX-diagram and let [Stim ZX](https://github.com/quantumlib/Stim/tree/main/glue/zx) to derive the stabilizers.\n", + "Then, we check whether these stabilizers are commutable with the ones in the specification.\n", + "If all are commutable, then our LaS is correct." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "specified:\n", + "+Z_Z_\n", + "+_ZZZ\n", + "+X_XX\n", + "+_X_X\n", + "==============================================================\n", + "resulting:\n", + "+X_XX\n", + "+Z_Z_\n", + "+_X_X\n", + "+_ZZZ\n", + "==============================================================\n", + "specified and resulting stabilizers are equivalent.\n" + ] + }, + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result.verify_stabilizers_stimzx(specification=input_dict, print_stabilizers=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using Other SAT solver" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "So far, we are using the [Z3 SMT solver](https://github.com/Z3Prover/z3) to do everything.\n", + "In our experience, it may be faster to generate an SAT problem with Z3 and solve it using other solvers, like Kissat.\n", + "For the user, it is very easy to change: just initiate the `LatticeSurgerySynthesizer` with the directory where Kissat is installed in your system." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "las_synth = LatticeSurgerySynthesizer(solver=\"kissat\", kissat_dir=\"\")\n", + "# you need to add the kissat dir based on where kissat is on your computer" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Adding constraints time: 0.207474946975708\n", + "CNF generation time: 0.004982948303222656\n", + "c ---- [ banner ] ------------------------------------------------------------\n", + "c\n", + "c Kissat SAT Solver\n", + "c \n", + "c Copyright (c) 2021-2023 Armin Biere University of Freiburg\n", + "c Copyright (c) 2019-2021 Armin Biere Johannes Kepler University Linz\n", + "c \n", + "c Version 3.1.1 71caafb4d182ced9f76cef45b00f37cc598f2a37\n", + "c Apple clang version 15.0.0 (clang-1500.3.9.4) -W -Wall -O3 -DNDEBUG\n", + "c Sun May 12 13:03:10 PDT 2024 Darwin MacBook-Pro-2 23.4.0 arm64\n", + "c\n", + "c ---- [ parsing ] -----------------------------------------------------------\n", + "c\n", + "c opened and reading DIMACS file:\n", + "c\n", + "c cnot.dimacs\n", + "c\n", + "c parsed 'p cnf 462 2231' header\n", + "c closing input after reading 40739 bytes (40 KB)\n", + "c finished parsing after 0.00 seconds\n", + "c\n", + "c ---- [ options ] -----------------------------------------------------------\n", + "c\n", + "c --seed=916189 (different from default '0')\n", + "c\n", + "c ---- [ solving ] -----------------------------------------------------------\n", + "c\n", + "c seconds switched conflicts irredundant variables\n", + "c MB reductions redundant trail remaining\n", + "c level restarts binary glue\n", + "c\n", + "c * 0.00 2 0 0 0 0 0 0 614 1557 0% 0 402 87%\n", + "c { 0.00 2 0 0 0 0 0 0 614 1557 0% 0 402 87%\n", + "c i 0.00 2 22 0 0 0 38 23 623 1556 44% 2 398 86%\n", + "c i 0.00 2 22 0 0 0 39 23 623 1556 44% 2 397 86%\n", + "c } 0.00 2 22 0 0 0 39 23 623 1556 44% 2 397 86%\n", + "c 1 0.00 2 22 0 0 0 39 23 623 1556 44% 2 397 86%\n", + "c\n", + "c ---- [ result ] ------------------------------------------------------------\n", + "c\n", + "s SATISFIABLE\n", + "v 1 -2 -3 -4 5 -6 -7 -8 9 10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22\n", + "v 23 -24 -25 -26 -27 28 -29 -30 -31 -32 33 -34 35 -36 37 38 39 40 41 42 43 44\n", + "v 45 46 47 48 -49 50 -51 -52 -53 54 -55 -56 -57 -58 -59 60 -61 62 -63 64 65\n", + "v -66 -67 -68 69 -70 71 -72 -73 -74 75 -76 -77 -78 79 -80 -81 -82 -83 -84 85\n", + "v 86 -87 -88 -89 90 91 92 93 94 95 -96 -97 -98 99 100 101 -102 -103 -104 -105\n", + "v -106 -107 -108 109 110 111 112 113 -114 115 116 117 118 119 120 121 122 -123\n", + "v -124 -125 -126 127 128 129 130 131 132 133 134 135 -136 137 -138 139 -140\n", + "v 141 142 143 -144 145 -146 147 148 -149 -150 -151 152 153 154 -155 -156 -157\n", + "v 158 159 160 -161 -162 163 -164 165 166 -167 168 169 -170 171 172 173 174\n", + "v -175 176 177 178 179 180 -181 182 183 184 185 -186 187 188 189 190 191 192\n", + "v 193 -194 -195 196 197 198 -199 -200 201 202 -203 204 205 206 207 208 209\n", + "v -210 -211 212 213 214 215 216 217 218 219 -220 221 -222 -223 -224 225 226\n", + "v 227 228 -229 230 -231 232 -233 234 235 236 -237 -238 239 240 241 242 243\n", + "v -244 245 246 247 -248 249 -250 251 -252 -253 254 255 256 257 258 -259 260\n", + "v 261 262 263 -264 -265 266 267 268 -269 270 -271 272 273 -274 275 276 277 278\n", + "v 279 280 281 282 283 284 -285 286 -287 288 289 290 -291 292 293 -294 -295 296\n", + "v 297 -298 299 300 301 302 303 -304 305 -306 307 -308 309 -310 311 312 313\n", + "v -314 315 -316 317 318 319 -320 321 322 323 -324 -325 326 327 328 -329 330\n", + "v 331 332 333 334 335 336 -337 -338 339 340 341 -342 -343 344 345 346 347 348\n", + "v 349 350 351 352 353 354 355 -356 357 -358 359 -360 361 362 363 -364 365 -366\n", + "v 367 368 369 -370 371 -372 373 -374 -375 376 -377 378 -379 380 -381 382 -383\n", + "v -384 385 386 -387 388 389 390 391 392 393 394 395 -396 -397 398 -399 400\n", + "v -401 402 403 -404 405 406 407 408 -409 -410 411 412 413 414 415 -416 -417\n", + "v 418 -419 420 421 422 423 424 425 426 427 428 429 430 -431 432 433 434 435\n", + "v 436 437 438 439 -440 441 442 443 444 445 446 447 448 -449 450 451 452 453\n", + "v 454 455 456 457 -458 459 -460 461 462 0\n", + "c\n", + "c ---- [ profiling ] ---------------------------------------------------------\n", + "c\n", + "c 0.00 39.95 % parse\n", + "c 0.00 36.66 % search\n", + "c 0.00 34.35 % focused\n", + "c 0.00 0.00 % simplify\n", + "c =============================================\n", + "c 0.00 100.00 % total\n", + "c\n", + "c ---- [ statistics ] --------------------------------------------------------\n", + "c\n", + "c conflicts: 39 12268.01 per second\n", + "c decisions: 186 4.77 per conflict\n", + "c jumped_reasons: 1002 29 % propagations\n", + "c propagations: 3417 1074866 per second\n", + "c queue_decisions: 186 100 % decision\n", + "c random_decisions: 0 0 % decision\n", + "c random_sequences: 0 0 interval\n", + "c score_decisions: 0 0 % decision\n", + "c switched: 0 0 interval\n", + "c vivify_checks: 0 0 per vivify\n", + "c vivify_units: 0 0 % variables\n", + "c\n", + "c ---- [ resources ] ---------------------------------------------------------\n", + "c\n", + "c maximum-resident-set-size: 1828716544 bytes 1744 MB\n", + "c process-time: 0.00 seconds\n", + "c\n", + "c ---- [ shutting down ] -----------------------------------------------------\n", + "c\n", + "c exit 10\n", + "kissat runtime: 0.008579015731811523\n", + "kissat SAT!\n", + "Construct a Z3 SMT model and solve...\n", + "elapsed time: 0.021113s\n", + "Z3 SAT\n", + "Total solving time: 0.04399609565734863\n" + ] + } + ], + "source": [ + "result = las_synth.solve(\n", + " specification=input_dict,\n", + " print_detail=True,\n", + " dimacs_file_name=\"cnot\",\n", + " sat_log_file_name=\"cnot\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We used a few optional arguments above.\n", + "`print_detail` will display the output of Kissat on the screen. \n", + "`dimacs_file_name` specifies where to store the SAT problem instance in the DIMACS format.\n", + "This instance is generated by Z3 and then solved by Kissat.\n", + "`sat_log_file_name` saves the output of Kissat, which is basically what you have seen as the output (from `c ---- [ banner ]` to `c exit 10`)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.19" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/glue/lattice_surgery/lassynth/__init__.py b/glue/lattice_surgery/lassynth/__init__.py new file mode 100644 index 000000000..9640f285c --- /dev/null +++ b/glue/lattice_surgery/lassynth/__init__.py @@ -0,0 +1,2 @@ +from .lattice_surgery_synthesis import LatticeSurgerySynthesizer +from .lattice_surgery_synthesis import LatticeSurgerySolution diff --git a/glue/lattice_surgery/lassynth/lattice_surgery_synthesis.py b/glue/lattice_surgery/lassynth/lattice_surgery_synthesis.py new file mode 100644 index 000000000..fe83bb23f --- /dev/null +++ b/glue/lattice_surgery/lassynth/lattice_surgery_synthesis.py @@ -0,0 +1,590 @@ +"""Two wrapper classes, rewrite passes, and translators.""" + +import functools +import itertools +import json +import time +import multiprocessing +import random +import networkx +from typing import Any, Literal, Mapping, Optional, Sequence +from lassynth.rewrite_passes.attach_fixups import attach_fixups +from lassynth.rewrite_passes.color_z import color_z +from lassynth.rewrite_passes.remove_unconnected import remove_unconnected +from lassynth.sat_synthesis.lattice_surgery_sat import LatticeSurgerySAT +from lassynth.tools.verify_stabilizers import verify_stabilizers +from lassynth.translators.gltf_generator import gltf_generator +from lassynth.translators.textfig_generator import textfig_generator +from lassynth.translators.zx_grid_graph import ZXGridGraph +from lassynth.translators.networkx_generator import networkx_generator + + +def check_lasre(lasre: Mapping[str, Any]) -> None: + """check aspects of LaSRe other than SMT constraints, i.e., data layout.""" + if "n_i" not in lasre: + raise ValueError( + f"upper bound of I dimension, `n_i`, is missing in lasre.") + if lasre["n_i"] <= 0: + raise ValueError("n_i <= 0.") + if "n_j" not in lasre: + raise ValueError( + f"upper bound of J dimension, `n_j`, is missing in lasre.") + if lasre["n_j"] <= 0: + raise ValueError("n_j <= 0.") + if "n_k" not in lasre: + raise ValueError( + f"upper bound of K dimension, `n_k`, is missing in lasre.") + if lasre["n_k"] <= 0: + raise ValueError("n_k <= 0.") + if "n_p" not in lasre: + raise ValueError(f"number of ports, `n_p`, is missing in lasre.") + if lasre["n_p"] <= 0: + raise ValueError("n_p <= 0.") + if "n_s" not in lasre: + raise ValueError(f"number of stabilizers, `n_s`, is missing in lasre.") + if lasre["n_s"] < 0: + raise ValueError("n_s < 0.") + if lasre["n_s"] == 0: + print("no stabilizer!") + + if "ports" not in lasre: + raise ValueError(f"`ports` is missing in lasre.") + if len(lasre["ports"]) != lasre["n_p"]: + raise ValueError("number of ports in `ports` is different from `n_p`.") + for port in lasre["ports"]: + if "i" not in port: + raise ValueError(f"location `i` missing from port {port}.") + if port["i"] not in range(lasre["n_i"]): + raise ValueError(f"i out of range in port {port}.") + if "j" not in port: + raise ValueError(f"location `j` missing from port {port}.") + if port["j"] not in range(lasre["n_j"]): + raise ValueError(f"j out of range in port {port}.") + if "k" not in port: + raise ValueError(f"location `k` missing from port {port}.") + if port["k"] not in range(lasre["n_k"]): + raise ValueError(f"k out of range in port {port}.") + if "d" not in port: + raise ValueError(f"direction `d` missing from port {port}.") + if port["d"] not in ["I", "J", "K"]: + raise ValueError(f"direction not I, J, or K in port {port}.") + if "e" not in port: + raise ValueError(f"open end `e` missing from port {port}.") + if port["e"] not in ["-", "+"]: + raise ValueError(f"open end not - or + in port {port}.") + if "c" not in port: + raise ValueError(f"color `c` missing from port {port}.") + if port["c"] not in [0, 1]: + raise ValueError(f"color not 0 or 1 in port {port}.") + + if "stabs" not in lasre: + raise ValueError(f"`stabs` is missing in lasre.") + if len(lasre["stabs"]) != lasre["n_s"]: + raise ValueError("number of stabs in `stabs` is different from `n_s`.") + for stab in lasre["stabs"]: + if len(stab) != lasre["n_p"]: + raise ValueError("number of boundary corrsurf is not `n_p`.") + for i, corrsurf in enumerate(stab): + for (k, v) in corrsurf.items(): + if lasre["ports"][i]["d"] == "I" and k not in ["IJ", "IK"]: + raise ValueError(f"stabs[{i}] key invalid {stab}.") + if lasre["ports"][i]["d"] == "J" and k not in ["JI", "JK"]: + raise ValueError(f"stabs[{i}] key invalid {stab}.") + if lasre["ports"][i]["d"] == "K" and k not in ["KI", "KJ"]: + raise ValueError(f"stabs[{i}] key invalid {stab}.") + if v not in [0, 1]: + raise ValueError(f"stabs[{i}] value not 0 or 1 {stab}.") + + port_cubes = [] + for p in lasre["ports"]: + # if e=-, (i,j,k); otherwise, +1 in the proper direction + if p["e"] == "-": + port_cubes.append((p["i"], p["j"], p["k"])) + elif p["d"] == "I": + port_cubes.append((p["i"] + 1, p["j"], p["k"])) + elif p["d"] == "J": + port_cubes.append((p["i"], p["j"] + 1, p["k"])) + elif p["d"] == "K": + port_cubes.append((p["i"], p["j"], p["k"] + 1)) + lasre["port_cubes"] = port_cubes + + if "optional" not in lasre: + lasre["optional"] = {} + + for key in [ + "NodeY", + "ExistI", + "ExistJ", + "ExistK", + "ColorI", + "ColorJ", + ]: + if key not in lasre: + raise ValueError(f"`{key}` missing from lasre.") + if len(lasre[key]) != lasre["n_i"]: + raise ValueError(f"dimension of {key} is wrong.") + for tmp in lasre[key]: + if len(tmp) != lasre["n_j"]: + raise ValueError(f"dimension of {key} is wrong.") + for tmptmp in tmp: + if len(tmptmp) != lasre["n_k"]: + raise ValueError(f"dimension of {key} is wrong.") + + if lasre["n_s"] > 0: + for key in [ + "CorrIJ", + "CorrIK", + "CorrJI", + "CorrJK", + "CorrKI", + "CorrKJ", + ]: + if key not in lasre: + raise ValueError(f"`{key}` missing from lasre.") + if len(lasre[key]) != lasre["n_s"]: + raise ValueError(f"dimension of {key} is wrong.") + for tmp in lasre[key]: + if len(tmp) != lasre["n_i"]: + raise ValueError(f"dimension of {key} is wrong.") + for tmptmp in tmp: + if len(tmptmp) != lasre["n_j"]: + raise ValueError(f"dimension of {key} is wrong.") + for tmptmptmp in tmptmp: + if len(tmptmptmp) != lasre["n_k"]: + raise ValueError(f"dimension of {key} is wrong.") + + +class LatticeSurgerySolution: + """A class for the result of synthesizer lattice surgery subroutine. + + It internally saves an LaSRe (Lattice Surgery Subroutine Representation) + and we can apply rewrite passes to it, or use translators to derive + other formats of the LaS solution + """ + + def __init__( + self, + lasre: Mapping[str, Any], + ) -> None: + """initialization for LatticeSurgerySubroutine + + Args: + lasre (Mapping[str, Any]): LaSRe + """ + check_lasre(lasre) + self.lasre = lasre + + def get_depth(self) -> int: + """get the depth/height of the LaS in LaSRe. + + Returns: + int: depth/height of the LaS in LaSRe + """ + return self.lasre["n_k"] + + def after_removing_disconnected_pieces(self): + """remove_unconnected.""" + return LatticeSurgerySolution(lasre=remove_unconnected(self.lasre)) + + def after_color_k_pipes(self): + """coloring K pipes.""" + return LatticeSurgerySolution(lasre=color_z(self.lasre)) + + def after_default_optimizations(self): + """default optimizations: remove unconnected, and then color K pipes.""" + solution = LatticeSurgerySolution(lasre=remove_unconnected(self.lasre)) + solution = LatticeSurgerySolution(lasre=color_z(solution.lasre)) + return solution + + def after_t_factory_default_optimizations(self): + """default optimization for T-factories.""" + solution = LatticeSurgerySolution(lasre=remove_unconnected(self.lasre)) + solution = LatticeSurgerySolution(lasre=color_z(solution.lasre)) + solution = LatticeSurgerySolution(lasre=attach_fixups(solution.lasre)) + return solution + + def save_lasre(self, file_name: str) -> None: + """save the current LaSRe to a file. + + Args: + file_name (str): file name including extension to save the LaSRe + """ + with open(file_name, "w") as f: + json.dump(self.lasre, f) + + def to_3d_model_gltf(self, + output_file_name: str, + stabilizer: int = -1, + tube_len: float = 2.0, + no_color_z: bool = False, + attach_axes: bool = False, + rm_dir: Optional[str] = None) -> None: + """generate gltf file (for 3D modelling). + + Args: + output_file_name (str): file name including extension to save gltf + stabilizer (int, optional): Defaults to -1 meaning do not draw + correlation surfaces. If the value is in [0, n_s), + the correlation surfaces corresponding to that stabilizer + are drawn and faces in one of the directions are revealed + to unveil the correlation surfaces. + tube_len (float, optional): Length of the pipe comapred to + the cube. Defaults to 2.0. + no_color_z (bool, optional): Do not color the K pipes. + Defaults to False. + attach_axes (bool, optional): attach IJK axes. Defaults to False. + If attached, the color coding is I->red, J->green, K->blue. + rm_dir (str, optional): the (+|-)(I|J|K) faces to remove. + Intended to reveal correlation surfaces. Default to None. + """ + gltf = gltf_generator( + self.lasre, + stabilizer=stabilizer, + tube_len=tube_len, + no_color_z=no_color_z, + attach_axes=attach_axes, + rm_dir=rm_dir if rm_dir else (":+J" if stabilizer >= 0 else None), + ) + with open(output_file_name, "w") as f: + json.dump(gltf, f) + + def to_zigxag_url( + self, + io_spec: Optional[Sequence[str]] = None, + ) -> str: + """generate a link that leads to a ZigXag figure. + + Args: + io_spec (Optional[Sequence[str]], optional): Specify whether + each port is an input or an output. Length must be the same + with the number of ports. Defaults to None, which means + all ports are outputs. + + Returns: + str: the ZigXag link + """ + zxgridgraph = ZXGridGraph(self.lasre) + return zxgridgraph.to_zigxag_url(io_spec=io_spec) + + def to_text_diagram(self) -> str: + """generate the text figure of LaS time slices. + + Returns: + str: text figure of the LaS + """ + return textfig_generator(self.lasre) + + def to_networkx_graph(self) -> networkx.Graph: + """generate a annotated networkx.Graph correponding to the LaS. + + Returns: + networkx.Graph: + """ + return networkx_generator(self.lasre) + + def verify_stabilizers_stimzx(self, + specification: Mapping[str, Any], + print_stabilizers: bool = False) -> bool: + """verify the stabilizer of the LaS. + + Use StimZX to deduce the stabilizers from the annotated networkx graph. + Then use Stim to ensure that this set of stabilizers and the set of + stabilizers specified in the input are equivalent. + + Args: + specification (Mapping[str, Any]): the LaS specification to verify + the current solution against. + print_stabilizers (bool, optional): If True, print the two sets of + stabilizers. Defaults to False. + + Returns: + bool: True if the two sets are equivalent; otherwise False. + """ + paulistrings = [ + paulistring.replace(".", "_") + for paulistring in specification["stabilizers"] + ] + return verify_stabilizers( + paulistrings, + self.to_networkx_graph(), + print_stabilizers=print_stabilizers, + ) + + +class LatticeSurgerySynthesizer: + """A class to synthesize LaS.""" + + def __init__( + self, + solver: Literal["kissat", "z3"] = "z3", + kissat_dir: Optional[str] = None, + ) -> None: + """initialize. + + Args: + solver (Literal["kissat", "z3"], optional): the solver to use. + Defaults to "z3". "kissat" is recommended. + kissat_dir (Optional[str], optional): directory of the kissat + executable. Defaults to None. + """ + self.solver = solver + self.kissat_dir = kissat_dir + + def solve( + self, + specification: Mapping[str, Any], + given_arrs: Optional[Mapping[str, Any]] = None, + given_vals: Optional[Sequence[Mapping[str, Any]]] = None, + print_detail: bool = False, + dimacs_file_name: Optional[str] = None, + sat_log_file_name: Optional[str] = None, + ) -> Optional[LatticeSurgerySolution]: + """solve an LaS synthesis problem. + + Args: + specification (Mapping[str, Any]): the LaS specification to solve. + given_arrs (Optional[Mapping[str, Any]], optional): given array of + known values to plug in. Defaults to None. + given_vals (Optional[Sequence[Mapping[str, Any]]], optional): given + known values to plug in. Defaults to None. Format should be + a sequence of dicts. Each one contains three fields: "array", + the name of the array, e.g., "ExistI"; "indices", a sequence of + the indices, e.g., [0, 0, 0]; and "value", 0 or 1. + print_detail (bool, optional): whether to print details in + SAT solving. Defaults to False. + dimacs_file_name (Optional[str], optional): file to save the + DIMACS. Defaults to None. + sat_log_file_name (Optional[str], optional): file to save the + SAT solver log. Defaults to None. + + Returns: + Optional[LatticeSurgerySubroutine]: if the problem is + unsatisfiable, this is None; otherwise, a + LatticeSurgerySolution initialized by the compiled result. + """ + start_time = time.time() + sat_synthesis = LatticeSurgerySAT( + input_dict=specification, + given_arrs=given_arrs, + given_vals=given_vals, + ) + if print_detail: + print(f"Adding constraints time: {time.time() - start_time}") + + start_time = time.time() + if self.solver == "z3": + if_sat = sat_synthesis.check_z3(print_progress=print_detail) + else: + if_sat = sat_synthesis.check_kissat( + dimacs_file_name=dimacs_file_name, + sat_log_file_name=sat_log_file_name, + print_progress=print_detail, + kissat_dir=self.kissat_dir, + ) + if print_detail: + print(f"Total solving time: {time.time() - start_time}") + + if if_sat: + solver_result = sat_synthesis.get_result() + return LatticeSurgerySolution(lasre=solver_result) + else: + return None + + def optimize_depth( + self, + specification: Mapping[str, Any], + start_depth: Optional[int] = None, + print_detail: bool = False, + dimacs_file_name_prefix: Optional[str] = None, + sat_log_file_name_prefix: Optional[str] = None, + ) -> LatticeSurgerySolution: + """find the optimal solution in terms of depth/height of the LaS. + + Args: + specification (Mapping[str, Any]): the LaS specification to solve. + start_depth (int, optional): starting depth of the exploration. If not + provided, use the depth given in the specification + print_detail (bool, optional): whether to print details in SAT solving. + Defaults to False. + dimacs_file_name_prefix (Optional[str], optional): file prefix to save + the DIMACS. The full file name will contain the specific depth + after this prefix. Defaults to None. + sat_log_file_name_prefix (Optional[str], optional): file prefix to save + the SAT log. The full file name will contain the specific depth + after this prefix. Defaults to None. + result_file_name_prefix (Optional[str], optional): file prefix to save + the variable assignments. The full file name will contain the + specific depth after this prefix. Defaults to None. + post_optimization (str, optional): optimization to perform when + initializing the LatticeSurgerySubroutine object for the result. + Defaults to "default". + + Raises: + ValueError: starting depth is too low. + + Returns: + LatticeSurgerySolution: compiled result with the optimal depth. + """ + self.specification = dict(specification) + if start_depth is None: + depth = self.specification["max_k"] + else: + depth = int(start_depth) + if depth < 2: + raise ValueError("depth too low.") + + checked_depth = {} + while True: + # the ports on the top floor will still be on the top floor when we + # increase the height. This is an assumption. Adapt to your case. + for port in self.specification["ports"]: + if port["location"][2] == self.specification["max_k"]: + port["location"][2] = depth + self.specification["max_k"] = depth + + result = self.solve( + specification=self.specification, + print_detail=print_detail, + dimacs_file_name=dimacs_file_name_prefix + + f"_d={depth}" if dimacs_file_name_prefix else None, + sat_log_file_name=sat_log_file_name_prefix + + f"_d={depth}" if sat_log_file_name_prefix else None, + ) + if result is None: + checked_depth[str(depth)] = "UNSAT" + if str(depth + 1) in checked_depth: + # since this depth leads to UNSAT, we need to increase + # the depth, but if depth+1 is already checked, we can stop + break + else: + depth += 1 + else: + checked_depth[str(depth)] = "SAT" + self.sat_result = LatticeSurgerySolution(lasre=result.lasre) + if str(depth - 1) in checked_depth: + # since this depth leads to SAT, we need to try decreasing + # the depth, but if depth-1 is already checked, we can stop + break + else: + depth -= 1 + + return self.sat_result + + def try_one_permutation( + self, + perm: Sequence[int], + specification: Mapping[str, Any], + print_detail: bool = False, + dimacs_file_name_prefix: Optional[str] = None, + sat_log_file_name_prefix: Optional[str] = None, + ) -> Optional[LatticeSurgerySolution]: + """check if the problem is satisfiable given a port permutation. + + Args: + specification (Mapping[str, Any]): the LaS specification to solve. + perm (Sequence[int]): the given permutation, which is an integer + tuple of length n (n being the number of ports permuted). + print_detail (bool, optional): whether to print details in + SAT solving. Defaults to False. + dimacs_file_name_prefix (Optional[str], optional): file prefix + to save the DIMACS. The full file name will contain the + specific permutation after this prefix. Defaults to None. + sat_log_file_name_prefix (Optional[str], optional): file prefix + to save the SAT log. The full file name will contain the + specific permutation after this prefix. Defaults to None. + + Returns: + Optional[LatticeSurgerySubroutine]: if the problem is + unsatisfiable, this is None; otherwise, a + LatticeSurgerySolution initialized by the compiled result. + """ + + # say `perm` is [0,3,2], then `original` is in order, i.e., [0,2,3] + # the full permutation is 0,1,2,3 -> 0,1,3,2 + original = sorted(perm) + this_spec = dict(specification) + new_ports = [] + for p, port in enumerate(specification["ports"]): + if p not in perm: + # the p-th port is not involved in `perm`, e.g., 1 is unchanged + new_ports.append(port) + + else: + # after the permutation, the index of the p-th port in + # specification is the k-th port in `perm` where k is the place + # of p in `original`. In this example, when p=0 and 1, nothing + # changed. When p=2, we find `place` to be 1, and perm[place]=3 + # so we attach port_3. When p=3, we end up attach port_2 + place = original.index(p) + new_ports.append(specification["ports"][perm[place]]) + this_spec["ports"] = new_ports + + result = self.solve( + specification=this_spec, + print_detail=print_detail, + dimacs_file_name=dimacs_file_name_prefix + "_" + + perm.__repr__().replace(" ", "") + if dimacs_file_name_prefix else None, + sat_log_file_name=sat_log_file_name_prefix + "_" + + perm.__repr__().replace(" ", "") + if sat_log_file_name_prefix else None, + ) + print(f"{perm}: {'SAT' if result else 'UNSAT'}") + return result + + def solve_all_port_permutations( + self, + permute_ports: Sequence[int], + parallelism: int = 1, + shuffle: bool = True, + **kwargs, + ) -> Mapping[str, Sequence[Sequence[int]]]: + """try all the permutations of given ports, which ones are satisfiable. + + Note that we do not check that the LaS after permuting the ports (we do + not permute the stabilizers accordingly) is functionally equivalent. + The user should use this method based on their judgement. Also, the + number of permutations scales exponentially with the number of ports + to permute, so this method can easily take an immense amount of time. + + Args: + permute_ports (Sequence[int]): the indices of ports to permute + parallelism (int, optional): number of parallel process. Each one + try one permutation. A New proess starts when an old one + finishes. Defaults to 1. + shuffle (bool, optional): whether using a random order to start the + processes. Defaults to True. + **kwargs: other arguments to `try_one_permutation`. + + Returns: + Mapping[str, Sequence[Sequence[int]]]: a dict with two keys. + "SAT": [.] a list containing all the satisfiable permutations; + "UNSAT": [.] all the unsatisfiable permutations. + """ + perms = list(itertools.permutations(permute_ports)) + if shuffle: + random.shuffle(perms) + + pool = multiprocessing.Pool(parallelism) + # issue the job one by one (chuck=1) + results = pool.map( + functools.partial( + self.try_one_permutation, + **kwargs, + ), + perms, + chunksize=1, + ) + + sat_perms = [] + unsat_perms = [] + for p, result in enumerate(results): + if result is None: + unsat_perms.append(perms[p]) + else: + sat_perms.append(perms[p]) + + return { + "SAT": sat_perms, + "UNSAT": unsat_perms, + } diff --git a/glue/lattice_surgery/lassynth/rewrite_passes/__init__.py b/glue/lattice_surgery/lassynth/rewrite_passes/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/glue/lattice_surgery/lassynth/rewrite_passes/attach_fixups.py b/glue/lattice_surgery/lassynth/rewrite_passes/attach_fixups.py new file mode 100644 index 000000000..cad628f34 --- /dev/null +++ b/glue/lattice_surgery/lassynth/rewrite_passes/attach_fixups.py @@ -0,0 +1,86 @@ +"""assuming all T injections requiring fixup are on the top floor. +The output is also on the top floor""" + +from typing import Mapping, Any + + +def attach_fixups(lasre: Mapping[str, Any]) -> Mapping[str, Any]: + n_s = lasre["n_s"] + n_i = lasre["n_i"] + n_j = lasre["n_j"] + n_k = lasre["n_k"] + + fixup_locs = [] + for p in lasre["optional"]["top_fixups"]: + fixup_locs.append((lasre["ports"][p]["i"], lasre["ports"][p]["j"])) + + lasre["n_k"] += 2 + # we add two layers on the top. The lower layer will contain fixups dressed + # as Y cubes that is connecting downwards. The upper layer will contain no + # new cubes. This corresponds to waiting the machine to apply the fixups + # because there is a finite interaction time from the machine knows whether + # the injection is T or T^dagger to apply the fixups. + for i in range(n_i): + for j in range(n_j): + if (i, j) in fixup_locs: + lasre["NodeY"][i][j].append(1) # fixup dressed as Y cubes + lasre["ExistK"][i][j][n_k - 1] = 1 # connect fixup downwards + else: + lasre["NodeY"][i][j].append(0) # no fixup + lasre["ExistK"][i][j][n_k - 1] = 0 + lasre["NodeY"][i][j].append(0) # the upper layer is empty + + # do not add any new pipes in the added layer + for arr in ["ExistI", "ExistJ", "ExistK"]: + lasre[arr][i][j].append(0) + lasre[arr][i][j].append(0) + for arr in ["ColorI", "ColorJ", "ColorKM", "ColorKP"]: + lasre[arr][i][j].append(-1) + lasre[arr][i][j].append(-1) + for s in range(n_s): + for arr in [ + "CorrIJ", "CorrIK", "CorrJK", "CorrJI", "CorrKI", + "CorrKJ" + ]: + lasre[arr][s][i][j].append(0) + lasre[arr][s][i][j].append(0) + + # the output ports need to be extended in the two added layers + for port in lasre["ports"]: + if "f" in port and port["f"] == "output": + ii, jj = port["i"], port["j"] + lasre["ExistK"][ii][jj][n_k - 1] = 1 + lasre["ExistK"][ii][jj][n_k] = 1 + lasre["ExistK"][ii][jj][n_k + 1] = 1 + lasre["ColorKM"][ii][jj][n_k] = lasre["ColorKP"][ii][jj][n_k - 1] + lasre["ColorKM"][ii][jj][n_k + 1] = lasre["ColorKM"][ii][jj][n_k] + lasre["ColorKP"][ii][jj][n_k] = lasre["ColorKM"][ii][jj][n_k] + lasre["ColorKP"][ii][jj][n_k + 1] = lasre["ColorKP"][ii][jj][n_k] + for s in range(n_s): + lasre["CorrKI"][s][ii][jj][n_k] = lasre["CorrKI"][s][ii][jj][ + n_k - 1] + lasre["CorrKI"][s][ii][jj][n_k + + 1] = lasre["CorrKI"][s][ii][jj][n_k] + lasre["CorrKJ"][s][ii][jj][n_k] = lasre["CorrKJ"][s][ii][jj][ + n_k - 1] + lasre["CorrKJ"][s][ii][jj][n_k + + 1] = lasre["CorrKJ"][s][ii][jj][n_k] + port["k"] += 2 + new_cubes = [] + for c in lasre["port_cubes"]: + if c[0] == port["i"] and c[1] == port["j"]: + new_cubes.append((c[0], c[1], port["k"] + 1)) + else: + new_cubes.append(c) + lasre["port_cubes"] = new_cubes + + t_injections = [] + for port in lasre["ports"]: + if port["f"] == "T": + if port["e"] == "+": + t_injections.append([port["i"], port["j"], port["k"] + 1]) + else: + t_injections.append([port["i"], port["j"], port["k"]]) + lasre["optional"]["t_injections"] = t_injections + + return lasre diff --git a/glue/lattice_surgery/lassynth/rewrite_passes/color_z.py b/glue/lattice_surgery/lassynth/rewrite_passes/color_z.py new file mode 100644 index 000000000..7c0c956db --- /dev/null +++ b/glue/lattice_surgery/lassynth/rewrite_passes/color_z.py @@ -0,0 +1,210 @@ +"""We do not have ColorZ from the SAT/SMT. Now we color the Z-pipes.""" + +from typing import Sequence, Mapping, Any, Union, Tuple + + +def if_uncolorK(n_i: int, n_j: int, n_k: int, + ExistK: Sequence[Sequence[Sequence[int]]], + ColorKP: Sequence[Sequence[Sequence[int]]], + ColorKM: Sequence[Sequence[Sequence[int]]]) -> bool: + """return whether there are uncolored K-pipes""" + for i in range(n_i): + for j in range(n_j): + for k in range(n_k): + if ExistK[i][j][k] and (ColorKP[i][j][k] == -1 + or ColorKM[i][j][k] == -1): + return True + return False + + +def in_bound(n_i: int, n_j: int, n_k: int, i: int, j: int, k: int) -> bool: + if i in range(n_i) and j in range(n_j) and k in range(n_k): + return True + return False + + +def propogate_IJcolor(n_i: int, n_j: int, n_k: int, + ExistI: Sequence[Sequence[Sequence[int]]], + ExistJ: Sequence[Sequence[Sequence[int]]], + ExistK: Sequence[Sequence[Sequence[int]]], + ColorI: Sequence[Sequence[Sequence[int]]], + ColorJ: Sequence[Sequence[Sequence[int]]], + ColorKP: Sequence[Sequence[Sequence[int]]], + ColorKM: Sequence[Sequence[Sequence[int]]]) -> None: + """propagate the color of I- and J-pipes to their neighbor K-pipes.""" + + for i in range(n_i): + for j in range(n_j): + for k in range(n_k): + if ExistK[i][j][k]: + # 4 possible neighbor I/J pipe for the minus end of K-pipe + if in_bound(n_i, n_j, n_k, i - 1, j, + k) and ExistI[i - 1][j][k]: + ColorKM[i][j][k] = 1 - ColorI[i - 1][j][k] + if ExistI[i][j][k]: + ColorKM[i][j][k] = 1 - ColorI[i][j][k] + if in_bound(n_i, n_j, n_k, i, j - 1, + k) and ExistJ[i][j - 1][k]: + ColorKM[i][j][k] = 1 - ColorJ[i][j - 1][k] + if ExistJ[i][j][k]: + ColorKM[i][j][k] = 1 - ColorJ[i][j][k] + + # 4 possible neighbor I/J pipe for the plus end of K-pipe + if (in_bound(n_i, n_j, n_k, i - 1, j, k + 1) + and ExistI[i - 1][j][k + 1]): + ColorKP[i][j][k] = 1 - ColorI[i - 1][j][k + 1] + if in_bound(n_i, n_j, n_k, i, j, + k + 1) and ExistI[i][j][k + 1]: + ColorKP[i][j][k] = 1 - ColorI[i][j][k + 1] + if (in_bound(n_i, n_j, n_k, i, j - 1, k + 1) + and ExistJ[i][j - 1][k + 1]): + ColorKP[i][j][k] = 1 - ColorJ[i][j - 1][k + 1] + if in_bound(n_i, n_j, n_k, i, j, + k + 1) and ExistJ[i][j][k + 1]: + ColorKP[i][j][k] = 1 - ColorJ[i][j][k + 1] + + +def propogate_Kcolor(n_i: int, n_j: int, n_k: int, + ExistK: Sequence[Sequence[Sequence[int]]], + ColorKP: Sequence[Sequence[Sequence[int]]], + ColorKM: Sequence[Sequence[Sequence[int]]], + NodeY: Sequence[Sequence[Sequence[int]]]) -> bool: + """propagate color from colored K-pipes to uncolored K-pipes. + If no new color can be assigned, return False; otherwise, return True.""" + + did_something = False + for i in range(n_i): + for j in range(n_j): + for k in range(n_k): + if ExistK[i][j][k]: + # consider propagate color from below + if in_bound( + n_i, n_j, n_k, i, j, k - + 1) and ExistK[i][j][k - 1] and NodeY[i][j][k - + 1] == 0: + if ColorKP[i][j][k - + 1] > -1 and ColorKM[i][j][k] == -1: + ColorKM[i][j][k] = ColorKP[i][j][k - 1] + did_something = True + # consider propagate color from above + if in_bound( + n_i, n_j, n_k, i, j, k + + 1) and ExistK[i][j][k + 1] and NodeY[i][j][k + + 1] == 0: + if ColorKM[i][j][k + + 1] > -1 and ColorKP[i][j][k] == -1: + ColorKP[i][j][k] = ColorKM[i][j][k + 1] + did_something = True + + # if K-pipe connects a Y Cube, two ends can be colored same + if (NodeY[i][j][k] and ColorKM[i][j][k] == -1 + and ColorKP[i][j][k] > -1): + ColorKM[i][j][k] = ColorKP[i][j][k] + did_something = True + if (in_bound(n_i, n_j, n_k, i, j, k + 1) + and NodeY[i][j][k + 1] and ColorKM[i][j][k] > -1 + and ColorKP[i][j][k] == -1): + ColorKP[i][j][k] = ColorKM[i][j][k] + did_something = True + return did_something + + +def assign_Kcolor(n_i: int, n_j: int, n_k: int, + ExistK: Sequence[Sequence[Sequence[int]]], + ColorKP: Sequence[Sequence[Sequence[int]]], + ColorKM: Sequence[Sequence[Sequence[int]]], + NodeY: Sequence[Sequence[Sequence[int]]]) -> None: + """when no color can be deducted by propagating from other K-pipes, we + assign some color variables at will. Then, we can continue to propagate.""" + + # assign a color by letting the two ends of a K-pipe to be the same + for i in range(n_i): + for j in range(n_j): + for k in range(n_k): + if ExistK[i][j][k]: + if ColorKM[i][j][k] > -1 and ColorKP[i][j][k] == -1: + ColorKP[i][j][k] = ColorKM[i][j][k] + break + # For K-pipes that have no color at both ends and connects a Y-cube + for i in range(n_i): + for j in range(n_j): + for k in range(n_k): + if ExistK[i][j][k]: + if NodeY[i][j][k] and ColorKM[i][j][k] == -1: + ColorKM[i][j][k] = 0 + break + if (in_bound(n_i, n_j, n_k, i, j, k + 1) + and NodeY[i][j][k + 1] and ColorKP[i][j][k] == -1): + ColorKP[i][j][k] = 0 + break + + +def color_ports(ports: Sequence[Mapping[str, Union[str, int]]], + ColorKP: Sequence[Sequence[Sequence[int]]], + ColorKM: Sequence[Sequence[Sequence[int]]]) -> None: + for port in ports: + if port['d'] == 'K': + if port['e'] == '+': + ColorKP[port['i']][port['j']][port['k']] = port['c'] + else: + ColorKM[port['i']][port['j']][port['k']] = port['c'] + + +def color_kp_km( + n_i: int, + n_j: int, + n_k: int, + ExistI: Sequence[Sequence[Sequence[int]]], + ExistJ: Sequence[Sequence[Sequence[int]]], + ExistK: Sequence[Sequence[Sequence[int]]], + ColorI: Sequence[Sequence[Sequence[int]]], + ColorJ: Sequence[Sequence[Sequence[int]]], + ports: Sequence[Mapping[str, Union[str, int]]], + NodeY: Sequence[Sequence[Sequence[int]]], +) -> Tuple[Sequence[Sequence[Sequence[int]]], + Sequence[Sequence[Sequence[int]]]]: + ColorKP = [[[-1 for _ in range(n_k)] for _ in range(n_j)] + for _ in range(n_i)] + ColorKM = [[[-1 for _ in range(n_k)] for _ in range(n_j)] + for _ in range(n_i)] + + # at ports, the color follows from the port configuration + color_ports(ports, ColorKP, ColorKM) + + # propogate the color of I-pipes and J-pipes to their neighboring K-pipes + propogate_IJcolor(n_i, n_j, n_k, ExistI, ExistJ, ExistK, ColorI, ColorJ, + ColorKP, ColorKM) + + # the rest of the K-pipes are only neighboring other K-pipes. Until all of + # them are colored, we propagate colors of the existing K-pipes. If at one + # point, nothing can be implied via propagation, we assign a color at will + # and continue. Because of the domain wall operation, we can do this. + while if_uncolorK(n_i, n_j, n_k, ExistK, ColorKP, ColorKM): + if not propogate_Kcolor(n_i, n_j, n_k, ExistK, ColorKP, ColorKM, + NodeY): + assign_Kcolor(n_i, n_j, n_k, ExistK, ColorKP, ColorKM, NodeY) + return ColorKP, ColorKM + + +def color_z(lasre: Mapping[str, Any]) -> Mapping[str, Any]: + n_i, n_j, n_k = ( + lasre['n_i'], + lasre['n_j'], + lasre['n_k'], + ) + ExistI, ColorI, ExistJ, ColorJ, ExistK = ( + lasre['ExistI'], + lasre['ColorI'], + lasre['ExistJ'], + lasre['ColorJ'], + lasre['ExistK'], + ) + NodeY = lasre['NodeY'] + ports = lasre['ports'] + + # for a K-pipe (i,j,k)-(i,j,k+1), ColorKP (plus) is its color at (i,j,k+1) + # and ColorKM (minus) is its color at (i,j,k) + lasre['ColorKP'], lasre['ColorKM'] = color_kp_km(n_i, n_j, n_k, ExistI, + ExistJ, ExistK, ColorI, + ColorJ, ports, NodeY) + return lasre diff --git a/glue/lattice_surgery/lassynth/rewrite_passes/remove_unconnected.py b/glue/lattice_surgery/lassynth/rewrite_passes/remove_unconnected.py new file mode 100644 index 000000000..e983b0092 --- /dev/null +++ b/glue/lattice_surgery/lassynth/rewrite_passes/remove_unconnected.py @@ -0,0 +1,152 @@ +"""In the generated LaS, there can be some 'floating donuts' not connecting to +any ports. These objects won't affect the function of the LaS. We remove them. +""" + +from typing import Mapping, Any, Sequence, Union, Tuple + + +def check_cubes( + n_i: int, n_j: int, n_k: int, ExistI: Sequence[Sequence[Sequence[int]]], + ExistJ: Sequence[Sequence[Sequence[int]]], + ExistK: Sequence[Sequence[Sequence[int]]], + ports: Sequence[Mapping[str, Union[str, int]]], + NodeY: Sequence[Sequence[Sequence[int]]] +) -> Sequence[Sequence[Sequence[int]]]: + # we linearize the cubes, cube at (i,j,k) -> index i*n_j*n_k + j*n_k + k + # construct adjancency list of the cubes from the pipes + adj = [[] for _ in range(n_i * n_j * n_k)] + for i in range(n_i): + for j in range(n_j): + for k in range(n_k): + if ExistI[i][j][k] and i + 1 < n_i: + adj[i * n_j * n_k + j * n_k + + k].append((i + 1) * n_j * n_k + j * n_k + k) + adj[(i + 1) * n_j * n_k + j * n_k + + k].append(i * n_j * n_k + j * n_k + k) + if ExistJ[i][j][k] and j + 1 < n_j: + adj[i * n_j * n_k + j * n_k + k].append(i * n_j * n_k + + (j + 1) * n_k + k) + adj[i * n_j * n_k + (j + 1) * n_k + + k].append(i * n_j * n_k + j * n_k + k) + if ExistK[i][j][k] and k + 1 < n_k: + adj[i * n_j * n_k + j * n_k + k].append(i * n_j * n_k + + j * n_k + k + 1) + adj[i * n_j * n_k + j * n_k + k + 1].append(i * n_j * n_k + + j * n_k + k) + + # if a cube can reach any of the vips, i.e., open cube for a port + vips = [p["i"] * n_j * n_k + p["j"] * n_k + p["k"] for p in ports] + + # first assume all cubes are nonconnected + connected_cubes = [[[0 for _ in range(n_k)] for _ in range(n_j)] + for _ in range(n_i)] + + # a Y cube is only effective if it is connected to a cube (i,j,k) that is + # connected to ports. In this case, (i,j,k) will be in `connected_cubes` + # and all pipes from (i,j,k) will be selected in `check_pipes`, so we can + # assume all the Y cubes to be nonconnected for now. + y_cubes = [ + i * n_j * n_k + j * n_k + k for i in range(n_i) for j in range(n_j) + for k in range(n_k) if NodeY[i][j][k] + ] + + for i in range(n_i): + for j in range(n_j): + for k in range(n_k): + # breadth first search for each cube + queue = [ + i * n_j * n_k + j * n_k + k, + ] + if i * n_j * n_k + j * n_k + k in y_cubes: + continue + visited = [0 for _ in range(n_i * n_j * n_k)] + while len(queue) > 0: + if queue[0] in vips: + connected_cubes[i][j][k] = 1 + break + visited[queue[0]] = 1 + for v in adj[queue[0]]: + if not visited[v] and v not in y_cubes: + queue.append(v) + queue.pop(0) + + return connected_cubes + + +def check_pipes( + n_i: int, n_j: int, n_k: int, ExistI: Sequence[Sequence[Sequence[int]]], + ExistJ: Sequence[Sequence[Sequence[int]]], + ExistK: Sequence[Sequence[Sequence[int]]], + connected_cubes: Sequence[Sequence[Sequence[int]]] +) -> Tuple[Sequence[Sequence[Sequence[int]]], + Sequence[Sequence[Sequence[int]]], + Sequence[Sequence[Sequence[int]]]]: + EffectI = [[[0 for _ in range(n_k)] for _ in range(n_j)] + for _ in range(n_i)] + EffectJ = [[[0 for _ in range(n_k)] for _ in range(n_j)] + for _ in range(n_i)] + EffectK = [[[0 for _ in range(n_k)] for _ in range(n_j)] + for _ in range(n_i)] + for i in range(n_i): + for j in range(n_j): + for k in range(n_k): + if ExistI[i][j][k] and (connected_cubes[i][j][k] or + (i + 1 < n_i + and connected_cubes[i + 1][j][k])): + EffectI[i][j][k] = 1 + if ExistJ[i][j][k] and (connected_cubes[i][j][k] or + (j + 1 < n_j + and connected_cubes[i][j + 1][k])): + EffectJ[i][j][k] = 1 + if ExistK[i][j][k] and (connected_cubes[i][j][k] or + (k + 1 < n_k + and connected_cubes[i][j][k + 1])): + EffectK[i][j][k] = 1 + return EffectI, EffectJ, EffectK + + +def array3DAnd( + arr0: Sequence[Sequence[Sequence[int]]], + arr1: Sequence[Sequence[Sequence[int]]] +) -> Sequence[Sequence[Sequence[int]]]: + """taking the AND of two arrays of bits""" + a = len(arr0) + b = len(arr0[0]) + c = len(arr0[0][0]) + arrAnd = [[[0 for _ in range(c)] for _ in range(b)] for _ in range(a)] + for i in range(a): + for j in range(b): + for k in range(c): + if arr0[i][j][k] == 1 and arr1[i][j][k] == 1: + arrAnd[i][j][k] = 1 + return arrAnd + + +def remove_unconnected(lasre: Mapping[str, Any]) -> Mapping[str, Any]: + n_i, n_j, n_k = ( + lasre["n_i"], + lasre["n_j"], + lasre["n_k"], + ) + ExistI, ExistJ, ExistK, NodeY = ( + lasre["ExistI"], + lasre["ExistJ"], + lasre["ExistK"], + lasre["NodeY"], + ) + ports = lasre["ports"] + + connected_cubes = check_cubes(n_i, n_j, n_k, ExistI, ExistJ, ExistK, ports, + NodeY) + connectedI, connectedJ, connectedK = check_pipes(n_i, n_j, n_k, ExistI, + ExistJ, ExistK, + connected_cubes) + maskedI, maskedJ, maskedK = ( + array3DAnd(ExistI, connectedI), + array3DAnd(ExistJ, connectedJ), + array3DAnd(ExistK, connectedK), + ) + lasre["ExistI"], lasre["ExistJ"], lasre[ + "ExistK"] = maskedI, maskedJ, maskedK + + return lasre diff --git a/glue/lattice_surgery/lassynth/sat_synthesis/__init__.py b/glue/lattice_surgery/lassynth/sat_synthesis/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/glue/lattice_surgery/lassynth/sat_synthesis/lattice_surgery_sat.py b/glue/lattice_surgery/lassynth/sat_synthesis/lattice_surgery_sat.py new file mode 100644 index 000000000..da6bd6706 --- /dev/null +++ b/glue/lattice_surgery/lassynth/sat_synthesis/lattice_surgery_sat.py @@ -0,0 +1,1842 @@ +"""LatticeSurgerySAT to encode the synthesis problem to SAT/SMT""" + +import os +import subprocess +import sys +import tempfile +import time +from typing import Any, Mapping, Sequence, Union, Tuple, Optional +import z3 + + +def var_given( + data: Mapping[str, Any], + arr: str, + i: int, + j: int, + k: int, + l: Optional[int] = None, +) -> bool: + """Check whether data[arr][i][j][k]([l]) is given. + + If the given indices are not found, return False; otherwise return True. + + Args: + data (Mapping[str, Any]): contain arrays + arr (str): ExistI, etc. + i (int): first index + j (int): second index + k (int): third index + l (int, optional): optional fourth index. Defaults to None. + + Returns: + bool: whether the variable value is given + + Raises: + ValueError: found value, but not 0 nor 1 nor -1. + """ + + if arr not in data: + return False + if l is None: + if data[arr][i][j][k] == -1: + return False + elif data[arr][i][j][k] != 0 and data[arr][i][j][k] != 1: + raise ValueError(f"{arr}[{i}, {j}, {k}] is not 0 nor 1 nor -1.") + return True + # l is not None, then + if data[arr][i][j][k][l] == -1: + return False + elif data[arr][i][j][k][l] != 0 and data[arr][i][j][k][l] != 1: + raise ValueError(f"{arr}[{i}, {j}, {k}, {l}] is not 0 nor 1 nor -1.") + return True + + +def port_incident_pipes( + port: Mapping[str, Union[str, int]], n_i: int, n_j: int, + n_k: int) -> Tuple[Sequence[str], Sequence[Tuple[int, int, int]]]: + """Compute the pipes incident to a port. + + A port is an pipe with a open end. The incident pipes of a port are the + five other pipes connecting to that end. However, some of these pipes + can be out of bound, we just want to compute those that are in bound. + + Args: + port (Mapping[str, Union[str, int]]): the port to consider + n_i (int): spatial bound on I direction + n_j (int): spatial bound on J direction + n_k (int): spatial bound on K direction + + Returns: + Tuple[Sequence[str], Sequence[Tuple[int, int, int]]]: + Two lists of the same length [0,6): (dirs, coords) + dirs: the direction of the incident pipes, can be "I", "J", or "K" + coords: the coordinates of the incident pipes, each one is (i,j,k) + """ + coords = [] + dirs = [] + + # first, just consider adjancency without caring about out-of-bound + if port["d"] == "I": + adj_dirs = ["I", "J", "J", "K", "K"] + if port["e"] == "-": # empty cube is (i,j,k) + adj_coords = [ + (port["i"] - 1, port["j"], port["k"]), # (i-1,j,k)---(i,j,k) + (port["i"], port["j"] - 1, port["k"]), # (i,j-1,k)---(i,j,k) + (port["i"], port["j"], port["k"]), # (i,j,k)---(i,j+1,k) + (port["i"], port["j"], port["k"] - 1), # (i,j,k-1)---(i,j,k) + (port["i"], port["j"], port["k"]), # (i,j,k)---(i,j,k+1) + ] + elif port["e"] == "+": # empty cube is (i+1,j,k) + adj_coords = [ + (port["i"] + 1, port["j"], port["k"]), # (i+1,j,k)---(i+2,j,k) + (port["i"] + 1, port["j"] - 1, + port["k"]), # (i+1,j-1,k)---(i+1,j,k) + (port["i"] + 1, port["j"], + port["k"]), # (i+1,j,k)---(i+1,j+1,k) + (port["i"] + 1, port["j"], + port["k"] - 1), # (i+1,j,k-1)---(i+1,j,k) + (port["i"] + 1, port["j"], + port["k"]), # (i+1,j,k)---(i+1,j,k+1) + ] + + if port["d"] == "J": + adj_dirs = ["J", "K", "K", "I", "I"] + if port["e"] == "-": + adj_coords = [ + (port["i"], port["j"] - 1, port["k"]), + (port["i"], port["j"], port["k"] - 1), + (port["i"], port["j"], port["k"]), + (port["i"] - 1, port["j"], port["k"]), + (port["i"], port["j"], port["k"]), + ] + elif port["e"] == "+": + adj_coords = [ + (port["i"], port["j"] + 1, port["k"]), + (port["i"], port["j"] + 1, port["k"] - 1), + (port["i"], port["j"] + 1, port["k"]), + (port["i"] - 1, port["j"] + 1, port["k"]), + (port["i"], port["j"] + 1, port["k"]), + ] + + if port["d"] == "K": + adj_dirs = ["K", "I", "I", "J", "J"] + if port["e"] == "-": + adj_coords = [ + (port["i"], port["j"], port["k"] - 1), + (port["i"] - 1, port["j"], port["k"]), + (port["i"], port["j"], port["k"]), + (port["i"], port["j"] - 1, port["k"]), + (port["i"], port["j"], port["k"]), + ] + elif port["e"] == "+": + adj_coords = [ + (port["i"], port["j"], port["k"] + 1), + (port["i"] - 1, port["j"], port["k"] + 1), + (port["i"], port["j"], port["k"] + 1), + (port["i"], port["j"] - 1, port["k"] + 1), + (port["i"], port["j"], port["k"] + 1), + ] + + # only keep the pipes in bound + for i, coord in enumerate(adj_coords): + if ((coord[0] in range(n_i)) and (coord[1] in range(n_j)) + and (coord[2] in range(n_k))): + coords.append(adj_coords[i]) + dirs.append(adj_dirs[i]) + + return dirs, coords + + +def cnf_even_parity_upto4(eles: Sequence[Any]) -> Any: + """Compute the CNF format of parity of up to four Z3 binary variables. + + Args: + eles (Sequence[Any]): the binary variables. + + Returns: + (Any) the Z3 constraint meaning the parity of the inputs is even. + + Raises: + ValueError: number of elements is not 1, 2, 3, or 4. + """ + + if len(eles) == 1: + # 1 var even parity -> this var is false + return z3.Not(eles[0]) + + elif len(eles) == 2: + # 2 vars even pairty -> both True or both False + return z3.Or(z3.And(z3.Not(eles[0]), z3.Not(eles[1])), + z3.And(eles[0], eles[1])) + + elif len(eles) == 3: + # 3 vars even parity -> all False, or 2 True and 1 False + return z3.Or( + z3.And(z3.Not(eles[0]), z3.Not(eles[1]), z3.Not(eles[2])), + z3.And(eles[0], eles[1], z3.Not(eles[2])), + z3.And(eles[0], z3.Not(eles[1]), eles[2]), + z3.And(z3.Not(eles[0]), eles[1], eles[2]), + ) + + elif len(eles) == 4: + # 4 vars even parity -> 0, 2, or 4 vars are True + return z3.Or( + z3.And(z3.Not(eles[0]), z3.Not(eles[1]), z3.Not(eles[2]), + z3.Not(eles[3])), + z3.And(z3.Not(eles[0]), z3.Not(eles[1]), eles[2], eles[3]), + z3.And(z3.Not(eles[0]), eles[1], z3.Not(eles[2]), eles[3]), + z3.And(z3.Not(eles[0]), eles[1], eles[2], z3.Not(eles[3])), + z3.And(eles[0], z3.Not(eles[1]), z3.Not(eles[2]), eles[3]), + z3.And(eles[0], z3.Not(eles[1]), eles[2], z3.Not(eles[3])), + z3.And(eles[0], eles[1], z3.Not(eles[2]), z3.Not(eles[3])), + z3.And(eles[0], eles[1], eles[2], eles[3]), + ) + + else: + raise ValueError("This function only supports 1, 2, 3, or 4 vars.") + + +class LatticeSurgerySAT: + """class of synthesizing LaSRe using Z3 SMT solver and Kissat SAT solver. + + It encodes a lattice surgery synthesis problem to SAT/SMT and checks + whether there is a solution. We are given certain spacetime volume, certain + ports, and certain stabilizers. LatticeSurgerySAT encodes the constraints + on LaSRe variables such that the resulting variable assignments consist of + a valid lattice surgery subroutine with the correct functionality + (satisfies all the given stabilizers). LatticeSurgerySAT finds the solution + with a SAT/SMT solver. + """ + + def __init__( + self, + input_dict: Mapping[str, Any], + color_ij: bool = True, + given_arrs: Optional[Mapping[str, Any]] = None, + given_vals: Optional[Sequence[Mapping[str, Any]]] = None, + ) -> None: + """initialization of LatticeSurgerySAT. + + Args: + input_dict (Mapping[str, Any]): specification of LaS. + color_ij (bool, optional): if the color matching constraints of + I and J pipes are imposed. Defaults to True. So far, we always + impose these constraints. + given_arrs (Mapping[str, Any], optional): + Arrays of values to plug in. Defaults to None. + given_vals (Sequence[Mapping[str, Any]], optional): + Values to plug in. Defaults to None. These values will + replace existing values if already set by given_arrs. + """ + self.input_dict = input_dict + self.color_ij = color_ij + self.goal = z3.Goal() + self.process_input(input_dict) + self.build_smt_model(given_arrs=given_arrs, given_vals=given_vals) + + def process_input(self, input_dict: Mapping[str, Any]) -> None: + """read input specification, mainly translating the info at the ports. + + Args: + input_dict (Mapping[str, Any]): LaS specification. + + Raises: + ValueError: missing key in input specification. + ValueError: some spatial bound <= 0. + ValueError: more stabilizers than ports. + ValueError: stabilizer length is not the same as + the number of ports. + ValueError: stabilizer contains things other than I, X, Y, or Z. + ValueError: missing key in port. + ValueError: port location is not a 3-tuple. + ValueError: port direction is not 2-string. + ValueError: port sign (which end is dangling) is not - or +. + ValueError: port axis is not I, J, or K. + ValueError: port location+direction is out of bound. + ValueError: port Z basis direction is not I, J, or K, and + the same with the pipe. + ValueError: forbidden cube location is not a 3-tuple. + ValueError: forbiddent cube location is out of bounds. + """ + data = input_dict + + for key in ["max_i", "max_j", "max_k", "ports", "stabilizers"]: + if key not in data: + raise ValueError(f"missing key {key} in input specification.") + + # load spatial bound, check > 0 + self.n_i = data["max_i"] + self.n_j = data["max_j"] + self.n_k = data["max_k"] + if min([self.n_i, self.n_j, self.n_k]) <= 0: + raise ValueError("max_i or _j or _k <= 0.") + + self.n_p = len(data["ports"]) + self.n_s = len(data["stabilizers"]) + # there should be at most as many stabilizers as ports + if self.n_s > self.n_p: + raise ValueError( + f"{self.n_s} stabilizers, too many for {self.n_p} ports.") + + # stabilizers should be paulistrings of length #ports + self.paulistrings = [s.replace(".", "I") for s in data["stabilizers"]] + for s in self.paulistrings: + if len(s) != self.n_p: + raise ValueError( + f"len({s}) = {len(s)}, but there are {self.n_p} ports.") + for i in range(len(s)): + if s[i] not in ["I", "X", "Y", "Z"]: + raise ValueError( + f"{s} has invalid Pauli. I, X, Y, and Z are allowed.") + + # transform port data + self.ports = [] + for port in data["ports"]: + for key in ["location", "direction", "z_basis_direction"]: + if key not in port: + raise ValueError(f"missing key {key} in port {port}") + + if len(port["location"]) != 3: + raise ValueError(f"port location should be 3-tuple {port}.") + + if len(port["direction"]) != 2: + raise ValueError( + f"port direction should have 2 characters {port}.") + if port["direction"][0] not in ["+", "-"]: + raise ValueError(f"port direction with invalid sign {port}.") + if port["direction"][1] not in ["I", "J", "K"]: + raise ValueError(f"port direction with invalid axis {port}.") + + if port["direction"][0] == "-" and port["direction"][ + 1] == "I" and (port["location"][0] not in range( + 1, self.n_i + 1)): + raise ValueError( + f"{port['location']} with direction {port['direction']}" + f" should be in range [1, f{self.n_i+1}).") + if port["direction"][0] == "+" and port["direction"][ + 1] == "I" and (port["location"][0] not in range( + 0, self.n_i)): + raise ValueError( + f"{port['location']} with direction {port['direction']}" + f" should be in range [0, f{self.n_i}).") + if port["direction"][0] == "-" and port["direction"][ + 1] == "J" and (port["location"][1] not in range( + 1, self.n_j + 1)): + raise ValueError( + f"{port['location']} with direction {port['direction']}" + f" should be in range [1, {self.n_j+1}).") + if port["direction"][0] == "+" and port["direction"][ + 1] == "J" and (port["location"][1] not in range( + 0, self.n_j)): + raise ValueError( + f"{port['location']} with direction {port['direction']}" + f" should be in range [0, f{self.n_j}).") + if port["direction"][0] == "-" and port["direction"][ + 1] == "K" and (port["location"][2] not in range( + 1, self.n_k + 1)): + raise ValueError( + f"{port['location']} with direction {port['direction']}" + f" should be in range [1, f{self.n_k+1}).") + if port["direction"][0] == "+" and port["direction"][ + 1] == "K" and (port["location"][2] not in range( + 0, self.n_k)): + raise ValueError( + f"{port['location']} with direction {port['direction']}" + f" should be in range [0, f{self.n_k}).") + + # internally, a port is an pipe. This is different from what we + # expose to the user: in LaS specification, a port is a cube and + # associated with a direction, e.g., cube [i,j,k] and direction + # "-K". This means the port should be the pipe connecting (i,j,k) + # downwards to the volume of LaS. Thus, that pipe is (i,j,k-1) -- + # (i,j,k) which by convention is the K-pipe (i,j,k-1). + # a port here has fields "i", "j", "k", "d", "e", "f", "c" + my_port = {} + + # "i", "j", and "k" are the i,j,k of the pipe + my_port["i"], my_port["j"], my_port["k"] = port["location"] + if port["direction"][0] == "-": + my_port[port["direction"][1].lower()] -= 1 + + # "d" is one of I, J, and K, corresponding to the port being an + # I-pipe, J-pipe, or a K-pipe + my_port["d"] = port["direction"][1] + + # "e" is the end of the pipe that is open. For example, if the port + # is a K-pipe (i,j,k), then "e"="+" means the cube (i,j,k+1) is + # open; otherwise, "e"="-" means cube (i,j,k) is open. + my_port["e"] = "-" if port["direction"][0] == "+" else "+" + + # "c" is the color variable of the pipe corresponding to the port + z_dir = port["z_basis_direction"] + if z_dir not in ["I", "J", "K"] or z_dir == my_port["d"]: + raise ValueError( + f"port with invalid Z basis direction {port}.") + if my_port["d"] == "I": + my_port["c"] = 0 if z_dir == "J" else 1 + if my_port["d"] == "J": + my_port["c"] = 0 if z_dir == "K" else 1 + if my_port["d"] == "K": + my_port["c"] = 0 if z_dir == "I" else 1 + + # "f" is the function of the pipe, e.g., it can say this port is a + # T injection. This field is not used in the SAT synthesis, but + # we keep this info to use in later stages like gltf generation + if "function" in port: + my_port["f"] = port["function"] + + self.ports.append(my_port) + + # from paulistrings to correlation surfaces + self.stabs = self.derive_corr_boundary(self.paulistrings) + + self.optional = {} + self.forbidden_cubes = [] + if "optional" in data: + self.optional = data["optional"] + + if "forbidden_cubes" in data["optional"]: + for cube in data["optional"]["forbidden_cubes"]: + if len(cube) != 3: + raise ValueError( + f"forbid cube should be 3-tuple {cube}.") + if (cube[0] not in range(self.n_i) + or cube[1] not in range(self.n_j) + or cube[2] not in range(self.n_k)): + raise ValueError( + f"forbidden {cube} out of range " + f"(i,j,k) < ({self.n_i, self.n_j, self.n_k})") + self.forbidden_cubes.append(cube) + + self.get_port_cubes() + + def get_port_cubes(self) -> None: + """calculate which cubes are the open cube for the ports. + Note that these are *** 3-tuples ***, not lists with 3 elements.""" + self.port_cubes = [] + for p in self.ports: + # if e=-, (i,j,k); otherwise, +1 in the proper direction + if p["e"] == "-": + self.port_cubes.append((p["i"], p["j"], p["k"])) + elif p["d"] == "I": + self.port_cubes.append((p["i"] + 1, p["j"], p["k"])) + elif p["d"] == "J": + self.port_cubes.append((p["i"], p["j"] + 1, p["k"])) + elif p["d"] == "K": + self.port_cubes.append((p["i"], p["j"], p["k"] + 1)) + + def derive_corr_boundary( + self, paulistrings: Sequence[str] + ) -> Sequence[Sequence[Mapping[str, int]]]: + """derive the boundary correlation surface variable values. + + From the color orientation of the ports and the stabilizers, we can + derive which correlation surface variables evaluates to True and which + to False at the ports for each stabilizer. + + Args: + paulistrings (Sequence[str]): stabilizers as a list of Paulistrings + + Returns: + Sequence[Sequence[Mapping[str, int]]]: Outer layer list is the + list of stabilizers. Inner layer list is the situation at each port + for one specifeic stabilizer. Each port is specified with a + dictionary of 2 bits for the 2 correaltion surfaces. + """ + stabs = [] + for paulistring in paulistrings: + corr = [] + for p in range(self.n_p): + if paulistring[p] == "I": + # I -> no corr surf should be present + if self.ports[p]["d"] == "I": + corr.append({"IJ": 0, "IK": 0}) + if self.ports[p]["d"] == "J": + corr.append({"JI": 0, "JK": 0}) + if self.ports[p]["d"] == "K": + corr.append({"KI": 0, "KJ": 0}) + + if paulistring[p] == "Y": + # Y -> both corr surf should be present + if self.ports[p]["d"] == "I": + corr.append({"IJ": 1, "IK": 1}) + if self.ports[p]["d"] == "J": + corr.append({"JI": 1, "JK": 1}) + if self.ports[p]["d"] == "K": + corr.append({"KI": 1, "KJ": 1}) + + if paulistring[p] == "X": + # X -> only corr surf touching red faces + if self.ports[p]["d"] == "I": + if self.ports[p]["c"]: + corr.append({"IJ": 1, "IK": 0}) + else: + corr.append({"IJ": 0, "IK": 1}) + if self.ports[p]["d"] == "J": + if self.ports[p]["c"]: + corr.append({"JI": 0, "JK": 1}) + else: + corr.append({"JI": 1, "JK": 0}) + if self.ports[p]["d"] == "K": + if self.ports[p]["c"]: + corr.append({"KI": 1, "KJ": 0}) + else: + corr.append({"KI": 0, "KJ": 1}) + + if paulistring[p] == "Z": + # Z -> only corr surf touching blue faces + if self.ports[p]["d"] == "I": + if not self.ports[p]["c"]: + corr.append({"IJ": 1, "IK": 0}) + else: + corr.append({"IJ": 0, "IK": 1}) + if self.ports[p]["d"] == "J": + if not self.ports[p]["c"]: + corr.append({"JI": 0, "JK": 1}) + else: + corr.append({"JI": 1, "JK": 0}) + if self.ports[p]["d"] == "K": + if not self.ports[p]["c"]: + corr.append({"KI": 1, "KJ": 0}) + else: + corr.append({"KI": 0, "KJ": 1}) + stabs.append(corr) + return stabs + + def build_smt_model( + self, + given_arrs: Optional[Mapping[str, Any]] = None, + given_vals: Optional[Sequence[Mapping[str, Any]]] = None, + ) -> None: + """build the SMT model with variables and constraints. + + Args: + given_arrs (Mapping[str, Any], optional): + Arrays of values to plug in. Defaults to None. + given_vals (Sequence[Mapping[str, Any]], optional): + Values to plug in. Defaults to None. These values will + replace existing values if already set by given_arrs. + """ + self.define_vars() + if given_arrs is not None: + self.plugin_arrs(given_arrs) + if given_vals is not None: + self.plugin_vals(given_vals) + + # baseline order of constraint sets, '...' menas name in the paper + + # validity constraints that directly set variables values + self.constraint_forbid_cube() + self.constraint_port() # 'no fanouts' + self.constraint_connect_outside() # 'no unexpected ports' + + # more complex validity constraints involving boolean logic + self.constraint_timelike_y() # 'time-like Y cubes' + self.constraint_no_deg1() # 'no degree-1 non-Y cubes' + if self.color_ij: + # 'matching colors at passthroughs' and '... at turns' + self.constraint_ij_color() + self.constraint_3d_corner() # 'no 3D corners' + + # simpler functionality constraints + self.constraint_corr_ports() # 'stabilizer as boundary conditions' + self.constraint_corr_y() # 'both or non at Y cubes' + + # more complex functionality constraints + # 'all or no orthogonal surfaces at non-Y cubes: + self.constraint_corr_perp() + # 'even parity of parallel surfaces at non-Y cubes': + self.constraint_corr_para() + + def define_vars(self) -> None: + """define the variables in Z3 into self.vars.""" + self.vars = { + "ExistI": + [[[z3.Bool(f"ExistI({i},{j},{k})") for k in range(self.n_k)] + for j in range(self.n_j)] for i in range(self.n_i)], + "ExistJ": + [[[z3.Bool(f"ExistJ({i},{j},{k})") for k in range(self.n_k)] + for j in range(self.n_j)] for i in range(self.n_i)], + "ExistK": + [[[z3.Bool(f"ExistK({i},{j},{k})") for k in range(self.n_k)] + for j in range(self.n_j)] for i in range(self.n_i)], + "NodeY": + [[[z3.Bool(f"NodeY({i},{j},{k})") for k in range(self.n_k)] + for j in range(self.n_j)] for i in range(self.n_i)], + "CorrIJ": + [[[[z3.Bool(f"CorrIJ({s},{i},{j},{k})") for k in range(self.n_k)] + for j in range(self.n_j)] for i in range(self.n_i)] + for s in range(self.n_s)], + "CorrIK": + [[[[z3.Bool(f"CorrIK({s},{i},{j},{k})") for k in range(self.n_k)] + for j in range(self.n_j)] for i in range(self.n_i)] + for s in range(self.n_s)], + "CorrJK": + [[[[z3.Bool(f"CorrJK({s},{i},{j},{k})") for k in range(self.n_k)] + for j in range(self.n_j)] for i in range(self.n_i)] + for s in range(self.n_s)], + "CorrJI": + [[[[z3.Bool(f"CorrJI({s},{i},{j},{k})") for k in range(self.n_k)] + for j in range(self.n_j)] for i in range(self.n_i)] + for s in range(self.n_s)], + "CorrKI": + [[[[z3.Bool(f"CorrKI({s},{i},{j},{k})") for k in range(self.n_k)] + for j in range(self.n_j)] for i in range(self.n_i)] + for s in range(self.n_s)], + "CorrKJ": + [[[[z3.Bool(f"CorrKJ({s},{i},{j},{k})") for k in range(self.n_k)] + for j in range(self.n_j)] for i in range(self.n_i)] + for s in range(self.n_s)], + } + + if self.color_ij: + self.vars["ColorI"] = [[[ + z3.Bool(f"ColorI({i},{j},{k})") for k in range(self.n_k) + ] for j in range(self.n_j)] for i in range(self.n_i)] + self.vars["ColorJ"] = [[[ + z3.Bool(f"ColorJ({i},{j},{k})") for k in range(self.n_k) + ] for j in range(self.n_j)] for i in range(self.n_i)] + + def plugin_arrs(self, data: Mapping[str, Any]) -> None: + """plug in the given arrays of values. + + Args: + data (Mapping[str, Any]): contains gieven values. + + Raises: + ValueError: data contains an invalid array name. + ValueError: array given has wrong dimensions. + """ + + for key in data: + if key in [ + "NodeY", + "ExistI", + "ExistJ", + "ExistK", + "ColorI", + "ColorJ", + ]: + if len(data[key]) != self.n_i: + raise ValueError(f"dimension of {key} is wrong.") + for tmp in data[key]: + if len(tmp) != self.n_j: + raise ValueError(f"dimension of {key} is wrong.") + for tmptmp in tmp: + if len(tmptmp) != self.n_k: + raise ValueError(f"dimension of {key} is wrong.") + elif key in [ + "CorrIJ", + "CorrIK", + "CorrJI", + "CorrJK", + "CorrKI", + "CorrKJ", + ]: + if len(data[key]) != self.n_s: + raise ValueError(f"dimension of {key} is wrong.") + for tmp in data[key]: + if len(tmp) != self.n_i: + raise ValueError(f"dimension of {key} is wrong.") + for tmptmp in tmp: + if len(tmptmp) != self.n_j: + raise ValueError(f"dimension of {key} is wrong.") + for tmptmptmp in tmptmp: + if len(tmptmptmp) != self.n_k: + raise ValueError( + f"dimension of {key} is wrong.") + else: + raise ValueError(f"{key} is not a valid array name") + + arrs = [ + "NodeY", + "ExistI", + "ExistJ", + "ExistK", + ] + if self.color_ij: + arrs += ["ColorI", "ColorJ"] + + for s in range(self.n_s): + for i in range(self.n_i): + for j in range(self.n_j): + for k in range(self.n_k): + if s == 0: # Exist, Node, and Color vars + for arr in arrs: + if var_given(data, arr, i, j, k): + self.goal.add( + self.vars[arr][i][j][k] + if data[arr][i][j][k] == + 1 else z3.Not(self.vars[arr][i][j][k])) + # Corr vars + for arr in [ + "CorrIJ", + "CorrIK", + "CorrJI", + "CorrJK", + "CorrKI", + "CorrKJ", + ]: + if var_given(data, arr, s, i, j, k): + self.goal.add( + self.vars[arr][s][i][j][k] + if data[arr][s][i][j][k] == + 1 else z3.Not(self.vars[arr][s][i][j][k])) + + def plugin_vals(self, data_set: Sequence[Mapping[str, Any]]): + """plug in the given values + + Args: + data (Sequence[Mapping[str, Any]]): given values as a sequence + of dicts. Each one contains three fields: "array", the name of + the array, e.g., "ExistI"; "indices", a sequence of the indices; + and "value". + + Raises: + ValueError: given_vals missing a field. + ValueError: array name is not valid. + ValueError: indices dimension for certain array is wrong. + ValueError: index value out of bound. + ValueError: given value is neither 0 nor 1. + """ + for data in data_set: + for key in ["array", "indices", "value"]: + if key not in data: + raise ValueError(f"{key} is not in given val") + if data["array"] not in [ + "NodeY", + "ExistI", + "ExistJ", + "ExistK", + "ColorI", + "ColorJ", + "CorrIJ", + "CorrIK", + "CorrJI", + "CorrJK", + "CorrKI", + "CorrKJ", + ]: + raise ValueError(f"{data['array']} is not a valid array.") + if data["array"] in [ + "NodeY", + "ExistI", + "ExistJ", + "ExistK", + "ColorI", + "ColorJ", + ]: + if len(data["indices"] != 3): + raise ValueError(f"Need 3 indices for {data['array']}.") + if data["indices"][0] not in range(self.n_i): + raise ValueError(f"i index out of range") + if data["indices"][1] not in range(self.n_j): + raise ValueError(f"j index out of range") + if data["indices"][2] not in range(self.n_k): + raise ValueError(f"k index out of range") + + if data["array"] in [ + "CorrIJ", + "CorrIK", + "CorrJI", + "CorrJK", + "CorrKI", + "CorrKJ", + ]: + if len(data["indices"] != 4): + raise ValueError(f"Need 4 indices for {data['array']}.") + if data["indices"][0] not in range(self.n_s): + raise ValueError(f"s index out of range") + if data["indices"][1] not in range(self.n_i): + raise ValueError(f"i index out of range") + if data["indices"][2] not in range(self.n_j): + raise ValueError(f"j index out of range") + if data["indices"][3] not in range(self.n_k): + raise ValueError(f"k index out of range") + + if data["value"] not in [0, 1]: + raise ValueError("Given value can only be 0 or 1.") + + (arr, idx) = data["array"], data["indices"] + if arr.startswith("Corr"): + s, i, j, k = idx + if data["value"] == 1: + self.goal.add(self.vars[arr][s][i][j][k]) + else: + self.goal.add(z3.Not(self.vars[arr][s][i][j][k])) + else: + i, j, k = idx + if data["value"] == 1: + self.goal.add(self.vars[arr][i][j][k]) + else: + self.goal.add(z3.Not(self.vars[arr][i][j][k])) + + def constraint_forbid_cube(self) -> None: + """forbid a list of cubes.""" + for cube in self.forbidden_cubes: + (i, j, k) = cube[0], cube[1], cube[2] + self.goal.add(z3.Not(self.vars["NodeY"][i][j][k])) + if i > 0: + self.goal.add(z3.Not(self.vars["ExistI"][i - 1][j][k])) + self.goal.add(z3.Not(self.vars["ExistI"][i][j][k])) + if j > 0: + self.goal.add(z3.Not(self.vars["ExistJ"][i][j - 1][k])) + self.goal.add(z3.Not(self.vars["ExistJ"][i][j][k])) + if k > 0: + self.goal.add(z3.Not(self.vars["ExistK"][i][j][k - 1])) + self.goal.add(z3.Not(self.vars["ExistK"][i][j][k])) + + def constraint_port(self) -> None: + """some pipes must exist and some must not depending on the ports.""" + for port in self.ports: + # the pipe specified by the port exists + self.goal.add(self.vars[f"Exist{port['d']}"][port["i"]][port["j"]][ + port["k"]]) + # if I- or J-pipe exist, set the color value too to the given one + if self.color_ij: + if port["d"] != "K": + if port["c"] == 1: + self.goal.add(self.vars[f"Color{port['d']}"][port["i"]] + [port["j"]][port["k"]]) + else: + self.goal.add( + z3.Not(self.vars[f"Color{port['d']}"][port["i"]][ + port["j"]][port["k"]])) + + # collect the pipes touching the port to forbid them + dirs, coords = port_incident_pipes(port, self.n_i, self.n_j, + self.n_k) + for i, coord in enumerate(coords): + self.goal.add( + z3.Not(self.vars[f"Exist{dirs[i]}"][coord[0]][coord[1]][ + coord[2]])) + + def constraint_connect_outside(self) -> None: + """no pipe should cross the spatial bound except for ports.""" + for i in range(self.n_i): + for j in range(self.n_j): + # consider K-pipes crossing K-bound and not a port + if (i, j, self.n_k) not in self.port_cubes: + self.goal.add( + z3.Not(self.vars["ExistK"][i][j][self.n_k - 1])) + for i in range(self.n_i): + for k in range(self.n_k): + if (i, self.n_j, k) not in self.port_cubes: + self.goal.add( + z3.Not(self.vars["ExistJ"][i][self.n_j - 1][k])) + for j in range(self.n_j): + for k in range(self.n_k): + if (self.n_i, j, k) not in self.port_cubes: + self.goal.add( + z3.Not(self.vars["ExistI"][self.n_i - 1][j][k])) + + def constraint_timelike_y(self) -> None: + """forbid all I- and J- pipes to Y cubes.""" + for i in range(self.n_i): + for j in range(self.n_j): + for k in range(self.n_k): + if (i, j, k) not in self.port_cubes: + self.goal.add( + z3.Implies( + self.vars["NodeY"][i][j][k], + z3.Not(self.vars["ExistI"][i][j][k]), + )) + self.goal.add( + z3.Implies( + self.vars["NodeY"][i][j][k], + z3.Not(self.vars["ExistJ"][i][j][k]), + )) + if i - 1 >= 0: + self.goal.add( + z3.Implies( + self.vars["NodeY"][i][j][k], + z3.Not(self.vars["ExistI"][i - 1][j][k]), + )) + if j - 1 >= 0: + self.goal.add( + z3.Implies( + self.vars["NodeY"][i][j][k], + z3.Not(self.vars["ExistJ"][i][j - 1][k]), + )) + + def constraint_ij_color(self) -> None: + """color matching for I- and J-pipes.""" + for i in range(self.n_i): + for j in range(self.n_j): + for k in range(self.n_k): + if i >= 1 and j >= 1: + # (i-1,j,k)-(i,j,k) and (i,j-1,k)-(i,j,k) + self.goal.add( + z3.Implies( + z3.And( + self.vars["ExistI"][i - 1][j][k], + self.vars["ExistJ"][i][j - 1][k], + ), + z3.Or( + z3.And( + self.vars["ColorI"][i - 1][j][k], + z3.Not(self.vars["ColorJ"][i][j - + 1][k]), + ), + z3.And( + z3.Not(self.vars["ColorI"][i - + 1][j][k]), + self.vars["ColorJ"][i][j - 1][k], + ), + ), + )) + + if i >= 1: + # (i-1,j,k)-(i,j,k) and (i,j,k)-(i,j+1,k) + self.goal.add( + z3.Implies( + z3.And( + self.vars["ExistI"][i - 1][j][k], + self.vars["ExistJ"][i][j][k], + ), + z3.Or( + z3.And( + self.vars["ColorI"][i - 1][j][k], + z3.Not(self.vars["ColorJ"][i][j][k]), + ), + z3.And( + z3.Not(self.vars["ColorI"][i - + 1][j][k]), + self.vars["ColorJ"][i][j][k], + ), + ), + )) + # (i-1,j,k)-(i,j,k) and (i,j,k)-(i+1,j,k) + self.goal.add( + z3.Implies( + z3.And( + self.vars["ExistI"][i - 1][j][k], + self.vars["ExistI"][i][j][k], + ), + z3.Or( + z3.And( + self.vars["ColorI"][i - 1][j][k], + self.vars["ColorI"][i][j][k], + ), + z3.And( + z3.Not(self.vars["ColorI"][i - + 1][j][k]), + z3.Not(self.vars["ColorI"][i][j][k]), + ), + ), + )) + + if j >= 1: + # (i,j,k)-(i+1,j,k) and (i,j-1,k)-(i,j,k) + self.goal.add( + z3.Implies( + z3.And( + self.vars["ExistI"][i][j][k], + self.vars["ExistJ"][i][j - 1][k], + ), + z3.Or( + z3.And( + self.vars["ColorI"][i][j][k], + z3.Not(self.vars["ColorJ"][i][j - + 1][k]), + ), + z3.And( + z3.Not(self.vars["ColorI"][i][j][k]), + self.vars["ColorJ"][i][j - 1][k], + ), + ), + )) + # (i,j-1,k)-(i,j,k) and (i,j,k)-(i,j+1,k) + self.goal.add( + z3.Implies( + z3.And( + self.vars["ExistJ"][i][j - 1][k], + self.vars["ExistJ"][i][j][k], + ), + z3.Or( + z3.And( + self.vars["ColorJ"][i][j - 1][k], + self.vars["ColorJ"][i][j][k], + ), + z3.And( + z3.Not(self.vars["ColorJ"][i][j - + 1][k]), + z3.Not(self.vars["ColorJ"][i][j][k]), + ), + ), + )) + + # (i,j,k)-(i+1,j,k) and (i,j,k)-(i,j+1,k) + self.goal.add( + z3.Implies( + z3.And(self.vars["ExistI"][i][j][k], + self.vars["ExistJ"][i][j][k]), + z3.Or( + z3.And( + self.vars["ColorI"][i][j][k], + z3.Not(self.vars["ColorJ"][i][j][k]), + ), + z3.And( + z3.Not(self.vars["ColorI"][i][j][k]), + self.vars["ColorJ"][i][j][k], + ), + ), + )) + + def constraint_3d_corner(self) -> None: + """at least in one direction, both pipes nonexist.""" + for i in range(self.n_i): + for j in range(self.n_j): + for k in range(self.n_k): + i_pipes = [ + self.vars["ExistI"][i][j][k], + ] + if i - 1 >= 0: + i_pipes.append(self.vars["ExistI"][i - 1][j][k]) + j_pipes = [ + self.vars["ExistJ"][i][j][k], + ] + if j - 1 >= 0: + j_pipes.append(self.vars["ExistJ"][i][j - 1][k]) + k_pipes = [ + self.vars["ExistK"][i][j][k], + ] + if k - 1 >= 0: + k_pipes.append(self.vars["ExistK"][i][j][k - 1]) + + # at least one of the three terms is true. The first term + # is that both I-pipes connecting to (i,j,k) do not exist. + self.goal.add( + z3.Or( + z3.Not(z3.Or(i_pipes)), + z3.Not(z3.Or(j_pipes)), + z3.Not(z3.Or(k_pipes)), + )) + + def constraint_no_deg1(self) -> None: + """forbid degree-1 X or Z cubes by considering incident pipes.""" + for i in range(self.n_i): + for j in range(self.n_j): + for k in range(self.n_k): + for d in ["I", "J", "K"]: + for e in ["-", "+"]: + cube = {"I": i, "J": j, "K": k} + cube[d] += 1 if e == "+" else 0 + + # construct fake ports to get incident pipes + p0 = { + "i": i, + "j": j, + "k": k, + "d": d, + "e": e, + "c": 0 + } + found_p0 = False + for port in self.ports: + if (i == port["i"] and j == port["j"] + and k == port["k"] and d == port["d"]): + found_p0 = True + + # only non-port pipes need to consider + if (not found_p0 and cube["I"] < self.n_i + and cube["J"] < self.n_j + and cube["K"] < self.n_k): + # only cubes inside bound need to consider + dirs, coords = port_incident_pipes( + p0, self.n_i, self.n_j, self.n_k) + pipes = [ + self.vars[f"Exist{dirs[l]}"][coord[0]][ + coord[1]][coord[2]] + for l, coord in enumerate(coords) + ] + # if the cube is not Y and the pipe exist, then + # at least one of its incident pipes exists. + self.goal.add( + z3.Implies( + z3.And( + z3.Not( + self.vars["NodeY"][cube["I"]][ + cube["J"]][cube["K"]]), + self.vars[f"Exist{d}"][i][j][k], + ), + z3.Or(pipes), + )) + + def constraint_corr_ports(self) -> None: + """plug in the correlation surface values at the ports.""" + for s, stab in enumerate(self.stabs): + for p, corrs in enumerate(stab): + for k, v in corrs.items(): + if v == 1: + self.goal.add( + self.vars[f"Corr{k}"][s][self.ports[p]["i"]][ + self.ports[p]["j"]][self.ports[p]["k"]]) + else: + self.goal.add( + z3.Not(self.vars[f"Corr{k}"][s][self.ports[p]["i"]] + [self.ports[p]["j"]][self.ports[p]["k"]])) + + def constraint_corr_y(self) -> None: + """correlation surfaces at Y-cubes should both exist or nonexist.""" + for s in range(self.n_s): + for i in range(self.n_i): + for j in range(self.n_j): + for k in range(self.n_k): + self.goal.add( + z3.Or( + z3.Not(self.vars["NodeY"][i][j][k]), + z3.Or( + z3.And( + self.vars["CorrKI"][s][i][j][k], + self.vars["CorrKJ"][s][i][j][k], + ), + z3.And( + z3.Not( + self.vars["CorrKI"][s][i][j][k]), + z3.Not( + self.vars["CorrKJ"][s][i][j][k]), + ), + ), + )) + if k - 1 >= 0: + self.goal.add( + z3.Or( + z3.Not(self.vars["NodeY"][i][j][k]), + z3.Or( + z3.And( + self.vars["CorrKI"][s][i][j][k - + 1], + self.vars["CorrKJ"][s][i][j][k - + 1], + ), + z3.And( + z3.Not(self.vars["CorrKI"][s][i][j] + [k - 1]), + z3.Not(self.vars["CorrKJ"][s][i][j] + [k - 1]), + ), + ), + )) + + def constraint_corr_perp(self) -> None: + """for corr surf perpendicular to normal vector, all or none exists.""" + for s in range(self.n_s): + for i in range(self.n_i): + for j in range(self.n_j): + for k in range(self.n_k): + if (i, j, k) not in self.port_cubes: + # only consider X or Z spider + # if normal is K meaning meaning both + # (i,j,k)-(i,j,k+1) and (i,j,k)-(i,j,k-1) are + # out of range, or in range but nonexistent + normal = z3.And( + z3.Not(self.vars["NodeY"][i][j][k]), + z3.Not(self.vars["ExistK"][i][j][k]), + ) + if k - 1 >= 0: + normal = z3.And( + normal, + z3.Not(self.vars["ExistK"][i][j][k - 1])) + + # for other pipes, we need to build an intermediate + # expression for (i,j,k)-(i+1,j,k) and + # (i,j,k)-(i,j+1,k), built expression meaning + # the pipe is nonexistent or exist and has + # the correlation surface perpendicular to + # the normal vector in them. + no_pipe_or_with_corr = [ + z3.Or( + z3.Not(self.vars["ExistI"][i][j][k]), + self.vars["CorrIJ"][s][i][j][k], + ), + z3.Or( + z3.Not(self.vars["ExistJ"][i][j][k]), + self.vars["CorrJI"][s][i][j][k], + ), + ] + + # for (i,j,k)-(i+1,j,k) and (i,j,k)-(i,j+1,k), + # build expression meaning the pipe is nonexistent + # or exist and does not have the correlation + # surface perpendicular to the normal vector. + no_pipe_or_no_corr = [ + z3.Or( + z3.Not(self.vars["ExistI"][i][j][k]), + z3.Not(self.vars["CorrIJ"][s][i][j][k]), + ), + z3.Or( + z3.Not(self.vars["ExistJ"][i][j][k]), + z3.Not(self.vars["CorrJI"][s][i][j][k]), + ), + ] + + if i - 1 >= 0: + # add (i-1,j,k)-(i,j,k) to the expression + no_pipe_or_with_corr.append( + z3.Or( + z3.Not(self.vars["ExistI"][i - + 1][j][k]), + self.vars["CorrIJ"][s][i - 1][j][k], + )) + no_pipe_or_no_corr.append( + z3.Or( + z3.Not(self.vars["ExistI"][i - + 1][j][k]), + z3.Not( + self.vars["CorrIJ"][s][i - + 1][j][k]), + )) + + if j - 1 >= 0: + # add (i,j-1,k)-(i,j,k) to the expression + no_pipe_or_with_corr.append( + z3.Or( + z3.Not(self.vars["ExistJ"][i][j - + 1][k]), + self.vars["CorrJI"][s][i][j - 1][k], + )) + no_pipe_or_no_corr.append( + z3.Or( + z3.Not(self.vars["ExistJ"][i][j - + 1][k]), + z3.Not( + self.vars["CorrJI"][s][i][j - + 1][k]), + )) + + # if normal vector is K, then in all its + # incident pipes that exist all correlation surface + # in I-J plane exist or nonexist + self.goal.add( + z3.Implies( + normal, + z3.Or( + z3.And(no_pipe_or_with_corr), + z3.And(no_pipe_or_no_corr), + ), + )) + + # if normal is I + normal = z3.And( + z3.Not(self.vars["NodeY"][i][j][k]), + z3.Not(self.vars["ExistI"][i][j][k]), + ) + if i - 1 >= 0: + normal = z3.And( + normal, + z3.Not(self.vars["ExistI"][i - 1][j][k])) + no_pipe_or_with_corr = [ + z3.Or( + z3.Not(self.vars["ExistJ"][i][j][k]), + self.vars["CorrJK"][s][i][j][k], + ), + z3.Or( + z3.Not(self.vars["ExistK"][i][j][k]), + self.vars["CorrKJ"][s][i][j][k], + ), + ] + no_pipe_or_no_corr = [ + z3.Or( + z3.Not(self.vars["ExistJ"][i][j][k]), + z3.Not(self.vars["CorrJK"][s][i][j][k]), + ), + z3.Or( + z3.Not(self.vars["ExistK"][i][j][k]), + z3.Not(self.vars["CorrKJ"][s][i][j][k]), + ), + ] + if j - 1 >= 0: + no_pipe_or_with_corr.append( + z3.Or( + z3.Not(self.vars["ExistJ"][i][j - + 1][k]), + self.vars["CorrJK"][s][i][j - 1][k], + )) + no_pipe_or_no_corr.append( + z3.Or( + z3.Not(self.vars["ExistJ"][i][j - + 1][k]), + z3.Not( + self.vars["CorrJK"][s][i][j - + 1][k]), + )) + if k - 1 >= 0: + no_pipe_or_with_corr.append( + z3.Or( + z3.Not(self.vars["ExistK"][i][j][k - + 1]), + self.vars["CorrKJ"][s][i][j][k - 1], + )) + no_pipe_or_no_corr.append( + z3.Or( + z3.Not(self.vars["ExistK"][i][j][k - + 1]), + z3.Not( + self.vars["CorrKJ"][s][i][j][k - + 1]), + )) + self.goal.add( + z3.Implies( + normal, + z3.Or( + z3.And(no_pipe_or_with_corr), + z3.And(no_pipe_or_no_corr), + ), + )) + + # if normal is J + normal = z3.And( + z3.Not(self.vars["NodeY"][i][j][k]), + z3.Not(self.vars["ExistJ"][i][j][k]), + ) + if j - 1 >= 0: + normal = z3.And( + normal, + z3.Not(self.vars["ExistJ"][i][j - 1][k])) + no_pipe_or_with_corr = [ + z3.Or( + z3.Not(self.vars["ExistI"][i][j][k]), + self.vars["CorrIK"][s][i][j][k], + ), + z3.Or( + z3.Not(self.vars["ExistK"][i][j][k]), + self.vars["CorrKI"][s][i][j][k], + ), + ] + no_pipe_or_no_corr = [ + z3.Or( + z3.Not(self.vars["ExistI"][i][j][k]), + z3.Not(self.vars["CorrIK"][s][i][j][k]), + ), + z3.Or( + z3.Not(self.vars["ExistK"][i][j][k]), + z3.Not(self.vars["CorrKI"][s][i][j][k]), + ), + ] + if i - 1 >= 0: + no_pipe_or_with_corr.append( + z3.Or( + z3.Not(self.vars["ExistI"][i - + 1][j][k]), + self.vars["CorrIK"][s][i - 1][j][k], + )) + no_pipe_or_no_corr.append( + z3.Or( + z3.Not(self.vars["ExistI"][i - + 1][j][k]), + z3.Not( + self.vars["CorrIK"][s][i - + 1][j][k]), + )) + if k - 1 >= 0: + no_pipe_or_with_corr.append( + z3.Or( + z3.Not(self.vars["ExistK"][i][j][k - + 1]), + self.vars["CorrKI"][s][i][j][k - 1], + )) + no_pipe_or_no_corr.append( + z3.Or( + z3.Not(self.vars["ExistK"][i][j][k - + 1]), + z3.Not( + self.vars["CorrKI"][s][i][j][k - + 1]), + )) + self.goal.add( + z3.Implies( + normal, + z3.Or( + z3.And(no_pipe_or_with_corr), + z3.And(no_pipe_or_no_corr), + ), + )) + + def constraint_corr_para(self) -> None: + """for corr surf parallel to the normal , even number of them exist.""" + for s in range(self.n_s): + for i in range(self.n_i): + for j in range(self.n_j): + for k in range(self.n_k): + if (i, j, k) not in self.port_cubes: + # only X or Z spiders, if normal is K + normal = z3.And( + z3.Not(self.vars["NodeY"][i][j][k]), + z3.Not(self.vars["ExistK"][i][j][k]), + ) + if k - 1 >= 0: + normal = z3.And( + normal, + z3.Not(self.vars["ExistK"][i][j][k - 1])) + + # unlike in constraint_corr_perp, we only care + # about the cases where the pipe exists and the + # correlation surface parallel to K also is present + # so we build intermediate expressions as below + pipe_with_corr = [ + z3.And( + self.vars["ExistI"][i][j][k], + self.vars["CorrIK"][s][i][j][k], + ), + z3.And( + self.vars["ExistJ"][i][j][k], + self.vars["CorrJK"][s][i][j][k], + ), + ] + + # add (i-1,j,k)-(i,j,k) to the expression + if i - 1 >= 0: + pipe_with_corr.append( + z3.And( + self.vars["ExistI"][i - 1][j][k], + self.vars["CorrIK"][s][i - 1][j][k], + )) + + # add (i,j-1,k)-(i,j,k) to the expression + if j - 1 >= 0: + pipe_with_corr.append( + z3.And( + self.vars["ExistJ"][i][j - 1][k], + self.vars["CorrJK"][s][i][j - 1][k], + )) + + # parity of the expressions must be even + self.goal.add( + z3.Implies( + normal, + cnf_even_parity_upto4(pipe_with_corr))) + + # if normal is I + normal = z3.And( + z3.Not(self.vars["NodeY"][i][j][k]), + z3.Not(self.vars["ExistI"][i][j][k]), + ) + if i - 1 >= 0: + normal = z3.And( + normal, + z3.Not(self.vars["ExistI"][i - 1][j][k])) + pipe_with_corr = [ + z3.And( + self.vars["ExistJ"][i][j][k], + self.vars["CorrJI"][s][i][j][k], + ), + z3.And( + self.vars["ExistK"][i][j][k], + self.vars["CorrKI"][s][i][j][k], + ), + ] + if j - 1 >= 0: + pipe_with_corr.append( + z3.And( + self.vars["ExistJ"][i][j - 1][k], + self.vars["CorrJI"][s][i][j - 1][k], + )) + if k - 1 >= 0: + pipe_with_corr.append( + z3.And( + self.vars["ExistK"][i][j][k - 1], + self.vars["CorrKI"][s][i][j][k - 1], + )) + self.goal.add( + z3.Implies( + normal, + cnf_even_parity_upto4(pipe_with_corr))) + + # if normal is J + normal = z3.And( + z3.Not(self.vars["NodeY"][i][j][k]), + z3.Not(self.vars["ExistJ"][i][j][k]), + ) + if j - 1 >= 0: + normal = z3.And( + normal, + z3.Not(self.vars["ExistJ"][i][j - 1][k])) + pipe_with_corr = [ + z3.And( + self.vars["ExistI"][i][j][k], + self.vars["CorrIJ"][s][i][j][k], + ), + z3.And( + self.vars["ExistK"][i][j][k], + self.vars["CorrKJ"][s][i][j][k], + ), + ] + if i - 1 >= 0: + pipe_with_corr.append( + z3.And( + self.vars["ExistI"][i - 1][j][k], + self.vars["CorrIJ"][s][i - 1][j][k], + )) + if k - 1 >= 0: + pipe_with_corr.append( + z3.And( + self.vars["ExistK"][i][j][k - 1], + self.vars["CorrKJ"][s][i][j][k - 1], + )) + self.goal.add( + z3.Implies( + normal, + cnf_even_parity_upto4(pipe_with_corr))) + + def check_z3(self, print_progress: bool = True) -> bool: + """check whether the built goal in self.goal is satisfiable. + + Args: + print_progress (bool, optional): if print out the progress made. + + Returns: + bool: True if SAT, False if UNSAT + """ + if print_progress: + print("Construct a Z3 SMT model and solve...") + start_time = time.time() + self.solver = z3.Solver() + self.solver.add(self.goal) + ifsat = self.solver.check() + elapsed = time.time() - start_time + if print_progress: + print("elapsed time: {:2f}s".format(elapsed)) + if ifsat == z3.sat: + if print_progress: + print("Z3 SAT") + return True + else: + if print_progress: + print("Z3 UNSAT") + return False + + def check_kissat( + self, + kissat_dir: str, + dimacs_file_name: Optional[str] = None, + sat_log_file_name: Optional[str] = None, + print_progress: bool = True, + ) -> bool: + """check whether there is a solution with Kissat + + Args: + kissat_dir (str): directory containing an executable named kissat + dimacs_file_name (str, optional): Defaults to None. Then, the + dimacs file is in a tmp directory. If specified, the dimacs + will be saved to that path. + sat_log_file_name (str, optional): Defaults to None. Then, the + sat log file is in a tmp directory. If specified, the sat log + will be saved to that path. + print_progress (bool, optional): whether print the SAT solving + process on screen. Defaults to True. + + Raises: + ValueError: kissat_dir is not a directory + ValueError: there is no executable kissat in kissat_dir + ValueError: the return code to kissat is neither SAT nor UNSAT + + Returns: + bool: True if SAT, False if UNSAT + """ + if not os.path.isdir(kissat_dir): + raise ValueError(f"{kissat_dir} is not a directory.") + if kissat_dir.endswith("/"): + solver_cmd = kissat_dir + "kissat" + else: + solver_cmd = kissat_dir + "/kissat" + if not os.path.isfile(solver_cmd): + raise ValueError(f"There is no kissat in {kissat_dir}.") + + if_solved = False + with tempfile.TemporaryDirectory() as tmp_dir: + # open a tmp directory as workspace + + # dimacs and sat log are either in the tmp dir, or as user specify + tmp_dimacs_file_name = (dimacs_file_name + ".dimacs" if dimacs_file_name else + tmp_dir + "/tmp.dimacs") + tmp_sat_log_file_name = (sat_log_file_name + ".kissat" if sat_log_file_name + else tmp_dir + "/tmp.sat") + + if self.write_cnf(tmp_dimacs_file_name, + print_progress=print_progress): + # continue if the CNF is non-trivial, i.e., write_cnf is True + kissat_start_time = time.time() + + with open(tmp_sat_log_file_name, "w") as log: + # use tmp_sat_log_file_name to record stdout of kissat + + kissat_return_code = -100 + # -100 if the return code is not updated later on. + + import random + with subprocess.Popen( + [ + solver_cmd, f'--seed={random.randrange(1000000)}', + tmp_dimacs_file_name + ], + stdout=subprocess.PIPE, + bufsize=1, + universal_newlines=True, + ) as run_kissat: + for line in run_kissat.stdout: + log.write(line) + if print_progress: + sys.stdout.write(line) + get_return_code = run_kissat.communicate()[0] + kissat_return_code = run_kissat.returncode + + if kissat_return_code == 10: + # 10 means SAT in Kissat + if_solved = True + if print_progress: + print( + f"kissat runtime: {time.time()-kissat_start_time}") + print("kissat SAT!") + + # we read the Kissat solution from the SAT log, then, plug + # those into the Z3 model and solved inside Z3 again. + # The reason is that Z3 did some simplification of the + # problem so not every variable appear in the DIMACS given + # to Kissat. We still need to know their value. + result = self.read_kissat_result( + tmp_dimacs_file_name, + tmp_sat_log_file_name, + ) + self.plugin_arrs(result) + self.check_z3(print_progress) + + elif kissat_return_code == 20: + if print_progress: + print(f"{solver_cmd} UNSAT") + + elif kissat_return_code == -100: + print("Did not get Kissat return code.") + + else: + raise ValueError( + f"Kissat return code {kissat_return_code} is neither" + " SAT nor UNSAT. Maybe you should add print_process=" + "True to enable the Kissat printout message to see " + "what is going on.") + # closing the tmp directory, the files and itself are removed + + return if_solved + + def write_cnf(self, + output_file_name: str, + print_progress: bool = False) -> bool: + """generate CNF for the problem. + + Args: + output_file_name (str): file to write CNF. + + Returns: + bool: False if the CNF is trivial, True otherwise + """ + cnf_start_time = time.time() + simplified = z3.Tactic("simplify")(self.goal)[0] + simplified = z3.Tactic("propagate-values")(simplified)[0] + cnf = z3.Tactic("tseitin-cnf")(simplified)[0] + dimacs = cnf.dimacs() + if print_progress: + print(f"CNF generation time: {time.time() - cnf_start_time}") + + with open(output_file_name, "w") as output_f: + output_f.write(cnf.dimacs()) + if dimacs.startswith("p cnf 1 1"): + print("Generated CNF is trivial meaning z3 concludes the instance" + " UNSAT during simplification.") + return False + else: + return True + + def read_kissat_result(self, dimacs_file: str, + result_file: str) -> Mapping[str, Any]: + """read result from external SAT solver + + Args: + dimacs_file (str): + result_file (str): log from Kissat containing SAT assignments + + Raises: + ValueError: in the dimacs file, the last lines are comments that + records the mapping from SAT variable indices to the variable + names in Z3. If the coordinates in this name is incorrect. + + Returns: + Mapping[str, Any]: variable assignment in arrays. All the one with + a corresponding SAT variable are read off from the SAT log. + The others are left with -1. + """ + results = { + "ExistI": [[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] + for _ in range(self.n_i)], + "ExistJ": [[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] + for _ in range(self.n_i)], + "ExistK": [[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] + for _ in range(self.n_i)], + "ColorI": [[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] + for _ in range(self.n_i)], + "ColorJ": [[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] + for _ in range(self.n_i)], + "NodeY": [[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] + for _ in range(self.n_i)], + "CorrIJ": [[[[-1 for _ in range(self.n_k)] + for _ in range(self.n_j)] for _ in range(self.n_i)] + for _ in range(self.n_s)], + "CorrIK": [[[[-1 for _ in range(self.n_k)] + for _ in range(self.n_j)] for _ in range(self.n_i)] + for _ in range(self.n_s)], + "CorrJK": [[[[-1 for _ in range(self.n_k)] + for _ in range(self.n_j)] for _ in range(self.n_i)] + for _ in range(self.n_s)], + "CorrJI": [[[[-1 for _ in range(self.n_k)] + for _ in range(self.n_j)] for _ in range(self.n_i)] + for _ in range(self.n_s)], + "CorrKI": [[[[-1 for _ in range(self.n_k)] + for _ in range(self.n_j)] for _ in range(self.n_i)] + for _ in range(self.n_s)], + "CorrKJ": [[[[-1 for _ in range(self.n_k)] + for _ in range(self.n_j)] for _ in range(self.n_i)] + for _ in range(self.n_s)], + } + + # in this file, the assigments are lines starting with "v" like + # v -1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 ... + # the vars starts from 1 and - means it's False; otherwise, it's True + # we scan through all these lines, and save the assignments to `sat` + with open(result_file, "r") as f: + sat_output = f.readlines() + sat = {} + for line in sat_output: + if line.startswith("v"): + assignments = line[1:].strip().split(" ") + for assignment in assignments: + tmp = int(assignment) + if tmp < 0: + sat[str(-tmp)] = 0 + elif tmp > 0: + sat[str(tmp)] = 1 + + # in the dimacs generated by Z3, there are lines starting with "c" like + # c 8804 CorrIJ(1,0,3,4) or c 60053 k!44404 + # which records the mapping from our variables to variables in dimacs + # the ones starting with k! are added in the translation, we don"t care + with open(dimacs_file, "r") as f: + dimacs = f.readlines() + for line in dimacs: + if line.startswith("c"): + _, index, name = line.strip().split(" ") + if name.startswith(( + "NodeY", + "ExistI", + "ExistJ", + "ExistK", + "ColorI", + "ColorJ", + "CorrIJ", + "CorrIK", + "CorrJI", + "CorrJK", + "CorrKI", + "CorrKJ", + )): + arr, tmp = name[:-1].split("(") + coords = [int(num) for num in tmp.split(",")] + if len(coords) == 3: + results[arr][coords[0]][coords[1]][ + coords[2]] = sat[index] + elif len(coords) == 4: + results[arr][coords[0]][coords[1]][coords[2]][ + coords[3]] = sat[index] + else: + raise ValueError("number of coord should be 3 or 4!") + + return results + + def get_result(self) -> Mapping[str, Any]: + """get the variable values. + + Returns: + Mapping[str, Any]: output in the LaSRe format + """ + model = self.solver.model() + data = { + "n_i": + self.n_i, + "n_j": + self.n_j, + "n_k": + self.n_k, + "n_p": + self.n_p, + "n_s": + self.n_s, + "ports": + self.ports, + "stabs": + self.stabs, + "port_cubes": + self.port_cubes, + "optional": + self.optional, + "ExistI": [[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] + for _ in range(self.n_i)], + "ExistJ": [[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] + for _ in range(self.n_i)], + "ExistK": [[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] + for _ in range(self.n_i)], + "ColorI": [[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] + for _ in range(self.n_i)], + "ColorJ": [[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] + for _ in range(self.n_i)], + "NodeY": [[[-1 for _ in range(self.n_k)] for _ in range(self.n_j)] + for _ in range(self.n_i)], + "CorrIJ": [[[[-1 for _ in range(self.n_k)] + for _ in range(self.n_j)] for _ in range(self.n_i)] + for _ in range(self.n_s)], + "CorrIK": [[[[-1 for _ in range(self.n_k)] + for _ in range(self.n_j)] for _ in range(self.n_i)] + for _ in range(self.n_s)], + "CorrJK": [[[[-1 for _ in range(self.n_k)] + for _ in range(self.n_j)] for _ in range(self.n_i)] + for _ in range(self.n_s)], + "CorrJI": [[[[-1 for _ in range(self.n_k)] + for _ in range(self.n_j)] for _ in range(self.n_i)] + for _ in range(self.n_s)], + "CorrKI": [[[[-1 for _ in range(self.n_k)] + for _ in range(self.n_j)] for _ in range(self.n_i)] + for _ in range(self.n_s)], + "CorrKJ": [[[[-1 for _ in range(self.n_k)] + for _ in range(self.n_j)] for _ in range(self.n_i)] + for _ in range(self.n_s)], + } + arrs = [ + "NodeY", + "ExistI", + "ExistJ", + "ExistK", + ] + if self.color_ij: + arrs += [ + "ColorI", + "ColorJ", + ] + for s in range(self.n_s): + for i in range(self.n_i): + for j in range(self.n_j): + for k in range(self.n_k): + if s == 0: # Exist, Node, and Color vars + for arr in arrs: + data[arr][i][j][k] = ( + 1 if model[self.vars[arr][i][j][k]] else 0) + + # Corr vars + for arr in [ + "CorrIJ", + "CorrIK", + "CorrJI", + "CorrJK", + "CorrKI", + "CorrKJ", + ]: + data[arr][s][i][j][k] = ( + 1 if model[self.vars[arr][s][i][j][k]] else 0) + return data diff --git a/glue/lattice_surgery/lassynth/tools/__init__.py b/glue/lattice_surgery/lassynth/tools/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/glue/lattice_surgery/lassynth/tools/verify_stabilizers.py b/glue/lattice_surgery/lassynth/tools/verify_stabilizers.py new file mode 100644 index 000000000..25fba0b81 --- /dev/null +++ b/glue/lattice_surgery/lassynth/tools/verify_stabilizers.py @@ -0,0 +1,38 @@ +from typing import Sequence +import stim +import stimzx + + +def verify_stabilizers( + specified_paulistrings: Sequence[str], + result_networkx, + print_stabilizers: bool = False, +) -> bool: + result_stabilizers = [ + stab.output + for stab in stimzx.zx_graph_to_external_stabilizers(result_networkx) + ] + specified_stabilizers = [ + stim.PauliString(paulistring) for paulistring in specified_paulistrings + ] + if print_stabilizers: + print("specified:") + for s in specified_stabilizers: + print(s) + print("==============================================================") + print("resulting:") + for s in result_stabilizers: + print(s) + print("==============================================================") + + for s in result_stabilizers: + for ss in specified_stabilizers: + if not ss.commutes(s): + print(f"result stabilizer {s} not commuting with " + f"specified stabilizer {ss}") + if print_stabilizers: + print("specified and resulting stabilizers not equivalent") + return False + if print_stabilizers: + print("specified and resulting stabilizers are equivalent.") + return True diff --git a/glue/lattice_surgery/lassynth/translators/__init__.py b/glue/lattice_surgery/lassynth/translators/__init__.py new file mode 100644 index 000000000..e93e0abc4 --- /dev/null +++ b/glue/lattice_surgery/lassynth/translators/__init__.py @@ -0,0 +1 @@ +from lassynth.translators.zx_grid_graph import ZXGridGraph diff --git a/glue/lattice_surgery/lassynth/translators/gltf_generator.py b/glue/lattice_surgery/lassynth/translators/gltf_generator.py new file mode 100644 index 000000000..20ce69be7 --- /dev/null +++ b/glue/lattice_surgery/lassynth/translators/gltf_generator.py @@ -0,0 +1,2622 @@ +"""generating a 3D modelling file in gltf format from our LaSRe.""" + +import json +from typing import Any, Mapping, Optional, Sequence, Tuple + +# constants +SQ2 = 0.707106769085 # square root of 2 +THICKNESS = 0.01 # half separation of front and back sides of each face +AXESTHICKNESS = 0.1 + +def float_to_little_endian_hex(f): + from struct import pack + + # Pack the float into a binary string using the little-endian format + binary_data = pack(" Mapping[str, Any]: + """generate basic gltf contents, i.e., independent from the LaS + + Args: + tubelen (float, optional): ratio of the length of the pipe with respect + to the length of a cube. Defaults to 2.0. + + Returns: + Mapping[str, Any]: gltf with everything here. + """ + # floats as hex, little endian + floats = { + "0": "00000000", + "1": "0000803F", + "-1": "000080BF", + "0.5": "0000003F", + "0.45": "6666E63E", + } + floats["tube"] = float_to_little_endian_hex(tubelen) + floats["+SQ2"] = float_to_little_endian_hex(SQ2) + floats["-SQ2"] = float_to_little_endian_hex(-SQ2) + floats["+T"] = float_to_little_endian_hex(THICKNESS) + floats["-T"] = float_to_little_endian_hex(-THICKNESS) + floats["1-T"] = float_to_little_endian_hex(1 - THICKNESS) + floats["T-1"] = float_to_little_endian_hex(THICKNESS - 1) + floats["0.5+T"] = float_to_little_endian_hex(0.5 + THICKNESS) + floats["0.5-T"] = float_to_little_endian_hex(0.5 - THICKNESS) + + # integers as hex + ints = ["0000", "0100", "0200", "0300", "0400", "0500", "0600", "0700"] + + gltf = { + "asset": { + "generator": "LaSRe CodeGen by Daniel Bochen Tan", + "version": "2.0" + }, + "scene": 0, + "scenes": [{ + "name": "Scene", + "nodes": [0] + }], + "nodes": [{ + "name": "Lattice Surgery Subroutine", + "children": [] + }], + } + gltf["accessors"] = [] + gltf["buffers"] = [] + gltf["bufferViews"] = [] + + # materials are the colors. baseColorFactor is (R, G, B, alpha) + gltf["materials"] = [ + { + "name": "0-blue", + "pbrMetallicRoughness": { + "baseColorFactor": [0, 0, 1, 1] + }, + "doubleSided": False, + }, + { + "name": "1-red", + "pbrMetallicRoughness": { + "baseColorFactor": [1, 0, 0, 1] + }, + "doubleSided": False, + }, + { + "name": "2-green", + "pbrMetallicRoughness": { + "baseColorFactor": [0, 1, 0, 1] + }, + "doubleSided": False, + }, + { + "name": "3-gray", + "pbrMetallicRoughness": { + "baseColorFactor": [0.5, 0.5, 0.5, 1] + }, + "doubleSided": False, + }, + { + "name": "4-cyan.3", + "pbrMetallicRoughness": { + "baseColorFactor": [0, 1, 1, 0.3] + }, + "doubleSided": False, + "alphaMode": "BLEND", + }, + { + "name": "5-black", + "pbrMetallicRoughness": { + "baseColorFactor": [0, 0, 0, 1] + }, + "doubleSided": False, + }, + { + "name": "6-yellow", + "pbrMetallicRoughness": { + "baseColorFactor": [1, 1, 0, 1] + }, + "doubleSided": False, + }, + { + "name": "7-white", + "pbrMetallicRoughness": { + "baseColorFactor": [1, 1, 1, 1] + }, + "doubleSided": False, + }, + ] + + # for a 3D coordinate (X,Y,Z), the convention of VEC3 in GLTF is (X,Z,-Y) + # below are the data we store into the embedded binary in the GLTF. + # For each data, we create a buffer, there is one and only one bufferView + # for this buffer, and there is one and only one accessor for this + # bufferView. This is for simplicity. So in what follows, we always gather + # the string corresponding to the data, whether they're a list of floats or + # a list of integers. Then, we append a buffer, a bufferView, and an + # accessor to the GLTF. This part is quite machinary. + + # GLTF itself support doubleside color in materials, but this can lead to + # problems when converting to other formats. So, for each face of a cube or + # a pipe, we will make it two sides, front and back. The POSITION of + # vertices of these two are shifted on the Z axis by 2*THICKNESS. Since we + # need their color to both facing outside, the back side require opposite + # NORMAL vectors, and the index order needs to be reversed. We begin with + # definition for the front sides. + + # 0, positions of square: [(+T,+T,-T),(1-T,+T,-T),(+T,1-T,-T),(1-T,1-T,-T)] + s = (floats["+T"] + floats["-T"] + floats["-T"] + floats["1-T"] + + floats["-T"] + floats["-T"] + floats["+T"] + floats["-T"] + + floats["T-1"] + floats["1-T"] + floats["-T"] + floats["T-1"]) + gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 0, + "byteLength": 48, + "byteOffset": 0, + "target": 34962 + }) + gltf["accessors"].append({ + "bufferView": 0, + "componentType": 5126, + "type": "VEC3", + "count": 4, + "max": [1 - THICKNESS, -THICKNESS, -THICKNESS], + "min": [THICKNESS, -THICKNESS, THICKNESS - 1], + }) + + # 1, positions of rectangle: [(0,0,-T),(L,0,-T),(0,1,-T),(L,1,-T)] + s = (floats["0"] + floats["-T"] + floats["0"] + floats["tube"] + + floats["-T"] + floats["0"] + floats["0"] + floats["-T"] + + floats["-1"] + floats["tube"] + floats["-T"] + floats["-1"]) + gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 1, + "byteLength": 48, + "byteOffset": 0, + "target": 34962 + }) + gltf["accessors"].append({ + "bufferView": 1, + "componentType": 5126, + "type": "VEC3", + "count": 4, + "max": [tubelen, -THICKNESS, 0], + "min": [0, -THICKNESS, -1], + }) + + # 2, normals of rect/sqr: (0,0,-1)*4 + s = (floats["0"] + floats["-1"] + floats["0"] + floats["0"] + + floats["-1"] + floats["0"] + floats["0"] + floats["-1"] + + floats["0"] + floats["0"] + floats["-1"] + floats["0"]) + gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 2, + "byteLength": 48, + "byteOffset": 0, + "target": 34962 + }) + gltf["accessors"].append({ + "bufferView": 2, + "componentType": 5126, + "type": "VEC3", + "count": 4 + }) + + # 3, vertices of rect/sqr: [1,0,3, 3,0,2] + s = ints[1] + ints[0] + ints[3] + ints[3] + ints[0] + ints[2] + gltf["buffers"].append({"byteLength": 12, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 3, + "byteLength": 12, + "byteOffset": 0, + "target": 34963 + }) + gltf["accessors"].append({ + "bufferView": 3, + "componentType": 5123, + "type": "SCALAR", + "count": 6 + }) + + # 4, positions of tilted rect: [(0,0,1/2+T),(1/2,0,+T),(0,1,1/2+T),(1/2,1,+T)] + s = (floats["0"] + floats["0.5+T"] + floats["0"] + floats["0.5"] + + floats["+T"] + floats["0"] + floats["0"] + floats["0.5+T"] + + floats["-1"] + floats["0.5"] + floats["+T"] + floats["-1"]) + gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 4, + "byteLength": 48, + "byteOffset": 0, + "target": 34962 + }) + gltf["accessors"].append({ + "bufferView": 4, + "componentType": 5126, + "type": "VEC3", + "count": 4, + "max": [0.5, 0.5 + THICKNESS, 0], + "min": [0, THICKNESS, -1], + }) + + # 5, normals of tilted rect: (-sqrt(2)/2, 0, -sqrt(2)/2)*4 + s = (floats["-SQ2"] + floats["-SQ2"] + floats["0"] + floats["-SQ2"] + + floats["-SQ2"] + floats["0"] + floats["-SQ2"] + floats["-SQ2"] + + floats["0"] + floats["-SQ2"] + floats["-SQ2"] + floats["0"]) + gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 5, + "byteLength": 48, + "byteOffset": 0, + "target": 34962 + }) + gltf["accessors"].append({ + "bufferView": 5, + "componentType": 5126, + "type": "VEC3", + "count": 4 + }) + + # 6, positions of Hadamard rectangle: [(0,0,-T),(15/32L,0,-T),(15/32L,1,-T), + # (15/32L,1,-T),(17/32L,0,-T),(17/32L,1,-T),(L,0,-T),(L,1,-T)] + floats["left"] = float_to_little_endian_hex(tubelen * 15 / 32) + floats["right"] = float_to_little_endian_hex(tubelen * 17 / 32) + s = (floats["0"] + floats["-T"] + floats["0"] + floats["left"] + + floats["-T"] + floats["0"] + floats["0"] + floats["-T"] + + floats["-1"] + floats["left"] + floats["-T"] + floats["-1"] + + floats["right"] + floats["-T"] + floats["0"] + floats["right"] + + floats["-T"] + floats["-1"] + floats["tube"] + floats["-T"] + + floats["0"] + floats["tube"] + floats["-T"] + floats["-1"]) + gltf["buffers"].append({"byteLength": 96, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 6, + "byteLength": 96, + "byteOffset": 0, + "target": 34962 + }) + gltf["accessors"].append({ + "bufferView": 6, + "componentType": 5126, + "type": "VEC3", + "count": 8, + "max": [tubelen, -THICKNESS, 0], + "min": [0, -THICKNESS, -1], + }) + + # 7, normals of Hadamard rect (0,0,-1)*8 + s = (floats["0"] + floats["-1"] + floats["0"] + floats["0"] + + floats["-1"] + floats["0"] + floats["0"] + floats["-1"] + + floats["0"] + floats["0"] + floats["-1"] + floats["0"] + floats["0"] + + floats["-1"] + floats["0"] + floats["0"] + floats["-1"] + + floats["0"] + floats["0"] + floats["-1"] + floats["0"] + floats["0"] + + floats["-1"] + floats["0"]) + gltf["buffers"].append({"byteLength": 96, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 7, + "byteLength": 96, + "byteOffset": 0, + "target": 34962 + }) + gltf["accessors"].append({ + "bufferView": 7, + "componentType": 5126, + "type": "VEC3", + "count": 8 + }) + + # 8, vertices of middle rect in Hadamard rect: [4,1,5, 5,1,3] + s = ints[4] + ints[1] + ints[5] + ints[5] + ints[1] + ints[3] + gltf["buffers"].append({"byteLength": 12, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 8, + "byteLength": 12, + "byteOffset": 0, + "target": 34963 + }) + gltf["accessors"].append({ + "bufferView": 8, + "componentType": 5123, + "type": "SCALAR", + "count": 6 + }) + + # 9, vertices of upper rect in Hadamard rect: [6,4,7, 7,4,5] + s = ints[6] + ints[4] + ints[7] + ints[7] + ints[4] + ints[5] + gltf["buffers"].append({"byteLength": 12, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 9, + "byteLength": 12, + "byteOffset": 0, + "target": 34963 + }) + gltf["accessors"].append({ + "bufferView": 9, + "componentType": 5123, + "type": "SCALAR", + "count": 6 + }) + + # 10, vertices of lines around a face: [0,1, 0,2, 2,3, 3,1] + # GLTF supports drawing lines, but there may be a problem converting to + # other formats. We have thus not used these data. + s = (ints[0] + ints[1] + ints[0] + ints[2] + ints[2] + ints[3] + ints[3] + + ints[1]) + gltf["buffers"].append({"byteLength": 16, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 10, + "byteLength": 16, + "byteOffset": 0, + "target": 34963 + }) + gltf["accessors"].append({ + "bufferView": 10, + "componentType": 5123, + "type": "SCALAR", + "count": 8 + }) + + # 11, positions of half-distance rectangle: [(0,0,-T),(0.45,0,-T),(0,1,-T),(0.45,1,-T)] + s = (floats["0"] + floats["-T"] + floats["0"] + floats["0.45"] + + floats["-T"] + floats["0"] + floats["0"] + floats["-T"] + + floats["-1"] + floats["0.45"] + floats["-T"] + floats["-1"]) + gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 11, + "byteLength": 48, + "byteOffset": 0, + "target": 34962 + }) + gltf["accessors"].append({ + "bufferView": 11, + "componentType": 5126, + "type": "VEC3", + "count": 4, + "max": [0.45, -THICKNESS, 0], + "min": [0, -THICKNESS, -1], + }) + + # 12, backside, positions of square: [(+T,+T,+T),(1-T,+T,+T),(+T,1-T,+T),(1-T,1-T,+T)] + s = (floats["+T"] + floats["+T"] + floats["-T"] + floats["1-T"] + + floats["+T"] + floats["-T"] + floats["+T"] + floats["+T"] + + floats["T-1"] + floats["1-T"] + floats["+T"] + floats["T-1"]) + gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 12, + "byteLength": 48, + "byteOffset": 0, + "target": 34962 + }) + gltf["accessors"].append({ + "bufferView": 12, + "componentType": 5126, + "type": "VEC3", + "count": 4, + "max": [1 - THICKNESS, THICKNESS, -THICKNESS], + "min": [THICKNESS, THICKNESS, THICKNESS - 1], + }) + + # 13, backside, normals of rect/sqr: (0,0,1)*4 + s = (floats["0"] + floats["1"] + floats["0"] + floats["0"] + floats["1"] + + floats["0"] + floats["0"] + floats["1"] + floats["0"] + floats["0"] + + floats["1"] + floats["0"]) + gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 13, + "byteLength": 48, + "byteOffset": 0, + "target": 34962 + }) + gltf["accessors"].append({ + "bufferView": 13, + "componentType": 5126, + "type": "VEC3", + "count": 4 + }) + + # For the cubes, we want to draw black lines around its boundaries to help + # people identify them visually. However, drawing lines in GLTF may become + # a problem when converting to other formats. Here, we define super thin + # rectangles at the boundaries of squares which will be seen as lines. + + # 14, frontside, positions of edge 0: [(+T,0,-T),(1-T,0,-T),(+T,+T,-T),(1-T,+T,-T)] + s = (floats["+T"] + floats["-T"] + floats["0"] + floats["1-T"] + + floats["-T"] + floats["0"] + floats["+T"] + floats["-T"] + + floats["-T"] + floats["1-T"] + floats["-T"] + floats["-T"]) + gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 14, + "byteLength": 48, + "byteOffset": 0, + "target": 34962 + }) + gltf["accessors"].append({ + "bufferView": 14, + "componentType": 5126, + "type": "VEC3", + "count": 4, + "max": [1 - THICKNESS, -THICKNESS, 0], + "min": [THICKNESS, -THICKNESS, -THICKNESS], + }) + + # 15, frontside, positions of edge 1: [(1-T,+T,-T),(1,+T,-T),(1-T,1-T,-T),(1,1-T,-T)] + s = (floats["1-T"] + floats["-T"] + floats["-T"] + floats["1"] + + floats["-T"] + floats["-T"] + floats["1-T"] + floats["-T"] + + floats["T-1"] + floats["1"] + floats["-T"] + floats["T-1"]) + gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 15, + "byteLength": 48, + "byteOffset": 0, + "target": 34962 + }) + gltf["accessors"].append({ + "bufferView": 15, + "componentType": 5126, + "type": "VEC3", + "count": 4, + "max": [1, -THICKNESS, -THICKNESS], + "min": [1 - THICKNESS, -THICKNESS, THICKNESS - 1], + }) + + # 16, frontside, positions of edge 2: [(0,+T,-T),(+T,+T,-T),(0,1-T,-T),(+T,1-T,-T)] + s = (floats["0"] + floats["-T"] + floats["-T"] + floats["+T"] + + floats["-T"] + floats["-T"] + floats["0"] + floats["-T"] + + floats["T-1"] + floats["+T"] + floats["-T"] + floats["T-1"]) + gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 16, + "byteLength": 48, + "byteOffset": 0, + "target": 34962 + }) + gltf["accessors"].append({ + "bufferView": 16, + "componentType": 5126, + "type": "VEC3", + "count": 4, + "max": [THICKNESS, -THICKNESS, -THICKNESS], + "min": [0, -THICKNESS, THICKNESS - 1], + }) + + # 17, frontside, positions of edge 3: [(+T,1-T,-T),(1-T,1-T,-T),(+T,1,-T),(1-T,1,-T)] + s = (floats["+T"] + floats["-T"] + floats["T-1"] + floats["1-T"] + + floats["-T"] + floats["T-1"] + floats["+T"] + floats["-T"] + + floats["-1"] + floats["1-T"] + floats["-T"] + floats["-1"]) + gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 17, + "byteLength": 48, + "byteOffset": 0, + "target": 34962 + }) + gltf["accessors"].append({ + "bufferView": 17, + "componentType": 5126, + "type": "VEC3", + "count": 4, + "max": [1 - THICKNESS, -THICKNESS, THICKNESS - 1], + "min": [THICKNESS, -THICKNESS, -1], + }) + + # 18, backside, positions of edge 0: [(+T,0,+T),(1-T,0,+T),(+T,+T,+T),(1-T,+T,+T)] + s = (floats["+T"] + floats["+T"] + floats["0"] + floats["1-T"] + + floats["+T"] + floats["0"] + floats["+T"] + floats["+T"] + + floats["-T"] + floats["1-T"] + floats["+T"] + floats["-T"]) + gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 18, + "byteLength": 48, + "byteOffset": 0, + "target": 34962 + }) + gltf["accessors"].append({ + "bufferView": 18, + "componentType": 5126, + "type": "VEC3", + "count": 4, + "max": [1 - THICKNESS, THICKNESS, 0], + "min": [THICKNESS, THICKNESS, -THICKNESS], + }) + + # 19, backside, positions of edge 1: [(1-T,+T,+T),(1,+T,+T),(1-T,1-T,+T),(1,1-T,+T)] + s = (floats["1-T"] + floats["+T"] + floats["-T"] + floats["1"] + + floats["+T"] + floats["-T"] + floats["1-T"] + floats["+T"] + + floats["T-1"] + floats["1"] + floats["+T"] + floats["T-1"]) + gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 19, + "byteLength": 48, + "byteOffset": 0, + "target": 34962 + }) + gltf["accessors"].append({ + "bufferView": 19, + "componentType": 5126, + "type": "VEC3", + "count": 4, + "max": [1, THICKNESS, -THICKNESS], + "min": [1 - THICKNESS, THICKNESS, THICKNESS - 1], + }) + + # 20, backside, positions of edge 2: [(0,+T,+T),(+T,+T,+T),(0,1-T,+T),(+T,1-T,+T)] + s = (floats["0"] + floats["+T"] + floats["-T"] + floats["+T"] + + floats["+T"] + floats["-T"] + floats["0"] + floats["+T"] + + floats["T-1"] + floats["+T"] + floats["+T"] + floats["T-1"]) + gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 20, + "byteLength": 48, + "byteOffset": 0, + "target": 34962 + }) + gltf["accessors"].append({ + "bufferView": 20, + "componentType": 5126, + "type": "VEC3", + "count": 4, + "max": [THICKNESS, THICKNESS, -THICKNESS], + "min": [0, THICKNESS, THICKNESS - 1], + }) + + # 21, backside, positions of edge 3: [(+T,1-T,+T),(1-T,1-T,+T),(+T,1,+T),(1-T,1,+T)] + s = (floats["+T"] + floats["+T"] + floats["T-1"] + floats["1-T"] + + floats["+T"] + floats["T-1"] + floats["+T"] + floats["+T"] + + floats["-1"] + floats["1-T"] + floats["+T"] + floats["-1"]) + gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 21, + "byteLength": 48, + "byteOffset": 0, + "target": 34962 + }) + gltf["accessors"].append({ + "bufferView": 21, + "componentType": 5126, + "type": "VEC3", + "count": 4, + "max": [1 - THICKNESS, THICKNESS, THICKNESS - 1], + "min": [THICKNESS, THICKNESS, -1], + }) + + # 22, backside vertices of rect/sqr: [1,3,0, 3,2,0] + s = ints[1] + ints[3] + ints[0] + ints[3] + ints[2] + ints[0] + gltf["buffers"].append({"byteLength": 12, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 22, + "byteLength": 12, + "byteOffset": 0, + "target": 34963 + }) + gltf["accessors"].append({ + "bufferView": 22, + "componentType": 5123, + "type": "SCALAR", + "count": 6 + }) + + # 23, backside, positions of rectangle: [(0,0,+T),(L,0,+T),(0,1,+T),(L,1,+T)] + s = (floats["0"] + floats["+T"] + floats["0"] + floats["tube"] + + floats["+T"] + floats["0"] + floats["0"] + floats["+T"] + + floats["-1"] + floats["tube"] + floats["+T"] + floats["-1"]) + gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 23, + "byteLength": 48, + "byteOffset": 0, + "target": 34962 + }) + gltf["accessors"].append({ + "bufferView": 23, + "componentType": 5126, + "type": "VEC3", + "count": 4, + "max": [tubelen, THICKNESS, 0], + "min": [0, THICKNESS, -1], + }) + + # 24, backside, positions of half-distance rectangle: [(0,0,+T),(0.45,0,+T),(0,1,+T),(0.45,1,+T)] + s = (floats["0"] + floats["+T"] + floats["0"] + floats["0.45"] + + floats["+T"] + floats["0"] + floats["0"] + floats["+T"] + + floats["-1"] + floats["0.45"] + floats["+T"] + floats["-1"]) + gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 24, + "byteLength": 48, + "byteOffset": 0, + "target": 34962 + }) + gltf["accessors"].append({ + "bufferView": 24, + "componentType": 5126, + "type": "VEC3", + "count": 4, + "max": [0.45, THICKNESS, 0], + "min": [0, THICKNESS, -1], + }) + + # 25, backside, positions of Hadamard rectangle: [(0,0,+T),(15/32L,0,+T),(15/32L,1,+T), + # (15/32L,1,+T),(17/32L,0,+T),(17/32L,1,+T),(L,0,+T),(L,1,+T)] + floats["left"] = float_to_little_endian_hex(tubelen * 15 / 32) + floats["right"] = float_to_little_endian_hex(tubelen * 17 / 32) + s = (floats["0"] + floats["+T"] + floats["0"] + floats["left"] + + floats["+T"] + floats["0"] + floats["0"] + floats["+T"] + + floats["-1"] + floats["left"] + floats["+T"] + floats["-1"] + + floats["right"] + floats["+T"] + floats["0"] + floats["right"] + + floats["+T"] + floats["-1"] + floats["tube"] + floats["+T"] + + floats["0"] + floats["tube"] + floats["+T"] + floats["-1"]) + gltf["buffers"].append({"byteLength": 96, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 25, + "byteLength": 96, + "byteOffset": 0, + "target": 34962 + }) + gltf["accessors"].append({ + "bufferView": 25, + "componentType": 5126, + "type": "VEC3", + "count": 8, + "max": [tubelen, THICKNESS, 0], + "min": [0, THICKNESS, -1], + }) + + # 26, backside, normals of Hadamard rect (0,0,1)*8 + s = (floats["0"] + floats["1"] + floats["0"] + floats["0"] + floats["1"] + + floats["0"] + floats["0"] + floats["1"] + floats["0"] + floats["0"] + + floats["1"] + floats["0"] + floats["0"] + floats["1"] + floats["0"] + + floats["0"] + floats["1"] + floats["0"] + floats["0"] + floats["1"] + + floats["0"] + floats["0"] + floats["1"] + floats["0"]) + gltf["buffers"].append({"byteLength": 96, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 26, + "byteLength": 96, + "byteOffset": 0, + "target": 34962 + }) + gltf["accessors"].append({ + "bufferView": 26, + "componentType": 5126, + "type": "VEC3", + "count": 8 + }) + + # 27, backside, vertices of middle rect in Hadamard rect: [4,5,1, 5,3,1] + s = ints[4] + ints[5] + ints[1] + ints[5] + ints[3] + ints[1] + gltf["buffers"].append({"byteLength": 12, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 27, + "byteLength": 12, + "byteOffset": 0, + "target": 34963 + }) + gltf["accessors"].append({ + "bufferView": 27, + "componentType": 5123, + "type": "SCALAR", + "count": 6 + }) + + # 28, backside, vertices of upper rect in Hadamard rect: [6,7,4, 7,5,4] + s = ints[6] + ints[7] + ints[4] + ints[7] + ints[5] + ints[4] + gltf["buffers"].append({"byteLength": 12, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 28, + "byteLength": 12, + "byteOffset": 0, + "target": 34963 + }) + gltf["accessors"].append({ + "bufferView": 28, + "componentType": 5123, + "type": "SCALAR", + "count": 6 + }) + + # 29, backside, positions of tilted rect: [(0,0,1/2+T),(1/2,0,+T),(0,1,1/2+T),(1/2,1,+T)] + s = (floats["0"] + floats["0.5-T"] + floats["0"] + floats["0.5"] + + floats["-T"] + floats["0"] + floats["0"] + floats["0.5-T"] + + floats["-1"] + floats["0.5"] + floats["-T"] + floats["-1"]) + gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 29, + "byteLength": 48, + "byteOffset": 0, + "target": 34962 + }) + gltf["accessors"].append({ + "bufferView": 29, + "componentType": 5126, + "type": "VEC3", + "count": 4, + "max": [0.5, 0.5 - THICKNESS, 0], + "min": [0, -THICKNESS, -1], + }) + + # 30, backside, normals of tilted rect: (sqrt(2)/2, 0, sqrt(2)/2)*4 + s = (floats["+SQ2"] + floats["+SQ2"] + floats["0"] + floats["+SQ2"] + + floats["+SQ2"] + floats["0"] + floats["+SQ2"] + floats["+SQ2"] + + floats["0"] + floats["+SQ2"] + floats["+SQ2"] + floats["0"]) + gltf["buffers"].append({"byteLength": 48, "uri": hex_to_bin(s)}) + gltf["bufferViews"].append({ + "buffer": 30, + "byteLength": 48, + "byteOffset": 0, + "target": 34962 + }) + gltf["accessors"].append({ + "bufferView": 30, + "componentType": 5126, + "type": "VEC3", + "count": 4 + }) + + # finished creating the binary + + # Now we create meshes. These are the real constructors of the 3D diagram. + # a mesh can contain multiple primitives. A primitive can be defined by a + # set of vertices POSITION, their NORMAL vectors, the order of going around + # these vertices, and the color (material) for the triangles defined by + # going around the vertices. `mode:4` means color these triangles. + gltf["meshes"] = [ + { + "name": + "0-square-blue", + "primitives": [ + # front side + { + "attributes": { + "NORMAL": 2, + "POSITION": 0 + }, + "indices": 3, + "material": 0, + "mode": 4, + }, + # back side + { + "attributes": { + "NORMAL": 13, + "POSITION": 12 + }, + "indices": 22, + "material": 0, + "mode": 4, + }, + # front side edge 0 + { + "attributes": { + "NORMAL": 2, + "POSITION": 14 + }, + "indices": 3, + "material": 5, + "mode": 4, + }, + # front side edge 1 + { + "attributes": { + "NORMAL": 2, + "POSITION": 15 + }, + "indices": 3, + "material": 5, + "mode": 4, + }, + # front side edge 2 + { + "attributes": { + "NORMAL": 2, + "POSITION": 16 + }, + "indices": 3, + "material": 5, + "mode": 4, + }, + # front side edge 3 + { + "attributes": { + "NORMAL": 2, + "POSITION": 17 + }, + "indices": 3, + "material": 5, + "mode": 4, + }, + # back side edge 0 + { + "attributes": { + "NORMAL": 13, + "POSITION": 18 + }, + "indices": 22, + "material": 5, + "mode": 4, + }, + # back side edge 1 + { + "attributes": { + "NORMAL": 13, + "POSITION": 19 + }, + "indices": 22, + "material": 5, + "mode": 4, + }, + # back side edge 2 + { + "attributes": { + "NORMAL": 13, + "POSITION": 20 + }, + "indices": 22, + "material": 5, + "mode": 4, + }, + # back side edge 3 + { + "attributes": { + "NORMAL": 13, + "POSITION": 21 + }, + "indices": 22, + "material": 5, + "mode": 4, + }, + ], + }, + { + "name": + "1-square-red", + "primitives": [ + # front side + { + "attributes": { + "NORMAL": 2, + "POSITION": 0 + }, + "indices": 3, + "material": 1, + "mode": 4, + }, + # back side + { + "attributes": { + "NORMAL": 13, + "POSITION": 12 + }, + "indices": 22, + "material": 1, + "mode": 4, + }, + # front side edge 0 + { + "attributes": { + "NORMAL": 2, + "POSITION": 14 + }, + "indices": 3, + "material": 5, + "mode": 4, + }, + # front side edge 1 + { + "attributes": { + "NORMAL": 2, + "POSITION": 15 + }, + "indices": 3, + "material": 5, + "mode": 4, + }, + # front side edge 2 + { + "attributes": { + "NORMAL": 2, + "POSITION": 16 + }, + "indices": 3, + "material": 5, + "mode": 4, + }, + # front side edge 3 + { + "attributes": { + "NORMAL": 2, + "POSITION": 17 + }, + "indices": 3, + "material": 5, + "mode": 4, + }, + # back side edge 0 + { + "attributes": { + "NORMAL": 13, + "POSITION": 18 + }, + "indices": 22, + "material": 5, + "mode": 4, + }, + # back side edge 1 + { + "attributes": { + "NORMAL": 13, + "POSITION": 19 + }, + "indices": 22, + "material": 5, + "mode": 4, + }, + # back side edge 2 + { + "attributes": { + "NORMAL": 13, + "POSITION": 20 + }, + "indices": 22, + "material": 5, + "mode": 4, + }, + # back side edge 3 + { + "attributes": { + "NORMAL": 13, + "POSITION": 21 + }, + "indices": 22, + "material": 5, + "mode": 4, + }, + ], + }, + { + "name": + "2-square-gray", + "primitives": [ + { + "attributes": { + "NORMAL": 2, + "POSITION": 0 + }, + "indices": 3, + "material": 3, + "mode": 4, + }, + # back side + { + "attributes": { + "NORMAL": 13, + "POSITION": 12 + }, + "indices": 22, + "material": 3, + "mode": 4, + }, + ], + }, + { + "name": + "3-square-green", + "primitives": [ + { + "attributes": { + "NORMAL": 2, + "POSITION": 0 + }, + "indices": 3, + "material": 2, + "mode": 4, + }, # back side + { + "attributes": { + "NORMAL": 13, + "POSITION": 12 + }, + "indices": 22, + "material": 2, + "mode": 4, + }, + ], + }, + { + "name": + "4-rectangle-blue", + "primitives": [ + { + "attributes": { + "NORMAL": 2, + "POSITION": 1 + }, + "indices": 3, + "material": 0, + "mode": 4, + }, + # backside + { + "attributes": { + "NORMAL": 13, + "POSITION": 23 + }, + "indices": 22, + "material": 0, + "mode": 4, + } + ], + }, + { + "name": + "5-rectangle-red", + "primitives": [ + { + "attributes": { + "NORMAL": 2, + "POSITION": 1 + }, + "indices": 3, + "material": 1, + "mode": 4, + }, + # backside + { + "attributes": { + "NORMAL": 13, + "POSITION": 23 + }, + "indices": 22, + "material": 1, + "mode": 4, + } + ], + }, + { + "name": + "6-rectangle-gray", + "primitives": [ + { + "attributes": { + "NORMAL": 2, + "POSITION": 1 + }, + "indices": 3, + "material": 3, + "mode": 4, + }, + # backside + { + "attributes": { + "NORMAL": 13, + "POSITION": 23 + }, + "indices": 22, + "material": 3, + "mode": 4, + } + ], + }, + { + "name": + "7-rectangle-red/yellow/blue", + "primitives": [ + { + "attributes": { + "NORMAL": 7, + "POSITION": 6 + }, + "indices": 3, + "material": 1, + "mode": 4, + }, + { + "attributes": { + "NORMAL": 7, + "POSITION": 6 + }, + "indices": 8, + "material": 6, + "mode": 4, + }, + { + "attributes": { + "NORMAL": 7, + "POSITION": 6 + }, + "indices": 9, + "material": 0, + "mode": 4, + }, + # backside + { + "attributes": { + "NORMAL": 26, + "POSITION": 25 + }, + "indices": 22, + "material": 1, + "mode": 4, + }, + { + "attributes": { + "NORMAL": 26, + "POSITION": 25 + }, + "indices": 27, + "material": 6, + "mode": 4, + }, + { + "attributes": { + "NORMAL": 26, + "POSITION": 25 + }, + "indices": 28, + "material": 0, + "mode": 4, + }, + ], + }, + { + "name": + "8-rectangle-blue/yellow/red", + "primitives": [ + { + "attributes": { + "NORMAL": 7, + "POSITION": 6 + }, + "indices": 3, + "material": 0, + "mode": 4, + }, + { + "attributes": { + "NORMAL": 7, + "POSITION": 6 + }, + "indices": 8, + "material": 6, + "mode": 4, + }, + { + "attributes": { + "NORMAL": 7, + "POSITION": 6 + }, + "indices": 9, + "material": 1, + "mode": 4, + }, + # backside + { + "attributes": { + "NORMAL": 26, + "POSITION": 25 + }, + "indices": 22, + "material": 0, + "mode": 4, + }, + { + "attributes": { + "NORMAL": 26, + "POSITION": 25 + }, + "indices": 27, + "material": 6, + "mode": 4, + }, + { + "attributes": { + "NORMAL": 26, + "POSITION": 25 + }, + "indices": 28, + "material": 1, + "mode": 4, + }, + ], + }, + { + "name": + "9-square-cyan.3", + "primitives": [ + { + "attributes": { + "NORMAL": 2, + "POSITION": 0 + }, + "indices": 3, + "material": 4, + "mode": 4, + }, # back side + { + "attributes": { + "NORMAL": 13, + "POSITION": 12 + }, + "indices": 22, + "material": 4, + "mode": 4, + }, + ], + }, + { + "name": + "10-rectangle-cyan.3", + "primitives": [ + { + "attributes": { + "NORMAL": 2, + "POSITION": 1 + }, + "indices": 3, + "material": 4, + "mode": 4, + }, + # backside + { + "attributes": { + "NORMAL": 13, + "POSITION": 23 + }, + "indices": 22, + "material": 4, + "mode": 4, + } + ], + }, + { + "name": + "11-tilted-cyan.3", + "primitives": [ + { + "attributes": { + "NORMAL": 5, + "POSITION": 4 + }, + "indices": 3, + "material": 4, + "mode": 4, + }, + # backside + { + "attributes": { + "NORMAL": 30, + "POSITION": 29 + }, + "indices": 22, + "material": 4, + "mode": 4, + }, + ], + }, + { + "name": + "12-half-distance-rectangle-green", + "primitives": [ + { + "attributes": { + "NORMAL": 2, + "POSITION": 11 + }, + "indices": 3, + "material": 2, + "mode": 4, + }, + # back side + { + "attributes": { + "NORMAL": 13, + "POSITION": 24 + }, + "indices": 22, + "material": 2, + "mode": 4, + }, + ], + }, + { + "name": + "13-square-black", + "primitives": [ + { + "attributes": { + "NORMAL": 2, + "POSITION": 0 + }, + "indices": 3, + "material": 5, + "mode": 4, + }, # back side + { + "attributes": { + "NORMAL": 13, + "POSITION": 12 + }, + "indices": 22, + "material": 5, + "mode": 4, + }, + ], + }, + { + "name": + "14-half-distance-rectangle-black", + "primitives": [ + { + "attributes": { + "NORMAL": 2, + "POSITION": 11 + }, + "indices": 3, + "material": 5, + "mode": 4, + }, + # back side + { + "attributes": { + "NORMAL": 13, + "POSITION": 24 + }, + "indices": 22, + "material": 5, + "mode": 4, + }, + ], + }, + ] + + return gltf + + +def axes_gen(SEP: float, max_i: int, max_j: int, + max_k: int) -> Sequence[Mapping[str, Any]]: + rectangles = [] + + # I axis, red + rectangles += [ + { + "name": f"axisI:-K", + "mesh": 5, + "translation": [-0.5, -0.5, 0.5], + "scale": [SEP * max_i / (SEP - 1), AXESTHICKNESS, AXESTHICKNESS], + }, + { + "name": f"axisI:+K", + "mesh": 5, + "translation": [-0.5, -0.5 + AXESTHICKNESS, 0.5], + "scale": [SEP * max_i / (SEP - 1), AXESTHICKNESS, AXESTHICKNESS], + }, + { + "name": f"axisI:-J", + "mesh": 5, + "translation": [-0.5, -0.5, 0.5], + "rotation": [SQ2, 0, 0, SQ2], + "scale": [SEP * max_i / (SEP - 1), AXESTHICKNESS, AXESTHICKNESS], + }, + { + "name": f"axisI:+J", + "mesh": 5, + "translation": [-0.5, -0.5, 0.5 - AXESTHICKNESS], + "rotation": [SQ2, 0, 0, SQ2], + "scale": [SEP * max_i / (SEP - 1), AXESTHICKNESS, AXESTHICKNESS], + }, + ] + + # J axis, green + rectangles += [ + { + "name": f"axisJ:-K", + "rotation": [0, SQ2, 0, SQ2], + "translation": [-0.5 + AXESTHICKNESS, -0.5, 0.5], + "mesh": 3, + "scale": [SEP * max_j, AXESTHICKNESS, AXESTHICKNESS], + }, + { + "name": f"axisJ:+K", + "rotation": [0, SQ2, 0, SQ2], + "translation": [ + -0.5 + AXESTHICKNESS, + -0.5 + AXESTHICKNESS, + 0.5, + ], + "mesh": 3, + "scale": [SEP * max_j, AXESTHICKNESS, AXESTHICKNESS], + }, + { + "name": f"axisJ:-I", + "rotation": [0.5, 0.5, -0.5, 0.5], + "translation": [-0.5, -0.5, 0.5], + "mesh": 3, + "scale": [SEP * max_j, AXESTHICKNESS, AXESTHICKNESS], + }, + { + "name": f"axisJ:+I", + "rotation": [0.5, 0.5, -0.5, 0.5], + "translation": [-0.5 + AXESTHICKNESS, -0.5, 0.5], + "mesh": 3, + "scale": [SEP * max_j, AXESTHICKNESS, AXESTHICKNESS], + }, + ] + + # K axis, blue + rectangles += [ + { + "name": f"axisK:-I", + "mesh": 4, + "rotation": [0, 0, SQ2, SQ2], + "translation": [-0.5, -0.5 + AXESTHICKNESS, 0.5], + "scale": [SEP * max_k / (SEP - 1), AXESTHICKNESS, AXESTHICKNESS], + }, + { + "name": f"axisK:+I", + "mesh": 4, + "rotation": [0, 0, SQ2, SQ2], + "translation": [-0.5 + AXESTHICKNESS, -0.5 + AXESTHICKNESS, 0.5], + "scale": [SEP * max_k / (SEP - 1), AXESTHICKNESS, AXESTHICKNESS], + }, + { + "name": f"axisK:-J", + "mesh": 4, + "rotation": [0.5, 0.5, 0.5, 0.5], + "translation": [-0.5 + AXESTHICKNESS, -0.5 + AXESTHICKNESS, 0.5], + "scale": [SEP * max_k / (SEP - 1), AXESTHICKNESS, AXESTHICKNESS], + }, + { + "name": + f"axisK:+J", + "mesh": + 4, + "rotation": [0.5, 0.5, 0.5, 0.5], + "translation": [ + -0.5 + AXESTHICKNESS, + -0.5 + AXESTHICKNESS, + 0.5 - AXESTHICKNESS, + ], + "scale": [SEP * max_k / (SEP - 1), AXESTHICKNESS, AXESTHICKNESS], + }, + ] + + return rectangles + + +def tube_gen(SEP: float, loc: Tuple[int, int, int], dir: str, color: int, + stabilizer: int, corr: Tuple[int, int], noColor: bool, + rm_dir: str) -> Sequence[Mapping[str, Any]]: + """compute the GLTF nodes for a pipe. This can include its four faces and + correlation surface inside, minus the face to remove specified by rm_dir. + + Args: + SEP (float): the distance, e.g., from I-pipe(i,j,k) to I-pipe(i+1,j,k). + loc (Tuple[int, int, int]): 3D coordinate of the pipe. + dir (str): direction of the pipe, "I", "J", or "K". + color (int): color variable of the pipe, can be -1(unknown), 0, or 1. + stabilizer (int): index of the stabilizer. + corr (Tuple[int, int]): two bits for two possible corr surface inside. + noColor (bool): K-pipe are not colored if this is True. + rm_dir (str): the direction of face to remove. if a stabilier is shown. + + Returns: + Sequence[Mapping[str, Any]]: list of constructed GLTF nodes, typically + 4 or 5 contiguous nodes in the list corredpond to one pipe. + """ + rectangles = [] + if dir == "I": + rectangles = [ + { + "name": f"edgeI{loc}:-K", + "mesh": 4 if color else 5, + "translation": [1 + SEP * loc[0], SEP * loc[2], -SEP * loc[1]], + }, + { + "name": f"edgeI{loc}:+K", + "mesh": 4 if color else 5, + "translation": + [1 + SEP * loc[0], 1 + SEP * loc[2], -SEP * loc[1]], + }, + { + "name": f"edgeI{loc}:-J", + "mesh": 5 if color else 4, + "translation": [1 + SEP * loc[0], SEP * loc[2], -SEP * loc[1]], + "rotation": [SQ2, 0, 0, SQ2], + }, + { + "name": f"edgeI{loc}:+J", + "mesh": 5 if color else 4, + "translation": + [1 + SEP * loc[0], SEP * loc[2], -1 - SEP * loc[1]], + "rotation": [SQ2, 0, 0, SQ2], + }, + ] + if corr[0]: + rectangles.append({ + "name": + f"edgeI{loc}:CorrIJ", + "mesh": + 10, + "translation": [ + 1 + SEP * loc[0], + 0.5 + SEP * loc[2], + -SEP * loc[1], + ], + }) + if corr[1]: + rectangles.append({ + "name": + f"edgeI{loc}:CorrIK", + "mesh": + 10, + "translation": [ + 1 + SEP * loc[0], + SEP * loc[2], + -0.5 - SEP * loc[1], + ], + "rotation": [SQ2, 0, 0, SQ2], + }) + elif dir == "J": + rectangles = [ + { + "name": f"edgeJ{loc}:-K", + "rotation": [0, SQ2, 0, SQ2], + "translation": + [1 + SEP * loc[0], SEP * loc[2], -1 - SEP * loc[1]], + "mesh": 5 if color else 4, + }, + { + "name": + f"edgeJ{loc}:+K", + "rotation": [0, SQ2, 0, SQ2], + "translation": [ + 1 + SEP * loc[0], + 1 + SEP * loc[2], + -1 - SEP * loc[1], + ], + "mesh": + 5 if color else 4, + }, + { + "name": f"edgeJ{loc}:-I", + "rotation": [0.5, 0.5, -0.5, 0.5], + "translation": [SEP * loc[0], SEP * loc[2], -1 - SEP * loc[1]], + "mesh": 4 if color else 5, + }, + { + "name": f"edgeJ{loc}:+I", + "rotation": [0.5, 0.5, -0.5, 0.5], + "translation": + [1 + SEP * loc[0], SEP * loc[2], -1 - SEP * loc[1]], + "mesh": 4 if color else 5, + }, + ] + if corr[0]: + rectangles.append({ + "name": + f"edgeJ{loc}:CorrJK", + "mesh": + 10, + "rotation": [0.5, 0.5, -0.5, 0.5], + "translation": [ + 0.5 + SEP * loc[0], + SEP * loc[2], + -1 - SEP * loc[1], + ], + }) + if corr[1]: + rectangles.append({ + "name": + f"edgeJ{loc}:CorrJI", + "mesh": + 10, + "rotation": [0, SQ2, 0, SQ2], + "translation": [ + 1 + SEP * loc[0], + 0.5 + SEP * loc[2], + -1 - SEP * loc[1], + ], + }) + + elif dir == "K": + colorKM = color // 7 + colorKP = color % 7 + rectangles = [ + { + "name": f"edgeJ{loc}:-I", + "mesh": 6, + "rotation": [0, 0, SQ2, SQ2], + "translation": [SEP * loc[0], 1 + SEP * loc[2], -SEP * loc[1]], + }, + { + "name": f"edgeJ{loc}:+I", + "mesh": 6, + "rotation": [0, 0, SQ2, SQ2], + "translation": + [1 + SEP * loc[0], 1 + SEP * loc[2], -SEP * loc[1]], + }, + { + "name": f"edgeK{loc}:-J", + "mesh": 6, + "rotation": [0.5, 0.5, 0.5, 0.5], + "translation": + [1 + SEP * loc[0], 1 + SEP * loc[2], -SEP * loc[1]], + }, + { + "name": + f"edgeJ{loc}:+J", + "mesh": + 6, + "rotation": [0.5, 0.5, 0.5, 0.5], + "translation": [ + 1 + SEP * loc[0], + 1 + SEP * loc[2], + -1 - SEP * loc[1], + ], + }, + ] + if not noColor: + if colorKM == 0 and colorKP == 0: + rectangles[0]["mesh"] = 4 + rectangles[1]["mesh"] = 4 + rectangles[2]["mesh"] = 5 + rectangles[3]["mesh"] = 5 + if colorKM == 1 and colorKP == 1: + rectangles[0]["mesh"] = 5 + rectangles[1]["mesh"] = 5 + rectangles[2]["mesh"] = 4 + rectangles[3]["mesh"] = 4 + if colorKM == 1 and colorKP == 0: + rectangles[0]["mesh"] = 7 + rectangles[1]["mesh"] = 7 + rectangles[2]["mesh"] = 8 + rectangles[3]["mesh"] = 8 + if colorKM == 0 and colorKP == 1: + rectangles[0]["mesh"] = 8 + rectangles[1]["mesh"] = 8 + rectangles[2]["mesh"] = 7 + rectangles[3]["mesh"] = 7 + + if corr[0]: + rectangles.append({ + "name": + f"edgeK{loc}:CorrKI", + "mesh": + 10, + "rotation": [0.5, 0.5, 0.5, 0.5], + "translation": [ + 1 + SEP * loc[0], + 1 + SEP * loc[2], + -0.5 - SEP * loc[1], + ], + }) + if corr[1]: + rectangles.append({ + "name": + f"edgeK{loc}:CorrKJ", + "mesh": + 10, + "rotation": [0, 0, SQ2, SQ2], + "translation": [ + 0.5 + SEP * loc[0], + 1 + SEP * loc[2], + -SEP * loc[1], + ], + }) + + rectangles = [rect for rect in rectangles if rm_dir not in rect["name"]] + if stabilizer == -1: + rectangles = [ + rect for rect in rectangles if "Corr" not in rect["name"] + ] + return rectangles + + +def cube_gen( + SEP: float, + loc: Tuple[int, int, int], + exists: Mapping[str, int], + colors: Mapping[str, int], + stabilizer: int, + corr: Mapping[str, Tuple[int, int]], + noColor: bool, + rm_dir: str, +) -> Sequence[Mapping[str, Any]]: + """compute the GLTF nodes for a cube. This can include its faces and + correlation surface inside, minus the face to remove specified by rm_dir. + + Args: + SEP (float): the distance, e.g., from cube(i,j,k) to cube(i+1,j,k). + loc (Tuple[int, int, int]): 3D coordinate of the pipe. + exists (Mapping[str, int]): whether there is a pipe in the 6 directions + to this cube. (+|-)(I|J|K). + colors (Mapping[str, int]): color variable of the pipe, can be + -1(unknown), 0, or 1. + stabilizer (int): index of the stabilizer. + corr (Mapping[str, Tuple[int, int]]): two bits for two possible + correlation surface inside a pipe. These info for all 6 pipes. + noColor (bool): K-pipe are not colored if this is True. + rm_dir (str): the direction of face to remove. if a stabilier is shown. + + Returns: + Sequence[Mapping[str, Any]]: list of constructed GLTF nodes. + """ + squares = [] + for face in ["-K", "+K"]: + if exists[face] == 0: + squares.append({ + "name": + f"spider{loc}:{face}", + "mesh": + 2, + "translation": [ + SEP * loc[0], + (1 if face == "+K" else 0) + SEP * loc[2], + -SEP * loc[1], + ], + }) + for dir in ["+I", "-I", "+J", "-J"]: + if exists[dir]: + if dir == "+I" or dir == "-I": + if colors[dir] == 1: + squares[-1]["mesh"] = 0 + else: + squares[-1]["mesh"] = 1 + else: + if colors[dir] == 0: + squares[-1]["mesh"] = 0 + else: + squares[-1]["mesh"] = 1 + break + for face in ["-I", "+I"]: + if exists[face] == 0: + squares.append({ + "name": + f"spider{loc}:{face}", + "mesh": + 2, + "translation": [ + (1 if face == "+I" else 0) + SEP * loc[0], + SEP * loc[2], + -SEP * loc[1], + ], + "rotation": [0, 0, SQ2, SQ2], + }) + for dir in ["+J", "-J", "+K", "-K"]: + if exists[dir]: + if dir == "+J" or dir == "-J": + if colors[dir] == 1: + squares[-1]["mesh"] = 0 + else: + squares[-1]["mesh"] = 1 + elif not noColor: + if colors[dir] == 1: + squares[-1]["mesh"] = 1 + elif colors[dir] == 0: + squares[-1]["mesh"] = 0 + for face in ["-J", "+J"]: + if exists[face] == 0: + squares.append({ + "name": + f"spider{loc}:{face}", + "mesh": + 2, + "translation": [ + 1 + SEP * loc[0], + SEP * loc[2], + (-1 if face == "+J" else 0) - SEP * loc[1], + ], + "rotation": [0.5, 0.5, 0.5, 0.5], + }) + for dir in ["+I", "-I", "+K", "-K"]: + if exists[dir]: + if dir == "+I" or dir == "-I": + if colors[dir] == 0: + squares[-1]["mesh"] = 0 + else: + squares[-1]["mesh"] = 1 + elif not noColor: + if colors[dir] == 1: + squares[-1]["mesh"] = 0 + elif colors[dir] == 0: + squares[-1]["mesh"] = 1 + + degree = sum([v for (k, v) in exists.items()]) + normal = {"I": 0, "J": 0, "K": 0} + if exists["-I"] == 0 and exists["+I"] == 0: + normal["I"] = 1 + if exists["-J"] == 0 and exists["+J"] == 0: + normal["J"] = 1 + if exists["-K"] == 0 and exists["+K"] == 0: + normal["K"] = 1 + if degree > 1: + if (exists["-I"] and exists["+I"] and exists["-J"] == 0 + and exists["+J"] == 0 and exists["-K"] == 0 + and exists["+K"] == 0): + if corr["-I"][0]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 9, + "translation": [ + SEP * loc[0], + 0.5 + SEP * loc[2], + -SEP * loc[1], + ], + }) + if corr["-I"][1]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 9, + "translation": [ + 1 + SEP * loc[0], + SEP * loc[2], + -0.5 - SEP * loc[1], + ], + "rotation": [0.5, 0.5, 0.5, 0.5], + }) + elif (exists["-I"] == 0 and exists["+I"] == 0 and exists["-J"] + and exists["+J"] and exists["-K"] == 0 and exists["+K"] == 0): + if corr["-J"][0]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 9, + "translation": [ + 0.5 + SEP * loc[0], + SEP * loc[2], + -SEP * loc[1], + ], + "rotation": [0, 0, SQ2, SQ2], + }) + if corr["-J"][1]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 9, + "translation": [ + SEP * loc[0], + 0.5 + SEP * loc[2], + -SEP * loc[1], + ], + }) + elif (exists["-I"] == 0 and exists["+I"] == 0 and exists["-J"] == 0 + and exists["+J"] == 0 and exists["-K"] and exists["+K"]): + if corr["-K"][0]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 9, + "translation": [ + 1 + SEP * loc[0], + SEP * loc[2], + -0.5 - SEP * loc[1], + ], + "rotation": [0.5, 0.5, 0.5, 0.5], + }) + if corr["-K"][1]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 9, + "translation": [ + 0.5 + SEP * loc[0], + SEP * loc[2], + -SEP * loc[1], + ], + "rotation": [0, 0, SQ2, SQ2], + }) + else: + if normal["I"]: + if corr["-J"][0] or corr["+J"][0] or corr["-K"][1] or corr[ + "+K"][1]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 9, + "translation": [ + 0.5 + SEP * loc[0], + SEP * loc[2], + -SEP * loc[1], + ], + "rotation": [0, 0, SQ2, SQ2], + }) + + if corr["-J"][1] and corr["+J"][1] and corr["-K"][0] and corr[ + "+K"][0]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 11, + "translation": [ + SEP * loc[0], + SEP * loc[2], + -1 - SEP * loc[1], + ], + "rotation": [0, -SQ2, 0, SQ2], + }) + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 11, + "translation": [ + SEP * loc[0], + 0.5 + SEP * loc[2], + -0.5 - SEP * loc[1], + ], + "rotation": [0, -SQ2, 0, SQ2], + }) + elif corr["-J"][1] and corr["+J"][1]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 9, + "translation": [ + SEP * loc[0], + 0.5 + SEP * loc[2], + -SEP * loc[1], + ], + }) + elif corr["-K"][0] and corr["+K"][0]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 9, + "translation": [ + 1 + SEP * loc[0], + SEP * loc[2], + -0.5 - SEP * loc[1], + ], + "rotation": [0.5, 0.5, 0.5, 0.5], + }) + elif corr["-J"][1] and corr["-K"][0]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 11, + "translation": [ + 1 + SEP * loc[0], + SEP * loc[2], + -SEP * loc[1], + ], + "rotation": [0, SQ2, 0, SQ2], + }) + elif corr["+J"][1] and corr["+K"][0]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 11, + "translation": [ + 1 + SEP * loc[0], + 0.5 + SEP * loc[2], + -0.5 - SEP * loc[1], + ], + "rotation": [0, SQ2, 0, SQ2], + }) + elif corr["+J"][1] and corr["-K"][0]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 11, + "translation": [ + SEP * loc[0], + SEP * loc[2], + -1 - SEP * loc[1], + ], + "rotation": [0, -SQ2, 0, SQ2], + }) + elif corr["-J"][1] and corr["+K"][0]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 11, + "translation": [ + SEP * loc[0], + 0.5 + SEP * loc[2], + -0.5 - SEP * loc[1], + ], + "rotation": [0, -SQ2, 0, SQ2], + }) + elif normal["J"]: + if corr["-K"][0] or corr["+K"][0] or corr["-I"][1] or corr[ + "+I"][1]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 9, + "translation": [ + 1 + SEP * loc[0], + SEP * loc[2], + -0.5 - SEP * loc[1], + ], + "rotation": [0.5, 0.5, 0.5, 0.5], + }) + + if corr["-K"][1] and corr["+K"][1] and corr["-I"][0] and corr[ + "+I"][0]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 11, + "translation": [ + 0.5 + SEP * loc[0], + 0.5 + SEP * loc[2], + -SEP * loc[1], + ], + "rotation": [0, 0, SQ2, SQ2], + }) + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 11, + "translation": [ + 1 + SEP * loc[0], + SEP * loc[2], + -SEP * loc[1], + ], + "rotation": [0, 0, SQ2, SQ2], + }) + elif corr["-K"][1] and corr["+K"][1]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 9, + "translation": [ + 0.5 + SEP * loc[0], + SEP * loc[2], + -SEP * loc[1], + ], + "rotation": [0, 0, SQ2, SQ2], + }) + elif corr["-I"][0] and corr["+I"][0]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 9, + "translation": [ + SEP * loc[0], + 0.5 + SEP * loc[2], + -SEP * loc[1], + ], + }) + elif corr["-K"][1] and corr["-I"][0]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 11, + "translation": + [SEP * loc[0], SEP * loc[2], -SEP * loc[1]], + }) + elif corr["+K"][1] and corr["+I"][0]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 11, + "translation": [ + 0.5 + SEP * loc[0], + 0.5 + SEP * loc[2], + -SEP * loc[1], + ], + }) + elif corr["+K"][1] and corr["-I"][0]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 11, + "translation": [ + 0.5 + SEP * loc[0], + 0.5 + SEP * loc[2], + -SEP * loc[1], + ], + "rotation": [0, 0, SQ2, SQ2], + }) + elif corr["-K"][1] and corr["+I"][0]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 11, + "translation": [ + 1 + SEP * loc[0], + SEP * loc[2], + -SEP * loc[1], + ], + "rotation": [0, 0, SQ2, SQ2], + }) + else: + if corr["-I"][0] or corr["+I"][0] or corr["-J"][1] or corr[ + "+J"][1]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 9, + "translation": [ + SEP * loc[0], + 0.5 + SEP * loc[2], + -SEP * loc[1], + ], + }) + + if corr["-I"][1] and corr["+I"][1] and corr["-J"][0] and corr[ + "+J"][0]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 11, + "translation": [ + 0.5 + SEP * loc[0], + SEP * loc[2], + -0.5 - SEP * loc[1], + ], + "rotation": [SQ2, 0, 0, SQ2], + }) + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 11, + "translation": [ + SEP * loc[0], + SEP * loc[2], + -1 - SEP * loc[1], + ], + "rotation": [SQ2, 0, 0, SQ2], + }) + elif corr["-I"][1] and corr["+I"][1]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 9, + "translation": [ + 1 + SEP * loc[0], + SEP * loc[2], + -0.5 - SEP * loc[1], + ], + "rotation": [0.5, 0.5, 0.5, 0.5], + }) + elif corr["-J"][0] and corr["+J"][0]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 9, + "translation": [ + 0.5 + SEP * loc[0], + SEP * loc[2], + -SEP * loc[1], + ], + "rotation": [0, 0, SQ2, SQ2], + }) + elif corr["-I"][1] and corr["-J"][0]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 11, + "translation": [ + SEP * loc[0], + 1 + SEP * loc[2], + -SEP * loc[1], + ], + "rotation": [-SQ2, 0, 0, SQ2], + }) + elif corr["+I"][1] and corr["+J"][0]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 11, + "translation": [ + 0.5 + SEP * loc[0], + 1 + SEP * loc[2], + -0.5 - SEP * loc[1], + ], + "rotation": [-SQ2, 0, 0, SQ2], + }) + elif corr["+I"][1] and corr["-J"][0]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 11, + "translation": [ + 0.5 + SEP * loc[0], + SEP * loc[2], + -0.5 - SEP * loc[1], + ], + "rotation": [SQ2, 0, 0, SQ2], + }) + elif corr["-I"][1] and corr["+J"][0]: + squares.append({ + "name": + f"spider{loc}:Corr", + "mesh": + 11, + "translation": [ + SEP * loc[0], + SEP * loc[2], + -1 - SEP * loc[1], + ], + "rotation": [SQ2, 0, 0, SQ2], + }) + + squares = [sqar for sqar in squares if rm_dir not in sqar["name"]] + if stabilizer == -1: + squares = [sqar for sqar in squares if "Corr" not in sqar["name"]] + return squares + + +def special_gen( + SEP: float, + loc: Tuple[int, int, int], + exists: Mapping[str, int], + type: str, + stabilizer: int, + rm_dir: str, +) -> Sequence[Mapping[str, Any]]: + """compute the GLTF nodes for special cubes. Currently Ycube and Tinjection + + Args: + SEP (float): the distance, e.g., from cube(i,j,k) to cube(i+1,j,k). + loc (Tuple[int, int, int]): 3D coordinate of the pipe. + exists (Mapping[str, int]): whether there is a pipe in the 6 directions + to this cube. (+|-)(I|J|K). + stabilizer (int): index of the stabilizer. + noColor (bool): K-pipe are not colored if this is True. + rm_dir (str): the direction of face to remove. if a stabilier is shown. + + Returns: + Sequence[Mapping[str, Any]]: list of constructed GLTF nodes. + """ + if type == "Y": + square_mesh = 3 + half_dist_mesh = 12 + elif type == "T": + square_mesh = 13 + half_dist_mesh = 14 + else: + square_mesh = -1 + half_dist_mesh = -1 + + shapes = [] + if exists["+K"]: + # need connect to top + shapes.append({ + "name": + f"spider{loc}:top:-K", + "mesh": + square_mesh, + "translation": [ + SEP * loc[0], + 0.55 + SEP * loc[2], + -SEP * loc[1], + ], + }) + shapes.append({ + "name": + f"spider{loc}:top:-I", + "mesh": + half_dist_mesh, + "rotation": [0, 0, SQ2, SQ2], + "translation": [SEP * loc[0], 0.55 + SEP * loc[2], -SEP * loc[1]], + }) + shapes.append({ + "name": + f"spider{loc}:top:+I", + "mesh": + half_dist_mesh, + "rotation": [0, 0, SQ2, SQ2], + "translation": + [1 + SEP * loc[0], 0.55 + SEP * loc[2], -SEP * loc[1]], + }) + shapes.append({ + "name": + f"spider{loc}:top:-J", + "mesh": + half_dist_mesh, + "rotation": [0.5, 0.5, 0.5, 0.5], + "translation": + [1 + SEP * loc[0], 0.55 + SEP * loc[2], -SEP * loc[1]], + }) + shapes.append({ + "name": + f"spider{loc}:top:+J", + "mesh": + half_dist_mesh, + "rotation": [0.5, 0.5, 0.5, 0.5], + "translation": [ + 1 + SEP * loc[0], + 0.55 + SEP * loc[2], + -SEP * loc[1] - 1, + ], + }) + + if exists["-K"]: + # need connect to bottom + shapes.append({ + "name": + f"spider{loc}:bottom:+K", + "mesh": + square_mesh, + "translation": [ + SEP * loc[0], + 0.45 + SEP * loc[2], + -SEP * loc[1], + ], + }) + shapes.append({ + "name": + f"spider{loc}:bottom:-I", + "mesh": + half_dist_mesh, + "rotation": [0, 0, SQ2, SQ2], + "translation": [SEP * loc[0], SEP * loc[2], -SEP * loc[1]], + }) + shapes.append({ + "name": + f"spider{loc}:bottom:+I", + "mesh": + half_dist_mesh, + "rotation": [0, 0, SQ2, SQ2], + "translation": [1 + SEP * loc[0], SEP * loc[2], -SEP * loc[1]], + }) + shapes.append({ + "name": + f"spider{loc}:bottom:-J", + "mesh": + half_dist_mesh, + "rotation": [0.5, 0.5, 0.5, 0.5], + "translation": [1 + SEP * loc[0], SEP * loc[2], -SEP * loc[1]], + }) + shapes.append({ + "name": + f"spider{loc}:bottom:+J", + "mesh": + half_dist_mesh, + "rotation": [0.5, 0.5, 0.5, 0.5], + "translation": [ + 1 + SEP * loc[0], + SEP * loc[2], + -SEP * loc[1] - 1, + ], + }) + + shapes = [shp for shp in shapes if rm_dir not in shp["name"]] + return shapes + + +def gltf_generator(lasre: Mapping[str, Any], + stabilizer: int = -1, + tube_len: float = 2.0, + no_color_z: bool = False, + attach_axes: bool = False, + rm_dir: Optional[str] = None) -> Mapping[str, Any]: + """generate gltf in a dict and write to a json file with extension .gltf + + Args: + lasre (Mapping[str, Any]): LaSRe of the LaS. + stabilizer (int, optional): index of the stabilizer. The correlation + surfaces corresponding to it will be drawn. Defaults to -1, which + means do not draw any correlation surfaces. + tube_len (float, optional): ratio of the length of the pipes compared + to the length of the cubes. Defaults to 2.0. + no_color_z (bool, optional): do not color the Z-pipes. Defaults to + False, which means by default Z-pipes are colored. + attach_axes (bool, optional): attach an IJK axes. Defaults to False. + rm_dir (str, optional): the direction of faces to remove to reveal + the correlation surfaces. Defaults to None. + + Raises: + ValueError: rm_dir is not any one of :(+|-)(I|J|K) + ValueError: the index of stabilizer is not -1 nor in [0, n_stabilizer) + + Returns: + Mapping[str, Any]: the constructed gltf in a dict. + """ + s, tubelen, noColor = ( + stabilizer, + tube_len, + no_color_z, + ) + if rm_dir is None: + rm_dir = ":II" + elif rm_dir not in [":+I", ":-I", ":+J", ":-J", ":+K", ":-K"]: + raise ValueError("rm_dir is not one of :+I, :-I, :+J, :-J, :+K, :-K") + + gltf = base_gen(tubelen) + + i_bound = lasre["n_i"] + j_bound = lasre["n_j"] + k_bound = lasre["n_k"] + NodeY = lasre["NodeY"] + ExistI = lasre["ExistI"] + ColorI = lasre["ColorI"] + ExistJ = lasre["ExistJ"] + ColorJ = lasre["ColorJ"] + ExistK = lasre["ExistK"] + if "CorrIJ" in lasre: + CorrIJ = lasre["CorrIJ"] + CorrIK = lasre["CorrIK"] + CorrJI = lasre["CorrJI"] + CorrJK = lasre["CorrJK"] + CorrKI = lasre["CorrKI"] + CorrKJ = lasre["CorrKJ"] + s_bound = len(CorrIJ) + if "ColorKP" not in lasre: + ColorKP = [[[-1 for _ in range(k_bound)] for _ in range(j_bound)] + for _ in range(i_bound)] + else: + ColorKP = lasre["ColorKP"] + if "ColorKM" not in lasre: + ColorKM = [[[-1 for _ in range(k_bound)] for _ in range(j_bound)] + for _ in range(i_bound)] + else: + ColorKM = lasre["ColorKM"] + port_cubes = lasre["port_cubes"] + t_injections = (lasre["optional"]["t_injections"] + if "t_injections" in lasre["optional"] else []) + + if s < -1 or (s_bound > 0 and s not in range(-1, s_bound)): + raise ValueError(f"No such stabilizer index {s}.") + + for i in range(i_bound): + for j in range(j_bound): + for k in range(k_bound): + if ExistI[i][j][k]: + gltf["nodes"] += tube_gen( + tubelen + 1.0, + (i, j, k), + "I", + ColorI[i][j][k], + s, + (CorrIJ[s][i][j][k], + CorrIK[s][i][j][k]) if s_bound else (0, 0), + noColor, + rm_dir, + ) + if ExistJ[i][j][k]: + gltf["nodes"] += tube_gen( + tubelen + 1.0, + (i, j, k), + "J", + ColorJ[i][j][k], + s, + (CorrJK[s][i][j][k], + CorrJI[s][i][j][k]) if s_bound else (0, 0), + noColor, + rm_dir, + ) + if ExistK[i][j][k]: + gltf["nodes"] += tube_gen( + tubelen + 1.0, + (i, j, k), + "K", + 7 * ColorKM[i][j][k] + ColorKP[i][j][k], + s, + (CorrKI[s][i][j][k], + CorrKJ[s][i][j][k]) if s_bound else (0, 0), + noColor, + rm_dir, + ) + + for i in range(i_bound): + for j in range(j_bound): + for k in range(k_bound): + exists = {"-I": 0, "+I": 0, "-K": 0, "+K": 0, "-J": 0, "+J": 0} + colors = {} + corr = { + "-I": (0, 0), + "+I": (0, 0), + "-J": (0, 0), + "+J": (0, 0), + "-K": (0, 0), + "+K": (0, 0), + } + if i > 0 and ExistI[i - 1][j][k]: + exists["-I"] = 1 + colors["-I"] = ColorI[i - 1][j][k] + corr["-I"] = (CorrIJ[s][i - 1][j][k], + CorrIK[s][i - 1][j][k]) if s_bound else (0, + 0) + if ExistI[i][j][k]: + exists["+I"] = 1 + colors["+I"] = ColorI[i][j][k] + corr["+I"] = (CorrIJ[s][i][j][k], + CorrIK[s][i][j][k]) if s_bound else (0, 0) + if j > 0 and ExistJ[i][j - 1][k]: + exists["-J"] = 1 + colors["-J"] = ColorJ[i][j - 1][k] + corr["-J"] = (CorrJK[s][i][j - 1][k], + CorrJI[s][i][j - 1][k]) if s_bound else (0, + 0) + if ExistJ[i][j][k]: + exists["+J"] = 1 + colors["+J"] = ColorJ[i][j][k] + corr["+J"] = (CorrJK[s][i][j][k], + CorrJI[s][i][j][k]) if s_bound else (0, 0) + if k > 0 and ExistK[i][j][k - 1]: + exists["-K"] = 1 + colors["-K"] = ColorKP[i][j][k - 1] + corr["-K"] = (CorrKI[s][i][j][k - 1], + CorrKJ[s][i][j][k - 1]) if s_bound else (0, + 0) + if ExistK[i][j][k]: + exists["+K"] = 1 + colors["+K"] = ColorKM[i][j][k] + corr["+K"] = (CorrKI[s][i][j][k], + CorrKJ[s][i][j][k]) if s_bound else (0, 0) + if sum([v for (k, v) in exists.items()]) > 0: + if (i, j, k) not in port_cubes: + if NodeY[i][j][k]: + gltf["nodes"] += special_gen( + tubelen + 1.0, + (i, j, k), + exists, + "Y", + s, + rm_dir, + ) + else: + gltf["nodes"] += cube_gen( + tubelen + 1.0, + (i, j, k), + exists, + colors, + s, + corr, + noColor, + rm_dir, + ) + elif [i, j, k] in t_injections: + gltf["nodes"] += special_gen( + tubelen + 1.0, + (i, j, k), + exists, + "T", + s, + rm_dir, + ) + + if attach_axes: + gltf["nodes"] += axes_gen(tube_len + 1.0, i_bound, j_bound, k_bound) + + gltf["nodes"][0]["children"] = list(range(1, len(gltf["nodes"]))) + + return gltf diff --git a/glue/lattice_surgery/lassynth/translators/networkx_generator.py b/glue/lattice_surgery/lassynth/translators/networkx_generator.py new file mode 100644 index 000000000..4003f75c4 --- /dev/null +++ b/glue/lattice_surgery/lassynth/translators/networkx_generator.py @@ -0,0 +1,53 @@ +"""generate a annotated networkx.Graph corresponding to the LaS.""" + +import networkx +from lassynth.translators import ZXGridGraph +import stimzx +from typing import Mapping, Any + + +def networkx_generator(lasre: Mapping[str, Any]) -> networkx.Graph: + n_i, n_j, n_k = lasre["n_i"], lasre["n_j"], lasre["n_k"] + port_cubes = lasre["port_cubes"] + zxgridgraph = ZXGridGraph(lasre) + edges = zxgridgraph.edges + nodes = zxgridgraph.nodes + + zx_nx_graph = networkx.Graph() + type_to_str = {"X": "X", "Z": "Z", "Pi": "in", "Po": "out", "I": "X"} + cnt = 0 + for (i, j, k) in port_cubes: + node = nodes[i][j][k] + zx_nx_graph.add_node(cnt, value=stimzx.ZxType(type_to_str[node.type])) + node.node_id = cnt + cnt += 1 + + for i in range(n_i + 1): + for j in range(n_j + 1): + for k in range(n_k + 1): + node = nodes[i][j][k] + if node.type not in ["N", "Po", "Pi"]: + zx_nx_graph.add_node(cnt, + value=stimzx.ZxType( + type_to_str[node.type])) + node.node_id = cnt + cnt += 1 + if node.y_tail_minus: + zx_nx_graph.add_node(cnt, value=stimzx.ZxType("Z", 1)) + zx_nx_graph.add_edge(node.node_id, cnt) + cnt += 1 + if node.y_tail_plus: + zx_nx_graph.add_node(cnt, value=stimzx.ZxType("Z", 3)) + zx_nx_graph.add_edge(node.node_id, cnt) + cnt += 1 + + for edge in edges: + if edge.type != "h": + zx_nx_graph.add_edge(edge.node0.node_id, edge.node1.node_id) + else: + zx_nx_graph.add_node(cnt, value=stimzx.ZxType("H")) + zx_nx_graph.add_edge(cnt, edge.node0.node_id) + zx_nx_graph.add_edge(cnt, edge.node1.node_id) + cnt += 1 + + return zx_nx_graph diff --git a/glue/lattice_surgery/lassynth/translators/textfig_generator.py b/glue/lattice_surgery/lassynth/translators/textfig_generator.py new file mode 100644 index 000000000..1024b46f2 --- /dev/null +++ b/glue/lattice_surgery/lassynth/translators/textfig_generator.py @@ -0,0 +1,217 @@ +"""Generate text figures of 2D time slices of the LaS.""" + +from lassynth.translators import ZXGridGraph + + +class TextLayer: + pad_i = 1 + pad_j = 1 + sep_i = 4 + sep_j = 4 + + def __init__(self, zx_graph: ZXGridGraph, k: int, if_middle: bool) -> None: + self.n_i, self.n_j, self.n_k = ( + zx_graph.n_i, + zx_graph.n_j, + zx_graph.n_k, + ) + self.chars = [[ + " " for _ in range(2 * TextLayer.pad_i + + (self.n_i - 1) * TextLayer.sep_i + 1) + ] + ["\n"] for _ in range(2 * TextLayer.pad_j + + (self.n_j - 1) * TextLayer.sep_j + 1)] + if if_middle: + self.compute_middle(zx_graph, k) + else: + self.compute_normal(zx_graph, k) + + def set_char(self, j: int, i: int, character): + self.chars[j][i] = character + + def compute_normal(self, zx_graph: ZXGridGraph, k: int): + """a normal layer corresponds to a layer of cubes in LaS, e.g., + / / + X X + | / + | + |/ + Z . + / + There are 2x2 tiles of surface codes. The bottom right one is not being + used, represented by a `.`; the top right one is identity in because + it has degree 2, but our convention is that these spiders have type `X` + The top left one is like that, too. The bottom left is a Z-spider with + three edges, which is non trivial. `-` and `|` I-pipes and J-pipes. + `/` are K-pipes. The `/` on the bottom left corner of a spider connects + to the previous moment. The `/` on the top right corner of a spider + connects to the next moment. + + Args: + zx_graph (ZXGridGraph): + k (int): the height of this layer. + """ + for i in range(self.n_i): + for j in range(self.n_j): + spider = zx_graph.nodes[i][j][k] + + if spider.type in ["N", "Pi", "Po"]: + self.set_char( + TextLayer.pad_j + j * TextLayer.sep_j, + TextLayer.pad_i + i * TextLayer.sep_i, + ".", + ) + continue + elif spider.type == "I": + self.set_char( + TextLayer.pad_j + j * TextLayer.sep_j, + TextLayer.pad_i + i * TextLayer.sep_i, + "X", + ) + else: + self.set_char( + TextLayer.pad_j + j * TextLayer.sep_j, + TextLayer.pad_i + i * TextLayer.sep_i, + spider.type, + ) + + # I pipes + if spider.exists["+I"]: + for offset in range(1, TextLayer.sep_i): + self.set_char( + TextLayer.pad_j + j * TextLayer.sep_j, + TextLayer.pad_i + i * TextLayer.sep_i + offset, + "-", + ) + + # J pipes + if spider.exists["+J"]: + for offset in range(1, TextLayer.sep_i): + self.set_char( + TextLayer.pad_j + j * TextLayer.sep_j + offset, + TextLayer.pad_i + i * TextLayer.sep_i, + "|", + ) + + # K pipes + if spider.exists["+K"]: + self.set_char( + TextLayer.pad_j + j * TextLayer.sep_j - 1, + TextLayer.pad_i + i * TextLayer.sep_i + 1, + "/", + ) + if spider.exists["-K"]: + self.set_char( + TextLayer.pad_j + j * TextLayer.sep_j + 1, + TextLayer.pad_i + i * TextLayer.sep_i - 1, + "/", + ) + + def compute_middle(self, zx_graph: ZXGridGraph, k: int): + """a middle layer is either a Hadmard edge or a normal edge, e.g., + / + . X + / + + / + H . + / + These layers cannot have `-` or `|`. It only has `/` which are K-pipes. + The node is either `H` meaning the edge is a Hadamard edge, or `X` + meaning the edge is a normal edge. We use `X` for identity here. + + Args: + zx_graph (ZXGridGraph): + k (int): the height of this layer. There is a middle layer after a + normal layer. + """ + for i in range(self.n_i): + for j in range(self.n_j): + self.set_char( + TextLayer.pad_j + j * TextLayer.sep_j, + TextLayer.pad_i + i * TextLayer.sep_i, + ".", + ) + spider = zx_graph.nodes[i][j][k] + color_sum = -1 + if k == self.n_k - 1: + try: + for port in zx_graph.lasre["ports"]: + if (port["i"], port["j"], port["k"]) == (i, j, k): + color_sum = port["c"] + spider.colors["+K"] + break + except ValueError: + print( + f"KPipe({i},{j},{k}) connect outside but not port." + ) + else: + upper_spider = zx_graph.nodes[i][j][k + 1] + if spider.exists["+K"] == 1 and upper_spider.exists[ + "-K"] == 1: + color_sum = spider.colors["+K"] + upper_spider.colors[ + "-K"] + if spider.exists["+K"] == 0 and upper_spider.exists[ + "-K"] == 1: + try: + for port in zx_graph.lasre["ports"]: + if (port["i"], port["j"], port["k"]) == (i, j, + k): + color_sum = port[ + "c"] + upper_spider.colors["-K"] + break + except ValueError: + print(f"KPipe({i},{j},{k})- should be a port.") + if spider.exists["+K"] == 1 and upper_spider.exists[ + "-K"] == 0: + try: + for port in zx_graph.lasre["ports"]: + if (port["i"], port["j"], + port["k"]) == (i, j, k + 1): + color_sum = port["c"] + spider.colors["+K"] + break + except ValueError: + print(f"KPipe({i},{j},{k + 1})- should be a port") + + if color_sum != -1: + self.set_char( + TextLayer.pad_j + j * TextLayer.sep_j - 1, + TextLayer.pad_i + i * TextLayer.sep_i + 1, + "/", + ) + self.set_char( + TextLayer.pad_j + j * TextLayer.sep_j + 1, + TextLayer.pad_i + i * TextLayer.sep_i - 1, + "/", + ) + if color_sum == 1: + self.set_char( + TextLayer.pad_j + j * TextLayer.sep_j, + TextLayer.pad_i + i * TextLayer.sep_i, + "H", + ) + else: + self.set_char( + TextLayer.pad_j + j * TextLayer.sep_j, + TextLayer.pad_i + i * TextLayer.sep_i, + "X", + ) + + def get_text(self): + text = "" + for j in range(2 * TextLayer.pad_j + (self.n_j - 1) * TextLayer.sep_j + + 1): + for i in range(2 * TextLayer.pad_i + + (self.n_i - 1) * TextLayer.sep_i + 1): + text += self.chars[j][i] + text += "\n" + return text + + +def textfig_generator(lasre: dict): + text = "======================================\n" + zx_graph = ZXGridGraph(lasre) + for k in range(lasre["n_k"] - 1, -1, -1): + text += TextLayer(zx_graph, k, True).get_text() + text += "======================================\n" + text += TextLayer(zx_graph, k, False).get_text() + text += "======================================\n" + return text diff --git a/glue/lattice_surgery/lassynth/translators/zx_grid_graph.py b/glue/lattice_surgery/lassynth/translators/zx_grid_graph.py new file mode 100644 index 000000000..4586e6df5 --- /dev/null +++ b/glue/lattice_surgery/lassynth/translators/zx_grid_graph.py @@ -0,0 +1,292 @@ +"""Classes ZXGridEdge, ZXGridSpider, and ZXGridGraph. ZXGridGraph is a graph +where nodes are the cubes in LaS and edges are pipes in LaS. +""" + +from typing import Any, Mapping, Sequence, Tuple, Optional + + +class ZXGridNode: + + def __init__(self, coord3: Tuple[int, int, int], + connectivity: Mapping[str, Mapping[str, int]]) -> None: + """initialize ZXGridNode for a cube in the LaS. + + self.type: type of ZX spider, 'N': no spider, 'X'/'Z': X/Z-spider, + 'S': Y cube, 'I': identity, 'Pi': input port, 'Po': output port. + self.i/j/k: 3D corrdinates of the cube in the LaS. + self.exists is a dictionary with six keys corresponding to whether a + pipe exist in the six directions to a cube in the LaS. + self.colors are the colors of these possible pipes. + self.y_tail_plus: if this node connects a Y on the top. + self.y_tail_minus: if this node connects a Y on the bottom. + + Args: + coord3 (Tuple[int, int, int]): 3D coordinate of the cube. + connectivity (Mapping[str, Mapping[str, int]]): contains exists + and colors of the six possible pipes to a cube + """ + self.i, self.j, self.k = coord3 + self.y_tail_plus = False + self.y_tail_minus = False + self.node_id = -1 + self.exists = connectivity["exists"] + self.colors = connectivity["colors"] + self.compute_type() + + def compute_type(self) -> None: + """decide the type of a ZXGridNoe + + Raises: + ValueError: node has degree=1, which should be forbidden earlier. + ValueError: node has degree>4, which should be forbidden earlier. + """ + deg = sum([v for (k, v) in self.exists.items()]) + if deg == 0: + self.type = "N" + return + elif deg == 1: + raise ValueError("There should not be deg-1 Z or X spiders.") + elif deg == 2: + self.type = "I" + elif deg >= 5: + raise ValueError("deg > 4: 3D corner exists") + else: # degree = 3 or 4 + if self.exists["-I"] == 0 and self.exists["+I"] == 0: + if self.exists["-J"]: + if self.colors["-J"] == 0: + self.type = "X" + else: + self.type = "Z" + else: # must exist +J + if self.colors["+J"] == 0: + self.type = "X" + else: + self.type = "Z" + + if self.exists["-J"] == 0 and self.exists["+J"] == 0: + if self.exists["-I"]: + if self.colors["-I"] == 0: + self.type = "Z" + else: + self.type = "X" + else: # must exist +I + if self.colors["+I"] == 0: + self.type = "Z" + else: + self.type = "X" + + if self.exists["-K"] == 0 and self.exists["+K"] == 0: + if self.exists["-I"]: + if self.colors["-I"] == 0: + self.type = "X" + else: + self.type = "Z" + else: # must exist +I + if self.colors["+I"] == 0: + self.type = "X" + else: + self.type = "Z" + + def zigxag_xy(self, n_j: int) -> Tuple[int, int]: + return (self.k * (n_j + 2) + self.j, -(n_j + 1) * self.i + self.j) + + def zigxag_str(self, n_j: int) -> str: + zigxag_type = { + 'Z': '@', + 'X': 'O', + 'S': 's', + 'W': 'w', + 'I': 'O', + 'Pi': 'in', + 'Po': 'out', + } + (x, y) = self.zigxag_xy(n_j) + return str(-y) + ',' + str(-x) + ',' + str(zigxag_type[self.type]) + + +class ZXGridEdge: + + def __init__(self, if_h: bool, node0: ZXGridNode, + node1: ZXGridNode) -> None: + """initialize ZXGridEdge for a pipe in the LaS. + + Args: + if_h (bool): if this edge is a Hadamard edge. + node0 (ZXGridNode): one end of the edge. + node1 (ZXGridNode): the other end of the edge. + + Raises: + ValueError: the two spiders are the same. + ValueError: the two spiders are not neighbors. + """ + + dist = abs(node0.i - node1.i) + abs(node0.j - node1.j) + abs(node0.k - + node1.k) + if dist == 0: + raise ValueError(f"{node0} and {node1} are the same.") + if dist > 1: + raise ValueError(f"{node0} and {node1} are not neighbors.") + self.node0, self.node1 = node0, node1 + self.type = "h" if if_h else "-" + + def zigxag_str(self, n_j: int) -> str: + (xa, ya) = self.node0.zigxag_xy(n_j) + (xb, yb) = self.node1.zigxag_xy(n_j) + return (str(-ya) + ',' + str(-xa) + ',' + str(-yb) + ',' + str(-xb) + + ',' + self.type) + + +class ZXGridGraph: + + def __init__(self, lasre: Mapping[str, Any]) -> None: + self.lasre = lasre + self.n_i, self.n_j, self.n_k = ( + lasre["n_i"], + lasre["n_j"], + lasre["n_k"], + ) + self.nodes = [[[ + ZXGridNode((i, j, k), self.gather_cube_connectivity(i, j, k)) + for k in range(self.n_k + 1) + ] for j in range(self.n_j + 1)] for i in range(self.n_i + 1)] + for (i, j, k) in self.lasre["port_cubes"]: + self.nodes[i][j][k].type = 'Po' + self.append_y_tails() + self.edges = [] + self.derive_edges() + + def gather_cube_connectivity(self, i: int, j: int, + k: int) -> Mapping[str, Mapping[str, int]]: + # exists and colors for no cube + exists = {"-I": 0, "+I": 0, "-K": 0, "+K": 0, "-J": 0, "+J": 0} + colors = { + "-I": -1, + "+I": -1, + "-K": -1, + "+K": -1, + "-J": -1, + "+J": -1, + } + if i in range(self.n_i) and j in range(self.n_j) and k in range( + self.n_k) and ((i, j, k) not in self.lasre["port_cubes"]) and ( + self.lasre["NodeY"][i][j][k] == 0): + if i > 0 and self.lasre["ExistI"][i - 1][j][k]: + exists["-I"] = 1 + colors["-I"] = self.lasre["ColorI"][i - 1][j][k] + if self.lasre["ExistI"][i][j][k]: + exists["+I"] = 1 + colors["+I"] = self.lasre["ColorI"][i][j][k] + if j > 0 and self.lasre["ExistJ"][i][j - 1][k]: + exists["-J"] = 1 + colors["-J"] = self.lasre["ColorJ"][i][j - 1][k] + if self.lasre["ExistJ"][i][j][k]: + exists["+J"] = 1 + colors["+J"] = self.lasre["ColorJ"][i][j][k] + if k > 0 and self.lasre["ExistK"][i][j][k - 1]: + exists["-K"] = 1 + colors["-K"] = self.lasre["ColorKP"][i][j][k - 1] + if self.lasre["ExistK"][i][j][k]: + exists["+K"] = 1 + colors["+K"] = self.lasre["ColorKM"][i][j][k] + return {"exists": exists, "colors": colors} + + def append_y_tails(self) -> None: + for i in range(self.n_i): + for j in range(self.n_j): + for k in range(self.n_k): + if self.lasre["NodeY"][i][j][k]: + if (k - 1 >= 0 and self.lasre["ExistK"][i][j][k - 1] + and (not self.lasre["NodeY"][i][j][k - 1])): + self.nodes[i][j][k - 1].y_tail_plus = True + if (k + 1 < self.n_k and self.lasre["ExistK"][i][j][k] + and (not self.lasre["NodeY"][i][j][k + 1])): + self.nodes[i][j][k + 1].y_tail_minus = True + + def derive_edges(self): + valid_types = ["Z", "X", "S", "I", "Pi", "Po"] + for i in range(self.n_i): + for j in range(self.n_j): + for k in range(self.n_k): + if (self.lasre["ExistI"][i][j][k] == 1 + and self.nodes[i][j][k].type in valid_types + and self.nodes[i + 1][j][k].type in valid_types): + self.edges.append( + ZXGridEdge(0, self.nodes[i][j][k], + self.nodes[i + 1][j][k])) + + if (self.lasre["ExistJ"][i][j][k] == 1 + and self.nodes[i][j][k].type in valid_types + and self.nodes[i][j + 1][k].type in valid_types): + self.edges.append( + ZXGridEdge(0, self.nodes[i][j][k], + self.nodes[i][j + 1][k])) + + if (self.lasre["ExistK"][i][j][k] == 1 + and self.nodes[i][j][k].type in valid_types + and self.nodes[i][j][k + 1].type in valid_types): + self.edges.append( + ZXGridEdge( + abs(self.lasre["ColorKM"][i][j][k] - + self.lasre["ColorKP"][i][j][k]), + self.nodes[i][j][k], + self.nodes[i][j][k + 1], + )) + + def to_zigxag_url(self, io_spec: Optional[Sequence[str]] = None) -> str: + """generate a url for ZigXag + + Args: + io_spec (Sequence[str], optional): specify whether each port is an + input port or an output port. + + Raises: + ValueError: len(io_spec) is not the same with the number of ports. + + Returns: + str: zigxag url + """ + if io_spec is not None: + if len(io_spec) != len(self.lasre["port_cubes"]): + raise ValueError( + f"io_spec has length {len(io_spec)} but there are " + f"{len(self.lasre['port_cubes'])} ports.") + for w, (i, j, k) in enumerate(self.lasre["port_cubes"]): + self.nodes[i][j][k].type = io_spec[w] + + valid_types = ["Z", "X", "S", "W", "I", "Pi", "Po"] + nodes_str = "" + first = True + for i in range(self.n_i + 1): + for j in range(self.n_j + 1): + for k in range(self.n_k + 1): + if self.nodes[i][j][k].type in valid_types: + if not first: + nodes_str += ";" + nodes_str += self.nodes[i][j][k].zigxag_str(self.n_j) + first = False + + edges_str = "" + for i, edge in enumerate(self.edges): + if i > 0: + edges_str += ";" + edges_str += edge.zigxag_str(self.n_j) + + # add nodes and edges for Y cubes + for i in range(self.n_i): + for j in range(self.n_j): + for k in range(self.n_k): + (x, y) = self.nodes[i][j][k].zigxag_xy(self.n_j) + if self.nodes[i][j][k].y_tail_plus: + nodes_str += (";" + str(x + self.n_j - j) + "," + + str(y) + ",s") + edges_str += (";" + str(x + self.n_j - j) + "," + + str(y) + "," + str(x) + "," + str(y) + + ",-") + if self.nodes[i][j][k].y_tail_minus: + nodes_str += (";" + str(x - j - 1) + "," + str(y) + + ",s") + edges_str += (";" + str(x - j - 1) + "," + str(y) + + "," + str(x) + "," + str(y) + ",-") + + zigxag_str = "https://algassert.com/zigxag#" + nodes_str + ":" + edges_str + return zigxag_str diff --git a/glue/lattice_surgery/setup.py b/glue/lattice_surgery/setup.py new file mode 100644 index 000000000..008b51bb0 --- /dev/null +++ b/glue/lattice_surgery/setup.py @@ -0,0 +1,28 @@ +from setuptools import find_packages, setup + +with open('README.md', encoding='UTF-8') as f: + long_description = f.read() + +__version__ = '0.1.0' + +setup( + name='LaSsynth', + version=__version__, + author='', + author_email='', + url='', + license='Apache 2', + packages=find_packages(), + description='Lattice Surgery Subroutine Synthesizer', + long_description=long_description, + long_description_content_type='text/markdown', + python_requires='>=3.6.0', + data_files=['README.md'], + install_requires=[ + 'z3-solver==4.12.1.0', + 'stim', + 'networkx', + 'ipykernel', + ], + tests_require=['pytest', 'python3-distutils'], +) diff --git a/glue/lattice_surgery/stimzx/__init__.py b/glue/lattice_surgery/stimzx/__init__.py new file mode 100644 index 000000000..93489c421 --- /dev/null +++ b/glue/lattice_surgery/stimzx/__init__.py @@ -0,0 +1,14 @@ +__version__ = '1.12.dev0' +from ._external_stabilizer import ( + ExternalStabilizer, +) + +from ._text_diagram_parsing import ( + text_diagram_to_networkx_graph, +) + +from ._zx_graph_solver import ( + zx_graph_to_external_stabilizers, + text_diagram_to_zx_graph, + ZxType, +) diff --git a/glue/lattice_surgery/stimzx/_external_stabilizer.py b/glue/lattice_surgery/stimzx/_external_stabilizer.py new file mode 100644 index 000000000..1363fed4d --- /dev/null +++ b/glue/lattice_surgery/stimzx/_external_stabilizer.py @@ -0,0 +1,90 @@ +from typing import List, Any + +import stim + + +class ExternalStabilizer: + """An input-to-output relationship enforced by a stabilizer circuit.""" + + def __init__(self, *, input: stim.PauliString, output: stim.PauliString): + self.input = input + self.output = output + + @staticmethod + def from_dual(dual: stim.PauliString, num_inputs: int) -> 'ExternalStabilizer': + sign = dual.sign + + # Transpose input. Ys get negated. + for k in range(num_inputs): + if dual[k] == 2: + sign *= -1 + + return ExternalStabilizer( + input=dual[:num_inputs], + output=dual[num_inputs:], + ) + + @staticmethod + def canonicals_from_duals(duals: List[stim.PauliString], num_inputs: int) -> List['ExternalStabilizer']: + if not duals: + return [] + duals = [e.copy() for e in duals] + num_qubits = len(duals[0]) + num_outputs = num_qubits - num_inputs + id_out = stim.PauliString(num_outputs) + + # Pivot on output qubits, to potentially isolate input-only stabilizers. + _eliminate_stabilizers(duals, range(num_inputs, num_qubits)) + + # Separate input-only stabilizers from the rest. + input_only_stabilizers = [] + output_using_stabilizers = [] + for dual in duals: + if dual[num_inputs:] == id_out: + input_only_stabilizers.append(dual) + else: + output_using_stabilizers.append(dual) + + # Separately canonicalize the output-using and input-only stabilizers. + _eliminate_stabilizers(output_using_stabilizers, range(num_qubits)) + _eliminate_stabilizers(input_only_stabilizers, range(num_inputs)) + + duals = input_only_stabilizers + output_using_stabilizers + return [ExternalStabilizer.from_dual(e, num_inputs) for e in duals] + + def __mul__(self, other: 'ExternalStabilizer') -> 'ExternalStabilizer': + return ExternalStabilizer(input=other.input * self.input, output=self.output * other.output) + + def __str__(self) -> str: + return str(self.input) + ' -> ' + str(self.output) + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, ExternalStabilizer): + return NotImplemented + return self.output == other.output and self.input == other.input + + def __ne__(self, other: Any) -> bool: + return not self == other + + def __repr__(self): + return f'stimzx.ExternalStabilizer(input={self.input!r}, output={self.output!r})' + + +def _eliminate_stabilizers(stabilizers: List[stim.PauliString], elimination_indices: range): + """Performs partial Gaussian elimination on the list of stabilizers.""" + min_pivot = 0 + for q in elimination_indices: + for b in [1, 3]: + for pivot in range(min_pivot, len(stabilizers)): + p = stabilizers[pivot][q] + if p == 2 or p == b: + break + else: + continue + for k, stabilizer in enumerate(stabilizers): + p = stabilizer[q] + if k != pivot and (p == 2 or p == b): + stabilizer *= stabilizers[pivot] + if min_pivot != pivot: + stabilizers[min_pivot], stabilizers[pivot] = stabilizers[pivot], stabilizers[min_pivot] + min_pivot += 1 diff --git a/glue/lattice_surgery/stimzx/_external_stabilizer_test.py b/glue/lattice_surgery/stimzx/_external_stabilizer_test.py new file mode 100644 index 000000000..b0e940b4e --- /dev/null +++ b/glue/lattice_surgery/stimzx/_external_stabilizer_test.py @@ -0,0 +1,7 @@ +import stim +import stimzx + + +def test_repr(): + e = stimzx.ExternalStabilizer(input=stim.PauliString("XX"), output=stim.PauliString("Y")) + assert eval(repr(e), {'stimzx': stimzx, 'stim': stim}) == e diff --git a/glue/lattice_surgery/stimzx/_text_diagram_parsing.py b/glue/lattice_surgery/stimzx/_text_diagram_parsing.py new file mode 100644 index 000000000..36c1068af --- /dev/null +++ b/glue/lattice_surgery/stimzx/_text_diagram_parsing.py @@ -0,0 +1,178 @@ +import re +from typing import Dict, Tuple, TypeVar, List, Set, Callable + +import networkx as nx + +K = TypeVar("K") + + +def text_diagram_to_networkx_graph(text_diagram: str, *, value_func: Callable[[str], K] = str) -> nx.MultiGraph: + r"""Converts a text diagram into a networkx multi graph. + + Args: + text_diagram: An ascii text diagram of the graph, linking nodes together with edges. Edges can be horizontal + (-), vertical (|), diagonal (/\), crossing (+), or changing direction (*). Nodes can be alphanumeric with + parentheses. It is assumed that all text is shown with a fixed-width font. + value_func: An optional transformation to apply to the node text in order to get the node's value. Otherwise + the node's value is just its text. + + Example: + + >>> import stimzx + >>> import networkx as nx + >>> actual = stimzx.text_diagram_to_networkx_graph(r''' + ... + ... A + ... | + ... NODE1--+--NODE2----------* + ... | | / + ... B | / + ... *------NODE4 + ... + ... ''') + >>> expected = nx.MultiGraph() + >>> expected.add_node(0, value='A') + >>> expected.add_node(1, value='NODE1') + >>> expected.add_node(2, value='NODE2') + >>> expected.add_node(3, value='B') + >>> expected.add_node(4, value='NODE4') + >>> _ = expected.add_edge(0, 3) + >>> _ = expected.add_edge(1, 2) + >>> _ = expected.add_edge(2, 4) + >>> _ = expected.add_edge(2, 4) + >>> nx.testing.assert_graphs_equal(actual, expected) + + Returns: + A networkx multi graph containing the graph from the text diagram. Nodes in the graph are integers (the ordering + of nodes is in the natural string ordering from left to right then top to bottom in the diagram), and have a + "value" attribute containing either the node's string from the diagram or else a function of that string if + value_func was specified. + """ + char_map = _text_to_char_map(text_diagram) + node_ids, nodes = _find_nodes(char_map, value_func) + edges = _find_all_edges(char_map, node_ids) + result = nx.MultiGraph() + for k, v in enumerate(nodes): + result.add_node(k, value=v) + for a, b in edges: + result.add_edge(a, b) + return result + + +def _text_to_char_map(text: str) -> Dict[complex, str]: + char_map = {} + x = 0 + y = 0 + for c in text: + if c == '\n': + x = 0 + y += 1 + continue + if c != ' ': + char_map[x + 1j*y] = c + x += 1 + return char_map + + +DIR_TO_CHARS = { + -1 - 1j: '\\', + 0 - 1j: '|+', + 1 - 1j: '/', + -1: '-+', + 1: '-+', + -1 + 1j: '/', + 1j: '|+', + 1 + 1j: '\\', +} + +CHAR_TO_DIR = { + '\\': 1 + 1j, + '-': 1, + '|': 1j, + '/': -1 + 1j, +} + + +def _find_all_edges(char_map: Dict[complex, str], terminal_map: Dict[complex, K]) -> List[Tuple[K, K]]: + edges = [] + already_travelled = set() + for xy, c in char_map.items(): + x = int(xy.real) + y = int(xy.imag) + if xy in terminal_map or xy in already_travelled or c in '*+': + continue + already_travelled.add(xy) + dxy = CHAR_TO_DIR.get(c) + if dxy is None: + raise ValueError(f"Character {x+1} ('{c}') in line {y+1} isn't part in a node or an edge") + n1 = _find_end_of_edge(xy + dxy, dxy, char_map, terminal_map, already_travelled) + n2 = _find_end_of_edge(xy - dxy, -dxy, char_map, terminal_map, already_travelled) + edges.append((n2, n1)) + return edges + + +def _find_end_of_edge(xy: complex, dxy: complex, char_map: Dict[complex, str], terminal_map: Dict[complex, K], already_travelled: Set[complex]): + while True: + c = char_map[xy] + if xy in terminal_map: + return terminal_map[xy] + + if c != '+': + if xy in already_travelled: + raise ValueError("Edge used twice.") + already_travelled.add(xy) + + next_deltas: List[complex] = [] + if c == '*': + for dx2 in [-1, 0, 1]: + for dy2 in [-1, 0, 1]: + dxy2 = dx2 + dy2 * 1j + c2 = char_map.get(xy + dxy2) + if dxy2 != 0 and dxy2 != -dxy and c2 is not None and c2 in DIR_TO_CHARS[dxy2]: + next_deltas.append(dxy2) + if len(next_deltas) != 1: + raise ValueError(f"Edge junction ('*') at character {int(xy.real)+1}$ in line {int(xy.imag)+1} doesn't have exactly 2 legs.") + dxy, = next_deltas + else: + expected = DIR_TO_CHARS[dxy] + if c not in expected: + raise ValueError(f"Dangling edge at character {int(xy.real)+1} in line {int(xy.imag)+1} travelling dx=${int(dxy.real)},dy={int(dxy.imag)}.") + xy += dxy + + +def _find_nodes(char_map: Dict[complex, str], value_func: Callable[[str], K]) -> Tuple[Dict[complex, int], List[K]]: + node_ids = {} + nodes = [] + + node_chars = re.compile("^[a-zA-Z0-9()]$") + next_node_id = 0 + + for xy, lead_char in char_map.items(): + if xy in node_ids: + continue + if not node_chars.match(lead_char): + continue + + n = 0 + nested = 0 + full_name = '' + while True: + c = char_map.get(xy + n, ' ') + if c == ' ' and nested > 0: + raise ValueError("Label ended before ')' to go with '(' was found.") + if nested == 0 and not node_chars.match(c): + break + full_name += c + if c == '(': + nested += 1 + elif c == ')': + nested -= 1 + n += 1 + + nodes.append(value_func(full_name)) + node_id = next_node_id + next_node_id += 1 + for k in range(n): + node_ids[xy + k] = node_id + + return node_ids, nodes diff --git a/glue/lattice_surgery/stimzx/_text_diagram_parsing_test.py b/glue/lattice_surgery/stimzx/_text_diagram_parsing_test.py new file mode 100644 index 000000000..eef8e9003 --- /dev/null +++ b/glue/lattice_surgery/stimzx/_text_diagram_parsing_test.py @@ -0,0 +1,149 @@ +import networkx as nx +import pytest +from ._text_diagram_parsing import _find_nodes, _text_to_char_map, _find_end_of_edge, _find_all_edges, text_diagram_to_networkx_graph + + +def test_text_to_char_map(): + assert _text_to_char_map(""" +ABC DEF +G + HI + """) == { + 0 + 1j: 'A', + 1 + 1j: 'B', + 2 + 1j: 'C', + 4 + 1j: 'D', + 5 + 1j: 'E', + 6 + 1j: 'F', + 0 + 2j: 'G', + 1 + 3j: 'H', + 2 + 3j: 'I', + } + + +def test_find_nodes(): + assert _find_nodes(_text_to_char_map(''), lambda e: e) == ({}, []) + with pytest.raises(ValueError, match="base 10"): + _find_nodes(_text_to_char_map('NOTANINT'), int) + with pytest.raises(ValueError, match=r"ended before '\)'"): + _find_nodes(_text_to_char_map('X(run_off'), str) + assert _find_nodes(_text_to_char_map('X'), str) == ( + { + 0j: 0, + }, + ['X'], + ) + assert _find_nodes(_text_to_char_map('\n X'), str) == ( + { + 3 + 1j: 0, + }, + ['X'], + ) + assert _find_nodes(_text_to_char_map('X(pi)'), str) == ( + { + 0: 0, + 1: 0, + 2: 0, + 3: 0, + 4: 0, + }, + ['X(pi)'], + ) + assert _find_nodes(_text_to_char_map('X--Z'), str) == ( + { + 0: 0, + 3: 1, + }, + ['X', 'Z'], + ) + assert _find_nodes(_text_to_char_map(""" +X--* + / + Z +"""), str) == ( + { + 1j: 0, + 1 + 3j: 1, + }, + ['X', 'Z'], + ) + assert _find_nodes(_text_to_char_map(""" +X(pi)--Z +"""), str) == ( + { + 0 + 1j: 0, + 1 + 1j: 0, + 2 + 1j: 0, + 3 + 1j: 0, + 4 + 1j: 0, + 7 + 1j: 1, + }, + ["X(pi)", "Z"], + ) + + +def test_find_end_of_edge(): + c = _text_to_char_map(r""" +1--------* + \ 2 | + 5 \ *--++-* + *-----+-* |/ + | | / + 2 |/ + * + """) + terminal = {1: 'ONE', 18 + 6j: 'TWO'} + seen = set() + assert _find_end_of_edge(1 + 1j, 1, c, terminal, seen) == 'TWO' + assert len(seen) == 31 + + +def test_find_all_edges(): + c = _text_to_char_map(r""" +X---Z H----X(pi/2) + / + Z(pi/2) + """) + node_ids, _ = _find_nodes(c, str) + assert _find_all_edges(c, node_ids) == [ + (0, 1), + (2, 3), + (2, 4), + ] + + +def test_from_text_diagram(): + actual = text_diagram_to_networkx_graph(""" +in---Z---H---------out + | +in---X---Z(-pi/2)---out + """) + expected = nx.MultiGraph() + expected.add_node(0, value='in'), + expected.add_node(1, value='Z'), + expected.add_node(2, value='H'), + expected.add_node(3, value='out'), + expected.add_node(4, value='in'), + expected.add_node(5, value='X'), + expected.add_node(6, value='Z(-pi/2)'), + expected.add_node(7, value='out'), + expected.add_edge(0, 1) + expected.add_edge(1, 2) + expected.add_edge(2, 3) + expected.add_edge(1, 5) + expected.add_edge(4, 5) + expected.add_edge(5, 6) + expected.add_edge(6, 7) + nx.testing.assert_graphs_equal(actual, expected) + + actual = text_diagram_to_networkx_graph(""" + Z-* + | | + X-* + """) + expected = nx.MultiGraph() + expected.add_node(0, value='Z') + expected.add_node(1, value='X') + expected.add_edge(0, 1) + expected.add_edge(0, 1) + nx.testing.assert_graphs_equal(actual, expected) diff --git a/glue/lattice_surgery/stimzx/_zx_graph_solver.py b/glue/lattice_surgery/stimzx/_zx_graph_solver.py new file mode 100644 index 000000000..ff5bd0918 --- /dev/null +++ b/glue/lattice_surgery/stimzx/_zx_graph_solver.py @@ -0,0 +1,196 @@ +from typing import Dict, Tuple, List, Any, Union +import stim +import networkx as nx + +from ._text_diagram_parsing import text_diagram_to_networkx_graph +from ._external_stabilizer import ExternalStabilizer + + +class ZxType: + """Data describing a ZX node.""" + + def __init__(self, kind: str, quarter_turns: int = 0): + self.kind = kind + self.quarter_turns = quarter_turns + + def __eq__(self, other): + if not isinstance(other, ZxType): + return NotImplemented + return self.kind == other.kind and self.quarter_turns == other.quarter_turns + + def __ne__(self, other): + return not self == other + + def __hash__(self): + return hash((ZxType, self.kind, self.quarter_turns)) + + def __repr__(self): + return f'ZxType(kind={self.kind!r}, quarter_turns={self.quarter_turns!r})' + + +ZX_TYPES = { + "X": ZxType("X"), + "X(pi/2)": ZxType("X", 1), + "X(pi)": ZxType("X", 2), + "X(-pi/2)": ZxType("X", 3), + "Z": ZxType("Z"), + "Z(pi/2)": ZxType("Z", 1), + "Z(pi)": ZxType("Z", 2), + "Z(-pi/2)": ZxType("Z", 3), + "H": ZxType("H"), + "in": ZxType("in"), + "out": ZxType("out"), +} + + +def text_diagram_to_zx_graph(text_diagram: str) -> nx.MultiGraph: + """Converts an ASCII text diagram into a ZX graph (represented as a networkx MultiGraph). + + Supported node types: + "X": X spider with angle set to 0. + "Z": Z spider with angle set to 0. + "X(pi/2)": X spider with angle set to pi/2. + "X(pi)": X spider with angle set to pi. + "X(-pi/2)": X spider with angle set to -pi/2. + "Z(pi/2)": X spider with angle set to pi/2. + "Z(pi)": X spider with angle set to pi. + "Z(-pi/2)": X spider with angle set to -pi/2. + "H": Hadamard node. Must have degree 2. + "in": Input node. Must have degree 1. + "out": Output node. Must have degree 1. + + Args: + text_diagram: A text diagram containing ZX nodes (e.g. "X(pi)") and edges (e.g. "------") connecting them. + + Example: + >>> import stimzx + >>> import networkx + >>> actual: networkx.MultiGraph = stimzx.text_diagram_to_zx_graph(r''' + ... in----X------out + ... | + ... in---Z(pi)---out + ... ''') + >>> expected = networkx.MultiGraph() + >>> expected.add_node(0, value=stimzx.ZxType("in")) + >>> expected.add_node(1, value=stimzx.ZxType("X")) + >>> expected.add_node(2, value=stimzx.ZxType("out")) + >>> expected.add_node(3, value=stimzx.ZxType("in")) + >>> expected.add_node(4, value=stimzx.ZxType("Z", quarter_turns=2)) + >>> expected.add_node(5, value=stimzx.ZxType("out")) + >>> _ = expected.add_edge(0, 1) + >>> _ = expected.add_edge(1, 2) + >>> _ = expected.add_edge(1, 4) + >>> _ = expected.add_edge(3, 4) + >>> _ = expected.add_edge(4, 5) + >>> networkx.testing.assert_graphs_equal(actual, expected) + + Returns: + A networkx MultiGraph containing the nodes and edges from the diagram. Nodes are numbered 0, 1, 2, etc in + reading ordering from the diagram, and have a "value" attribute of type `stimzx.ZxType`. + """ + return text_diagram_to_networkx_graph(text_diagram, value_func=ZX_TYPES.__getitem__) + + +def _reduced_zx_graph(graph: Union[nx.Graph, nx.MultiGraph]) -> nx.Graph: + """Return an equivalent graph without self edges or repeated edges.""" + reduced_graph = nx.Graph() + odd_parity_edges = set() + for n1, n2 in graph.edges(): + if n1 == n2: + continue + odd_parity_edges ^= {frozenset([n1, n2])} + for n, value in graph.nodes('value'): + reduced_graph.add_node(n, value=value) + for n1, n2 in odd_parity_edges: + reduced_graph.add_edge(n1, n2) + return reduced_graph + + +def zx_graph_to_external_stabilizers(graph: Union[nx.Graph, nx.MultiGraph]) -> List[ExternalStabilizer]: + """Computes the external stabilizers of a ZX graph; generators of Paulis that leave it unchanged including sign. + + Args: + graph: A non-contradictory connected ZX graph with nodes annotated by 'type' and optionally by 'angle'. + Allowed types are 'x', 'z', 'h', and 'out'. + Allowed angles are multiples of `math.pi/2`. Only 'x' and 'z' node types can have angles. + 'out' nodes must have degree 1. + 'h' nodes must have degree 2. + + Returns: + A list of canonicalized external stabilizer generators for the graph. + """ + + graph = _reduced_zx_graph(graph) + sim = stim.TableauSimulator() + + # Interpret each edge as a cup producing an EPR pair. + # - The qubits of the EPR pair fly away from the center of the edge, towards their respective nodes. + # - The qubit keyed by (a, b) is the qubit heading towards b from the edge between a and b. + qubit_ids: Dict[Tuple[Any, Any], int] = {} + for n1, n2 in graph.edges: + qubit_ids[(n1, n2)] = len(qubit_ids) + qubit_ids[(n2, n1)] = len(qubit_ids) + sim.h(qubit_ids[(n1, n2)]) + sim.cnot(qubit_ids[(n1, n2)], qubit_ids[(n2, n1)]) + + # Interpret each internal node as a family of post-selected parity measurements. + for n, node_type in graph.nodes('value'): + if node_type.kind in 'XZ': + # Surround X type node with Hadamards so it can be handled as if it were Z type. + if node_type.kind == 'X': + for neighbor in graph.neighbors(n): + sim.h(qubit_ids[(neighbor, n)]) + elif node_type.kind == 'H': + # Hadamard one input so the H node can be handled as if it were Z type. + neighbor, _ = graph.neighbors(n) + sim.h(qubit_ids[(neighbor, n)]) + elif node_type.kind in ['out', 'in']: + continue # Don't measure qubits leaving the system. + else: + raise ValueError(f"Unknown node type {node_type!r}") + + # Handle Z type node. + # - Postselects the ZZ observable over each pair of incoming qubits. + # - Postselects the (S**quarter_turns X S**-quarter_turns)XX..X observable over all incoming qubits. + neighbors = [n2 for n2 in graph.neighbors(n) if n2 != n] + center = qubit_ids[(neighbors[0], n)] # Pick one incoming qubit to be the common control for the others. + # Handle node angle using a phasing operation. + [id, sim.s, sim.z, sim.s_dag][node_type.quarter_turns](center) + # Use multi-target CNOT and Hadamard to transform postselected observables into single-qubit Z observables. + for n2 in neighbors[1:]: + sim.cnot(center, qubit_ids[(n2, n)]) + sim.h(center) + # Postselect the observables. + for n2 in neighbors: + _pseudo_postselect(sim, qubit_ids[(n2, n)]) + + # Find output qubits. + in_nodes = sorted(n for n, value in graph.nodes('value') if value.kind == 'in') + out_nodes = sorted(n for n, value in graph.nodes('value') if value.kind == 'out') + ext_nodes = in_nodes + out_nodes + out_qubits = [] + for out in ext_nodes: + (neighbor,) = graph.neighbors(out) + out_qubits.append(qubit_ids[(neighbor, out)]) + + # Remove qubits corresponding to non-external edges. + for i, q in enumerate(out_qubits): + sim.swap(q, len(qubit_ids) + i) + for i, q in enumerate(out_qubits): + sim.swap(i, len(qubit_ids) + i) + sim.set_num_qubits(len(out_qubits)) + + # Stabilizers of the simulator state are the external stabilizers of the graph. + dual_stabilizers = sim.canonical_stabilizers() + return ExternalStabilizer.canonicals_from_duals(dual_stabilizers, len(in_nodes)) + + +def _pseudo_postselect(sim: stim.TableauSimulator, target: int): + """Pretend to postselect by using classical feedback to consistently get into the measurement-was-false state.""" + measurement_result, kickback = sim.measure_kickback(target) + if kickback is not None: + for qubit, pauli in enumerate(kickback): + feedback_op = [None, sim.cnot, sim.cy, sim.cz][pauli] + if feedback_op is not None: + feedback_op(stim.target_rec(-1), qubit) + assert kickback is not None or not measurement_result, "Impossible postselection. Graph contained a contradiction." diff --git a/glue/lattice_surgery/stimzx/_zx_graph_solver_test.py b/glue/lattice_surgery/stimzx/_zx_graph_solver_test.py new file mode 100644 index 000000000..9db02fdf9 --- /dev/null +++ b/glue/lattice_surgery/stimzx/_zx_graph_solver_test.py @@ -0,0 +1,137 @@ +from typing import List + +import stim + +from ._zx_graph_solver import zx_graph_to_external_stabilizers, text_diagram_to_zx_graph, ExternalStabilizer + + +def test_disconnected(): + assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" + in---X X---out + """)) == [ + ExternalStabilizer(input=stim.PauliString("Z"), output=stim.PauliString("_")), + ExternalStabilizer(input=stim.PauliString("_"), output=stim.PauliString("Z")), + ] + assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" + in---Z---out + | + X + """)) == [ + ExternalStabilizer(input=stim.PauliString("Z"), output=stim.PauliString("_")), + ExternalStabilizer(input=stim.PauliString("_"), output=stim.PauliString("Z")), + ] + assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" + in---Z---X---out + | | + *---* + """)) == [ + ExternalStabilizer(input=stim.PauliString("X"), output=stim.PauliString("_")), + ExternalStabilizer(input=stim.PauliString("_"), output=stim.PauliString("Z")), + ] + + +def test_cnot(): + assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" + in---X---out + | + in---Z---out + """)) == external_stabilizers_of_circuit(stim.Circuit("CNOT 1 0")) + + assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" + in---Z---out + | + in---X---out + """)) == external_stabilizers_of_circuit(stim.Circuit("CNOT 0 1")) + + +def test_cz(): + assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" + in---Z---out + | + H + | + in---Z---out + """)) == external_stabilizers_of_circuit(stim.Circuit("CZ 0 1")) + + +def test_s(): + assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" + in---Z(pi/2)---out + """)) == external_stabilizers_of_circuit(stim.Circuit("S 0")) + + +def test_s_dag(): + assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" + in---Z(-pi/2)---out + """)) == external_stabilizers_of_circuit(stim.Circuit("S_DAG 0")) + + +def test_sqrt_x(): + assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" + in---X(pi/2)---out + """)) == external_stabilizers_of_circuit(stim.Circuit("SQRT_X 0")) + + +def test_sqrt_x_sqrt_x(): + assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" + in---X(pi/2)---X(pi/2)---out + """)) == external_stabilizers_of_circuit(stim.Circuit("X 0")) + + +def test_sqrt_z_sqrt_z(): + assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" + in---Z(pi/2)---Z(pi/2)---out + """)) == external_stabilizers_of_circuit(stim.Circuit("Z 0")) + + +def test_sqrt_x_dag(): + assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" + in---X(-pi/2)---out + """)) == external_stabilizers_of_circuit(stim.Circuit("SQRT_X_DAG 0")) + + +def test_x(): + assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" + in---X(pi)---out + """)) == external_stabilizers_of_circuit(stim.Circuit("X 0")) + + +def test_z(): + assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" + in---Z(pi)---out + """)) == external_stabilizers_of_circuit(stim.Circuit("Z 0")) + + +def test_id(): + assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(""" + in---X---Z---out + """)) == external_stabilizers_of_circuit(stim.Circuit("I 0")) + + +def test_s_state_distill(): + assert zx_graph_to_external_stabilizers(text_diagram_to_zx_graph(r""" + * *---------------Z--------------------Z-------Z(pi/2) + / \ | | | + *-----* *------------Z---+---------------+---Z----------------+-------Z(pi/2) + | | | | | | + X---X---Z(pi/2) X---X---Z(pi/2) X---X---Z(pi/2) X---X---Z(pi/2) + | | | | | | + *---+------------------Z-------------------+--------------------+---Z---Z(pi/2) + | | | + in-------Z--------------------------------------Z-------------------Z(pi)--------out + """)) == external_stabilizers_of_circuit(stim.Circuit("S 0")) + + +def external_stabilizers_of_circuit(circuit: stim.Circuit) -> List[ExternalStabilizer]: + n = circuit.num_qubits + s = stim.TableauSimulator() + s.do(circuit) + t = s.current_inverse_tableau()**-1 + stabilizers = [] + for k in range(n): + p = [0] * n + p[k] = 1 + stabilizers.append(stim.PauliString(p) + t.x_output(k)) + p[k] = 3 + stabilizers.append(stim.PauliString(p) + t.z_output(k)) + return [ExternalStabilizer.from_dual(e, circuit.num_qubits) for e in stabilizers]