├── Dockerfile ├── LICENSE ├── README.md ├── WORKSPACE ├── errata └── Errata.pdf ├── external ├── numpy.BUILD └── python.BUILD ├── make_libxgates.sh ├── resources ├── README.CentOS.md ├── README.Docker.md ├── README.Linux.md ├── README.MacOS.md ├── README.SageMath.md ├── README.Windows.md ├── README.buildxgates.md ├── README.md └── quickstart.md └── src ├── BUILD ├── amplitude_estimation.py ├── arith_classic.py ├── arith_quantum.py ├── bell_basis.py ├── benchmarks ├── BUILD ├── larose_benchmark.py └── tensor_math.py ├── bernstein.py ├── chsh.py ├── counting.py ├── deutsch.py ├── deutsch_jozsa.py ├── entanglement_swap.py ├── estimate_pi.py ├── euclidean_distance.py ├── graph_coloring.py ├── grover.py ├── hadamard_test.py ├── hamiltonian_cycle.py ├── hamiltonian_encoding.py ├── hhl.py ├── hhl_2x2.py ├── inversion_test.py ├── lib ├── BUILD ├── __init__.py ├── bell.py ├── bell_test.py ├── circuit.py ├── circuit_test.py ├── dumpers.py ├── equalities_test.py ├── helper.py ├── helper_test.py ├── ir.py ├── measure_test.py ├── ops.py ├── ops_test.py ├── optimizer.py ├── startup.py ├── state.py ├── state_test.py ├── tensor.py ├── tensor_test.py ├── test_all.sh └── xgates.cc ├── libq ├── BUILD ├── apply.cc ├── gates.cc ├── gates_jit.cc ├── libq.h ├── libq_arith_test.cc ├── libq_order22_test.cc ├── libq_test.cc └── qureg.cc ├── max_cut.py ├── minimum_finding.py ├── oracle_synth.py ├── order_finding.py ├── pauli_rep.py ├── phase_estimation.py ├── phase_kick.py ├── purification.py ├── qram.py ├── quantum_mean.py ├── quantum_median.py ├── quantum_pca.py ├── quantum_walk.py ├── runall.sh ├── sat3.py ├── schmidt_decomp.py ├── shor_classic.py ├── simon.py ├── simon_general.py ├── solovay_kitaev.py ├── spectral_decomp.py ├── state_prep.py ├── state_prep_mottonen.py ├── subset_sum.py ├── superdense.py ├── supremacy.py ├── swap_test.py ├── teleportation.py ├── tools ├── BUILD └── random_walk.py ├── vqe_simple.py └── zy_decomp.py /Dockerfile: -------------------------------------------------------------------------------- 1 | # This Dockerfile creates a container for running the algorithms, tests, and benchmarks in the open-source repository 2 | # for Quantum Computing For Programmers. In order to ensure that everything is ready for running the algorithms and 3 | # benchmarks, the library is built and the tests are run by default. 4 | # 5 | # Copyright 2022, Abdolhamid Pourghazi 6 | # SPDX-License-Identifier: Apache-2.0 7 | 8 | FROM debian:11 9 | 10 | LABEL maintainer="Abdolhamid Pourghazi " 11 | LABEL maintainer="Stefan Klessinger " 12 | 13 | ENV DEBIAN_FRONTEND noninteractive 14 | ENV LANG="C.UTF-8" 15 | ENV LC_ALL="C.UTF-8" 16 | 17 | # Install packages 18 | RUN apt-get update && apt-get install -y --no-install-recommends \ 19 | build-essential \ 20 | git \ 21 | libpython3-dev \ 22 | python3 \ 23 | python3-pip \ 24 | wget 25 | 26 | 27 | RUN python3 -m pip install absl-py numpy scipy 28 | 29 | # Add user 30 | RUN useradd -m -G sudo -s /bin/bash repro && echo "repro:repro" | chpasswd 31 | USER repro 32 | 33 | # Install bazelisk 34 | RUN mkdir -p /home/repro/bin/ 35 | RUN wget -O /home/repro/bin/bazel https://github.com/bazelbuild/bazelisk/releases/latest/download/bazelisk-linux-amd64 36 | RUN chmod +x /home/repro/bin/bazel 37 | 38 | ENV PATH="/home/repro/bin/:${PATH}" 39 | 40 | # Get qcc source 41 | RUN mkdir -p /home/repro/sources/ 42 | WORKDIR /home/repro/sources/ 43 | RUN git clone https://github.com/qcc4cp/qcc.git 44 | 45 | # Build qcc 46 | WORKDIR /home/repro/sources/qcc/src/lib 47 | RUN bazel build all 48 | 49 | ENV PYTHONPATH=$PYTHONPATH:/home/repro/sources/qcc/bazel-bin/src/lib 50 | 51 | # Run tests 52 | RUN bazel test ... 53 | RUN bazel run circuit_test 54 | 55 | WORKDIR /home/repro/sources/qcc/src/libq 56 | RUN bazel test ... 57 | 58 | # Set initial directory 59 | WORKDIR /home/repro/sources/qcc/src 60 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | new_local_repository( 2 | name = "third_party_python", 3 | build_file = "//external:python.BUILD", 4 | # Configure: 5 | path = "/usr/include/python3.9", 6 | ) 7 | 8 | new_local_repository( 9 | name = "third_party_numpy", 10 | build_file = "//external:numpy.BUILD", 11 | # Configure: 12 | path = "/usr/local/lib/python3.9/dist-packages/numpy/_core/", 13 | ) 14 | -------------------------------------------------------------------------------- /errata/Errata.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qcc4cp/qcc/52097a54521acf26c582434a8b2f1d73340f2a20/errata/Errata.pdf -------------------------------------------------------------------------------- /external/numpy.BUILD: -------------------------------------------------------------------------------- 1 | package( 2 | default_visibility = ["//visibility:public"] 3 | ) 4 | 5 | cc_library( 6 | name = "numpy", 7 | srcs = [ 8 | ], 9 | hdrs = glob([ # the glob takes all the headers needed 10 | "**/*.h", 11 | "**/*.hpp", 12 | ]), 13 | includes = ["include"], 14 | ) -------------------------------------------------------------------------------- /external/python.BUILD: -------------------------------------------------------------------------------- 1 | package( 2 | default_visibility = ["//visibility:public"] 3 | ) 4 | 5 | cc_library( 6 | name = "python", 7 | srcs = [ 8 | ], 9 | hdrs = glob([ 10 | "**/*.h", 11 | ]), 12 | includes = [""], 13 | ) -------------------------------------------------------------------------------- /make_libxgates.sh: -------------------------------------------------------------------------------- 1 | # Simple command line to build libxgates.so 2 | # 3 | # The directories and libraries need to be adjusted for a given setup. 4 | # This has been tested on MacOS and Ubuntu. 5 | 6 | # 7 | # get numpy include directory: 8 | # 9 | NUMPY=`python3 -c 'import numpy;\ 10 | print(numpy.get_include())'` 11 | echo "numpy : ${NUMPY}" 12 | 13 | # 14 | # Directory where to find the Python.h file in: 15 | # 16 | PY=`python3 -c 'import distutils.sysconfig;\ 17 | print(distutils.sysconfig.get_python_inc())'` 18 | echo "Python : ${PY}" 19 | 20 | 21 | # 22 | # Python library. It can be hard to find on your system but may be close 23 | # to one of the paths determined above. 24 | # 25 | # Example: MacOS / Darwin 26 | # 27 | LIB=/Library/Frameworks/Python.framework/Versions/3.11/lib/python3.11\ 28 | /./config-3.11-darwin/libpython3.11.dylib 29 | # 30 | # Example Linux / Ubuntu 31 | # 32 | # LIB=/usr/lib/python3.11/config-3.11-x86_64-linux-gnu/libpython3.11.so 33 | # 34 | echo "Library: ${LIB}" 35 | 36 | # 37 | # Command-line option to make shared module 38 | # 39 | SHARED="" 40 | OS=`uname -a | awk '{print $1}'` 41 | if [[ ${OS} == "Darwin" ]]; then 42 | SHARED="-dynamiclib" 43 | fi 44 | if [[ ${OS} == "Linux" ]]; then 45 | SHARED="-shared" 46 | fi 47 | if [[ ${SHARED} == "" ]]; then 48 | echo "WARNING: Could not recognize the OS ($OS)." 49 | echo " Check flags to make shared object." 50 | exit 1 51 | fi 52 | echo "Flags : ${SHARED}" 53 | 54 | # 55 | # Target 56 | # 57 | OUT=./libxgates.so 58 | echo "Target : ${OUT}" 59 | 60 | # 61 | # Main compiler invokation: 62 | # 63 | cc -I${NUMPY} -I${PY} ${LIB} -O3 -ffast-math -DNPY_NO_DEPRECATED_API \ 64 | -fPIC -std=c++0x ${SHARED} -o ${OUT} \ 65 | src/lib/xgates.cc || exit 1 66 | 67 | echo "Made :" 68 | ls -l ${OUT} 69 | -------------------------------------------------------------------------------- /resources/README.CentOS.md: -------------------------------------------------------------------------------- 1 | These are install instructions specific for CentOS. 2 | 3 | On a new VM, these packages need to be installed: 4 | ``` 5 | sudo yum install -y python3 6 | sudo yum install -y python3-devel 7 | sudo yum install -y git 8 | sudo yum install -y gcc 9 | sudo yum install -y gcc-c++ 10 | ``` 11 | 12 | Bazel is a little bit more complicated. Download the file found here: 13 | ``` 14 | https://copr.fedorainfracloud.org/coprs/vbatts/bazel/repo/epel-7/vbatts-bazel-epel-7.repo 15 | ``` 16 | 17 | to this directory as `bazel3.repo` (the file should only contain ~10 lines): 18 | ``` 19 | /etc/yum.repos.d/ 20 | ``` 21 | 22 | and run: 23 | ``` 24 | sudo yum install -y bazel3 25 | ``` 26 | 27 | All other instructions from the main `README.md` still apply. 28 | -------------------------------------------------------------------------------- /resources/README.Docker.md: -------------------------------------------------------------------------------- 1 | # Get Started Using Docker 2 | 3 | If you can run docker in your environment, the setup is very simple. 4 | It is all contained in the `Dockerfile`, which was gratefully provided by 5 | Abdolhamid Pourghazi () and 6 | Stefan Klessinger (), both from the 7 | University of Passau, Germany. 8 | 9 | From the main directory `qcc` (which is the directory that contains the `Dockerfile`), simply run: 10 | 11 | ``` 12 | docker build -t qcc4cp:latest . 13 | docker run -t -d --name qcc4cp qcc4cp:latest 14 | docker exec -it qcc4cp /bin/bash 15 | ``` 16 | 17 | Then run all the algorithms (which are all on `.py` files in the `qcc/src` directory, with: 18 | 19 | ``` 20 | for algo in `ls -1 *py | sed s@.py@@g` 21 | do 22 | bazel run $algo 23 | done 24 | ``` 25 | 26 | 27 | -------------------------------------------------------------------------------- /resources/README.Linux.md: -------------------------------------------------------------------------------- 1 | # Manual Installation on Linux 2 | 3 | The following instructions focus on Debian Linux but should work for Ubuntu as well. 4 | Note that if you can use Docker, many of these steps are performed for you by Docker when 5 | the container is being created. 6 | 7 | ## Dependencies 8 | 9 | To run the code a few tools are needed: 10 | 11 | * The use of the `bazel` build system is optional but can be helpful.\ 12 | Install from [bazel's homepage](https://docs.bazel.build/versions/master/install.html) 13 | 14 | * We will need Python's `pip` tool to install packages and `git` to manage the source. 15 | Here is one way to install them: 16 | ``` 17 | sudo apt-get install python3-pip 18 | sudo apt-get install git 19 | ``` 20 | 21 | * We need Google's `absl` library, as well as `numpy` and `scipy`. Install with 22 | ``` 23 | sudo python3 -m pip install absl-py 24 | sudo python3 -m pip install numpy 25 | sudo python3 -m pip install scipy 26 | ``` 27 | 28 | * Finally, to get these source onto your computer: 29 | ``` 30 | git clone https://github.com/qcc4cp/qcc.git 31 | ``` 32 | 33 | ## Build 34 | 35 | Much of the code is in Python and will run out of the box. There is 36 | some C++ for the high performance simulation which requires 37 | configuration. *The Python code will run without C++ acceleration, just much slower.* 38 | 39 | There are two ways to build the accelerated library: 40 | 1. Manually, with the script [qcc/make_libxgates.so](../make_libxgates.sh), 41 | documented [here](README.buildxgates.md). 42 | 43 | 44 | 3. Using `bazel`. This is described in the following. It only works for `bazel` 45 | version 5, 6, and 7 (not version 8 and `Bzlmod`). 46 | 47 | The file `src/lib/BUILD` contains the build rule for the C++ xgates 48 | extension module. This module needs to be able to access the Python 49 | header (`Python.h`), as well as certain `numpy` headers. These files' 50 | location may be different on your build machine. The location 51 | is controlled with the `numpy` and `python` dependencies, which we 52 | explain in a second: 53 | 54 | ``` 55 | cc_library( 56 | name = "xgates", 57 | srcs = [ 58 | "xgates.cc", 59 | ], 60 | copts = [ 61 | "-O3", 62 | "-ffast-math", 63 | "-DNPY_NO_DEPRECATED_API", 64 | "-DNPY_1_7_API_VERSION", 65 | ], 66 | deps = [ 67 | "@third_party_numpy//:numpy", 68 | "@third_party_python//:python", 69 | ], 70 | ) 71 | ``` 72 | 73 | There is a subtlety about `bazel`: All headers must be within the 74 | source tree, or in `/usr/include/...` To work around this, we have to 75 | point `bazel` to the installation directories of `numpy` and `python`. 76 | 77 | To find the Python headers, you can run 78 | ``` 79 | python3 -c 'import distutils.sysconfig; print(distutils.sysconfig.get_python_inc())' 80 | ``` 81 | To find the numpy headers, you can run 82 | ``` 83 | python3 -c 'import numpy; print(numpy.get_include())'` 84 | ``` 85 | The specification for the external installations is in the `WORKSPACE` 86 | file. Point `path` to your installation's header files, 87 | excluding the final `include` part of the path. The `include` path is 88 | specified in the files `external/numpy.BUILD` and `external/python.BUILD`. 89 | 90 | *Hint*: For example for `numpy`, assume file `ndarraytype.h` is in a directory 91 | `.../numpy/core/include/numpy/ndarraytypes.h`. In the specification, 92 | copy everything up to and not including the `include/...` parts, with a trailing `/`. 93 | 94 | *Hint*: For `python` headers, assume `Python.h` is 95 | in a directory `.../include/python3.9/`. Include 96 | this path in the Python spec below, including the trailing `/`. 97 | 98 | ``` 99 | new_local_repository( 100 | name = "third_party_numpy", 101 | build_file = __workspace_dir__ + "/numpy.BUILD", 102 | # Configure: 103 | path = "/usr/local/lib/python3.7/dist-packages/numpy/core/", 104 | ) 105 | 106 | new_local_repository( 107 | name = "third_party_python", 108 | build_file = __workspace_dir__ + "/python.BUILD", 109 | # Configure: 110 | path = "/usr/include/python3.9", 111 | ) 112 | ``` 113 | 114 | Once `xgates` builds successfully, it is imported into `circuit.py`. At the top of this 115 | file is the import statement that might need to be adjusted: 116 | 117 | ``` 118 | # Configure: This line might have to change, depending on 119 | # the current build environment. 120 | # 121 | # Google internal: 122 | # import xgates 123 | # 124 | # GitHub Linux: 125 | import libxgates as xgates 126 | ``` 127 | 128 | Additionally, to enable Python to find the extension module, make sure 129 | to include in `PYTHONPATH` the directory where the generated 130 | `xgates.so` or `libxgates.so` is being generated. For example: 131 | 132 | ``` 133 | export PYTHONPATH=$PYTHONPATH:/home/usrname/qcc/bazel-bin/src/lib 134 | ``` 135 | 136 | `bazel` also attempts to use the Python 2 interpreter `python`. If it 137 | is not available on a system, install via: 138 | 139 | ``` 140 | sudo apt-get install python 141 | ``` 142 | 143 | ## Run 144 | To build the library and test for correct installation, go to `src/lib` and run: 145 | 146 | ``` 147 | bazel build all 148 | bazel test ... 149 | 150 | # Make sure to set PYTHONPATH (once): 151 | export PYTHONPATH=$PYTHONPATH:/home/usrname/qcc/bazel-bin/src/lib 152 | 153 | # Ensure xgates was built properly: 154 | bazel run circuit_test 155 | ``` 156 | 157 | Refer to the main page to learn how to run the individual algorithms. 158 | Typically, in the `qcc/src` directory, you would run something like: 159 | 160 | ``` 161 | for algo in `ls -1 *py | sed s@.py@@g` 162 | do 163 | bazel run $algo 164 | done 165 | ``` 166 | 167 | ## Minimal Setup 168 | If you can't get `xgates` to build, you can still run all Python algorithms by using the Python interpreter directly, for example: 169 | ``` 170 | python3 ./estimate_pi.py 171 | ``` 172 | All algorithms will work, they might just run much slower. 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /resources/README.MacOS.md: -------------------------------------------------------------------------------- 1 | These instructions may be helpful for MacOS. 2 | 3 | The first problem you encounter may be that the header Python.h cannot be found. 4 | It may be necessary to edit the file [`qcc/WORKSPACE`](WORKSPACE) and modify the 'external 5 | repository', pointing to your Python installation. For example: 6 | 7 | ``` 8 | [...] 9 | new_local_repository( 10 | name = "third_party_python", 11 | path = "[system path]/Python/3.7/include/python3.7m", 12 | build_file = __workspace_dir__ + "/python.BUILD", 13 | ) 14 | ``` 15 | 16 | With a corresponding file [`python.BUILD`](python.BUILD). You have to ensure that the paths 17 | are set according to your machine setup: 18 | 19 | ``` 20 | package( 21 | default_visibility = ["//visibility:public"] 22 | ) 23 | 24 | cc_library( 25 | name = "python", 26 | srcs = [ 27 | ], 28 | hdrs = glob([ 29 | "**/*.h", 30 | ]), 31 | includes = [""], 32 | ) 33 | ``` 34 | 35 | The BUILD file [`qcc/src/lib/BUILD`](src/lib/BUILD) should already point and use this external 36 | repository: 37 | 38 | ``` 39 | cc_library( 40 | name = "xgates", 41 | srcs = [ 42 | "xgates.cc", 43 | ], 44 | copts = [ 45 | "-O3", 46 | "-ffast-math", 47 | "-march=skylake", 48 | "-DNPY_NO_DEPRECATED_API", 49 | "-DNPY_1_7_API_VERSION", 50 | ], 51 | deps = [ 52 | "@third_party_numpy//:numpy", 53 | "@third_party_python//:python", 54 | ], 55 | ) 56 | ``` 57 | 58 | On MacOS it appears to make a difference whether or not the command-line option `-c opt` is passed 59 | to build targets. For example the file [`runall.sh`](src/runall.sh) uses this flag. 60 | 61 | Run these commands to verify things work as expected. Replace ... with the appropriate path in your system. 62 | 63 | ``` 64 | # This should build libqgates.so in .../qcc/bazel-bin/src/lib 65 | # Some systems require 66 | # bazel build -c opt [target] 67 | # on all build/run targets. 68 | 69 | cd .../qcc/src/lib 70 | bazel build xgates 71 | 72 | # Set PYTHONPATH to point to this directory 73 | export PYTHONPATH=.../qcc/bazel-bin/src/lib 74 | 75 | # Test it 76 | bazel run circuit_test 77 | 78 | # Test all tests 79 | bazel test ... 80 | 81 | # Run all the algorithms 82 | cd .../qcc/src 83 | ./runall.sh 84 | -------------------------------------------------------------------------------- /resources/README.SageMath.md: -------------------------------------------------------------------------------- 1 | These instructions may be helpful for running this code base in SageMath. 2 | 3 | SageMath is based on Python and it is comparatively easy to run 4 | the code in this project under SageMath, either via command-line 5 | or interactively. 6 | 7 | To run a particular algorithm on the command-line, simply run something like: 8 | 9 | ``` 10 | $ sage deutsch.py # or any of the other algorithms. 11 | ``` 12 | 13 | To start an interactive SageMath session with all the code preloaded, including 14 | the accelerator library (`xgates`), this may be a way to do it: 15 | #### Prepare a file `startup.py` 16 | 17 | Create a file `startup.py` (or any other name of your chosing). 18 | This file should import all the source files in the `src/lib` directory. 19 | For example: 20 | ``` 21 | """Initialize and load all qcc packages.""" 22 | 23 | # pylint: disable=unused-import 24 | import numpy as np 25 | 26 | from src.lib import bell 27 | from src.lib import circuit 28 | from src.lib import helper 29 | from src.lib import ops 30 | from src.lib import state 31 | from src.lib import tensor 32 | print('QCC: ' + __file__ + ': initialized') 33 | ``` 34 | 35 | #### Set environment 36 | Once the file is setup, point the `PYTHONSTARTUP` environment variable to it 37 | (also set `PYTHONPATH` to find the accelerated `xgates` library). For example (with similar constructions on Windows): 38 | ``` 39 | export PYTHONPATH=$HOME/qcc:$HOME/qcc/bazel-bin/src/lib 40 | export PYTHONSTARTUP=$HOME/qcc/src/lib/startup.py 41 | ``` 42 | 43 | #### Start SageMath 44 | On startup, the file will be preloaded and SageMath should print something like this, ready for an interactive session. 45 | ``` 46 | $ sage 47 | SageMath version 9.4, Release Date: 2021-08-22 48 | QCC: /usr/local/google/home/rhundt/qcc/src/lib/startup.py: initialized 49 | sage: psi = state.bitstring(1, 0, 1, 1) 50 | sage: psi 51 | State([0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 52 | 0.+0.j 1.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j]) 53 | sage: psi = ops.PauliX()(psi, 1) 54 | sage: psi 55 | State([0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 56 | 0.+0.j 0.+0.j 0.+0.j 0.+0.j 0.+0.j 1.+0.j]) 57 | sage: 58 | ``` 59 | -------------------------------------------------------------------------------- /resources/README.Windows.md: -------------------------------------------------------------------------------- 1 | These instructions may be helpful for Windows, which is currently only _partially_ supported: 2 | * You **can** run **all** algorithms and tests 3 | * `blaze test ...` does **not** work currently 4 | * The C++ accelerated library `libxgates` is currently **not** compiled to a DLL. Hence all code runs via Pythonm which is typically not a problem, except for Shor's algorithm (which will run very slowly). 5 | 6 | You have to ensure that you have installed `bazel` [(installation instructions)](https://bazel.build/install/windows) and `Python` [(installation instructions)](https://www.python.org/downloads/). With `Python`, you need the following packages, which can all be installed via `pip install `: 7 | * absl-py 8 | * numpy 9 | * scipy 10 | 11 | Edit the `WORKSPACE` file in the root directory and adjust the paths according to your installation and following Windows' path syntax. For example (for Robert's current installation - your directories may be different): 12 | ``` 13 | new_local_repository( 14 | name = "third_party_python", 15 | build_file = __workspace_dir__ + "/python.BUILD", 16 | # Configure: 17 | path = "C:\\Program Files\\Python37\\include" 18 | ) 19 | 20 | new_local_repository( 21 | name = "third_party_numpy", 22 | build_file = __workspace_dir__ + "/numpy.BUILD", 23 | # Configure: 24 | path = "C:\\Users\\robert_hundt\\AppData\\Roaming\\Python\\Python37\\site-packages\\numpy\\core" 25 | ) 26 | ``` 27 | 28 | Finally, point the environment variable `PYTHONPATH` to the root directory. For example, for `cmd.exe`: 29 | ``` 30 | set PYTHONPATH = "C:\Users\robert_hundt\qcc" 31 | ``` 32 | for Powershell: 33 | ``` 34 | $Env:PYTHONPATH = "C:\Users\robert_hundt\qcc" 35 | ``` 36 | 37 | With this, you can run everything, for example: 38 | ``` 39 | qcc $ cd src 40 | qcc/src $ bazel run deutsch # and all other algos 41 | qcc/src $ cd lib 42 | qcc/src/lib $ bazel run bell_test # and all other tests 43 | ``` 44 | -------------------------------------------------------------------------------- /resources/README.buildxgates.md: -------------------------------------------------------------------------------- 1 | # Building libxgates.so 2 | 3 | The book described how to accelerate Python with a C++ library and this 4 | document described how to build this library. 5 | 6 | #### Ingredients 7 | The main source file for the library is in `src/lib/xgates.cc`. It has dependencies on Python headers, the Python library, 8 | and the numpy headers. 9 | 10 | To find the Python headers, you can run 11 | ``` 12 | python3 -c 'import distutils.sysconfig; print(distutils.sysconfig.get_python_inc())' 13 | ``` 14 | 15 | To find the numpy headers, you can run 16 | ``` 17 | python3 -c 'import numpy; print(numpy.get_include())'` 18 | ``` 19 | 20 | The Python library will be somewhere in the neighborhood of these directories or in standard 21 | Linux directories, eg 22 | ``` 23 | LIB=/usr/lib/python3.11/config-3.11-x86_64-linux-gnu/libpython3.11.so 24 | ``` 25 | 26 | Once these are found, you can build the library manually. All these steps are 27 | in a script called [`qcc/make_libxgates.sh`](../make_libxgates.sh). It is recommended to just modify 28 | this script to build the library on your system. 29 | 30 | The script determines the compiler option to build a loadable module (eg., `-shared`) and 31 | calls the compiler to build `qcc/libxgates.so`. For example: 32 | ``` 33 | OUT=./libxgates.so 34 | cc -I${NUMPY} -I${PY} ${LIB} -O3 -ffast-math -DNPY_NO_DEPRECATED_API \ 35 | -fPIC -std=c++0x ${SHARED} -o ${OUT} \ 36 | src/lib/xgates.cc || exit 1 37 | ``` 38 | 39 | This builds the library in the root directory, which means you have to point the environment variable 40 | `PYTHONPATH` to this directory (which you have to do anyways in order to import the other 41 | Python modules). 42 | -------------------------------------------------------------------------------- /resources/README.md: -------------------------------------------------------------------------------- 1 | # Additional Resources 2 | 3 | ## Readme Files 4 | All the README files for the various platforms are located in this directory, 5 | including documentation for the script to build `libxgates.so` 6 | 7 | -------------------------------------------------------------------------------- /src/amplitude_estimation.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Amplitude Estimation.""" 3 | 4 | import math 5 | import random 6 | from typing import List 7 | 8 | from absl import app 9 | import numpy as np 10 | from src.lib import helper 11 | from src.lib import ops 12 | from src.lib import state 13 | 14 | 15 | # Amplitude estimation (AE) is a generalization of the counting 16 | # algorithm (or, rather, counting is a special case of AE). 17 | # 18 | # In counting, we have a state in equal superposition 19 | # (achieved via Hadamard^\otimes(nbits) where some of the states 20 | # are 'good' (solutions) and the rest are 'bad' (non-solutions). 21 | # 22 | # In the general case, the probabilities for each state can be 23 | # different. A general algorithm A generates a state. Then, similar 24 | # to grover, one can think of the space that the orthogonal good 25 | # and bad states span as: 26 | # \psi = \alpha \psi_{good} + \beta \psi_{bad} 27 | # 28 | # AE estimates this amplitude \alpha. 29 | 30 | 31 | def make_f(nbits: int, solutions: List[int]): 32 | """Construct function that will return 1 for 'solutions' bits.""" 33 | 34 | answers = np.zeros(1 << nbits, dtype=np.int32) 35 | answers[solutions] = 1 36 | return lambda bits: answers[helper.bits2val(bits)] 37 | 38 | 39 | def run_experiment(nbits_phase: int, 40 | nbits_grover: int, 41 | algo: ops.Operator, 42 | solutions: List[int]) -> float: 43 | """Run full experiment for a given A and set of solutions.""" 44 | 45 | # The state for the AE algorithm. 46 | # We reserve nbits_phase for the phase estimation. 47 | # We reserve nbits_grover for the oracle. 48 | # We also add the |1> for the oracle's y value. 49 | # 50 | # These numbers can be adjusted to achieve various levels 51 | # of accuracy. 52 | psi = state.zeros(nbits_phase + nbits_grover) * state.ones(1) 53 | 54 | # Apply Hadamard to all the qubits. 55 | psi = ops.Hadamard(nbits_phase + nbits_grover + 1)(psi) 56 | 57 | # Construct the Grover operator. First phase invesion via Oracle. 58 | f = make_f(nbits_grover, solutions) 59 | u = ops.OracleUf(nbits_grover + 1, f) 60 | 61 | # Reflection over mean. 62 | op_zero = ops.ZeroProjector(nbits_grover) 63 | reflection = op_zero * 2.0 - ops.Identity(nbits_grover) 64 | 65 | # Now construct the combined Grover operator. 66 | inversion = algo.adjoint()(reflection(algo)) * ops.Identity() 67 | grover = inversion(u) 68 | 69 | # Now that we have the Grover operator, we apply phase estimation. 70 | psi = ops.PhaseEstimation(grover, psi, nbits_phase, nbits_phase) 71 | 72 | # Reverse QFT gives us the phase as a fraction of 2*pi. 73 | psi = ops.Qft(nbits_phase).adjoint()(psi) 74 | 75 | # Get the state with highest probability and estimate a phase. 76 | maxbits, _ = psi.maxprob() 77 | ampl = np.sin(np.pi * helper.bits2frac(maxbits[:nbits_phase])) 78 | 79 | print(' AE: ampl: {:.2f} prob: {:5.1f}% {}/{} solutions ({})' 80 | .format(ampl, ampl * ampl * 100, len(solutions), 81 | 1 << nbits_grover, solutions)) 82 | return ampl 83 | 84 | 85 | def main(argv): 86 | if len(argv) > 1: 87 | raise app.UsageError('Too many command-line arguments.') 88 | print('Amplitude Estimation...') 89 | 90 | # Equal superposition. 91 | print('Algorithm: Hadamard (equal superposition)') 92 | algorithm = ops.Hadamard(3) 93 | for nsolutions in range(9): 94 | ampl = run_experiment(7, 3, algorithm, 95 | random.sample(range(2**3), nsolutions)) 96 | if not math.isclose(ampl, np.sqrt(nsolutions / 2**3), abs_tol=0.02): 97 | raise AssertionError('Incorrect AE.') 98 | 99 | # Make a somewhat random algorithm (and state). 100 | print('Algorithm: Random (unequal superposition), single solution') 101 | i1 = ops.Identity(1) 102 | algorithm = (ops.Hadamard(3) @ 103 | (ops.RotationY(random.random()/2) * i1 * i1) @ 104 | (i1 * ops.RotationY(random.random()/2) * i1) @ 105 | (i1 * i1 * ops.RotationY(random.random()/2))) 106 | psi = algorithm(state.zeros(3)) 107 | for i in range(len(psi)): 108 | ampl = run_experiment(7, 3, algorithm, [i]) 109 | if not np.allclose(ampl, psi[i], atol=0.02): 110 | raise AssertionError('Incorrect AE.') 111 | 112 | # Accumulative amplitude computation. 113 | print('Algorithm: Random (unequal superposition), multiple solutions') 114 | for i in range(len(psi) + 1): 115 | ampl = run_experiment(7, 3, algorithm, [i for i in range(i)]) 116 | if not np.allclose(ampl, np.sqrt(sum([p*p.conj() for p in psi[0:i]])), 117 | atol=0.02): 118 | raise AssertionError('Incorrect AE.') 119 | 120 | 121 | if __name__ == '__main__': 122 | app.run(main) 123 | -------------------------------------------------------------------------------- /src/arith_classic.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Arithmetic with Quantum Circuits used classically.""" 3 | 4 | import math 5 | 6 | from absl import app 7 | 8 | from src.lib import circuit 9 | from src.lib import ops 10 | from src.lib import state 11 | 12 | 13 | # We want to control this (non-quantum-effect-exploiting) circuit: 14 | # 15 | # a ----o-----o---o------- 16 | # b ----|--o--o---|--o---- 17 | # cin ----|--|--|---o--o--o- 18 | # sum ----X--X--|---|--|--X- 19 | # cout ----------X---X--X---- 20 | # 21 | # For educational purposes, we construct this circuit using the 22 | # basic Operators in matrix form as well as the more compact 23 | # circuit form. 24 | 25 | 26 | def fulladder_qc(qc: circuit.qc): 27 | """Non-quantum-exploiting, classic full adder.""" 28 | 29 | qc.cx(0, 3) 30 | qc.cx(1, 3) 31 | qc.ccx(0, 1, 4) 32 | qc.ccx(0, 2, 4) 33 | qc.ccx(1, 2, 4) 34 | qc.cx(2, 3) 35 | 36 | 37 | def fulladder_matrix(psi: state.State): 38 | """Non-quantum-exploiting, classic full adder.""" 39 | 40 | psi = ops.Cnot(0, 3)(psi, 0) 41 | psi = ops.Cnot(1, 3)(psi, 1) 42 | psi = ops.ControlledU(0, 1, ops.Cnot(1, 4))(psi, 0) 43 | psi = ops.ControlledU(0, 2, ops.Cnot(2, 4))(psi, 0) 44 | psi = ops.ControlledU(1, 2, ops.Cnot(2, 4))(psi, 1) 45 | psi = ops.Cnot(2, 3)(psi, 2) 46 | return psi 47 | 48 | 49 | def experiment_qc(a: int, b: int, cin: int, 50 | expected_sum: int, expected_cout: int): 51 | """Run a simple classic experiment, check results.""" 52 | 53 | qc = circuit.qc('classic') 54 | 55 | qc.bitstring(a, b, cin, 0, 0) 56 | fulladder_qc(qc) 57 | 58 | bsum, _ = qc.measure_bit(3, tostate=1, collapse=False) 59 | bout, _ = qc.measure_bit(4, tostate=1, collapse=False) 60 | print(f'a: {a} b: {b} cin: {cin} sum: {bsum:.0f} cout: {bout:.0f}') 61 | if (not math.isclose(bsum, expected_sum, abs_tol=1e-5) or 62 | not math.isclose(bout, expected_cout, abs_tol=1e-5)): 63 | raise AssertionError('invalid results') 64 | 65 | 66 | def experiment_matrix(a: int, b: int, cin: int, 67 | expected_sum: int, expected_cout: int): 68 | """Run a simple classic experiment, check results.""" 69 | 70 | psi = state.bitstring(a, b, cin, 0, 0) 71 | psi = fulladder_matrix(psi) 72 | 73 | bsum, _ = ops.Measure(psi, 3, tostate=1, collapse=False) 74 | bout, _ = ops.Measure(psi, 4, tostate=1, collapse=False) 75 | print(f'a: {a} b: {b} cin: {cin} sum: {bsum:.0f} cout: {bout:.0f}') 76 | if (not math.isclose(bsum, expected_sum) or 77 | not math.isclose(bout, expected_cout)): 78 | raise AssertionError('invalid results') 79 | 80 | 81 | def add_classic(): 82 | """Full eval of the full adder.""" 83 | 84 | for exp_function in [experiment_matrix, experiment_qc]: 85 | exp_function(0, 0, 0, 0, 0) 86 | exp_function(0, 1, 0, 1, 0) 87 | exp_function(1, 0, 0, 1, 0) 88 | exp_function(1, 1, 0, 0, 1) 89 | exp_function(0, 0, 1, 1, 0) 90 | exp_function(0, 1, 1, 0, 1) 91 | exp_function(1, 0, 1, 0, 1) 92 | exp_function(1, 1, 1, 1, 1) 93 | 94 | 95 | def main(argv): 96 | if len(argv) > 1: 97 | raise app.UsageError('Too many command-line arguments.') 98 | add_classic() 99 | 100 | 101 | if __name__ == '__main__': 102 | app.run(main) 103 | -------------------------------------------------------------------------------- /src/bell_basis.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Decompose states in Bell basis.""" 3 | 4 | from absl import app 5 | import numpy as np 6 | 7 | from src.lib import bell 8 | from src.lib import state 9 | 10 | 11 | # Decompose a state into the Bell basis: 12 | # psi = c_0 * b_00 + c1 * b_01 + c2 * b_10 + c3 * b_11. 13 | # 14 | # We produce a random state 'psi' first. 15 | # 16 | # Then we compute the inner product between psi and 17 | # all of the four Bell states (the Bell basis) to compute 18 | # the factors c_i. 19 | # 20 | # Finally, we reconstruct the state by multiplying and 21 | # adding the factor with the respective Bell states. 22 | # 23 | # This new state and the original state must be identical. 24 | # 25 | def run_experiment(): 26 | """Run an single state decomposition.""" 27 | 28 | psi = np.random.random([4]) + np.random.random([4]) * 1j 29 | psi = state.State(psi / np.linalg.norm(psi)) 30 | 31 | bells = [bell.bell_state(0, 0), 32 | bell.bell_state(0, 1), 33 | bell.bell_state(1, 0), 34 | bell.bell_state(1, 1)] 35 | 36 | c = [0] * 4 37 | for idx, b in enumerate(bells): 38 | c[idx] = np.inner(psi, b) 39 | 40 | new_psi = [0] * 4 41 | for idx in range(4): 42 | new_psi = new_psi + c[idx] * bells[idx] 43 | 44 | assert np.allclose(psi, new_psi), 'Incorrect result.' 45 | 46 | 47 | def main(argv): 48 | if len(argv) > 1: 49 | raise app.UsageError('Too many command-line arguments.') 50 | print('Express 1000 random states in the Bell basis.') 51 | 52 | for _ in range(1000): 53 | run_experiment() 54 | 55 | 56 | if __name__ == '__main__': 57 | app.run(main) 58 | -------------------------------------------------------------------------------- /src/benchmarks/BUILD: -------------------------------------------------------------------------------- 1 | py_library( 2 | name = "qcall", 3 | srcs = [], 4 | deps = [ 5 | "//src/lib:bell", 6 | "//src/lib:circuit", 7 | "//src/lib:helper", 8 | "//src/lib:ops", 9 | "//src/lib:optimizer", 10 | "//src/lib:state", 11 | "//src/lib:tensor", 12 | ], 13 | ) 14 | 15 | py_binary( 16 | name = "larose_benchmark", 17 | srcs = ["larose_benchmark.py"], 18 | python_version = "PY3", 19 | srcs_version = "PY3", 20 | deps = [ 21 | ":qcall", 22 | ], 23 | ) 24 | 25 | py_binary( 26 | name = "tensor_math", 27 | srcs = ["tensor_math.py"], 28 | python_version = "PY3", 29 | srcs_version = "PY3", 30 | deps = [ 31 | ":qcall", 32 | ], 33 | ) 34 | -------------------------------------------------------------------------------- /src/benchmarks/larose_benchmark.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """A benchmark based on the paper from Ryan LaRose.""" 3 | 4 | # The paper can be found here: 5 | # Overview and Comparison of Gate Level Quantum Software Platforms 6 | # https://arxiv.org/pdf/1807.02500.pdf 7 | # 8 | # We write the benchmark in Python and then generated the various flavors 9 | # from it, eg., libq, projectq, qasm, etc. 10 | 11 | import random 12 | 13 | from absl import app 14 | from absl import flags 15 | 16 | from src.lib import circuit 17 | 18 | flags.DEFINE_integer('nbits', 28, 'Number of Qubits') 19 | flags.DEFINE_integer('depth', 28, 'Depth of Circuit') 20 | 21 | # Informal benchmarking on my workstation shows that 22 | # this xgate accelerated benchmark runs in about: 23 | # 24 | # qubit time 25 | # 26 7 secs 26 | # 27 14 secs 27 | # 28 29 secs 28 | # 29 58 secs 29 | # 30 122 secs 30 | # 31 | # A 28/28 libq based circuit runs in about 3 seconds. But that's 32 | # because this (kind of silly) benchmark does not introduce a 33 | # lot of states with non-zero amplitudes. In other words, 34 | # this is a good benchmark for full-state sims, but not for 35 | # simulations based on spare representations, like libq. 36 | 37 | 38 | def main(argv): 39 | if len(argv) > 1: 40 | raise app.UsageError('Too many command-line arguments.') 41 | 42 | print(f'LaRose benchmark with {flags.FLAGS.nbits} qubits, ' + 43 | f'depth: {flags.FLAGS.depth}...') 44 | 45 | qc = circuit.qc(eager=False) 46 | qc.reg(flags.FLAGS.nbits, random.randint(0, 2^flags.FLAGS.nbits), name='q') 47 | 48 | for d in range(flags.FLAGS.depth): 49 | print(f' depth: {d}') 50 | for bit in range(flags.FLAGS.nbits): 51 | qc.h(bit) 52 | qc.v(bit) 53 | if bit > 0: 54 | qc.cx(bit, 0) 55 | qc.dump_to_file() 56 | 57 | 58 | if __name__ == '__main__': 59 | app.run(main) 60 | -------------------------------------------------------------------------------- /src/bernstein.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Bernstein-Vasirani Algorithm.""" 3 | 4 | from typing import Tuple 5 | from absl import app 6 | import numpy as np 7 | from src.lib import helper 8 | from src.lib import ops 9 | from src.lib import state 10 | 11 | # The goal of this experiment is as follows. There is 'secret' string 12 | # in the Oracle Uf, such that the input bit string and this secret 13 | # string compute a dot product modulo 2, resulting in 1. For example, 14 | # 15 | # Secret String: 0, 1, 0 16 | # |----| 17 | # |0> --- | | 18 | # |1> --- | Uf | -- 0 or 1 as in (i0*o0 + i1*o1 + i1*o2) = 1 19 | # |1> --- | | 20 | # |----| 21 | # 22 | # On a classical computer, one would have to try one by one, with 23 | # input strings that just have 1 bit set, thus requiring N queries. 24 | # In the quantum case, the answer can be found in just 1 query. 25 | # 26 | # The code shows two ways to achieve this results, one with 27 | # an explicit Uf construction, one using the Deutsch OracleUf. 28 | 29 | 30 | def check_result(nbits: int, c: Tuple[int, ...], psi: state.State) -> None: 31 | """Check expected vs achieved results.""" 32 | 33 | # The state with the 'flipped' bits will have probability 1.0. 34 | # It will be found on the very first try. 35 | # 36 | print('Expect:', c) 37 | for bits in helper.bitprod(nbits): 38 | if psi.prob(*bits) > 0.1: 39 | print(f'Found : {bits[:-1]}, with prob: {psi.prob(*bits):.1f}') 40 | assert bits[:-1] == c, 'Invalid result' 41 | 42 | 43 | def make_c(nbits: int) -> Tuple[int, ...]: 44 | """Make a random constant c from {0,1}, which we try to find.""" 45 | 46 | constant_c = [0] * nbits 47 | for idx in range(nbits - 1): 48 | constant_c[idx] = int(np.random.random() < 0.5) 49 | return tuple(constant_c) 50 | 51 | 52 | def make_u(nbits: int, constant_c: Tuple[int, ...]) -> ops.Operator: 53 | """Make general Bernstein Oracle.""" 54 | 55 | # For each '1' at index i in the constant_c, build a Cnot from 56 | # bit 0 to the bottom bit. For example for string |101> 57 | # 58 | # |0> --- H --- o-------- 59 | # |0> --- H ----|-------- 60 | # |0> --- H ----|-- o --- 61 | # | | 62 | # |1> --- H --- X - X --- 63 | # 64 | op = ops.Identity(nbits) 65 | for idx in range(nbits - 1): 66 | if constant_c[idx]: 67 | op = ops.Identity(idx) * ops.Cnot(idx, nbits - 1) @ op 68 | 69 | # Note that in the |+> basis, a cnot is the same as a single Z-gate. 70 | # Hence, this would also work: 71 | # op = (ops.Identity(idx) * ops.PauliZ() * 72 | # ops.Identity(nbits - 1 - idx)) @ op 73 | 74 | assert op.is_unitary(), 'Constructed non-unitary operator.' 75 | return op 76 | 77 | 78 | def run_experiment(nbits: int) -> None: 79 | """Run full experiment for a given number of bits.""" 80 | 81 | c = make_c(nbits - 1) 82 | u = make_u(nbits, c) 83 | 84 | psi = state.zeros(nbits - 1) * state.ones(1) 85 | psi = ops.Hadamard(nbits)(psi) 86 | psi = u(psi) 87 | psi = ops.Hadamard(nbits)(psi) 88 | 89 | check_result(nbits, c, psi) 90 | 91 | 92 | # Alternative way to achieve the same result, using the 93 | # Deutsch Oracle Uf. 94 | # 95 | def make_oracle_f(c: Tuple[int, ...]) -> ops.Operator: 96 | """Return a function computing the dot product mod 2 of bits, c.""" 97 | 98 | def f(bit_string: Tuple[int, ...]) -> int: 99 | val = 0 100 | for idx, v in enumerate(bit_string): 101 | val += c[idx] * v 102 | return val % 2 103 | 104 | return f 105 | 106 | 107 | def run_oracle_experiment(nbits: int) -> None: 108 | """Run full experiment for a given number of nbits bits.""" 109 | 110 | c = make_c(nbits - 1) 111 | f = make_oracle_f(c) 112 | u = ops.OracleUf(nbits, f) 113 | 114 | psi = state.zeros(nbits - 1) * state.ones(1) 115 | psi = ops.Hadamard(nbits)(psi) 116 | psi = u(psi) 117 | psi = ops.Hadamard(nbits)(psi) 118 | 119 | check_result(nbits, c, psi) 120 | 121 | 122 | def main(argv): 123 | assert len(argv) <= 1, 'Too many command-line parameters' 124 | 125 | for _ in range(5): 126 | run_experiment(8) 127 | run_oracle_experiment(8) 128 | 129 | 130 | if __name__ == '__main__': 131 | app.run(main) 132 | -------------------------------------------------------------------------------- /src/chsh.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: CHSH implementation and measurement.""" 3 | 4 | import random 5 | 6 | from absl import app 7 | import numpy as np 8 | from src.lib import bell 9 | from src.lib import helper 10 | from src.lib import ops 11 | from src.lib import state 12 | 13 | 14 | # The CHSH game, named after Clauser, Horne, Shimony, and Holt, 15 | # https://journals.aps.org/prl/pdf/10.1103/PhysRevLett.23.880 16 | # is a simplified form of the Bell equation and is used to 17 | # demonstrate the power of entanglement. 18 | # 19 | # As Prof. Vazirani says, we should think of entanglement as a 20 | # resource that allows us to compute certain things better or 21 | # faster than with classical resources. 22 | # 23 | # In the CHSH game, both Alice and Bob receive random bits 24 | # $x$ and $y$ from a referee Charlie. Based on the bit values and 25 | # a strategy discussed between Alice and Bob beforehand they will 26 | # respond with bit values $a$ and $b$. During the game, Alice and 27 | # Bob cannot communicate. The goal of the game is to produce matching 28 | # bits $a$ and $b$, except when both $x = y = 1$. In this case 29 | # $a$ and $b$ must differ. 30 | # 31 | # In closed form, the winning condition can be written as: 32 | # if x * y == (a + b) % 2: 33 | # wins += 1 34 | # 35 | # The best possible classical strategy for both Alice and Bob is 36 | # to always respond with a 0, which leads to a $3/4$ success probability. 37 | # Just using entanglement also doesn't work - on measurement, both Alice's 38 | # and Bob's qubit will produce a matching value and this also represents 39 | # a winning percentage of 3/4 (this strategy doesn't handle the [1, 1] 40 | # case). 41 | # 42 | # In the quantum case, Alice and Bob share an entangled qubit in the state 43 | # $\psi = 1/\sqrt{2}(|0_A0_B\rangle + |1_A1_B\rangle)$. 44 | # When Alice receives a bit $x = 0$ she measures in the 45 | # $|0\rangle, |1\rangle$ basis 46 | # and if she gets $x = 1$ she measures in the Hadamard 47 | # $|+\rangle, |-\rangle$ basis. 48 | # Correspondingly, if Bob receives $y = 0$ he measure in 49 | # $|a_0\rangle, |a_1\rangle$, where 50 | # $a_0 = \cos(\pi/8)|0\rangle + \sin(\pi/8)|1\rangle$ 51 | # $a_1 = -\sin(\pi/8)|0\rangle + \cos(\pi/8)|1\rangle$ 52 | # If he receives $y = 1$ he measures in 53 | # $|b_0\rangle, |b_1\rangle$, where 54 | # $b_0 = \cos(\pi/8)|0\rangle - \sin(\pi/8)|1\rangle$ 55 | # $b_1 = \sin(\pi/8)|0\rangle + \cos(\pi/8)|1\rangle$ 56 | # 57 | # All of the measurement bases are rotated bt $\pi/8$ from each other. 58 | # With this, it can be shown that the success probability increases from 59 | # 3/4 to cos^2 pi/8 (= ~0.86). Let's try this out here with simulated 60 | # random measurements in the various bases. 61 | 62 | 63 | def measure(psi: state.State): 64 | """Simulated, probabilistic measurement.""" 65 | 66 | # We assume that random numbers are evenly distributed, 67 | # which will ensure that states are selected weighted 68 | # by their probabilities. 69 | # 70 | r = random.random() - 1e-6 71 | total = 0 72 | for idx, val in enumerate(psi): 73 | total += val * val.conj() 74 | if r < total: 75 | bits = helper.val2bits(idx, 2) 76 | return bits[0], bits[1] 77 | 78 | 79 | def run_experiments(experiments: int, alpha: float) -> float: 80 | """Run CHSH experiments for a given angle.""" 81 | 82 | wins = 0 83 | for _ in range(experiments): 84 | x = random.randint(0, 1) 85 | y = random.randint(0, 1) 86 | psi = bell.bell_state(0, 0) 87 | 88 | if x == 0: 89 | pass 90 | if x == 1: 91 | psi = ops.RotationY(2.0 * alpha)(psi, 0) 92 | if y == 0: 93 | psi = ops.RotationY(alpha)(psi, 1) 94 | if y == 1: 95 | psi = ops.RotationY(-alpha)(psi, 1) 96 | 97 | a, b = measure(psi) 98 | if x * y == (a + b) % 2: 99 | wins += 1 100 | 101 | return wins / experiments * 100.0 102 | 103 | 104 | def main(argv): 105 | if len(argv) > 1: 106 | raise app.UsageError('Too many command-line arguments.') 107 | 108 | print('Quantum CHSH evaluation.') 109 | 110 | # Compute results for optimal value. 111 | # 112 | percent = run_experiments(1000, 2.0 * np.pi / 8) 113 | print(f'Optimal Angle 2 pi / 8, winning: {percent:.1f}%') 114 | assert percent > 80.0, 'Incorrect result, should reach above 80%' 115 | 116 | # Run a few incrementals and see how the results change. 117 | # 118 | steps = 32 119 | inc_angle = (2.0 * np.pi / 8) / (steps / 2) 120 | for i in range(0, 66, 2): 121 | percent = run_experiments(500, inc_angle * i) 122 | s = '(opt)' if i == 16 else '' 123 | print( 124 | f'{i:2d} * Pi/64 = {inc_angle * i:.2f}: winning: {percent:5.2f}% ' 125 | f'{"#" * int(percent/3)}{s}' 126 | ) 127 | 128 | 129 | if __name__ == '__main__': 130 | app.run(main) 131 | -------------------------------------------------------------------------------- /src/counting.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Quantum Counting - Find # of solutions for Grover.""" 3 | 4 | import math 5 | import random 6 | 7 | from absl import app 8 | import numpy as np 9 | from src.lib import helper 10 | from src.lib import ops 11 | from src.lib import state 12 | 13 | # This algorithm is an interesting combination of Grover's search 14 | # and phase estimation. Unlike Grover Search, which attempts to find 15 | # the one special x with f(x) = 1, Quantum Counting tells us 16 | # for how many x's the function returns 1. How many special elements are 17 | # in the dataset, or the function? 18 | # 19 | # In essence we cleverly construct a phase estimation circuit with 20 | # the Grover operator as it's unitary. 21 | # 22 | # We will find the phase phi and from it we estimate the number 23 | # M of solutions out of the N element solution space, with: 24 | # 25 | # sin(phi/2) = sqrt(M/N) 26 | # M = N * sin(phi/2)^2 27 | 28 | 29 | def make_f(d: int = 3, nsolutions: int = 1): 30 | """Construct function that will return 1 for 'solutions' bits.""" 31 | 32 | answers = np.zeros(1 << d, dtype=np.int32) 33 | solutions = random.sample(range(1 << d), nsolutions) 34 | answers[solutions] = 1 35 | return lambda bits: answers[helper.bits2val(bits)] 36 | 37 | 38 | def run_experiment(nbits_phase: int, nbits_grover: int, solutions: int) -> None: 39 | """Run full experiment for a given number of solutions.""" 40 | 41 | # The state for the counting algorithm. 42 | # We reserve nbits for the phase estimation. 43 | # We also reserve nbits for the oracle. 44 | # These numbers could be adjusted to achieve better 45 | # accuracy. Yet, this keeps the code a little bit simpler, 46 | # while trading off a few off-by-1 estimation errors. 47 | # 48 | # We also add the |1> for the oracle. 49 | # 50 | psi = state.zeros(nbits_phase + nbits_grover) * state.ones(1) 51 | 52 | # Apply Hadamard to all the qubits. 53 | for i in range(nbits_phase + nbits_grover + 1): 54 | psi.apply1(ops.Hadamard(), i) 55 | 56 | # Construct the Grover operator. First phase inversion via Oracle. 57 | f = make_f(nbits_grover, solutions) 58 | u = ops.OracleUf(nbits_grover + 1, f) 59 | 60 | # Reflection over mean. 61 | op_zero = ops.ZeroProjector(nbits_grover) 62 | reflection = op_zero * 2.0 - ops.Identity(nbits_grover) 63 | 64 | # Now construct the combined Grover operator, using 65 | # Hadamards as the 'algorithm' (equal superposition). 66 | hn = ops.Hadamard(nbits_grover) 67 | inversion = hn(reflection(hn)) * ops.Identity() 68 | grover = inversion(u) 69 | 70 | # Now that we have the Grover operator, we have to perform 71 | # phase estimation. This loop is a copy from phase_estimation.py 72 | # with more comments there. 73 | cu = grover 74 | for inv in reversed(range(nbits_phase)): 75 | psi = ops.ControlledU(inv, nbits_phase, cu)(psi, inv) 76 | cu = cu(cu) 77 | 78 | # Reverse QFT gives us the phase as a fraction of 2*pi. 79 | psi = ops.Qft(nbits_phase).adjoint()(psi) 80 | 81 | # Get the state with highest probability and compute the phase 82 | # as a binary fraction. Note that the probability decreases 83 | # as M, the number of solutions, gets closer and closer to N, 84 | # the total mnumber of states. 85 | maxbits, maxprob = psi.maxprob() 86 | phi_estimate = helper.bits2frac(maxbits) 87 | 88 | # We know that after phase estimation, this holds: 89 | # sin(phi/2) = sqrt(M/N) 90 | # M = N * sin(phi/2)^2 91 | # Hence we can compute M. We keep the result to 2 digit to visualize 92 | # the errors. Note that the phi_estimate is a fraction of 2*PI, hence 93 | # the 1/2 in above formula cancels out against the 2 and we compute: 94 | m = round(2**nbits_grover * math.sin(phi_estimate * math.pi) ** 2, 2) 95 | 96 | print( 97 | f'Estimate: {phi_estimate:.4f} prob: {maxprob * 100.0:5.2f}% ' 98 | f'--> m: {m:5.2f}, want: {solutions:2d}' 99 | ) 100 | assert np.allclose(np.round(m), solutions), 'Incorrect result.' 101 | 102 | 103 | def main(argv): 104 | if len(argv) > 1: 105 | raise app.UsageError('Too many command-line arguments.') 106 | 107 | for solutions in range(1, 6): 108 | run_experiment(7, 4, solutions) 109 | 110 | 111 | if __name__ == '__main__': 112 | app.run(main) 113 | -------------------------------------------------------------------------------- /src/deutsch.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Deutsch's Algorithm.""" 3 | 4 | import math 5 | from typing import Callable 6 | 7 | from absl import app 8 | import numpy as np 9 | from src.lib import ops 10 | from src.lib import state 11 | 12 | 13 | def make_f(flavor: int) -> Callable[[int], int]: 14 | """Return a 1-bit constant or balanced function f. 4 flavors.""" 15 | 16 | # The 4 versions are: 17 | # f(0) -> 0, f(1) -> 0 constant 18 | # f(0) -> 0, f(1) -> 1 balanced 19 | # f(0) -> 1, f(1) -> 0 balanced 20 | # f(0) -> 1, f(1) -> 1 constant 21 | flavors = [[0, 0], [0, 1], [1, 0], [1, 1]] 22 | 23 | def f(bit: int) -> int: 24 | """Return f(bit) for one of the 4 possible function types.""" 25 | return flavors[flavor][bit] 26 | 27 | return f 28 | 29 | 30 | def make_uf(f: Callable[[int], int]) -> ops.Operator: 31 | """Simple way to generate the 2-qubit, 4x4 Deutsch Oracle.""" 32 | 33 | # This is how the Deutsch Oracle is being constructed. 34 | # 35 | # The input state is one of these 2 qubit tensor products: 36 | # 37 | # |00> = [1, 0, 0, 0].T 38 | # |01> = [0, 1, 0, 0].T 39 | # |10> = [0, 0, 1, 0].T 40 | # |11> = [0, 0, 0, 1].T 41 | # 42 | # Only the 2nd qubit is being modified by f (note that f 43 | # is a function of x): 44 | # |x, y> -> |x, y ^ f(x)> (xor) 45 | # 46 | # For f(0)=0, f(1)=0 (^ being add modulo 2, or xor): 47 | # 48 | # x y ^ f(x) = new state 49 | # --------------------------- 50 | # 0, 0 ^ 0 0 0, 0 51 | # 0, 1 ^ 0 1 0, 1 52 | # 1, 0 ^ 0 0 1, 0 53 | # 1, 1 ^ 0 1 1, 1 54 | # 55 | # Which is being achieved by the identity matrix: 56 | # 1 0 0 0 57 | # 0 1 0 0 58 | # 0 0 1 0 59 | # 0 0 0 1 60 | # 61 | # For f(0)=0, f(1)=1: 62 | # 63 | # x y ^ f(x) = new state 64 | # --------------------------- 65 | # 0, 0 ^ 0 0 0, 0 66 | # 0, 1 ^ 0 1 0, 1 67 | # 1, 0 ^ 1 1 1, 1 68 | # 1, 1 ^ 1 0 1, 0 69 | # 70 | # Which is being achieved by the identity matrix: 71 | # 1 0 0 0 72 | # 0 1 0 0 73 | # 0 0 0 1 74 | # 0 0 1 0 75 | # 76 | # The unitary matrices for the other 2 cases can be computed 77 | # the exact same, mechanical way. Interpret the (x,y) as bits 78 | # indexing the 4x4 operator array, then this code will build Uf: 79 | # 80 | u = np.zeros(16).reshape(4, 4) 81 | for col in range(4): 82 | y = col & 1 83 | x = col & 2 84 | fx = f(x >> 1) 85 | xor = y ^ fx 86 | u[col][x + xor] = 1.0 87 | 88 | op = ops.Operator(u) 89 | assert op.is_unitary(), 'Produced non-unitary operator.' 90 | return op 91 | 92 | 93 | def run_experiment(flavor: int) -> None: 94 | """Run full experiment for a given flavor of f().""" 95 | 96 | f = make_f(flavor) 97 | u = make_uf(f) 98 | h = ops.Hadamard() 99 | 100 | psi = h(state.zeros(1)) * h(state.ones(1)) 101 | psi = u(psi) 102 | psi = h(psi) 103 | 104 | p0, _ = ops.Measure(psi, 0, tostate=0, collapse=False) 105 | 106 | print(f'f(0) = {f(0)}, f(1) = {f(1)} -> ', end='') 107 | if math.isclose(p0, 0.0, abs_tol=1e-5): 108 | print('balanced') 109 | assert flavor in [1, 2], 'Invalid result, expected balanced.' 110 | else: 111 | print('constant') 112 | assert flavor in [0, 3], 'Invalid result, expected constant.' 113 | 114 | 115 | def main(argv): 116 | if len(argv) > 1: 117 | raise app.UsageError('Too many command-line arguments.') 118 | 119 | run_experiment(0b00) 120 | run_experiment(0b01) 121 | run_experiment(0b10) 122 | run_experiment(0b11) 123 | 124 | 125 | if __name__ == '__main__': 126 | app.run(main) 127 | -------------------------------------------------------------------------------- /src/deutsch_jozsa.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Deutsch-Jozsa Algorithm.""" 3 | 4 | import math 5 | from typing import Callable, List 6 | from absl import app 7 | import numpy as np 8 | 9 | 10 | from src.lib import helper 11 | from src.lib import ops 12 | from src.lib import state 13 | 14 | # Functions are either constant or balanced. Distinguish via strings. 15 | exp_constant = 'constant' 16 | exp_balanced = 'balanced' 17 | 18 | 19 | def make_f(dim: int = 1, 20 | flavor: str = exp_constant) -> Callable[[List[int]], int]: 21 | """Return a constant or balanced function f over 2**dim bits.""" 22 | 23 | power2 = 2**dim 24 | bits = np.zeros(power2, dtype=np.uint8) 25 | if flavor == exp_constant: 26 | bits[:] = int(np.random.random() < 0.5) 27 | else: 28 | bits[np.random.choice(power2, size=power2 // 2, replace=False)] = 1 29 | 30 | # In this generalization of single-bit Deutsch, the f function 31 | # accepts a string of bits. We compute an index from this 32 | # binary representation and return the value in bits[] found there. 33 | # 34 | def f(bit_string: List[int]) -> int: 35 | """Return f(bits) for one of the 2 possible function types.""" 36 | 37 | # pylint: disable=no-value-for-parameter 38 | idx = helper.bits2val(bit_string) 39 | return bits[idx] 40 | 41 | return f 42 | 43 | 44 | def run_experiment(nbits: int, flavor: str): 45 | """Run full experiment for a given flavor of f().""" 46 | 47 | f = make_f(nbits - 1, flavor) 48 | u = ops.OracleUf(nbits, f) 49 | 50 | psi = (ops.Hadamard(nbits - 1)(state.zeros(nbits - 1)) * 51 | ops.Hadamard()(state.ones(1))) 52 | psi = u(psi) 53 | psi = (ops.Hadamard(nbits - 1) * ops.Identity(1))(psi) 54 | 55 | # Measure all of |0>. If all close to 1.0, f() is constant. 56 | for idx in range(nbits - 1): 57 | p0, _ = ops.Measure(psi, idx, tostate=0, collapse=False) 58 | if not math.isclose(p0, 1.0, abs_tol=1e-5): 59 | return exp_balanced 60 | return exp_constant 61 | 62 | 63 | def main(argv): 64 | if len(argv) > 1: 65 | raise app.UsageError('Too many command-line arguments.') 66 | 67 | for qubits in range(2, 8): 68 | result = run_experiment(qubits, exp_constant) 69 | print(f'Found: {result} ({qubits} qubits) (Want: {exp_constant})') 70 | assert result == exp_constant, f'Want: {exp_constant}' 71 | 72 | result = run_experiment(qubits, exp_balanced) 73 | print(f'Found: {result} ({qubits} qubits) (Want: {exp_balanced})') 74 | assert result == exp_balanced, f'Want: {exp_balanced}' 75 | 76 | if __name__ == '__main__': 77 | app.run(main) 78 | -------------------------------------------------------------------------------- /src/entanglement_swap.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Entanglement Swapping.""" 3 | 4 | import math 5 | 6 | from absl import app 7 | import numpy as np 8 | 9 | from src.lib import circuit 10 | 11 | 12 | def main(argv): 13 | if len(argv) > 1: 14 | raise app.UsageError('Too many command-line arguments.') 15 | 16 | qc = circuit.qc('Entanglement Swap') 17 | qc.reg(4, 0) 18 | 19 | # Alice has qubits 0, 1 20 | # Bob has qubits 2, 3 21 | # 22 | # Entangle 0, 2 and 1, 3 23 | # ---------------------- 24 | # Alice 0 1 25 | # | | 26 | # Bob 2 3 27 | # 28 | qc.h(0) 29 | qc.cx(0, 2) 30 | qc.h(1) 31 | qc.cx(1, 3) 32 | 33 | # Now Alice and Bob physically separate. 34 | # Alice keeps qubits 1 and 4 on Earth. 35 | # Bob takes qubits 2 and 3 to the Moon. 36 | # ... travel, space ships, etc... 37 | # Alice performs a Bell measurement between her qubits 0 and 1, 38 | # which means to apply a reverse entangler circuit: 39 | # 40 | # Alice 0~~~BM~~~1 41 | # | | 42 | # Bob 2 3 43 | # 44 | qc.cx(0, 1) 45 | qc.h(0) 46 | 47 | # At this point, Alice will physically measure her qubits 0, 1. 48 | # There are four possible outcomes, |00>, |01>, |10>, and |11>, 49 | # 50 | # Depending on that outcome, now qubits 2 and 3 will also be 51 | # in a corresponding Bell state! The entanglement has been 52 | # teleported to qubits 2 and 3, even though they never 53 | # interacted before! 54 | # 55 | # Alice 0 1 56 | # 57 | # Bob 2 ~~~~~~ 3 58 | # 59 | # To check the results: 60 | # Iterate over the four possibilities for measurement 61 | # The table covers all expected results. There will only be 62 | # two states with probability >0.0. For example, the 63 | # first line below represents: 64 | # 0 0 0 0 p=0.707... 65 | # 0 0 1 1 p=0.707... 66 | # Ignore the first two qubits. This shows a 67 | # superposition of state 00 and 11 in a Bell state. 68 | # 69 | # Qubits 70 | # 0 1 2 3 0 1 2 3 factor Bell 71 | # --------------------------------------------- 72 | cases = [ 73 | [0, 0, 0, 0, 0, 0, 1, 1, 1.0], # b00 74 | [0, 1, 0, 1, 0, 1, 1, 0, 1.0], # b01 75 | [1, 0, 0, 0, 1, 0, 1, 1, -1.0], # b10 76 | [1, 1, 0, 1, 1, 1, 1, 0, -1.0], # b11 77 | ] 78 | 79 | c07 = 1 / math.sqrt(2) 80 | psi = qc.psi 81 | 82 | for c in cases: 83 | qc.psi = psi 84 | qc.measure_bit(0, c[0], collapse=True) 85 | qc.measure_bit(1, c[1], collapse=True) 86 | qc.psi.dump(f'after measuring |{c[0]}..{c[3]}>') 87 | 88 | if not np.allclose( 89 | np.real(qc.psi.ampl(c[0], c[1], c[2], c[3])), c07 90 | ): 91 | raise AssertionError('Invalid measurement results') 92 | if not np.allclose( 93 | np.real(qc.psi.ampl(c[4], c[5], c[6], c[7])), c07 * c[8] 94 | ): 95 | raise AssertionError('Invalid measurement results') 96 | 97 | 98 | if __name__ == '__main__': 99 | app.run(main) 100 | -------------------------------------------------------------------------------- /src/estimate_pi.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Estimate Pi via Phase Estimation.""" 3 | 4 | 5 | # This is a rather simple application of phase estimation. However, 6 | # in order to understand this example, phase estimation must be 7 | # understood first. 8 | # 9 | # Despite it being simple, it can be used effectively to estimate the 10 | # accuracy of one-qubit operations on real machines, as shown in: 11 | # https://arxiv.org/abs/1912.12037 12 | # 13 | # In this implementation we only simulate the technique. A good explanation 14 | # can also be found in Qiskit: 15 | # https://qiskit.org/textbook/ch-demos/piday-code.html 16 | 17 | 18 | from absl import app 19 | import numpy as np 20 | from src.lib import circuit 21 | from src.lib import helper 22 | 23 | 24 | def run_experiment(nbits_phase): 25 | """Estimate Pi with nbits_phase qubits.""" 26 | 27 | qc = circuit.qc('pi estimator') 28 | qclock = qc.reg(nbits_phase) 29 | qbit = qc.reg(1) 30 | 31 | # We perform phase estimation on the operator U1(1.0), which corresponds 32 | # to the matrix: 33 | # 34 | # | 1.0 0.0 | 35 | # | 0.0 exp(i phi) | 36 | # 37 | # and we set phi = 1.0. Note that the eigenvalues are 1.0 and exp(i phi). 38 | # 39 | # Phase estimation gives us the phase phi of an operator as the fraction: 40 | # exp(2 Pi i phi) 41 | # 42 | # We did set phi to 1.0 above. So we know 2 Pi phi = 1.0 or 43 | # Pi = 1 / (2 phi) 44 | # 45 | qc.x(qbit) 46 | 47 | qc.h(qclock) 48 | for inv in range(nbits_phase): 49 | qc.cu1(qclock[inv], qbit[0], 2 ** (nbits_phase - inv - 1)) 50 | qc.inverse_qft(qclock) 51 | 52 | bits, _ = qc.psi.maxprob() 53 | theta = helper.bits2frac(bits[:nbits_phase][::-1]) 54 | pi = 1 / (2 * theta) 55 | delta = np.abs(pi - np.pi) 56 | 57 | print(f'Pi Estimate: {pi:.6f} (qb: {nbits_phase:2d}) Delta: {delta:.6f}') 58 | assert delta < 0.06, 'Incorrect Estimation of Pi.' 59 | 60 | 61 | # pylint: disable=unused-argument 62 | def main(argv): 63 | print('Estimate Pi via phase estimation...') 64 | 65 | for nbits in range(5, 21): 66 | run_experiment(nbits) 67 | 68 | 69 | if __name__ == '__main__': 70 | app.run(main) 71 | -------------------------------------------------------------------------------- /src/euclidean_distance.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Euclidean Distance.""" 3 | 4 | 5 | import random 6 | from absl import app 7 | import numpy as np 8 | 9 | from src.lib import ops 10 | from src.lib import state 11 | 12 | 13 | def run_experiment(a, b): 14 | """Compute Euclidean Distance between vectors a and b.""" 15 | 16 | print(f'Quantum Euclidean Distance between a={a} b={b}') 17 | 18 | norm_a = np.linalg.norm(a) 19 | norm_b = np.linalg.norm(b) 20 | assert norm_a != 0 and norm_b != 0, 'Invalid zero-vectors.' 21 | normed_a = a / norm_a 22 | normed_b = b / norm_b 23 | z = (norm_a**2) + (norm_b**2) 24 | 25 | # Create state phi: 26 | # |phi> = 1 / sqrt(z) (||a|| |0> - ||b|| |1>) 27 | # 28 | phi = state.State(1 / np.sqrt(z) * np.array([norm_a, -norm_b])) 29 | 30 | # Create state psi: 31 | # |psi> = 1 / sqrt(2) |0>|a> + |1>|b>) 32 | # 33 | psi = (state.bitstring(0) * state.State(normed_a) + 34 | state.bitstring(1) * state.State(normed_b)) / np.sqrt(2) 35 | 36 | # Make a combined state with an ancilla (|0>), phi, and psi: 37 | # 38 | combo = state.bitstring(0) * phi * psi 39 | 40 | # Construct a swap test and find the measurement probability 41 | # of the ancilla. 42 | # 43 | combo = ops.Hadamard()(combo, 0) 44 | combo = ops.ControlledU(0, 1, ops.Swap(1, 2))(combo) 45 | combo = ops.Hadamard()(combo, 0) 46 | 47 | p0, _ = ops.Measure(combo, 0) 48 | 49 | # Now compute the euclidian norm from the probability. 50 | # 51 | eucl_dist_q = (4 * z * (p0 - 0.5)) ** 0.5 52 | 53 | # We can also compute the euclidian distance classically. 54 | # 55 | eucl_dist_c = np.linalg.norm(a - b) 56 | 57 | assert np.allclose(eucl_dist_q, eucl_dist_c, atol=1e-4), 'Whaaa' 58 | print(f' Classic: {eucl_dist_c:.2f}, quantum: {eucl_dist_q:.2f}, Correct') 59 | 60 | 61 | def main(argv): 62 | if len(argv) > 1: 63 | raise app.UsageError('Too many command-line arguments.') 64 | 65 | print('Compute Quantum Euclidean Distance.') 66 | 67 | for _ in range(10): 68 | a = np.array(random.choices(range(10), k=4)) 69 | b = np.array(random.choices(range(10), k=4)) 70 | run_experiment(a, b) 71 | 72 | for _ in range(10): 73 | a = np.array(random.choices(range(100), k=8)) 74 | b = np.array(random.choices(range(100), k=8)) 75 | run_experiment(a, b) 76 | 77 | 78 | if __name__ == '__main__': 79 | app.run(main) 80 | -------------------------------------------------------------------------------- /src/hadamard_test.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Hadamard Test for two states.""" 3 | 4 | import cmath 5 | 6 | from absl import app 7 | import numpy as np 8 | from scipy.stats import unitary_group 9 | 10 | from src.lib import circuit 11 | from src.lib import ops 12 | from src.lib import state 13 | 14 | 15 | def make_rand_operator(): 16 | """Make a unitary operator U, derive u0, u1.""" 17 | 18 | # We think of operators as the following, 19 | # so that multiplication with |0> extracts the state 20 | # values (u0, u1): 21 | # | u0 u2 | | 1 | | u0 | 22 | # | u1 u3 | | 0 | = | u1 | 23 | # 24 | # pylint: disable=invalid-name 25 | U = ops.Operator(unitary_group.rvs(2)) 26 | assert U.is_unitary(), 'Error: Generated non-unitary operator' 27 | 28 | psi = U(state.bitstring(0)) 29 | return (U, psi[0], psi[1]) 30 | 31 | 32 | def hadamard_test(): 33 | """Perform Hadamard Test.""" 34 | 35 | # pylint: disable=invalid-name 36 | A, a0, a1 = make_rand_operator() 37 | B, b0, b1 = make_rand_operator() 38 | 39 | # ====================================== 40 | # Step 1: Verify P(|0>) = 2 Re() 41 | # ====================================== 42 | 43 | # Construct the desired end state psi as an explicit expression. 44 | # psi = 1/sqrt(2)(|0>|a> + |1>|b>) 45 | psi = (1 / cmath.sqrt(2) * 46 | (state.bitstring(0) * state.State([a0, a1]) + 47 | state.bitstring(1) * state.State([b0, b1]))) 48 | 49 | # Let's see how to make this state with a circuit. 50 | qc = circuit.qc('Hadamard test - initial state construction.') 51 | qc.reg(2, 0) 52 | qc.h(0) 53 | qc.applyc(A, [0], 1) # Controlled-by-0 54 | qc.applyc(B, 0, 1) # Controlled-by-1 55 | 56 | # The two states should be identical! 57 | if not np.allclose(qc.psi, psi): 58 | raise AssertionError('Incorrect result') 59 | 60 | # Now let's apply a final Hadamard to the ancilla. 61 | qc.h(0) 62 | 63 | # At this point, this inner product estimation should hold: 64 | # P(|0>) = 1/2 + 1/2 Re() 65 | # Or 66 | # 2 * P(|0>) - 1 = Re() 67 | # 68 | # Let's verify... 69 | dot = np.dot(np.array([a0, a1]).conj(), np.array([b0, b1])) 70 | p0 = qc.psi.prob(0, 0) + qc.psi.prob(0, 1) 71 | if not np.allclose(2 * p0 - 1, dot.real, atol=1e-6): 72 | raise AssertionError('Incorrect inner product estimation') 73 | 74 | # ====================================== 75 | # Step 2: Verify P(|1>) = 2 Im() 76 | # ====================================== 77 | 78 | # Now let's try the same to get to the imaginary parts. 79 | # 80 | # psi = 1/sqrt(2)(|0>|a> - i|1>|b>) 81 | # 82 | psi = (1 / cmath.sqrt(2) * 83 | (state.bitstring(0) * state.State([a0, a1]) - 84 | 1.0j * state.bitstring(1) * state.State([b0, b1]))) 85 | 86 | # Let's see how to make this state with a circuit. 87 | # 88 | qc = circuit.qc('Hadamard test - initial state construction.') 89 | qc.reg(2, 0) 90 | qc.h(0) 91 | qc.sdag(0) 92 | qc.applyc(A, [0], 1) # Controlled-by-0 93 | qc.applyc(B, 0, 1) # Controlled-by-1 94 | 95 | # The two states should be identical! 96 | # 97 | if not np.allclose(qc.psi, psi): 98 | raise AssertionError('Incorrect result') 99 | 100 | # Now let's apply a final Hadamard to the ancilla. 101 | qc.h(0) 102 | 103 | # At this point, this inner product estimation should hold: 104 | # P(|0>) = 1/2 + 1/2 Im() 105 | # Or 106 | # 2 * P(|0>) - 1 = Im() 107 | # 108 | # Let's verify... 109 | dot = np.dot(np.array([a0, a1]).conj(), np.array([b0, b1])) 110 | p0 = qc.psi.prob(0, 0) + qc.psi.prob(0, 1) 111 | if not np.allclose(2 * p0 - 1, dot.imag, atol=1e-6): 112 | raise AssertionError('Incorrect inner product estimation') 113 | 114 | 115 | def main(argv): 116 | if len(argv) > 1: 117 | raise app.UsageError('Too many command-line arguments.') 118 | 119 | iterations = 1000 120 | print(f'Perform {iterations} random hadamard tests.') 121 | 122 | for _ in range(iterations): 123 | hadamard_test() 124 | print('Success') 125 | 126 | if __name__ == '__main__': 127 | app.run(main) 128 | -------------------------------------------------------------------------------- /src/hamiltonian_cycle.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Hamiltonian Cycle via Grover's Algorithm.""" 3 | 4 | 5 | # ============================================== 6 | # This is WIP (also not working as intended yet) 7 | # ============================================== 8 | 9 | import math 10 | from typing import List 11 | from absl import app 12 | import numpy as np 13 | 14 | from src.lib import circuit 15 | from src.lib import ops 16 | 17 | 18 | class Graph: 19 | """Hold a graph definition.""" 20 | 21 | def __init__(self, num_vertices: int, expected: bool, desc: str, 22 | edges: List[int]): 23 | self.num = num_vertices 24 | self.edges = edges 25 | self.desc = desc 26 | self.expected = expected 27 | 28 | def verify(self, bits): 29 | """Verify whether there is a non-|0> in bits.""" 30 | 31 | for bit in bits: 32 | if bit: 33 | return True 34 | return False 35 | 36 | 37 | def diffuser(qc: circuit.qc, reg, checker, aux): 38 | """Simple diffuser gate. Input qubits are in a register.""" 39 | 40 | qc.h(reg) 41 | qc.x(reg) 42 | qc.multi_control(reg, checker, 43 | aux, ops.PauliX(), 'Diffuser Gate') 44 | qc.x(reg) 45 | qc.h(reg) 46 | 47 | 48 | def build_circuit(g: Graph): 49 | """Build the circuit for the Grover Search.""" 50 | 51 | print(f'Graph {g.desc}: v: {g.num}, e: {len(g.edges)}', end='') 52 | iterations = int(math.pi / 4 * math.sqrt(2**len(g.edges))) 53 | qc = circuit.qc('graph') 54 | v = qc.reg(g.num, 0) 55 | e = qc.reg(len(g.edges), 0) 56 | chk = qc.reg(1, 0)[0] 57 | aux = qc.reg(g.num*2) 58 | 59 | qc.h(e) 60 | qc.x(chk) 61 | qc.h(chk) 62 | 63 | for _ in range(iterations): 64 | sc = qc.sub() 65 | for idx, edge in enumerate(g.edges): 66 | sc.cry(e[idx], edge[0], np.pi/2) 67 | sc.cry(e[idx], edge[1], np.pi/2) 68 | 69 | qc.qc(sc) 70 | qc.multi_control(v, chk, aux, ops.PauliX(), 'multi-X') 71 | qc.qc(sc.inverse()) 72 | 73 | diffuser(qc, e, chk, aux) 74 | 75 | maxbits, _ = qc.psi.maxprob() 76 | if g.expected != g.verify(maxbits[:g.num]): 77 | print(' INCORRECT', maxbits[:g.num]) 78 | # raise AssertionError('INCORRECT') 79 | else: 80 | print(' Has circle: ', g.expected, ' (correct)') 81 | 82 | 83 | def main(argv): 84 | if len(argv) > 1: 85 | raise app.UsageError('Too many command-line arguments.') 86 | 87 | print('Quantum Hamiltonian Cycle Finder (WIP)') 88 | 89 | build_circuit(Graph(3, True, 'triangle', 90 | [(0, 1), (1, 2), (2, 1)])) 91 | build_circuit(Graph(4, True, 'rect', 92 | [(0, 1), (1, 2), (2, 3), (3, 0)])) 93 | build_circuit(Graph(4, True, 'rect+diag', 94 | [(0, 1), (1, 2), (2, 3), (3, 0), (0, 2)])) 95 | build_circuit(Graph(5, True, 'loop-5', 96 | [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)])) 97 | build_circuit(Graph(5, True, 'loop-5+diag', 98 | [(0, 1), (1, 2), (0, 2), (2, 3), (3, 4), (4, 5)])) 99 | build_circuit(Graph(4, False, 'line', 100 | [(0, 1), (1, 2), (2, 3)])) 101 | build_circuit(Graph(5, False, 'triangle with stray 0', 102 | [(0, 1), (1, 2), (2, 1), (0, 4)])) 103 | build_circuit(Graph(5, False, 'triangle with stray 1', 104 | [(0, 1), (1, 2), (2, 1), (1, 4)])) 105 | build_circuit(Graph(5, False, 'triangle with stray 2', 106 | [(0, 1), (1, 2), (2, 1), (2, 4)])) 107 | build_circuit(Graph(4, False, 'star formation', 108 | [(0, 1), (0, 2), (0, 3)])) 109 | build_circuit(Graph(4, False, 'two lines', 110 | [(0, 1), (2, 3)])) 111 | build_circuit(Graph(5, False, 'two lines with stray', 112 | [(0, 1), (1, 2), (2, 3), (1, 4)])) 113 | build_circuit(Graph(5, True, 'two lines with connector', 114 | [(0, 1), (1, 2), (2, 3), (1, 4), (3, 4)])) 115 | 116 | 117 | if __name__ == '__main__': 118 | app.run(main) 119 | -------------------------------------------------------------------------------- /src/hamiltonian_encoding.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Hamiltonian encoding and evolution. A few experiments.""" 3 | 4 | import random 5 | from absl import app 6 | import numpy as np 7 | 8 | from src.lib import ops 9 | from src.lib import state 10 | 11 | 12 | def make_hermitian(a: ops.Operator): 13 | """Construct a Hermitian matrix from a given A.""" 14 | 15 | if a.is_hermitian(): 16 | return a 17 | 18 | # There are a few ways to make a Hermitian out of A. The first 19 | # trick is to make a block matrix of this form, which is of 20 | # twice the size of the original matrix. 21 | # 22 | b = ops.Operator(np.block([[np.zeros(a.shape), a], 23 | [a.transpose().conjugate(), np.zeros(a.shape)]])) 24 | if not b.is_hermitian(): 25 | raise AssertionError('Making 2*n Hermitian failed.') 26 | return b 27 | 28 | # Alternatively (disabled for now): 29 | # Another way is to simple compute A + A.transpose().conjugate() 30 | # 31 | # c = a + a.transpose().conjugate() 32 | # if not c.is_hermitian(): 33 | # raise AssertionError('Making A + A.T Hermitian failed.') 34 | # return c 35 | 36 | 37 | def run_experiment(a): 38 | """Run a single experiment.""" 39 | 40 | # We want to make A a Hamiltonian and for this purpose it has 41 | # to be made Hermitian. 42 | # 43 | a = make_hermitian(a) 44 | dim = a.shape[0] 45 | 46 | # Let's compute eigenvalues and eigenvectors: 47 | # 48 | lam, v = np.linalg.eig(a) 49 | 50 | # A is Hermitian with v being a basis for complex vectors in space dim x 1. 51 | # 52 | # This means than any dim x 1 state can be computed from this basis as: 53 | # psi = gamma_0 v_0 + gamma_1 v_1 + ... + gamma_dim v_dim 54 | # 55 | # where we can compute: 56 | # gamma_i = (psi^dagger * v_i) 57 | # 58 | # Let's try this out with a random complex state. 59 | # 60 | psi = state.State([complex(random.random(), random.random()) 61 | for _ in range(dim)]).normalize() 62 | print('Random complex state:', psi) 63 | 64 | # Let's compute gamma: 65 | # 66 | gamma = np.array([np.dot(psi.conj(), v[i]) for i in range(dim)]) 67 | 68 | # Let's double check that we can construct psi from the basis v and gamma: 69 | # psi = (gamma[0]^dagger * v[0]) + ... + (gamma[dim]^dagger * v[dim]) 70 | # 71 | psi_new = np.zeros(dim) 72 | for i in range(dim): 73 | psi_new = psi_new + (gamma[i].conj() * v[i]) 74 | if not np.allclose(psi, psi_new, atol=1e-5): 75 | raise AssertionError('Incorrect computation.') 76 | 77 | # Applying the Hamiltonian H_A to a state means: 78 | # a' = exp(-i H_A t) a 79 | # = exp(-i lam_0 t) gamma_0^dag v_0+...+exp(-i lam_3 t) gamma_3^dag v_3 80 | # 81 | def apply_hamiltonian(t: float): 82 | psi = np.zeros(dim) 83 | for i in range(dim): 84 | psi = psi + np.exp(-1j * lam[i] * t) * gamma[i].conj() * v[i] 85 | return psi 86 | 87 | # At time t = 0 we must get the same result as above: 88 | # 89 | psi_new = apply_hamiltonian(0) 90 | if not np.allclose(psi, psi_new, atol=1e-5): 91 | raise AssertionError('Incorrect computation.') 92 | 93 | # Print example evolutions: 94 | # 95 | for i in range(5): 96 | psi = apply_hamiltonian(0.2*i) 97 | print(f'Hamiltonian encoded at t = {0.2 * i:.1f}: ', end='') 98 | for i in range(dim): 99 | print(f'{psi[i]:.3f} ', end='') 100 | print() 101 | 102 | 103 | def main(argv): 104 | if len(argv) > 1: 105 | raise app.UsageError('Too many command-line arguments.') 106 | 107 | # Let's try a complex matrix: 108 | # 109 | a = ops.Operator([[2.0, -1/3 + 1j/8], [-1/3 - 1j/8, 1]]) 110 | run_experiment(a) 111 | 112 | # Let's use the matrix that was described in: 113 | # "Machine Learning with Quantum Computing" by 114 | # Maria Schuld and Francesco Petruccione, page 118 115 | # 116 | a = ops.Operator([[0.073, -0.438], [0.730, 0.000]]) 117 | run_experiment(a) 118 | 119 | # The numerical example from: 120 | # "Step-by-Step HHL Algorithm Walkthrough..." by 121 | # Morrell, Zaman, Wong 122 | # (which is a 2x2 Hermitian matrix) 123 | # 124 | a = ops.Operator([[1.0, -1/3], [-1/3, 1]]) 125 | run_experiment(a) 126 | 127 | 128 | if __name__ == '__main__': 129 | np.set_printoptions(precision=3) 130 | app.run(main) 131 | -------------------------------------------------------------------------------- /src/inversion_test.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Inversion Test to estimate dot product between two states.""" 3 | 4 | from absl import app 5 | import numpy as np 6 | from scipy.stats import unitary_group 7 | 8 | from src.lib import circuit 9 | from src.lib import ops 10 | from src.lib import state 11 | 12 | 13 | def make_rand_operator(): 14 | """Make a unitary operator U, derive u0, u1.""" 15 | 16 | # We think of operators as the following, 17 | # so that multiplication with |0> extracts the state 18 | # values (u0, u1): 19 | # | u0 u2 | | 1 | | u0 | 20 | # | u1 u3 | | 0 | = | u1 | 21 | # 22 | # pylint: disable=invalid-name 23 | U = ops.Operator(unitary_group.rvs(2)) 24 | if not U.is_unitary(): 25 | raise AssertionError('Error: Generated non-unitary operator') 26 | psi = U(state.bitstring(0)) 27 | u0 = psi[0] 28 | u1 = psi[1] 29 | return (U, u0, u1) 30 | 31 | 32 | def inversion_test(): 33 | """Perform Inversion Test.""" 34 | 35 | # The inversion test allows keeping the number of qubits to a minimum 36 | # when trying to determine the overlap between two states. However, it 37 | # requires a precise way to generate states a and b, as well as the 38 | # adjoint for one of them. 39 | # 40 | # If we have operators A and B (similar to the Hadamard Test) producing: 41 | # A |0> = a 42 | # B |0> = b 43 | # To determine the overlap between a and be (), we run: 44 | # B_adjoint A |0> 45 | # and determine the probability p0 of measuring |0>. p0 is an 46 | # a precise estimate for . 47 | 48 | # pylint: disable=invalid-name 49 | A, a0, a1 = make_rand_operator() 50 | B, b0, b1 = make_rand_operator() 51 | 52 | # For the inversion test, we will need B^\dagger: 53 | Bdag = B.adjoint() 54 | 55 | # Compute the dot product : 56 | dot = np.dot(np.array([a0, a1]).conj(), np.array([b0, b1])) 57 | 58 | # Here is the inversion test. We run B^\dagger A |0> and find 59 | # the probability of measuring |0>: 60 | qc = circuit.qc('Hadamard test - initial state construction.') 61 | qc.reg(1, 0) 62 | qc.apply1(A, 0) 63 | qc.apply1(Bdag, 0) 64 | 65 | # The probability amplitude of measuring |0> should be the 66 | # same value as the dot product. 67 | p0, _ = qc.measure_bit(0, 0) 68 | if not np.allclose(dot.conj() * dot, p0): 69 | raise AssertionError('Incorrect inner product estimation') 70 | 71 | 72 | def main(argv): 73 | if len(argv) > 1: 74 | raise app.UsageError('Too many command-line arguments.') 75 | 76 | iterations = 1000 77 | print(f'Perform {iterations} random inversion tests.') 78 | 79 | for _ in range(iterations): 80 | inversion_test() 81 | print('Success') 82 | 83 | 84 | if __name__ == '__main__': 85 | app.run(main) 86 | -------------------------------------------------------------------------------- /src/lib/BUILD: -------------------------------------------------------------------------------- 1 | cc_binary( 2 | name = "libxgates.so", 3 | linkshared = True, 4 | srcs = [ 5 | "xgates.cc", 6 | ], 7 | copts = [ 8 | "-O3", 9 | "-ffast-math", 10 | "-DNPY_NO_DEPRECATED_API", 11 | "-DNPY_1_7_API_VERSION", 12 | ], 13 | deps = [ 14 | "@third_party_numpy//:numpy", 15 | "@third_party_python//:python", 16 | ], 17 | ) 18 | 19 | py_library( 20 | name = "tensor", 21 | visibility = ["//visibility:public"], 22 | srcs = [ 23 | "tensor.py", 24 | ], 25 | srcs_version = "PY3", 26 | ) 27 | 28 | py_library( 29 | name = "helper", 30 | visibility = ["//visibility:public"], 31 | srcs = [ 32 | "helper.py", 33 | ], 34 | srcs_version = "PY3", 35 | deps = [ 36 | ], 37 | ) 38 | 39 | py_library( 40 | name = "state", 41 | visibility = ["//visibility:public"], 42 | srcs = [ 43 | "state.py", 44 | ], 45 | srcs_version = "PY3", 46 | deps = [ 47 | ":helper", 48 | ":tensor", 49 | ], 50 | ) 51 | 52 | py_library( 53 | name = "ops", 54 | visibility = ["//visibility:public"], 55 | srcs = [ 56 | "ops.py", 57 | ], 58 | srcs_version = "PY3", 59 | deps = [ 60 | ":helper", 61 | ":state", 62 | ":tensor", 63 | ], 64 | ) 65 | 66 | py_library( 67 | name = "bell", 68 | visibility = ["//visibility:public"], 69 | srcs = [ 70 | "bell.py", 71 | ], 72 | srcs_version = "PY3", 73 | deps = [ 74 | ], 75 | ) 76 | 77 | py_library( 78 | name = "ir", 79 | visibility = ["//visibility:public"], 80 | srcs = [ 81 | "ir.py", 82 | ], 83 | srcs_version = "PY3", 84 | deps = [ 85 | ":ops", 86 | ":state", 87 | ":tensor", 88 | ], 89 | ) 90 | 91 | py_library( 92 | name = "dumpers", 93 | visibility = ["//visibility:public"], 94 | srcs = [ 95 | "dumpers.py", 96 | ], 97 | srcs_version = "PY3", 98 | deps = [ 99 | ":ir", 100 | ], 101 | ) 102 | 103 | py_library( 104 | name = "optimizer", 105 | visibility = ["//visibility:public"], 106 | srcs = [ 107 | "optimizer.py", 108 | ], 109 | srcs_version = "PY3", 110 | deps = [ 111 | ":ir", 112 | ], 113 | ) 114 | 115 | py_library( 116 | name = "circuit", 117 | visibility = ["//visibility:public"], 118 | srcs = [ 119 | "circuit.py", 120 | ], 121 | srcs_version = "PY3", 122 | deps = [ 123 | ":dumpers", 124 | ":ir", 125 | ":ops", 126 | ":state", 127 | ":tensor", 128 | ], 129 | ) 130 | 131 | # Catch all libraries. 132 | py_library( 133 | name = "qcall", 134 | deps = [ 135 | ":bell", 136 | ":circuit", 137 | ":helper", 138 | ":ir", 139 | ":ops", 140 | ":state", 141 | ":tensor", 142 | ], 143 | ) 144 | 145 | py_test( 146 | name = "bell_test", 147 | size = "small", 148 | srcs = ["bell_test.py"], 149 | python_version = "PY3", 150 | srcs_version = "PY3", 151 | deps = [ 152 | ":tensor", 153 | ":state", 154 | ":ops", 155 | ":bell", 156 | ], 157 | ) 158 | 159 | py_test( 160 | name = "tensor_test", 161 | size = "small", 162 | srcs = ["tensor_test.py"], 163 | python_version = "PY3", 164 | srcs_version = "PY3", 165 | deps = [ 166 | ":tensor", 167 | ], 168 | ) 169 | 170 | py_test( 171 | name = "state_test", 172 | size = "small", 173 | srcs = ["state_test.py"], 174 | python_version = "PY3", 175 | srcs_version = "PY3", 176 | deps = [ 177 | ":helper", 178 | ":state", 179 | ], 180 | ) 181 | 182 | py_test( 183 | name = "ops_test", 184 | size = "small", 185 | srcs = ["ops_test.py"], 186 | python_version = "PY3", 187 | srcs_version = "PY3", 188 | deps = [ 189 | ":ops", 190 | ":state", 191 | ], 192 | ) 193 | 194 | py_test( 195 | name = "helper_test", 196 | size = "small", 197 | srcs = ["helper_test.py"], 198 | python_version = "PY3", 199 | srcs_version = "PY3", 200 | deps = [ 201 | ":tensor", 202 | ":state", 203 | ":ops", 204 | 205 | ], 206 | ) 207 | 208 | py_test( 209 | name = "measure_test", 210 | size = "small", 211 | srcs = ["measure_test.py"], 212 | python_version = "PY3", 213 | srcs_version = "PY3", 214 | deps = [ 215 | ":ops", 216 | ":state", 217 | ], 218 | ) 219 | 220 | py_test( 221 | name = "equalities_test", 222 | size = "small", 223 | srcs = ["equalities_test.py"], 224 | python_version = "PY3", 225 | srcs_version = "PY3", 226 | deps = [ 227 | ":bell", 228 | ":ops", 229 | ":state", 230 | ], 231 | ) 232 | 233 | py_test( 234 | name = "circuit_test", 235 | size = "small", 236 | srcs = ["circuit_test.py"], 237 | python_version = "PY3", 238 | srcs_version = "PY3", 239 | deps = [ 240 | ":qcall", 241 | ], 242 | ) 243 | -------------------------------------------------------------------------------- /src/lib/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/lib/bell.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Generators for various entangled states, eg., the Bell states.""" 3 | 4 | import numpy as np 5 | 6 | from src.lib import ops 7 | from src.lib import state 8 | 9 | 10 | def bell_state(a: int, b: int) -> state.State: 11 | """Make one of the four bell states with a, b from {0,1}.""" 12 | 13 | assert a in [0, 1] and b in [0, 1], 'Bits must be 0 or 1.' 14 | psi = state.bitstring(a, b) 15 | psi = ops.Hadamard()(psi) 16 | return ops.Cnot()(psi) 17 | 18 | 19 | def ghz_state(nbits: int) -> state.State: 20 | """Make a maximally entangled nbits state (GHZ State).""" 21 | 22 | # Simple construction via: 23 | # 24 | # |0> --- H --- o --------- 25 | # |0> ----------X --- o --- 26 | # |0> ----------------X --- ... 27 | # 28 | psi = state.zeros(nbits) 29 | psi = ops.Hadamard()(psi) 30 | for offset in range(nbits - 1): 31 | psi = ops.Cnot(0, 1)(psi, offset) 32 | return psi 33 | 34 | 35 | def w_state() -> state.State: 36 | """Make a 3-qubit |W> state).""" 37 | 38 | # A 3-qubit |W> state (named after Wolfgang Duerr (2002)) is this state: 39 | # 1/sqrt(3)(|001> + |010> + |100>) 40 | # 41 | # This construction follows https://en.wikipedia.org/wiki/W_state: 42 | # 43 | # |0> -- Ry(phi3) - o ------o - X -- 44 | # | | 45 | # |0> ------------- H - o - X ------ 46 | # | 47 | # |0> ----------------- X ---------- 48 | # 49 | psi = state.zeros(3) 50 | phi3 = 2 * np.arccos(1 / np.sqrt(3)) 51 | psi = ops.RotationY(phi3)(psi, 0) 52 | psi = ops.ControlledU(0, 1, ops.Hadamard())(psi, 0) 53 | psi = ops.Cnot(1, 2)(psi, 1) 54 | psi = ops.Cnot(0, 1)(psi, 0) 55 | psi = ops.PauliX()(psi, 0) 56 | return psi 57 | -------------------------------------------------------------------------------- /src/lib/bell_test.py: -------------------------------------------------------------------------------- 1 | # python3 2 | import math 3 | from absl.testing import absltest 4 | import numpy as np 5 | 6 | from src.lib import bell 7 | from src.lib import ops 8 | from src.lib import state 9 | 10 | 11 | class BellTest(absltest.TestCase): 12 | 13 | def test_bell(self): 14 | """Check successful entanglement.""" 15 | 16 | b00 = bell.bell_state(0, 0) 17 | self.assertTrue( 18 | b00.is_close((state.zeros(2) + state.ones(2)) / math.sqrt(2)) 19 | ) 20 | 21 | # Note the order is reversed from pictorials. 22 | op_exp = ops.Cnot(0, 1) @ (ops.Hadamard() * ops.Identity()) 23 | b00_exp = op_exp(state.zeros(2)) 24 | self.assertTrue(b00.is_close(b00_exp)) 25 | 26 | def test_not_pure(self): 27 | """Bell states are pure states.""" 28 | 29 | for a in [0, 1]: 30 | for b in [0, 1]: 31 | b = bell.bell_state(a, b) 32 | self.assertTrue(b.density().is_pure()) 33 | self.assertTrue( 34 | math.isclose(np.real(np.trace(b.density())), 1.0, abs_tol=1e-6) 35 | ) 36 | 37 | def test_measure(self): 38 | b00 = bell.bell_state(0, 1) 39 | self.assertTrue(math.isclose(b00.prob(0, 1), 0.5, abs_tol=1e-6)) 40 | self.assertTrue(math.isclose(b00.prob(1, 0), 0.5, abs_tol=1e-6)) 41 | 42 | _, b00 = ops.Measure(b00, 0, tostate=0) 43 | self.assertTrue(math.isclose(b00.prob(0, 1), 1.0, abs_tol=1e-6)) 44 | self.assertTrue(math.isclose(b00.prob(1, 0), 0.0, abs_tol=1e-6)) 45 | 46 | # This state can't be measured, all zeros. 47 | _, b00 = ops.Measure(b00, 1, tostate=1) 48 | self.assertTrue(math.isclose(b00.prob(1, 0), 0.0, abs_tol=1e-6)) 49 | 50 | b00 = bell.bell_state(0, 1) 51 | self.assertTrue(math.isclose(b00.prob(0, 1), 0.5, abs_tol=1e-6)) 52 | self.assertTrue(math.isclose(b00.prob(1, 0), 0.5, abs_tol=1e-6)) 53 | 54 | _, b00 = ops.Measure(b00, 0, tostate=1) 55 | self.assertTrue(math.isclose(b00.prob(0, 1), 0.0, abs_tol=1e-6)) 56 | self.assertTrue(math.isclose(b00.prob(1, 0), 1.0, abs_tol=1e-6)) 57 | 58 | # This state can't be measured, all zeros. 59 | p, _ = ops.Measure(b00, 1, tostate=1, collapse=False) 60 | self.assertEqual(p, 0.0) 61 | 62 | def test_measure_order(self): 63 | """Order of measurement must not make a difference.""" 64 | 65 | b00 = bell.bell_state(0, 0) 66 | _, b00 = ops.Measure(b00, 0, tostate=0) 67 | _, b00 = ops.Measure(b00, 1, tostate=0) 68 | self.assertTrue(math.isclose(b00.prob(0, 0), 1.0)) 69 | self.assertTrue(math.isclose(b00.prob(1, 1), 0.0)) 70 | 71 | b00 = bell.bell_state(0, 0) 72 | _, b00 = ops.Measure(b00, 1, tostate=0) 73 | _, b00 = ops.Measure(b00, 0, tostate=0) 74 | self.assertTrue(math.isclose(b00.prob(0, 0), 1.0)) 75 | self.assertTrue(math.isclose(b00.prob(1, 1), 0.0)) 76 | 77 | def test_ghz(self): 78 | ghz = bell.ghz_state(3) 79 | self.assertGreater(ghz.prob(0, 0, 0), 0.49) 80 | self.assertGreater(ghz.prob(1, 1, 1), 0.49) 81 | 82 | ghz = bell.ghz_state(10) 83 | self.assertGreater(ghz.prob(0, 0, 0, 0, 0, 0, 0, 0, 0, 0), 0.49) 84 | self.assertGreater(ghz.prob(1, 1, 1, 1, 1, 1, 1, 1, 1, 1), 0.49) 85 | 86 | def test_bell_and_pauli(self): 87 | b00 = bell.bell_state(0, 0) 88 | 89 | bell_xz = ops.PauliX()(b00) 90 | bell_xz = ops.PauliZ()(bell_xz) 91 | 92 | bell_iy = (1j * ops.PauliY())(b00) 93 | 94 | self.assertTrue(np.allclose(bell_xz, bell_iy)) 95 | 96 | def test_w_state(self): 97 | psi = bell.w_state() 98 | self.assertGreater(psi.prob(0, 0, 1), 0.3) 99 | self.assertGreater(psi.prob(0, 1, 0), 0.3) 100 | self.assertGreater(psi.prob(1, 0, 0), 0.3) 101 | 102 | 103 | if __name__ == '__main__': 104 | absltest.main() 105 | -------------------------------------------------------------------------------- /src/lib/helper.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Helper functions.""" 3 | 4 | import itertools 5 | import math 6 | from typing import Iterable, List, Tuple 7 | 8 | import numpy as np 9 | 10 | 11 | def bitprod(nbits: int) -> Iterable[Tuple[int, ...]]: 12 | """Produce the iterable cartesian of nbits {0, 1}.""" 13 | 14 | for bits in itertools.product([0, 1], repeat=nbits): 15 | yield bits 16 | 17 | 18 | def bits2val(bits: List[int]) -> int: 19 | """For given bits, compute the decimal integer.""" 20 | 21 | # We assume bits are given in high to low order. For example, 22 | # the bits [1, 1, 0] will produce the value 6. 23 | return sum(v * (1 << (len(bits) - i - 1)) for i, v in enumerate(bits)) 24 | 25 | 26 | def val2bits(val: int, nbits: int): 27 | """Convert decimal integer to list of {0, 1}.""" 28 | 29 | # We return the bits in order high to low. For example, 30 | # the value 6 is being returned as [1, 1, 0]. 31 | return [int(c) for c in format(val, f'0{nbits}b')] 32 | 33 | 34 | def bits2frac(bits: Tuple[int, ...]) -> float: 35 | """For given bits, compute the binary fraction.""" 36 | 37 | return sum(bit * 2 ** (-idx - 1) for idx, bit in enumerate(bits)) 38 | 39 | 40 | def frac2bits(val: float, nbits: int): 41 | """Approximate a float with n binary fractions.""" 42 | 43 | assert val < 1.0, 'frac2bits: value must be strictly < 1.0' 44 | res = [] 45 | while nbits: 46 | nbits -= 1 47 | val *= 2 48 | res.append(int(val)) 49 | val -= int(val) 50 | return res 51 | 52 | 53 | def density_to_cartesian(rho: np.ndarray) -> Tuple[float, float, float]: 54 | """Compute Bloch sphere coordinates from 2x2 density matrix.""" 55 | 56 | a = rho[0, 0] 57 | b = rho[1, 0] 58 | x = 2.0 * b.real 59 | y = 2.0 * b.imag 60 | z = 2.0 * a - 1.0 61 | 62 | return np.real(x), np.real(y), np.real(z) 63 | 64 | 65 | def qubit_to_bloch(psi: np.ndarray): 66 | """Compute Bloch spere coordinates from 2x1 state vector/qubit.""" 67 | 68 | return density_to_cartesian(np.outer(psi, psi.conj())) 69 | 70 | 71 | def dump_bloch(x: float, y: float, z: float): 72 | """Textual output for Bloch sphere coordinates.""" 73 | 74 | print(f'x: {x:.2f}, y: {y:.2f}, z: {z:.2f}') 75 | 76 | 77 | def qubit_dump_bloch(psi: np.ndarray): 78 | """Print Bloch coordinates for state psi.""" 79 | 80 | x, y, z = qubit_to_bloch(psi) 81 | dump_bloch(x, y, z) 82 | 83 | 84 | def pi_fractions(val: float, pi: str = 'pi') -> str: 85 | """Convert a value in fractions of pi.""" 86 | 87 | if val is None: 88 | return '' 89 | if val == 0: 90 | return '0' 91 | for pi_multiplier in range(1, 4): 92 | for denom in range(-128, 128): 93 | if denom and math.isclose(val, pi_multiplier * math.pi / denom): 94 | pi_str = '' 95 | if pi_multiplier != 1: 96 | pi_str = f'{abs(pi_multiplier)}*' 97 | if denom == -1: 98 | return f'-{pi_str}{pi}' 99 | if denom < 0: 100 | return f'-{pi_str}{pi}/{-denom}' 101 | if denom == 1: 102 | return f'{pi_str}{pi}' 103 | return f'{pi_str}{pi}/{denom}' 104 | 105 | # couldn't find fractional, just return original value. 106 | return f'{val}' 107 | -------------------------------------------------------------------------------- /src/lib/helper_test.py: -------------------------------------------------------------------------------- 1 | # python3 2 | import math 3 | from absl.testing import absltest 4 | import numpy as np 5 | 6 | from src.lib import helper 7 | from src.lib import ops 8 | from src.lib import state 9 | 10 | 11 | class HelpersTest(absltest.TestCase): 12 | 13 | def test_bits_converstions(self): 14 | bits = helper.val2bits(6, 3) 15 | self.assertEqual(bits, [1, 1, 0]) 16 | 17 | val = helper.bits2val(bits) 18 | self.assertEqual(val, 6) 19 | 20 | def test_density_to_cartesian(self): 21 | """Test density to cartesian conversion.""" 22 | 23 | q0 = state.zeros(1) 24 | rho = q0.density() 25 | x, y, z = helper.density_to_cartesian(rho) 26 | self.assertEqual(x, 0.0) 27 | self.assertEqual(y, 0.0) 28 | self.assertEqual(z, 1.0) 29 | 30 | q1 = state.ones(1) 31 | rho = q1.density() 32 | x, y, z = helper.density_to_cartesian(rho) 33 | self.assertEqual(x, 0.0) 34 | self.assertEqual(y, 0.0) 35 | self.assertEqual(z, -1.0) 36 | 37 | qh = ops.Hadamard()(q0) 38 | rho = qh.density() 39 | x, y, z = helper.density_to_cartesian(rho) 40 | self.assertTrue(math.isclose(np.real(x), 1.0, abs_tol=1e-6)) 41 | self.assertTrue(math.isclose(np.real(y), 0.0)) 42 | self.assertTrue(math.isclose(np.real(z), 0.0, abs_tol=1e-6)) 43 | 44 | qr = ops.RotationZ(math.pi / 2)(qh) 45 | rho = qr.density() 46 | x, y, z = helper.density_to_cartesian(rho) 47 | self.assertTrue(math.isclose(np.real(x), 0.0, abs_tol=1e-6)) 48 | self.assertTrue(math.isclose(np.real(y), 1.0, abs_tol=1e-6)) 49 | self.assertTrue(math.isclose(np.real(z), 0.0, abs_tol=1e-6)) 50 | 51 | def test_frac(self): 52 | self.assertEqual(helper.bits2frac((0,)), 0) 53 | self.assertEqual(helper.bits2frac((1,)), 0.5) 54 | self.assertEqual(helper.bits2frac((0, 1)), 0.25) 55 | self.assertEqual(helper.bits2frac((1, 0)), 0.5) 56 | self.assertEqual(helper.bits2frac((1, 1)), 0.75) 57 | 58 | def test_frac2bits(self): 59 | self.assertEqual(helper.frac2bits(0, 3), [0, 0, 0]) 60 | self.assertEqual(helper.frac2bits(0.5, 3), [1, 0, 0]) 61 | self.assertEqual(helper.frac2bits(0.25, 3), [0, 1, 0]) 62 | self.assertEqual(helper.frac2bits(0.5 + 0.25, 3), [1, 1, 0]) 63 | self.assertEqual(helper.frac2bits(0.5 + 0.5 / 2 - 0.5 / 4, 3), [1, 0, 1]) 64 | self.assertEqual(helper.frac2bits(0.5 / 4, 3), [0, 0, 1]) 65 | tval = 0.5 + 0.5 / 2 + 0.5 / 4 + 0.5 / 8 - 0.0001 66 | self.assertEqual(helper.frac2bits(tval, 8), [1, 1, 1, 0, 1, 1, 1, 1]) 67 | val = helper.bits2frac([1, 1, 1, 0, 1, 1, 1, 1]) 68 | self.assertLess(tval - val, 0.004) 69 | 70 | def test_pi_fractions(self) -> None: 71 | self.assertEqual(helper.pi_fractions(-3 * math.pi / 2), '-3*pi/2') 72 | self.assertEqual(helper.pi_fractions(3 * math.pi / 2), '3*pi/2') 73 | self.assertEqual(helper.pi_fractions(-3 * math.pi), '-3*pi') 74 | self.assertEqual(helper.pi_fractions(math.pi * 3), '3*pi') 75 | self.assertEqual(helper.pi_fractions(math.pi / 3), 'pi/3') 76 | self.assertEqual(helper.pi_fractions(math.pi / -2), '-pi/2') 77 | 78 | def test_qubit_to_bloch(self) -> None: 79 | psi = state.bitstring(0, 0) 80 | x, y, z = helper.qubit_to_bloch(psi) 81 | self.assertEqual(x, 0.0) 82 | self.assertEqual(y, 0.0) 83 | self.assertEqual(z, 1.0) 84 | 85 | 86 | if __name__ == '__main__': 87 | absltest.main() 88 | -------------------------------------------------------------------------------- /src/lib/ir.py: -------------------------------------------------------------------------------- 1 | # python3 2 | # pylint: disable=invalid-name 3 | 4 | """Compiler IR.""" 5 | 6 | import enum 7 | 8 | from src.lib import helper 9 | 10 | 11 | class Op(enum.Enum): 12 | UNK = 0 13 | SINGLE = 1 14 | CTL = 2 15 | SECTION = 3 16 | END_SECTION = 4 17 | 18 | 19 | class Node: 20 | """Single node in the IR.""" 21 | 22 | def __init__(self, opcode, name, idx0, idx1, gate, val): 23 | self._opcode = opcode 24 | self._name = name 25 | self._idx0 = idx0 26 | self._idx1 = idx1 27 | self._gate = gate 28 | self._val = val 29 | 30 | def __str__(self): 31 | s = '' 32 | if self.is_single(): 33 | s = '{}({})'.format(self.name, self.idx0) 34 | if self.is_ctl(): 35 | s = '{}({}, {})'.format(self.name, self.ctl, self.idx1) 36 | if self._val: 37 | s += '({})'.format(helper.pi_fractions(self.val)) 38 | if self.is_section(): 39 | s += '|-- {} ---'.format(self.name) 40 | if self.is_end_section(): 41 | s += '' 42 | return s 43 | 44 | def to_ctl(self, ctl): 45 | self._opcode = Op.CTL 46 | self._idx1 = self._idx0 47 | self._idx0 = ctl 48 | self._name = 'c' + self._name 49 | 50 | def is_single(self): 51 | return self._opcode == Op.SINGLE 52 | 53 | def is_ctl(self): 54 | return self._opcode == Op.CTL 55 | 56 | def is_gate(self): 57 | return self.is_single() or self.is_ctl() 58 | 59 | def is_section(self): 60 | return self._opcode == Op.SECTION 61 | 62 | def is_end_section(self): 63 | return self._opcode == Op.END_SECTION 64 | 65 | @property 66 | def opcode(self): 67 | return self._opcode 68 | 69 | @property 70 | def name(self): 71 | if not self._name: 72 | return '*unk*' 73 | return self._name 74 | 75 | @property 76 | def desc(self): 77 | return self._name 78 | 79 | @property 80 | def idx0(self): 81 | if not self.is_single(): 82 | raise AssertionError('Invalid use of idx0(), must be single gate.') 83 | return self._idx0 84 | 85 | @property 86 | def ctl(self): 87 | if not self.is_ctl(): 88 | raise AssertionError('Invalid use of ctl(), must be controlled gate.') 89 | return self._idx0 90 | 91 | @property 92 | def idx1(self): 93 | if not self.is_ctl(): 94 | raise AssertionError('Invalid use of idx1(), must be controlled gate.') 95 | return self._idx1 96 | 97 | @property 98 | def val(self): 99 | return self._val 100 | 101 | @property 102 | def gate(self): 103 | return self._gate 104 | 105 | 106 | class Ir: 107 | """Compiler IR.""" 108 | 109 | def __init__(self): 110 | self._ngates = 0 # gates in this IR 111 | self.gates = [] # [] of gates 112 | self.regs = [] # [] of tuples (global reg index, name, reg index) 113 | self.nregs = 0 # number of registers 114 | self.regset = [] # [] of tuples (name, size, reg) for register files 115 | 116 | def __str__(self): 117 | nesting = 0 118 | s = '' 119 | for node in self.gates: 120 | if node.is_section(): 121 | nesting = nesting + 1 122 | if node.is_end_section(): 123 | nesting = nesting - 1 124 | continue 125 | s = s + (' ' * nesting) + str(node) + '\n' 126 | return s 127 | 128 | def reg(self, size, name, register): 129 | self.regset.append((name, size, register)) 130 | for i in range(size): 131 | self.regs.append((self.nregs + i, name, i)) 132 | self.nregs += size 133 | 134 | def add_node(self, node): 135 | self.gates.append(node) 136 | self._ngates += 1 137 | 138 | def single(self, name, idx0, gate, val=None): 139 | self.gates.append(Node(Op.SINGLE, name, idx0, None, gate, val)) 140 | self._ngates += 1 141 | 142 | def controlled(self, name, idx0, idx1, gate, val=None): 143 | self.gates.append(Node(Op.CTL, name, idx0, idx1, gate, val)) 144 | self._ngates += 1 145 | 146 | def section(self, desc): 147 | self.gates.append(Node(Op.SECTION, desc, 0, 0, None, None)) 148 | 149 | def end_section(self): 150 | self.gates.append(Node(Op.END_SECTION, 0, 0, 0, None, None)) 151 | 152 | @property 153 | def ngates(self): 154 | return self._ngates 155 | -------------------------------------------------------------------------------- /src/lib/measure_test.py: -------------------------------------------------------------------------------- 1 | # python3 2 | import math 3 | 4 | from absl.testing import absltest 5 | 6 | from src.lib import ops 7 | from src.lib import state 8 | 9 | 10 | class MeasureTest(absltest.TestCase): 11 | 12 | def test_measure(self): 13 | psi = state.zeros(2) 14 | psi = ops.Hadamard()(psi) 15 | psi = ops.Cnot(0, 1)(psi) 16 | 17 | p0, psi2 = ops.Measure(psi, 0) 18 | self.assertTrue(math.isclose(p0, 0.5, abs_tol=1e-5)) 19 | 20 | # Measure again - now state should have collapsed. 21 | p0, _ = ops.Measure(psi2, 0) 22 | self.assertTrue(math.isclose(p0, 1.0, abs_tol=1e-6)) 23 | 24 | 25 | if __name__ == '__main__': 26 | absltest.main() 27 | -------------------------------------------------------------------------------- /src/lib/optimizer.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Optimize the IR with a variety of (simple) techniques.""" 3 | 4 | from src.lib import ir 5 | 6 | # Currently, this is work-in-progress. 7 | # The code just builds a 2D grid representation from the IR. 8 | 9 | 10 | def build_2d_grid(parm_ir): 11 | """Build simple grid with a column for each gate.""" 12 | 13 | grid = [] 14 | for g in parm_ir.gates: 15 | step = [None] * parm_ir.ngates 16 | if g.is_single(): 17 | step[g.idx0] = g 18 | if g.is_ctl(): 19 | step[g.ctl] = g.ctl 20 | step[g.idx1] = g 21 | grid.append(step) 22 | return grid 23 | 24 | 25 | def ir_from_grid(grid): 26 | """From a grid, reconstruct the IR.""" 27 | 28 | new_ir = ir.Ir() 29 | for step in grid: 30 | for i in range(len(step)): 31 | if not step[i]: 32 | continue 33 | if isinstance(step[i], ir.Node): 34 | new_ir.add_node(step[i]) 35 | return new_ir 36 | 37 | 38 | def optimize(parm_ir): 39 | """Optimize the IR with a variety of techniques.""" 40 | 41 | grid = build_2d_grid(parm_ir) 42 | new_ir = ir_from_grid(grid) 43 | return new_ir 44 | -------------------------------------------------------------------------------- /src/lib/startup.py: -------------------------------------------------------------------------------- 1 | # You can use this file to initialize an interactive python repl. 2 | # For example, after building, set and adjust to your system: 3 | # 4 | # export PYTHONPATH=$HOME/qcc:$HOME/qcc/bazel-bin/src/lib 5 | # export PYTHONSTARTUP=$HOME/qcc/src/lib/startup.py 6 | # 7 | # python3 8 | # >> state.zeros(2) 9 | # State([1.+0.j 0.+0.j 0.+0.j 0.+0.j]) 10 | 11 | """Initialize and load all qcc packages.""" 12 | 13 | # pylint: disable=unused-import 14 | import numpy as np 15 | 16 | from src.lib import bell 17 | from src.lib import circuit 18 | from src.lib import helper 19 | from src.lib import ops 20 | from src.lib import state 21 | from src.lib import tensor 22 | 23 | np.set_printoptions(precision=3) 24 | print('QCC: ' + __file__ + ': initialized, precision=3') 25 | -------------------------------------------------------------------------------- /src/lib/state_test.py: -------------------------------------------------------------------------------- 1 | # python3 2 | 3 | import random 4 | 5 | from absl.testing import absltest 6 | import numpy as np 7 | 8 | from src.lib import state 9 | 10 | 11 | class StateTest(absltest.TestCase): 12 | 13 | def test_simple_state(self): 14 | psi = state.zeros(1) 15 | self.assertEqual(psi[0], 1) 16 | self.assertEqual(psi[1], 0) 17 | 18 | psi = state.ones(1) 19 | self.assertEqual(psi[0], 0) 20 | self.assertEqual(psi[1], 1) 21 | 22 | psi = state.zeros(8) 23 | self.assertEqual(psi[0], 1) 24 | for i in range(1, 2**8 - 1): 25 | self.assertEqual(psi[i], 0) 26 | 27 | psi = state.ones(8) 28 | for i in range(0, 2**8 - 2): 29 | self.assertEqual(psi[i], 0) 30 | self.assertEqual(psi[2**8 - 1], 1) 31 | 32 | psi = state.rand_bits(8) 33 | self.assertEqual(psi.nbits, 8) 34 | 35 | def test_state_gen(self): 36 | hadamard = 1 / np.sqrt(2) * np.array([[1.0, 1.0], [1.0, -1.0]]) 37 | sgate = np.array([[1.0, 0.0], [0.0, 1.0j]]) 38 | 39 | psi = state.zeros(1) 40 | psi = hadamard @ psi 41 | self.assertTrue(psi.is_close(state.plus())) 42 | 43 | psi = state.ones(1) 44 | psi = hadamard @ psi 45 | self.assertTrue(psi.is_close(state.minus())) 46 | 47 | psi = state.zeros(1) 48 | psi = hadamard @ psi 49 | psi = sgate @ psi 50 | self.assertTrue(psi.is_close(state.plusi())) 51 | 52 | psi = state.ones(1) 53 | psi = hadamard @ psi 54 | psi = sgate @ psi 55 | self.assertTrue(psi.is_close(state.minusi())) 56 | 57 | def test_probabilities(self): 58 | psi = state.bitstring(0, 1, 1) 59 | self.assertEqual(psi.prob(0, 0, 0), 0.0) 60 | self.assertEqual(psi.prob(0, 0, 1), 0.0) 61 | self.assertEqual(psi.prob(0, 1, 0), 0.0) 62 | self.assertEqual(psi.prob(0, 1, 1), 1.0) 63 | self.assertEqual(psi.prob(1, 0, 0), 0.0) 64 | self.assertEqual(psi.prob(1, 0, 1), 0.0) 65 | self.assertEqual(psi.prob(1, 1, 0), 0.0) 66 | self.assertEqual(psi.prob(1, 1, 1), 0.0) 67 | 68 | def test_density(self): 69 | psi = state.bitstring(1, 0) 70 | rho = psi.density() 71 | self.assertTrue(rho.is_density()) 72 | self.assertTrue(rho.is_hermitian()) 73 | self.assertTrue(rho.is_pure()) 74 | self.assertFalse(rho.is_unitary()) 75 | 76 | # The sum of eigenvalues for a pure state must be 1.0. 77 | e, _ = np.linalg.eig(rho) 78 | self.assertEqual(np.sum(e), 1.0) 79 | 80 | # Density matrix of a pure state must have rank 1. 81 | rank = np.linalg.matrix_rank(rho) 82 | self.assertEqual(rank, 1) 83 | 84 | def test_regs(self): 85 | a = state.Reg(3, 1, 0) 86 | self.assertEqual('|001>', str(a)) 87 | a = state.Reg(3, 6, 3) 88 | self.assertEqual('|110>', str(a)) 89 | a = state.Reg(3, 7, 6) 90 | self.assertEqual('|111>', str(a)) 91 | 92 | a = state.Reg(3, [1, 0, 0], 3) 93 | self.assertEqual('|100>', str(a)) 94 | a = state.Reg(3, [0, 1, 1], 6) 95 | self.assertEqual('|011>', str(a)) 96 | a = state.Reg(3, [1, 1, 1], 9) 97 | self.assertEqual('|111>', str(a)) 98 | 99 | def test_ordering(self): 100 | a = state.Reg(3, [0, 0, 0], 0) 101 | self.assertGreater(a.psi()[0], 0.99) 102 | a = state.Reg(3, [0, 0, 1], 3) 103 | self.assertGreater(a.psi()[1], 0.99) 104 | a = state.Reg(3, [1, 1, 0], 6) 105 | self.assertGreater(a.psi()[6], 0.99) 106 | a = state.Reg(3, [1, 1, 1], 9) 107 | self.assertGreater(a.psi()[7], 0.99) 108 | 109 | psi = state.bitstring(0, 0, 0) 110 | self.assertGreater(psi[0], 0.99) 111 | psi = state.bitstring(0, 0, 1) 112 | self.assertGreater(psi[1], 0.99) 113 | psi = state.bitstring(1, 1, 0) 114 | self.assertGreater(psi[6], 0.99) 115 | psi = state.bitstring(1, 1, 1) 116 | self.assertGreater(psi[7], 0.99) 117 | 118 | def test_mult_conjugates(self): 119 | a = state.qubit(0.6) 120 | b = state.qubit(0.8) 121 | psi = a * b 122 | psi_adj = np.conj(a) * np.conj(b) 123 | self.assertTrue(np.allclose(psi_adj, np.conj(psi))) 124 | 125 | i1 = np.conj(np.inner(a, b)) 126 | i2 = np.inner(b, a) 127 | self.assertTrue(np.allclose(i1, i2)) 128 | 129 | def test_inner_tensor_product(self): 130 | p1 = state.qubit(random.random()) 131 | p2 = state.qubit(random.random()) 132 | x1 = state.qubit(random.random()) 133 | x2 = state.qubit(random.random()) 134 | 135 | psi1 = p1 * x1 136 | psi2 = p2 * x2 137 | inner1 = np.inner(psi1.conj(), psi2) 138 | inner2 = np.inner(p1.conj(), p2) * np.inner(x1.conj(), x2) 139 | self.assertTrue(np.allclose(inner1, inner2)) 140 | 141 | self.assertTrue(np.allclose(np.inner(psi1.conj(), psi1), 1.0)) 142 | self.assertTrue( 143 | np.allclose(np.inner(p1.conj(), p1) * np.inner(x1.conj(), x1), 1.0) 144 | ) 145 | 146 | def test_normalize(self) -> None: 147 | denormalized = state.State([1.0, 1.0]) 148 | denormalized.normalize() 149 | self.assertTrue( 150 | np.allclose(denormalized, state.State([0.5**0.5, 0.5**0.5])) 151 | ) 152 | 153 | def test_diff(self) -> None: 154 | s0 = state.bitstring(0, 1, 0, 0, 0) 155 | s1 = state.bitstring(0, 1, 0, 0, 1) 156 | self.assertFalse(s0.diff(s1, False)) 157 | self.assertTrue(s0.diff(s0, False)) 158 | 159 | 160 | if __name__ == '__main__': 161 | absltest.main() 162 | -------------------------------------------------------------------------------- /src/lib/tensor.py: -------------------------------------------------------------------------------- 1 | # python3 2 | # pylint: disable=invalid-name 3 | 4 | """Implementation of the Tensor base class.""" 5 | 6 | # This file contains the implementation of the base "tensor" class for all the 7 | # math in this compiler/simulator. This wrapping is not a unique idea, 8 | # many open-source implementations wrap a limted set of core numpy 9 | # functions this way, which should make compilation to, eg., TPU 10 | # much more straight-forward. 11 | 12 | from __future__ import annotations 13 | import math 14 | from absl import flags 15 | import numpy as np 16 | 17 | 18 | # We define the numerical FP bit width with a command-line argument. 19 | # Usage: 20 | # bazel run algorithm -- --tensor_width=128 21 | # python3 algorithm.py --tensor_width=128 22 | # 23 | # For the interactive use in a Python REPL, it is possible that 24 | # the absl command-line parser has not yet been called. This is why 25 | # we bracked tensor_width() in an exception block. 26 | 27 | 28 | flags.DEFINE_integer('tensor_width', 64, 'Bitwidth of FP numbers (64 or 128)') 29 | 30 | 31 | def tensor_width(): 32 | """Return global floating point bit width.""" 33 | 34 | try: # May be neccessary for interactive use. 35 | return flags.FLAGS.tensor_width 36 | except Exception: 37 | return 64 38 | 39 | 40 | # All vectors/matrices in this package will use this base type. 41 | # Valid values are np.complex128 or np.complex64 42 | def tensor_type(): 43 | """Return complex type based on command-line flag.""" 44 | 45 | assert tensor_width() == 64 or tensor_width() == 128 46 | return np.complex64 if tensor_width() == 64 else np.complex128 47 | 48 | 49 | class Tensor(np.ndarray): 50 | """Tensor is a numpy array representing a state or operator.""" 51 | 52 | def __new__(cls, input_array, op_name=None) -> Tensor: 53 | cls.name = op_name 54 | return np.asarray(input_array, dtype=tensor_type()).view(cls) 55 | 56 | def __array_finalize__(self, obj) -> None: 57 | if obj is None: 58 | return 59 | # np.ndarray has complex construction patterns. Because of this, 60 | # if new attributes are needed, this is the place to add them, like this: 61 | # self.info = getattr(obj, 'info', None) 62 | 63 | @property 64 | def nbits(self) -> int: 65 | return int(math.log2(self.shape[0])) 66 | 67 | def is_close(self, arg, tolerance: float = 1e-6) -> bool: 68 | """Check that a 1D or 2D tensor is numerically close to arg.""" 69 | 70 | return np.allclose(self, arg, atol=tolerance) 71 | 72 | def is_hermitian(self) -> bool: 73 | """Check if this tensor is hermitian - Udag = U.""" 74 | 75 | if self.ndim != 2 or self.shape[0] != self.shape[1]: 76 | return False 77 | return self.is_close(np.conj(self.transpose())) 78 | 79 | def is_unitary(self) -> bool: 80 | """Check if this tensor is unitary - Udag*U = I.""" 81 | 82 | return Tensor(np.conj(self.transpose()) @ self).is_close( 83 | Tensor(np.eye(self.shape[0])) 84 | ) 85 | 86 | def is_density(self) -> bool: 87 | """Check if this tensor is a density operator.""" 88 | 89 | if not self.is_hermitian(): 90 | return False 91 | if np.trace(self) > 1.0: 92 | return False 93 | return True 94 | 95 | def is_pure(self) -> bool: 96 | """Check if this tensor describes a pure state (else it is mixed).""" 97 | 98 | if not self.is_density(): 99 | raise ValueError('ispure() can only be applied to a density matrix.') 100 | 101 | tr_rho2 = np.real(np.trace(self @ self)) 102 | return np.allclose(tr_rho2, 1.0) 103 | 104 | def is_permutation(self) -> bool: 105 | """Check whether a tensor is a true permutation matrix.""" 106 | 107 | x = self 108 | return ( 109 | x.ndim == 2 110 | and x.shape[0] == x.shape[1] 111 | and (x.sum(axis=0) == 1).all() 112 | and (x.sum(axis=1) == 1).all() 113 | and ((x == 1) | (x == 0)).all() 114 | ) 115 | 116 | def kron(self, arg: Tensor) -> Tensor: 117 | """Return the kronecker product of this object with arg.""" 118 | 119 | lhs = self.name if (hasattr(self, 'name') and self.name) else '?' 120 | rhs = arg.name if (hasattr(arg, 'name') and arg.name) else '?' 121 | return self.__class__(np.kron(self, arg), lhs + '*' + rhs) 122 | 123 | def __mul__(self, arg: Tensor) -> Tensor: # type: ignore[override] 124 | """Inline * operator maps to kronecker product.""" 125 | 126 | return self.kron(arg) 127 | 128 | def kpow(self, n: int) -> Tensor: 129 | """Return the tensor product of this object with itself `n` times.""" 130 | 131 | if n == 0: 132 | return self.__class__(1.0) 133 | if n == 1: 134 | return self.__class__(self, self.name) 135 | t = self 136 | for _ in range(n - 1): 137 | t = np.kron(t, self) 138 | return self.__class__(t, (t.name if t.name else '?') + f'^{n}') 139 | -------------------------------------------------------------------------------- /src/lib/tensor_test.py: -------------------------------------------------------------------------------- 1 | # python3 2 | 3 | from absl.testing import absltest 4 | from src.lib import tensor 5 | 6 | 7 | class TensorTest(absltest.TestCase): 8 | 9 | def test_pow(self): 10 | t = tensor.Tensor([1.0, 1.0]) 11 | self.assertLen(t.shape, 1) 12 | self.assertEqual(t.shape[0], 2) 13 | 14 | t0 = t.kpow(0.0) 15 | self.assertEqual(t0, 1.0) 16 | 17 | t1 = t.kpow(1) 18 | self.assertLen(t1.shape, 1) 19 | self.assertEqual(t1.shape[0], 2) 20 | 21 | t2 = t.kpow(2) 22 | self.assertLen(t2.shape, 1) 23 | self.assertEqual(t2.shape[0], 4) 24 | 25 | m = tensor.Tensor([[1.0, 1.0], [1.0, 1.0]]) 26 | self.assertLen(m.shape, 2) 27 | self.assertEqual(m.shape[0], 2) 28 | self.assertEqual(m.shape[1], 2) 29 | 30 | m0 = m.kpow(0.0) 31 | self.assertEqual(m0, 1.0) 32 | 33 | m1 = m.kpow(1) 34 | self.assertLen(m1.shape, 2) 35 | self.assertEqual(m1.shape[0], 2) 36 | self.assertEqual(m1.shape[1], 2) 37 | 38 | m2 = m.kpow(2) 39 | self.assertLen(m2.shape, 2) 40 | self.assertEqual(m2.shape[0], 4) 41 | self.assertEqual(m2.shape[1], 4) 42 | 43 | def test_hermitian(self): 44 | t = tensor.Tensor([[2.0, 0.0], [0.0, 2.0]]) 45 | self.assertTrue(t.is_hermitian()) 46 | self.assertFalse(t.is_unitary()) 47 | 48 | 49 | if __name__ == '__main__': 50 | absltest.main() 51 | -------------------------------------------------------------------------------- /src/lib/test_all.sh: -------------------------------------------------------------------------------- 1 | for t in `ls *test.py`; 2 | do 3 | echo 4 | echo "Running $t" 5 | python3 $t || exit 1 6 | done 7 | -------------------------------------------------------------------------------- /src/lib/xgates.cc: -------------------------------------------------------------------------------- 1 | // Python extension to accelerate gate applications. 2 | // 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | typedef std::complex cmplxd; 14 | typedef std::complex cmplxf; 15 | 16 | // apply1 applies a single gate to a state. 17 | // 18 | // Gates are typically 2x2 matrices, but in this implementation they 19 | // are flattened to a 1x4 array: 20 | // | a b | 21 | // | c d | -> | a b c d | 22 | // 23 | template 24 | void apply1(cmplx_type *psi, cmplx_type gate[4], 25 | int nbits, int tgt) { 26 | tgt = nbits - tgt - 1; 27 | int q2 = 1 << tgt; 28 | if (q2 < 0) { 29 | fprintf(stderr, "***Error***: Negative qubit index in apply1().\n"); 30 | fprintf(stderr, " Perhaps using wrongly shaped state?\n"); 31 | exit(EXIT_FAILURE); 32 | } 33 | for (int g = 0; g < 1 << nbits; g += (1 << (tgt+1))) { 34 | for (int i = g; i < g + q2; ++i) { 35 | cmplx_type t1 = gate[0] * psi[i] + gate[1] * psi[i + q2]; 36 | cmplx_type t2 = gate[2] * psi[i] + gate[3] * psi[i + q2]; 37 | psi[i] = t1; 38 | psi[i + q2] = t2; 39 | } 40 | } 41 | } 42 | 43 | // applyc applies a controlled gate to a state. 44 | // 45 | template 46 | void applyc(cmplx_type *psi, cmplx_type gate[4], 47 | int nbits, int ctl, int tgt) { 48 | tgt = nbits - tgt - 1; 49 | ctl = nbits - ctl - 1; 50 | int q2 = 1 << tgt; 51 | if (q2 < 0) { 52 | fprintf(stderr, "***Error***: Negative qubit index in applyc().\n"); 53 | fprintf(stderr, " Perhaps using wrongly shaped state?\n"); 54 | exit(EXIT_FAILURE); 55 | } 56 | for (int g = 0; g < 1 << nbits; g += 1 << (tgt+1)) { 57 | for (int i = g; i < g + q2; ++i) { 58 | int idx = g * (1 << nbits) + i; 59 | if (idx & (1 << ctl)) { 60 | cmplx_type t1 = gate[0] * psi[i] + gate[1] * psi[i + q2]; 61 | cmplx_type t2 = gate[2] * psi[i] + gate[3] * psi[i + q2]; 62 | psi[i] = t1; 63 | psi[i + q2] = t2; 64 | } 65 | } 66 | } 67 | } 68 | 69 | // --------------------------------------------------------------- 70 | // Python wrapper functions to call above accelerators. 71 | 72 | template 73 | void apply1_python(PyObject *param_psi, PyObject *param_gate, 74 | int nbits, int tgt) { 75 | PyArrayObject *psi_arr = 76 | (PyArrayObject*) PyArray_FROM_OTF(param_psi, npy_type, NPY_IN_ARRAY); 77 | cmplx_type *psi = ((cmplx_type *)PyArray_GETPTR1(psi_arr, 0)); 78 | 79 | PyArrayObject *gate_arr = 80 | (PyArrayObject*) PyArray_FROM_OTF(param_gate, npy_type, NPY_IN_ARRAY); 81 | cmplx_type *gate = ((cmplx_type *)PyArray_GETPTR1(gate_arr, 0)); 82 | 83 | apply1(psi, gate, nbits, tgt); 84 | 85 | Py_DECREF(psi_arr); 86 | Py_DECREF(gate_arr); 87 | } 88 | 89 | static PyObject *apply1_c(PyObject *dummy, PyObject *args) { 90 | PyObject *param_psi = NULL; 91 | PyObject *param_gate = NULL; 92 | int nbits; 93 | int tgt; 94 | int bit_width; 95 | 96 | if (!PyArg_ParseTuple(args, "OOiii", ¶m_psi, ¶m_gate, 97 | &nbits, &tgt, &bit_width)) 98 | return NULL; 99 | if (bit_width == 128) { 100 | apply1_python(param_psi, 101 | param_gate, nbits, tgt); 102 | } else { 103 | apply1_python(param_psi, 104 | param_gate, nbits, tgt); 105 | } 106 | Py_RETURN_NONE; 107 | } 108 | 109 | template 110 | void applyc_python(PyObject *param_psi, PyObject *param_gate, 111 | int nbits, int ctl, int tgt) { 112 | PyArrayObject *psi_arr = 113 | (PyArrayObject*) PyArray_FROM_OTF(param_psi, npy_type, NPY_IN_ARRAY); 114 | cmplx_type *psi = ((cmplx_type *)PyArray_GETPTR1(psi_arr, 0)); 115 | 116 | PyArrayObject *gate_arr = 117 | (PyArrayObject*) PyArray_FROM_OTF(param_gate, npy_type, NPY_IN_ARRAY); 118 | cmplx_type *gate = ((cmplx_type *)PyArray_GETPTR1(gate_arr, 0)); 119 | 120 | applyc(psi, gate, nbits, ctl, tgt); 121 | 122 | Py_DECREF(psi_arr); 123 | Py_DECREF(gate_arr); 124 | } 125 | 126 | static PyObject *applyc_c(PyObject *dummy, PyObject *args) { 127 | PyObject *param_psi = NULL; 128 | PyObject *param_gate = NULL; 129 | int nbits; 130 | int ctl; 131 | int tgt; 132 | int bit_width; 133 | 134 | if (!PyArg_ParseTuple(args, "OOiiii", ¶m_psi, ¶m_gate, 135 | &nbits, &ctl, &tgt, &bit_width)) 136 | return NULL; 137 | if (bit_width == 128) { 138 | applyc_python(param_psi, 139 | param_gate, nbits, ctl, tgt); 140 | } else { 141 | applyc_python(param_psi, 142 | param_gate, nbits, ctl, tgt); 143 | } 144 | Py_RETURN_NONE; 145 | } 146 | 147 | // --------------------------------------------------------------- 148 | // Python boilerplate to expose above wrappers to programs. 149 | // 150 | static PyMethodDef xgates_methods[] = { 151 | {"apply1", apply1_c, METH_VARARGS, 152 | "Apply single-qubit gate, complex double"}, 153 | {"applyc", applyc_c, METH_VARARGS, 154 | "Apply controlled qubit gate, complex double"}, 155 | {NULL, NULL, 0, NULL}}; 156 | 157 | static struct PyModuleDef xgates_definition = { 158 | PyModuleDef_HEAD_INIT, 159 | "xgates", 160 | "Python extension to accelerate quantum simulation math", 161 | -1, 162 | xgates_methods 163 | }; 164 | 165 | PyMODINIT_FUNC PyInit_xgates(void) { 166 | Py_Initialize(); 167 | import_array(); 168 | return PyModule_Create(&xgates_definition); 169 | } 170 | 171 | // To accommodate different build environments, 172 | // this one might be needed. 173 | PyMODINIT_FUNC PyInit_libxgates(void) { 174 | return PyInit_xgates(); 175 | } 176 | -------------------------------------------------------------------------------- /src/libq/BUILD: -------------------------------------------------------------------------------- 1 | cc_library( 2 | name = "libq", 3 | srcs = [ 4 | "apply.cc", 5 | "gates.cc", 6 | "qureg.cc", 7 | ], 8 | hdrs = ["libq.h"], 9 | copts = [ 10 | "-O3", 11 | "-ffast-math", 12 | "-fstrict-aliasing", 13 | ], 14 | ) 15 | 16 | cc_library( 17 | name = "libq_jit", 18 | srcs = [ 19 | "apply.cc", 20 | "gates_jit.cc", 21 | "qureg.cc", 22 | ], 23 | hdrs = ["libq.h"], 24 | copts = [ 25 | "-O3", 26 | "-ffast-math", 27 | "-fstrict-aliasing", 28 | ], 29 | ) 30 | 31 | cc_test( 32 | name = "libq_test", 33 | srcs = ["libq_test.cc"], 34 | deps = [ 35 | ":libq", 36 | ], 37 | ) 38 | 39 | cc_test( 40 | name = "libq_arith_test", 41 | srcs = ["libq_arith_test.cc"], 42 | copts = [ 43 | "-O3", 44 | "-ffast-math", 45 | "-fstrict-aliasing", 46 | ], 47 | deps = [ 48 | ":libq", 49 | ], 50 | ) 51 | 52 | cc_test( 53 | name = "libq_arith_jit_test", 54 | srcs = ["libq_arith_test.cc"], 55 | copts = [ 56 | "-O3", 57 | "-ffast-math", 58 | "-fstrict-aliasing", 59 | ], 60 | deps = [ 61 | ":libq_jit", 62 | ], 63 | ) 64 | 65 | cc_test( 66 | name = "libq_order22_test", 67 | srcs = ["libq_order22_test.cc"], 68 | copts = [ 69 | "-O3", 70 | "-ffast-math", 71 | "-fstrict-aliasing", 72 | ], 73 | deps = [ 74 | ":libq", 75 | ], 76 | ) 77 | 78 | cc_test( 79 | name = "libq_order22_jit_test", 80 | srcs = ["libq_order22_test.cc"], 81 | copts = [ 82 | "-O3", 83 | "-ffast-math", 84 | "-fstrict-aliasing", 85 | ], 86 | deps = [ 87 | ":libq_jit", 88 | ], 89 | ) 90 | -------------------------------------------------------------------------------- /src/libq/apply.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | 6 | #include "libq.h" 7 | 8 | namespace libq { 9 | 10 | static inline unsigned int hash64(state_t key, int width) { 11 | unsigned int k32 = (key & 0xFFFFFFFF) ^ (key >> 32); 12 | k32 *= 0x9e370001UL; 13 | k32 = k32 >> (32 - width); 14 | return k32; 15 | } 16 | 17 | state_t get_state(state_t a, qureg *reg) { 18 | unsigned int i = hash64(a, reg->hashw); 19 | while (reg->hash[i]) { 20 | if (reg->state[reg->hash[i] - 1] == a) { 21 | return reg->hash[i] - 1; 22 | } 23 | i++; 24 | if (i == static_cast((1 << reg->hashw))) { 25 | break; 26 | } 27 | } 28 | return -1; 29 | } 30 | 31 | void libq_add_hash(state_t a, int pos, qureg *reg) { 32 | int mark = 0; 33 | 34 | int i = hash64(a, reg->hashw); 35 | while (reg->hash[i]) { 36 | i++; 37 | if (i == (1 << reg->hashw)) { 38 | if (!mark) { 39 | i = 0; 40 | mark = 1; 41 | } else { 42 | // TODO(rhundt): Handle full hashtable. 43 | } 44 | } 45 | } 46 | reg->hash[i] = pos + 1; 47 | if (reg->hash_caching && reg->hits < HASH_CACHE_SIZE) { 48 | reg->hash_hits[reg->hits] = i; 49 | reg->hits += 1; 50 | } 51 | } 52 | 53 | void libq_reconstruct_hash(qureg *reg) { 54 | reg->hash_computes += 1; 55 | 56 | if (reg->hash_caching && reg->hits < HASH_CACHE_SIZE) { 57 | for (int i = 0; i < reg->hits; ++i) { 58 | reg->hash[reg->hash_hits[i]] = 0; 59 | reg->hash_hits[i] = 0; 60 | } 61 | reg->hits = 0; 62 | } else { 63 | memset(reg->hash, 0, (1 << reg->hashw) * sizeof(int)); 64 | memset(reg->hash_hits, 0, reg->hits * sizeof(int)); 65 | reg->hits = 0; 66 | // TODO(rhundt): Apparently the compiler doesn't convert this loop. 67 | // Investigate. 68 | // for (int i = 0; i < (1 << reg->hashw); ++i) { 69 | // reg->hash[i] = 0; 70 | // } 71 | } 72 | for (int i = 0; i < reg->size; ++i) { 73 | libq_add_hash(reg->state[i], i, reg); 74 | } 75 | 76 | } 77 | 78 | void libq_gate1(int target, cmplx m[4], qureg *reg) { 79 | int addsize = 0; 80 | 81 | libq_reconstruct_hash(reg); 82 | 83 | /* calculate the number of basis states to be added */ 84 | for (int i = 0; i < reg->size; ++i) { 85 | /* determine whether XORed basis state already exists */ 86 | if (get_state(reg->state[i] ^ (static_cast(1) << target), reg) == 87 | static_cast(-1)) 88 | addsize++; 89 | } 90 | 91 | /* allocate memory for the new basis states */ 92 | if (addsize) { 93 | reg->state = static_cast( 94 | realloc(reg->state, (reg->size + addsize) * sizeof(state_t))); 95 | reg->amplitude = static_cast( 96 | realloc(reg->amplitude, (reg->size + addsize) * sizeof(cmplx))); 97 | 98 | memset(®->state[reg->size], 0, addsize * sizeof(int)); 99 | memset(®->amplitude[reg->size], 0, addsize * sizeof(cmplx)); 100 | if (reg->size + addsize > reg->maxsize) { 101 | reg->maxsize = reg->size + addsize; 102 | } 103 | } 104 | 105 | char *done = static_cast(calloc(reg->size + addsize, sizeof(char))); 106 | int next_state = reg->size; 107 | float limit = (1.0 / (static_cast(1) << reg->width)) * 1e-6; 108 | 109 | /* perform the actual matrix multiplication */ 110 | for (int i = 0; i < reg->size; ++i) { 111 | if (!done[i]) { 112 | /* determine if the target of the basis state is set */ 113 | int is_set = reg->state[i] & (static_cast(1) << target); 114 | int xor_index = 115 | get_state(reg->state[i] ^ (static_cast(1) << target), reg); 116 | cmplx tnot = xor_index >= 0 ? reg->amplitude[xor_index] : 0; 117 | cmplx t = reg->amplitude[i]; 118 | 119 | if (is_set) { 120 | reg->amplitude[i] = m[2] * tnot + m[3] * t; 121 | } else { 122 | reg->amplitude[i] = m[0] * t + m[1] * tnot; 123 | } 124 | 125 | if (xor_index >= 0) { 126 | if (is_set) { 127 | reg->amplitude[xor_index] = m[0] * tnot + m[1] * t; 128 | } else { 129 | reg->amplitude[xor_index] = m[2] * t + m[3] * tnot; 130 | } 131 | } else { /* new basis state will be created */ 132 | if (abs(m[1]) == 0.0 && is_set) break; 133 | if (abs(m[2]) == 0.0 && !is_set) break; 134 | 135 | reg->state[next_state] = 136 | reg->state[i] ^ (static_cast(1) << target); 137 | reg->amplitude[next_state] = is_set ? m[1] * t : m[2] * t; 138 | next_state += 1; 139 | } 140 | if (xor_index >= 0) { 141 | done[xor_index] = 1; 142 | } 143 | } 144 | } 145 | 146 | reg->size += addsize; 147 | free(done); 148 | 149 | /* remove basis states with extremely small amplitude */ 150 | if (reg->hashw) { 151 | int decsize = 0; 152 | for (int i = 0, j = 0; i < reg->size; ++i) { 153 | if (probability(reg->amplitude[i]) < limit) { 154 | j++; 155 | decsize++; 156 | } else if (j) { 157 | reg->state[i - j] = reg->state[i]; 158 | reg->amplitude[i - j] = reg->amplitude[i]; 159 | } 160 | } 161 | 162 | if (decsize) { 163 | reg->size -= decsize; 164 | 165 | // These seem redundant. 166 | // reg->amplitude = static_cast( 167 | // realloc(reg->amplitude, reg->size * sizeof(cmplx))); 168 | // reg->state = static_cast( 169 | // realloc(reg->state, reg->size * sizeof(state_t))); 170 | } 171 | } 172 | 173 | if (reg->size > (1 << (reg->hashw - 1))) 174 | fprintf(stderr, "Warning: inefficient hash table (size %i vs hash %i)\n", 175 | reg->size, 1 << reg->hashw); 176 | } 177 | 178 | } // namespace libq 179 | -------------------------------------------------------------------------------- /src/libq/gates.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | 5 | #include "libq.h" 6 | 7 | namespace libq { 8 | 9 | void v(int target, qureg *reg) { 10 | static cmplx mv[4] = {cmplx(0.5, 0.5), cmplx(0.5, 0.5), 11 | cmplx(0.5, -0.5), cmplx(0.5, 0.5)}; 12 | for (int i = 0; i < reg->size; ++i) { 13 | libq_gate1(target, mv, reg); 14 | } 15 | } 16 | 17 | void x(int target, qureg *reg) { 18 | for (int i = 0; i < reg->size; ++i) { 19 | reg->bit_xor(i, target); 20 | } 21 | } 22 | 23 | void y(int target, qureg *reg) { 24 | for (int i = 0; i < reg->size; ++i) { 25 | reg->bit_xor(i, target); 26 | if (reg->bit_is_set(i, target)) 27 | reg->amplitude[i] *= cmplx(0, 1.0); 28 | else 29 | reg->amplitude[i] *= cmplx(0, -1.0); 30 | } 31 | } 32 | 33 | void z(int target, qureg *reg) { 34 | for (int i = 0; i < reg->size; ++i) { 35 | if (reg->bit_is_set(i, target)) { 36 | reg->amplitude[i] *= -1; 37 | } 38 | } 39 | } 40 | 41 | void h(int target, qureg *reg) { 42 | static cmplx mh[4] = {sqrt(1.0/2), sqrt(1.0/2), sqrt(1.0/2), 43 | -sqrt(1.0/2)}; 44 | libq_gate1(target, mh, reg); 45 | } 46 | 47 | 48 | void yroot(int target, qureg *reg) { 49 | static cmplx mv[4] = {cmplx(0.5, 0.5), cmplx(-0.5, -0.5), 50 | cmplx(0.5, 0.5), cmplx(0.5, 0.5)}; 51 | for (int i = 0; i < reg->size; ++i) { 52 | libq_gate1(target, mv, reg); 53 | } 54 | } 55 | 56 | void walsh(int width, qureg *reg) { 57 | for (int i = 0; i < width; ++i) { 58 | h(i, reg); 59 | } 60 | } 61 | 62 | cmplx cexp(float phi) { 63 | return cmplx(std::cos(phi), 0.0) + 64 | cmplx(0.0, 1.0) * cmplx(std::sin(phi), 0.0); 65 | } 66 | 67 | void u1(int target, float gamma, qureg *reg) { 68 | cmplx z = cexp(gamma); 69 | for (int i = 0; i < reg->size; ++i) { 70 | if (reg->bit_is_set(i, target)) { 71 | reg->amplitude[i] *= z; 72 | } 73 | } 74 | } 75 | 76 | void t(int target, qureg *reg) { 77 | cmplx z = cexp(M_PI / 4.0); 78 | for (int i = 0; i < reg->size; ++i) { 79 | if (reg->bit_is_set(i, target)) { 80 | reg->amplitude[i] *= z; 81 | } 82 | } 83 | } 84 | 85 | void cu1(int control, int target, float gamma, qureg *reg) { 86 | cmplx z = cexp(gamma); 87 | for (int i = 0; i < reg->size; ++i) { 88 | if (reg->bit_is_set(i, control)) { 89 | if (reg->bit_is_set(i, target)) { 90 | reg->amplitude[i] *= z; 91 | } 92 | } 93 | } 94 | } 95 | 96 | void cv(int control, int target, qureg *reg) { 97 | for (int i = 0; i < reg->size; ++i) { 98 | if (reg->bit_is_set(i, control)) { 99 | if (reg->bit_is_set(i, target)) { 100 | static cmplx mv[4] = {cmplx(0.5, 0.5), cmplx(0.5, -0.5), 101 | cmplx(0.5, -0.5), cmplx(0.5, 0.5)}; 102 | libq_gate1(target, mv, reg); 103 | } 104 | } 105 | } 106 | } 107 | 108 | void cv_adj(int control, int target, qureg *reg) { 109 | for (int i = 0; i < reg->size; ++i) { 110 | if (reg->bit_is_set(i, control)) { 111 | if (reg->bit_is_set(i, target)) { 112 | static cmplx mv[4] = {cmplx(0.5, -0.5), cmplx(0.5, 0.5), 113 | cmplx(0.5, 0.5), cmplx(0.5, -0.5)}; 114 | libq_gate1(target, mv, reg); 115 | } 116 | } 117 | } 118 | } 119 | 120 | void cx(int control, int target, qureg *reg) { 121 | for (int i = 0; i < reg->size; ++i) { 122 | if (reg->bit_is_set(i, control)) { 123 | reg->bit_xor(i, target); 124 | } 125 | } 126 | } 127 | 128 | void cz(int control, int target, qureg *reg) { 129 | for (int i = 0; i < reg->size; ++i) { 130 | if (reg->bit_is_set(i, control)) { 131 | if (reg->bit_is_set(i, target)) { 132 | reg->amplitude[i] *= -1; 133 | } 134 | } 135 | } 136 | } 137 | 138 | void ccx(int control0, int control1, int target, qureg *reg) { 139 | for (int i = 0; i < reg->size; ++i) { 140 | if (reg->bit_is_set(i, control0)) { 141 | if (reg->bit_is_set(i, control1)) { 142 | reg->bit_xor(i, target); 143 | } 144 | } 145 | } 146 | } 147 | 148 | void flush(qureg* reg) { 149 | print_qureg_stats(reg); 150 | } 151 | 152 | 153 | } // namespace libq 154 | -------------------------------------------------------------------------------- /src/libq/libq.h: -------------------------------------------------------------------------------- 1 | #ifndef LIBQ_LIBQ_H_ 2 | #define LIBQ_LIBQ_H_ 3 | 4 | #include 5 | 6 | // #include "base/integral_types.h" 7 | 8 | namespace libq { 9 | 10 | typedef std::complex cmplx; 11 | typedef unsigned long long state_t; 12 | #define HASH_CACHE_SIZE (1024 * 64) 13 | 14 | struct qureg_t { 15 | cmplx* amplitude; 16 | state_t* state; 17 | int width; /* number of qubits in the qureg */ 18 | int size; /* number of non-zero vectors */ 19 | int maxsize; /* largest size reached */ 20 | int hash_computes; /* how often is the hash table computed */ 21 | int hashw; /* width of the hash array */ 22 | int* hash; 23 | 24 | // Certain circuits have a very large number of theoretical states, 25 | // but only a very small number of states with non-zero probability. 26 | // For those inputs, it can be beneficial to not memset the whole 27 | // hash table, but only the elements that have been set. 28 | // 29 | // This can be enabled by setting hash_caching to true. 30 | // 31 | bool hash_caching; /* Cache hash values, optional */ 32 | int* hash_hits; 33 | int hits; 34 | 35 | bool bit_is_set(int index, int target) __attribute__ ((pure)) { 36 | return state[index] & (static_cast(1) << target); 37 | } 38 | void bit_xor(int index, int target) { 39 | state[index] ^= (static_cast(1) << target); 40 | } 41 | }; 42 | typedef struct qureg_t qureg; 43 | 44 | qureg *new_qureg(state_t initval, int width); 45 | void delete_qureg(qureg *reg); 46 | void print_qureg(qureg *reg); 47 | void print_qureg_stats(qureg *reg); 48 | void flush(qureg* reg); 49 | 50 | void x(int target, qureg *reg); 51 | void y(int target, qureg *reg); 52 | void z(int target, qureg *reg); 53 | void h(int target, qureg *reg); 54 | void t(int target, qureg *reg); 55 | void v(int target, qureg *reg); 56 | void yroot(int target, qureg *reg); 57 | void walsh(int width, qureg *reg); 58 | void cx(int control, int target, qureg *reg); 59 | void cz(int control, int target, qureg *reg); 60 | void ccx(int control0, int control1, int target, qureg *reg); 61 | void u1(int target, float gamma, qureg *reg); 62 | void cu1(int control, int target, float gamma, qureg *reg); 63 | void cv(int control, int target, qureg *reg); 64 | void cv_adj(int control, int target, qureg *reg); 65 | 66 | // -- Internal ---------------------------------------- 67 | 68 | float probability(cmplx ampl); 69 | void libq_gate1(int target, cmplx m[4], qureg *reg); 70 | 71 | } // namespace libq 72 | 73 | #endif // LIBQ_LIBQ_H_ 74 | -------------------------------------------------------------------------------- /src/libq/libq_test.cc: -------------------------------------------------------------------------------- 1 | #include "libq.h" 2 | 3 | #include 4 | #include 5 | 6 | int main(int argc, char* argv[]) { 7 | libq::qureg* q = libq::new_qureg(0, 2); 8 | 9 | libq::h(0, q); 10 | libq::cx(0, 1, q); 11 | libq::u1(1, M_PI / 8.0, q); 12 | printf(" # States: %d\n", q->size); 13 | libq::print_qureg(q); 14 | 15 | libq::delete_qureg(q); 16 | return EXIT_SUCCESS; 17 | } 18 | -------------------------------------------------------------------------------- /src/libq/qureg.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "libq.h" 4 | 5 | namespace libq { 6 | 7 | float probability(cmplx ampl) { 8 | return ampl.real() * ampl.real() + ampl.imag() * ampl.imag(); 9 | } 10 | 11 | qureg *new_qureg(state_t initval, int width) { 12 | qureg *reg = new qureg; 13 | 14 | reg->width = width; 15 | reg->size = 1; 16 | reg->maxsize = 0; 17 | reg->hash_computes = 0; 18 | reg->hashw = width + 2; 19 | 20 | /* Allocate memory for 1 base state */ 21 | reg->state = static_cast(calloc(1, sizeof(state_t))); 22 | reg->amplitude = static_cast(calloc(1, sizeof(cmplx))); 23 | 24 | /* Allocate the hash table */ 25 | reg->hash = static_cast(calloc(1 << reg->hashw, sizeof(int))); 26 | 27 | // For libq_arith_test, this technique brings runtime down from 3.2 secs 28 | // to 1.2 secs! Having super efficient hash-tables + management will make 29 | // all the difference for sparse quantum circuits. 30 | // 31 | reg->hash_caching = true; 32 | reg->hash_hits = nullptr; 33 | reg->hits = 0; 34 | if (reg->hash_caching) { 35 | reg->hash_hits = static_cast(calloc(HASH_CACHE_SIZE, sizeof(int))); 36 | } 37 | 38 | /* Initialize the quantum register */ 39 | reg->state[0] = initval; 40 | reg->amplitude[0] = 1; 41 | 42 | return reg; 43 | } 44 | 45 | void delete_qureg(qureg *reg) { 46 | if (reg->hash) { 47 | free(reg->hash); 48 | reg->hash = nullptr; 49 | } 50 | if (reg->hash_hits) { 51 | free(reg->hash_hits); 52 | reg->hash_hits = nullptr; 53 | reg->hits = 0; 54 | } 55 | free(reg->amplitude); 56 | reg->amplitude = nullptr; 57 | if (reg->state) { 58 | free(reg->state); 59 | reg->state = nullptr; 60 | } 61 | delete (reg); 62 | } 63 | 64 | void print_qureg(qureg *reg) { 65 | printf("States with non-zero probability:\n"); 66 | for (int i = 0; i < reg->size; ++i) { 67 | printf(" % f %+fi|%llu> (%e) (|", reg->amplitude[i].real(), 68 | reg->amplitude[i].imag(), reg->state[i], 69 | probability(reg->amplitude[i])); 70 | for (int j = reg->width - 1; j >= 0; --j) { 71 | if (j % 4 == 3) { 72 | printf(" "); 73 | } 74 | printf("%i", (((static_cast(1) << j) & reg->state[i]) > 0)); 75 | } 76 | printf(">)\n"); 77 | } 78 | } 79 | 80 | void print_qureg_stats(qureg *reg) { 81 | printf("# of qubits : %d\n", reg->width); 82 | printf("# of hash computes : %d\n", reg->hash_computes); 83 | printf("Maximum # of states: %d, theoretical: %d, %.3f%%\n", 84 | reg->maxsize, 2 << reg->width, 85 | 100.0 * reg->maxsize / (2 << reg->width)); 86 | } 87 | 88 | } // namespace libq 89 | -------------------------------------------------------------------------------- /src/max_cut.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Max Cut Algorithm for bi-directional graph.""" 3 | 4 | import random 5 | from typing import List, Tuple 6 | 7 | from absl import app 8 | from absl import flags 9 | 10 | import numpy as np 11 | from src.lib import helper 12 | from src.lib import ops 13 | 14 | 15 | flags.DEFINE_integer('nodes', 12, 'Number of graph nodes') 16 | flags.DEFINE_boolean('graph', False, 'Dump graph in dot format') 17 | flags.DEFINE_integer('iterations', 10, 'Number of experiments') 18 | 19 | 20 | def build_graph(num: int = 0) -> Tuple[int, List[Tuple[int, int, float]]]: 21 | """Build a graph of num nodes.""" 22 | 23 | assert num >= 3, 'Must request graph of at least 3 nodes.' 24 | 25 | # Nodes are tuples: (from: int, to: int, weight: float). 26 | weight = 5.0 27 | nodes = [(0, 1, 1.0), (1, 2, 2.0), (0, 2, 3.0)] 28 | for i in range(num - 3): 29 | rand_nodes = random.sample(range(0, 3 + i - 1), 2) 30 | nodes.append((3 + i, rand_nodes[0], weight * np.random.random())) 31 | nodes.append((3 + i, rand_nodes[1], weight * np.random.random())) 32 | return num, nodes 33 | 34 | 35 | def graph_to_dot(n: int, nodes: List[Tuple[int, int, float]], max_cut) -> None: 36 | """Convert graph (up to 64 nodes) to dot file.""" 37 | 38 | print('graph {') 39 | print(' {\n node [ style=filled ]') 40 | pattern = bin(max_cut)[2:].zfill(n) 41 | for idx, val in enumerate(pattern): 42 | if val == '0': 43 | print(f' "{idx}" [fillcolor=lightgray]') 44 | print(' }') 45 | for node in nodes: 46 | print(' "{}" -- "{}" [label="{:.1f}",weight="{:.2f}"];' 47 | .format(node[0], node[1], node[2], node[2])) 48 | print('}') 49 | 50 | 51 | def graph_to_hamiltonian(n: int, 52 | nodes: List[Tuple[int, int, float]]) -> ops.Operator: 53 | """Compute Hamiltonian matrix from graph.""" 54 | 55 | hamil = np.zeros((2**n, 2**n)) 56 | for node in nodes: 57 | idx1 = max(node[0], node[1]) 58 | idx2 = min(node[0], node[1]) 59 | 60 | op = ops.Identity(idx1) * (node[2] * ops.PauliZ()) 61 | op = op * ops.Identity(idx2 - idx1 + 1) 62 | op = op * (node[2] * ops.PauliZ()) 63 | op = op * ops.Identity(n - idx2 + 1) 64 | 65 | hamil = hamil + op 66 | return ops.Operator(hamil) 67 | 68 | 69 | def tensor_diag(n: int, fr: int, to: int, w: float): 70 | """Construct a tensor product from diagonal matrices.""" 71 | 72 | def tensor_product(w1: float, w2: float, diag): 73 | # pylint: disable=g-complex-comprehension 74 | return [j for i in zip([x * w1 for x in diag], 75 | [x * w2 for x in diag]) for j in i] 76 | 77 | diag = [w, -w] if (0 == fr or 0 == to) else [1, 1] 78 | for i in range(1, n): 79 | if i == fr or i == to: 80 | diag = tensor_product(w, -w, diag) 81 | else: 82 | diag = tensor_product(1, 1, diag) 83 | return diag 84 | 85 | 86 | def graph_to_diagonal_h(n: int, 87 | nodes: List[Tuple[int, int, float]]) -> List[float]: 88 | """Construct diag(H).""" 89 | 90 | h = [0.0] * 2**n 91 | for node in nodes: 92 | diag = tensor_diag(n, node[0], node[1], node[2]) 93 | for idx, val in enumerate(diag): 94 | h[idx] += val 95 | return h 96 | 97 | 98 | def compute_max_cut(n: int, 99 | nodes: List[Tuple[int, int, float]]) -> int: 100 | """Compute (inefficiently) the max cut, exhaustively.""" 101 | 102 | max_cut = -1000.0 103 | for bits in helper.bitprod(n): 104 | # Collect in/out sets. 105 | iset = [] 106 | oset = [] 107 | for idx, val in enumerate(bits): 108 | if val == 0: 109 | iset.append(idx) 110 | else: 111 | oset.append(idx) 112 | 113 | # Compute costs for this cut, record maximum. 114 | cut = 0.0 115 | for node in nodes: 116 | if node[0] in iset and node[1] in oset: 117 | cut += node[2] 118 | if node[1] in iset and node[0] in oset: 119 | cut += node[2] 120 | if cut > max_cut: 121 | max_cut_in, max_cut_out = iset.copy(), oset.copy() 122 | max_cut = cut 123 | max_bits = bits 124 | 125 | state = bin(helper.bits2val(max_bits))[2:].zfill(n) 126 | print('Max Cut. N: {}, Max: {:.1f}, {}-{}, |{}>' 127 | .format(n, np.real(max_cut), max_cut_in, max_cut_out, 128 | state)) 129 | return helper.bits2val(max_bits) 130 | 131 | 132 | def run_experiment(num_nodes: int): 133 | """Run an experiment, compute H, match against max-cut.""" 134 | 135 | n, nodes = build_graph(num_nodes) 136 | max_cut = compute_max_cut(n, nodes) 137 | # 138 | # These two lines are the basic implementation, where 139 | # a full matrix is being constructed. However, these 140 | # are diagonal, and can be constructed much faster. 141 | # H = graph_to_hamiltonian(n, nodes) 142 | # diag = H.diagonal() 143 | # 144 | diag = graph_to_diagonal_h(n, nodes) 145 | min_idx = np.argmin(diag) 146 | if flags.FLAGS.graph: 147 | graph_to_dot(n, nodes, max_cut) 148 | 149 | # Results... 150 | if min_idx == max_cut: 151 | print('SUCCESS: {:+10.2f} |{}>' 152 | .format(np.real(diag[min_idx]), 153 | bin(min_idx)[2:].zfill(n))) 154 | else: 155 | print('FAIL : {:+10.2f} |{}> ' 156 | .format(np.real(diag[min_idx]), 157 | bin(min_idx)[2:].zfill(n)), 158 | end='') 159 | print('Max-Cut: {:+10.2f} |{}>' 160 | .format(np.real(diag[max_cut]), 161 | bin(max_cut)[2:].zfill(n))) 162 | 163 | 164 | def main(argv): 165 | if len(argv) > 1: 166 | raise app.UsageError('Too many command-line arguments.') 167 | 168 | for _ in range(flags.FLAGS.iterations): 169 | run_experiment(flags.FLAGS.nodes) 170 | 171 | 172 | if __name__ == '__main__': 173 | app.run(main) 174 | -------------------------------------------------------------------------------- /src/oracle_synth.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Oracle Synthesis via BQSKit.""" 3 | 4 | # Explore Oracle-to-gate synthesis with BQSKit 5 | # 6 | # For the four potential Deutsch Oracles (listed below in the 7 | # list 'deutsch', we use BQSKit to synthesize circuits that would 8 | # produce those Oracle matrices. 9 | # 10 | # This needs BQSKit to be installed. 11 | # 12 | # Run (without bazel) as: 13 | # python3 oracle_synth.py 14 | 15 | import shutil 16 | import sys 17 | 18 | from src.lib import ops 19 | 20 | 21 | # ========================================================= 22 | # First, let's make sure bqskit has been installed. 23 | # 24 | try: 25 | # pylint: disable=g-import-not-at-top 26 | # pylint: disable=redefined-builtin 27 | from bqskit import compile 28 | except Exception: # pylint: disable=broad-except 29 | print('*** WARNING ***') 30 | print('Could not import bqskit.') 31 | print('Please install before trying to run this script.\n') 32 | sys.exit(0) 33 | 34 | # bqskit relies on 'dask-scheduler', let's make sure it can be found 35 | # 36 | try: 37 | sched = shutil.which('dask-scheduler') 38 | if sched is None: 39 | print('*** WARNING ***') 40 | print('Could not locate binary "dask-scheduler", required by bqskit') 41 | sys.exit(0) 42 | except Exception: # pylint: disable=broad-except 43 | print('Something wrong with importing "shutil"') 44 | sys.exit(0) 45 | # ========================================================= 46 | 47 | 48 | # The four possible Deutsch Oracles: 49 | # BQSKit fails on the identity gate - nothing to be done. 50 | deutsch = [[[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]], 51 | [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]], 52 | [[0, 1, 0, 0], [1, 0, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]], 53 | [[0, 1, 0, 0], [1, 0, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]]] 54 | 55 | # The circuits as they were being produced by BQSKit. 56 | # 57 | # We list the circuits here and ensure they are correct. 58 | # Below in this script we re-generate the circuits. 59 | # 60 | circuits = [ops.Identity() * ops.Identity(), 61 | ops.Cnot(0, 1), 62 | ops.Cnot(0, 1) @ 63 | (ops.Identity() * 64 | ops.U(3.1415926, 0.0, 3.1415926)), 65 | ops.Identity() * 66 | ops.U(3.1415926, 0.0, 3.1415926)] 67 | 68 | 69 | # We upfront compare the (generated) operators to the 70 | # intended operators. 71 | # 72 | for idx, gate in enumerate(deutsch): 73 | for i in range(4): 74 | for j in range(4): 75 | diff = gate[i][j] - circuits[idx][i][j] 76 | if abs(diff) > 0.00001: 77 | raise AssertionError('Gates DIFFER', i, j, '->', diff) 78 | print(f'Gate[{idx}]: Match') 79 | 80 | 81 | # We synthesize the circuit gates here via BQSKit's compile(). 82 | # 83 | print('Re-generate the gates') 84 | for idx, gate in enumerate(deutsch): 85 | print(f'Gate[{idx}]:', gate) 86 | try: 87 | circ = compile(gate, optimization_level=3) 88 | except Exception: # pylint: disable=broad-except 89 | print(' Compilation failed (expected at opt-level=3 for Gate[0]).') 90 | continue 91 | 92 | filename = '/tmp/deutsch' + str(idx) + '.qasm' 93 | print('Gates :', circ.gate_counts, ' write to:', filename) 94 | try: 95 | circ.save(filename) 96 | file = open(filename, 'r+') 97 | print(file.read()) 98 | except Exception: # pylint: disable=broad-except 99 | print('*** WARNING ***') 100 | print('Cannot write to file:', filename) 101 | -------------------------------------------------------------------------------- /src/pauli_rep.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Pauli Representation of single and two-qubit system.""" 3 | 4 | import math 5 | import random 6 | 7 | from absl import app 8 | import numpy as np 9 | 10 | from src.lib import circuit 11 | from src.lib import ops 12 | from src.lib import state 13 | 14 | 15 | def single_qubit(): 16 | """Compute Pauli representation of single qubit.""" 17 | 18 | for i in range(10): 19 | # First we construct a circuit with just one, very random qubit. 20 | # 21 | qc = circuit.qc('random qubit') 22 | qc.random() 23 | 24 | # Every qubit (rho) can be put in the Pauli Representation, 25 | # which is this Sum over i from 0 to 3 inclusive, representing 26 | # the four Pauli matrices (including the Identity): 27 | # 28 | # 3 29 | # rho = 1/2 * Sum(c_i * Pauli_i) 30 | # i=0 31 | # 32 | # To compute the various factors c_i, we multiply the Pauli 33 | # matrices with the density matrix and take the trace. This 34 | # trace is the computed factor: 35 | # 36 | rho = qc.psi.density() 37 | c = np.trace(ops.Identity() @ rho) 38 | x = np.trace(ops.PauliX() @ rho) 39 | y = np.trace(ops.PauliY() @ rho) 40 | z = np.trace(ops.PauliZ() @ rho) 41 | 42 | # Let's verify the result and construct a density matrix 43 | # from the Pauli matrices using the computed factors: 44 | # 45 | new_rho = 0.5 * ( 46 | c * ops.Identity() 47 | + x * ops.PauliX() 48 | + y * ops.PauliY() 49 | + z * ops.PauliZ() 50 | ) 51 | assert np.allclose(rho, new_rho, atol=1e-06), 'Invalid Pauli Representation' 52 | 53 | print(f'qubit({qc.psi[0]:11.2f}, {qc.psi[1]:11.2f}) = ', end='') 54 | print(f'{i:11.2f} I + {x:11.2f} X + {y:11.2f} Y + {z:11.2f} Z') 55 | 56 | # There is another way to decompose 2x2 matrices in terms of 57 | # Pauli matrices and rank-one projectors. See: 58 | # https://quantumcomputing.stackexchange.com/q/29497/11582 59 | # 60 | zero_projector = state.zeros(1).density() 61 | one_projector = state.ones(1).density() 62 | plus_projector = state.plus(1).density() 63 | i_projector = state.plusi(1).density() 64 | 65 | a1 = (c + z) * zero_projector 66 | a2 = (c - z) * one_projector 67 | a3 = x * (2 * plus_projector - zero_projector - one_projector) 68 | a4 = y * (2 * i_projector - zero_projector - one_projector) 69 | a = 0.5 * (a1 + a2 + a3 + a4) 70 | assert np.allclose(rho, a, atol=1e-06), 'Invalid projector representation' 71 | 72 | 73 | def two_qubit(): 74 | """Compute Pauli representation for two-qubit system.""" 75 | 76 | for _ in range(10): 77 | # First we construct a circuit with two, very random qubits. 78 | # 79 | qc = circuit.qc('random qubit') 80 | qc.random(2) 81 | 82 | # Potentially entangle them. 83 | qc.h(0) 84 | qc.cx(0, 1) 85 | 86 | # Additionally rotate around randomly. 87 | for i in range(2): 88 | qc.rx(i, math.pi * random.random()) 89 | qc.ry(i, math.pi * random.random()) 90 | qc.rz(i, math.pi * random.random()) 91 | 92 | # Compute density matrix. 93 | rho = qc.psi.density() 94 | 95 | # Every rho can be put in the 2-qubit Pauli representation, 96 | # which is this Sum over i, j from 0 to 3 inclusive, representing 97 | # the four Pauli matrices (including the Identity): 98 | # 99 | # 3 100 | # rho = 1/4 * Sum(c_ij * Pauli_i kron Pauli_j) 101 | # i,j=0 102 | # 103 | # To compute the various factors c_ij, we multiply the Pauli 104 | # tensor products with the density matrix and take the trace. This 105 | # trace is the computed factor: 106 | paulis = [ops.Identity(), ops.PauliX(), ops.PauliY(), ops.PauliZ()] 107 | c = np.zeros((4, 4), dtype=np.complex64) 108 | for i in range(4): 109 | for j in range(4): 110 | tprod = paulis[i] * paulis[j] 111 | c[i][j] = np.trace(rho @ tprod) 112 | 113 | # Let's verify the result and construct a density matrix 114 | # from the Pauli matrices using the computed factors: 115 | new_rho = np.zeros((4, 4), dtype=np.complex64) 116 | for i in range(4): 117 | for j in range(4): 118 | tprod = paulis[i] * paulis[j] 119 | new_rho = new_rho + c[i][j] * tprod 120 | 121 | assert np.allclose(rho, new_rho / 4, atol=1e-5), 'Invalid' 122 | 123 | 124 | def main(argv): 125 | if len(argv) > 1: 126 | raise app.UsageError('Too many command-line arguments.') 127 | 128 | single_qubit() 129 | two_qubit() 130 | 131 | 132 | if __name__ == '__main__': 133 | app.run(main) 134 | -------------------------------------------------------------------------------- /src/phase_kick.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Simple test program: Implementation of phase kick.""" 3 | 4 | from absl import app 5 | import numpy as np 6 | 7 | from src.lib import ops 8 | from src.lib import state 9 | 10 | # Simple phase kick. 11 | # 12 | # The top 2 qubits are put in super position. The third qubit 13 | # is initialized to |1>. Two controlled gates are being applied: 14 | # a controlled S (90 deg) from 0 to 2 15 | # a controlled T (45 deg) from 1 to 2 16 | # 17 | # The interesting part is that the result is additive, the state 18 | # representing |111> adds the two angles for the first 2 qubits: 19 | # 20 | # |001> (|1>): ampl: 0.50+0.00j prob: 0.25 Phase: 0.00 21 | # |011> (|3>): ampl: 0.35+0.35j prob: 0.25 Phase: 45.00 22 | # |101> (|5>): ampl: 0.00+0.50j prob: 0.25 Phase: 90.00 23 | # |111> (|7>): ampl: -0.35+0.35j prob: 0.25 Phase: 135.00 24 | # 25 | # This phase kick is what underlies the Quantum Fourier Transform 26 | # as well as quantum arithmetic. 27 | 28 | 29 | def simple_kick(): 30 | psi = state.bitstring(0, 0, 1) 31 | psi = ops.Hadamard(2)(psi) 32 | psi = ops.ControlledU(0, 2, ops.Sgate())(psi) 33 | psi = ops.ControlledU(1, 2, ops.Tgate())(psi, 1) 34 | psi.dump() 35 | 36 | 37 | # Simple form of a phase kick, as it used in Bernstein. 38 | # 39 | # 1) Input is all |0> plus an additional |1> 40 | # 41 | # 2) Hadamard all |0-> inputs to put them in the corresponding 42 | # |+> basis, the |1> will become |->. 43 | # 44 | # 3) A Cnot to |-> (former |1>) will flip the |+> to a |-> 45 | # This is the key "trick" used in Bernstein. 46 | # 47 | # 4) The final Hadamard will flip back |+> to |0>, but the flipped 48 | # |-> will become |1> (or -|1>, to be precise) 49 | 50 | 51 | def basis_kick1(): 52 | """Simple H-Cnot-H phase kick.""" 53 | 54 | psi = state.zeros(3) * state.ones(1) 55 | psi = ops.Hadamard(4)(psi) 56 | psi = ops.Cnot(2, 3)(psi, 2) 57 | psi = ops.Hadamard(4)(psi) 58 | if psi.prob(0, 0, 1, 1) < 0.9: 59 | raise AssertionError('Something is wrong with the phase kick') 60 | 61 | 62 | def basis_kick2(): 63 | """Another way to look at this H-Cnot-H phase kick.""" 64 | 65 | # This produces the vector [0, 1, 0, 0] 66 | psi = state.bitstring(0, 1) 67 | 68 | # Applying Hadamard: [0.5, -0.5, 0.5, -0.5] 69 | h2 = ops.Hadamard(2) 70 | psi = h2(psi) 71 | 72 | # Acting Cnot on this vector: [0.5, -0.5, -0.5, 0.5] 73 | psi = ops.Cnot()(psi) 74 | 75 | # Final Hadamard: [0, 0, 0, 1] 76 | psi = h2(psi) 77 | 78 | # which is |11> 79 | p11 = state.bitstring(1, 1) 80 | if not psi.is_close(p11): 81 | raise AssertionError('Something is wrong with the phase kick') 82 | 83 | 84 | def basis_changes(): 85 | """Explore basis changes via Hadamard.""" 86 | 87 | # Generate [1, 0] 88 | psi = state.zeros(1) 89 | 90 | # Hadamard will result in 1/sqrt(2) [1, 1] (|+>) 91 | psi = ops.Hadamard()(psi) 92 | 93 | # Generate [0, 1] 94 | psi = state.ones(1) 95 | 96 | # Hadamard on |1> will result in 1/sqrt(2) [1, -1] (|->) 97 | psi = ops.Hadamard()(psi) 98 | 99 | # Simple PauliX will result in 1/sqrt(2) [-1, 1] 100 | psi = ops.PauliX()(psi) 101 | 102 | # But back to computational, will result in -|1>. 103 | # Global phases can be ignored. 104 | psi = ops.Hadamard()(psi) 105 | if not np.allclose(psi[1], -1.0): 106 | raise AssertionError('Invalid basis change.') 107 | 108 | 109 | def main(argv): 110 | if len(argv) > 1: 111 | raise app.UsageError('Too many command-line arguments.') 112 | 113 | simple_kick() 114 | basis_changes() 115 | basis_kick1() 116 | basis_kick2() 117 | 118 | 119 | if __name__ == '__main__': 120 | app.run(main) 121 | -------------------------------------------------------------------------------- /src/purification.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Purification.""" 3 | 4 | 5 | # State purification is the complement to Schmidt decomposition. 6 | # Given a possibly mixed state with n qubits, purification creates 7 | # a state with 2n qubits which is pure. 8 | # 9 | # This code is based on a version provided by Michael Broughton 10 | # (Thank you so much!) 11 | # 12 | 13 | 14 | from absl import app 15 | import numpy as np 16 | 17 | from src.lib import bell 18 | from src.lib import ops 19 | from src.lib import state 20 | 21 | 22 | def purify(rho: ops.Operator, nbits: int): 23 | """Purify a quantum state / density matrix.""" 24 | 25 | rho_eig_val, rho_eig_vec = np.linalg.eig(rho) 26 | 27 | # Use stinespring dilation. 28 | # We know 29 | # rho = sum_k pk |psi_k> tensor |psi_k> 33 | # 34 | # There are two (equivalent) ways to implement this: 35 | # 36 | # Version 1: 37 | # 38 | psi1 = np.zeros((2**(nbits * 2)), dtype=np.complex128) 39 | for i in range(len(rho_eig_val)): 40 | psi1 += (np.sqrt(rho_eig_val[i]) * 41 | np.kron(rho_eig_vec[:, i], rho_eig_vec[:, i])) 42 | 43 | mat = psi1.reshape((2**nbits, 2**nbits)) 44 | assert np.allclose(np.trace(mat@mat), 1.0, atol = 1e-5) 45 | 46 | # Version 2 using einsum's: 47 | # 48 | psi2 = np.einsum('k,ki,kj->ij', np.sqrt(rho_eig_val), 49 | rho_eig_vec.T, rho_eig_vec.T).reshape(-1) 50 | assert np.allclose(psi1, psi2), 'Wrong purification.' 51 | 52 | # Verify the original reduced density matrix with the method 53 | # used in quantum_pca.py: 54 | # 55 | reduced = np.dot(psi1.reshape((2**nbits, 2**nbits)), 56 | psi1.reshape((2**nbits, 2**nbits)).transpose()) 57 | assert np.allclose(rho, reduced), 'Wrong reduced density' 58 | 59 | # Another way to compute the reduced density matrix: 60 | reduced = ops.TraceOut(state.State(psi1).density(), 61 | [x for x in range(int(nbits), 62 | int(nbits*2))]) 63 | assert np.allclose(rho, reduced), 'Wrong reduced density' 64 | 65 | 66 | def main(argv): 67 | if len(argv) > 1: 68 | raise app.UsageError('Too many command-line arguments.') 69 | 70 | print('State purification(s).') 71 | 72 | # This is the density matrix from the example in: 73 | # quantum_pca.py: 74 | # 75 | print(' Single qubit.') 76 | purify(ops.Operator([(0.22704306, 0.34178495), 77 | (0.34178495, 0.77295694)]), 1) 78 | 79 | print(' Bell states.') 80 | purify(bell.bell_state(0, 0).density(), 2) 81 | purify(bell.bell_state(0, 1).density(), 2) 82 | purify(bell.bell_state(1, 0).density(), 2) 83 | purify(bell.bell_state(1, 1).density(), 2) 84 | 85 | print(' GHZ state.') 86 | purify(bell.ghz_state(4).density(), 4) 87 | 88 | # A handful of random states. 89 | print(' Random 2 qubit states.') 90 | for _ in range(1000): 91 | psi = state.State(np.random.rand(4)).normalize() 92 | purify(psi.density(), 2) 93 | 94 | print(' Random 4 qubit states.') 95 | for _ in range(100): 96 | psi = state.State(np.random.rand(16)).normalize() 97 | purify(psi.density(), 4) 98 | 99 | print('Done.') 100 | 101 | 102 | if __name__ == '__main__': 103 | app.run(main) 104 | -------------------------------------------------------------------------------- /src/qram.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: QRAM.""" 3 | 4 | from absl import app 5 | from src.lib import circuit 6 | from src.lib import ops 7 | 8 | 9 | # This is a very simple and simplified implementation of 10 | # simple QRAM's, mostly to show the principles of a specific 11 | # way to materialize a QRAM. 12 | # 13 | # Currently, only 1 address qubit -> 2 values and 2 address 14 | # qubits -> 1 value are implemented. It should be easy to see 15 | # that the principles can be easily extended to arbitrary 16 | # n -> m mappings. The corresponding circuits, however, can 17 | # become quite unwieldy, hence we're not showing those here. 18 | 19 | 20 | def qram_1_to_2(): 21 | """Map a single qubit to 2 data bits.""" 22 | 23 | # This example is taken from: 24 | # https://youtu.be/eBN5MXMirYs 25 | # A single address qubit is |0> or |1> and maps to: 26 | # |0> -> |01> 27 | # |1> -> |11> 28 | # 29 | print('1 address bit, 2 data bits.') 30 | qc = circuit.qc('test_1_2') 31 | a = qc.reg(1) 32 | d = qc.reg(2) 33 | qc.h(a) 34 | qc.cx0(a, d[1]) 35 | qc.cx(a, d[0]) 36 | qc.cx(a, d[1]) 37 | 38 | if qc.psi[0b011] < 0.7 or qc.psi[0b101] < 0.7: 39 | raise AssertionError('Incorrect results') 40 | 41 | 42 | def qram_2_to_1(): 43 | """Map two address qubits to 0 or 1.""" 44 | 45 | # Two address bits contain either a 1 or 0. 46 | # |00> -> |0> 47 | # |01> -> |1> 48 | # |10> -> |1> 49 | # |11> -> |0> 50 | print('2 address bits, 1 data bit.') 51 | qc = circuit.qc('test') 52 | a = qc.reg(2) 53 | _ = qc.reg(1) 54 | 55 | qc.h(a) 56 | qc.multi_control([[0], 1], 2, None, ops.PauliX(), 'ccx') 57 | qc.multi_control([0, [1]], 2, None, ops.PauliX(), 'ccx') 58 | 59 | if qc.psi[0b011] < 0.4 or qc.psi[0b101] < 0.4: 60 | raise AssertionError('Incorrect results') 61 | if qc.psi[0b000] < 0.4 or qc.psi[0b110] < 0.4: 62 | raise AssertionError('Incorrect results') 63 | 64 | 65 | def main(argv): 66 | if len(argv) > 1: 67 | raise app.UsageError('Too many command-line arguments.') 68 | print('QRAM...') 69 | 70 | # These simple examples show how to binary encode address 71 | # bits and value bits and how to multi-control the value 72 | # bits from the address qubits. Extending this to longer 73 | # addresses and values is straight-forward (but quite 74 | # unwieldy). 75 | # 76 | qram_1_to_2() 77 | qram_2_to_1() 78 | 79 | 80 | if __name__ == '__main__': 81 | app.run(main) 82 | -------------------------------------------------------------------------------- /src/quantum_mean.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Quantum Mean Computation.""" 3 | 4 | import itertools 5 | import random 6 | from absl import app 7 | import numpy as np 8 | 9 | from src.lib import circuit 10 | from src.lib import helper 11 | from src.lib import ops 12 | 13 | 14 | def run_experiment(nbits: int): 15 | """Run a single mean computation.""" 16 | 17 | # Random numbers, positive and negative. 18 | x = np.array([random.randint(0, 100) - 50 for _ in range(2**nbits)]) 19 | xn = x / np.linalg.norm(x) 20 | 21 | # Define a unitary which does: 22 | # $$ 23 | # U_a:|0\rangle|0\rangle\mapsto 24 | # \frac{1}{2^{n/2}} 25 | # \sum_x|x\rangle(\sqrt{1-F(x)}|0\rangle+\sqrt{F(x)}|1\rangle). 26 | # $$ 27 | # In other words: 28 | # |000> -> np.sqrt(1 - a) |000> 29 | # |000> -> np.sqrt(a) |001> 30 | # 31 | # Which can be done for x_i with a controlled rotations about y by 32 | # 2 arcsin(x_i) 33 | # 34 | # Measuring the state |001| should give the mean. This can be done by 35 | # repeated experiments, but we could also use amplitude 36 | # estimation (as suggested in the book by Moscha). 37 | # 38 | qc = circuit.qc('mean calculator') 39 | inp = qc.reg(nbits, 0) # State input 40 | aux = qc.reg(nbits - 1, 0) # Aux qubits for the multi-controlled gates 41 | ext = qc.reg(1, 0) # Target 'extra' qubit 42 | 43 | # The trick is to control the rotations with the bit 44 | # patterns of the indices (encoded via qubits) into x. 45 | qc.h(inp) 46 | for bits in itertools.product([0, 1], repeat=nbits): 47 | idx = helper.bits2val(bits) 48 | # Control-by-zero is indicated with a single-element list. 49 | ctl = [i if bit == 1 else [i] for i, bit in enumerate(bits)] 50 | qc.multi_control(ctl, ext, aux, 51 | ops.RotationY(2 * np.arcsin(xn[idx]))) 52 | qc.h(inp) 53 | 54 | # We 'measure' via peak-a-boo of state |00...001> 55 | qmean = np.real(qc.psi[1]) 56 | qclas = np.real(qc.psi[1] * np.linalg.norm(x)) 57 | 58 | # Check results. 59 | assert np.allclose(np.mean(xn), qmean, atol=0.001), 'Whaaa' 60 | assert np.allclose(np.mean(x), qclas, atol=0.001), 'Whaaa' 61 | print(f' Mean ({nbits} qb): classic: {np.mean(x):.3f}, quantum: {qclas:.3f}') 62 | 63 | 64 | def main(argv): 65 | if len(argv) > 1: 66 | raise app.UsageError('Too many command-line arguments.') 67 | print('Quantum Mean Computation.') 68 | 69 | for nbits in range(2, 8): 70 | run_experiment(nbits) 71 | 72 | if __name__ == '__main__': 73 | np.set_printoptions(precision=4) 74 | app.run(main) 75 | -------------------------------------------------------------------------------- /src/quantum_median.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Quantum Median Computation, simulated classically.""" 3 | 4 | import random 5 | from absl import app 6 | import numpy as np 7 | 8 | 9 | # This algorithm utilizes two quantum techniques: 10 | # Quantum Mean Estimation (in quantum_mean.py) 11 | # Minimum Finding (in minimum_finding.py) 12 | # 13 | # We do _not_ reimplement those here, we just check classically whether 14 | # the overall approach using these techniques would work. 15 | # 16 | # What is this approach? 17 | # 18 | # According to [https://arxiv.org/abs/1106.4267], the median z is the 19 | # value that minimizes 20 | # 21 | # \sum_x |f(x) - f(z)| 22 | # 23 | # For each value z in the input array, We compute the difference 24 | # vector [x[0] - z, x[1] - z, ..., x[n-1] - z]. 25 | # 26 | # Then we compute the mean of this vector with the quantum 27 | # algorithm and finally find the minimum element z that would 28 | # minimize the mean. That will have to be done with the quantum 29 | # algorithm. 30 | # 31 | # Note: Computing the mean and minimum is clear. It is not clear 32 | # how to compute the difference vector quantum'ly. It may just be 33 | # one of those cases where we assume that that's easily doable. 34 | 35 | 36 | def run_experiment(nbits: int): 37 | """Run a single median computation.""" 38 | 39 | # Create the random state vector. 40 | x = np.array([random.randint(0, 2**nbits) for _ in range(2**nbits)]) 41 | xn = x / np.linalg.norm(x) 42 | 43 | # Compute the mean(s) for each difference vector. 44 | median = min_mean = 1000 45 | for idx, z in enumerate(xn): 46 | # Make the difference vector. Note that this vector 47 | # may not be normalized! 48 | diff = [abs(xval - z) for xval in xn] 49 | 50 | # Normalization (required for quantum, also improves 51 | # accuracy by an order of magnitude). 52 | diffnorm = np.linalg.norm(diff) 53 | 54 | # Compute the mean (which we know how to do quantumly). 55 | mean = np.mean(diffnorm) 56 | 57 | # Find the minimum (which we would also know how to do quantumly). 58 | if mean < min_mean: 59 | min_mean = mean 60 | median = idx 61 | 62 | # Print and check results, we allow for a 1% deviation. 63 | print( 64 | f' Median ({nbits} qb): Classic: {np.mean(x):.3f},' 65 | f' Quantum: {x[median]:.0f}' 66 | ) 67 | if max(np.mean(x), x[median]) / min(np.mean(x), x[median]) > 1.02: 68 | raise AssertionError('Incorrect median computation.') 69 | 70 | 71 | def main(argv): 72 | if len(argv) > 1: 73 | raise app.UsageError('Too many command-line arguments.') 74 | print('Classic Sim of Quantum Median Computation.') 75 | 76 | for _ in range(10): 77 | run_experiment(nbits=10) 78 | 79 | 80 | if __name__ == '__main__': 81 | np.set_printoptions(precision=4) 82 | app.run(main) 83 | -------------------------------------------------------------------------------- /src/quantum_pca.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Quantum Principal Component Analysis (PCA).""" 3 | 4 | 5 | # This PCA data set and implementation is from this paper: 6 | # Quantum Algorithm Implementation for Beginners 7 | # https://arxiv.org/pdf/1804.03719.pdf 8 | # 9 | # A sample implementation can be found here: 10 | # https://github.com/Haokai-Zhang/ExampleQPCA 11 | # 12 | # The implementation here closely follows the sample implementation 13 | # in order to avoid confusion. Thanks to Haokai-Zhang for his 14 | # contributions (couldn't have done it without it!). 15 | 16 | import random 17 | from absl import app 18 | import numpy as np 19 | 20 | from src.lib import circuit 21 | from src.lib import state 22 | 23 | 24 | def pca(x): 25 | """A single quantum principal component analysis.""" 26 | 27 | # We center and normalize the data. 28 | # We use a factor of 2 for x[1] for numerical stability. 29 | x[0] = x[0] - np.average(x[0]) 30 | x[0] = x[0] / np.linalg.norm(x[0]) 31 | x[1] = (x[1] - np.average(x[1])) 32 | x[1] = 2 * x[1] / np.linalg.norm(x[1]) 33 | 34 | # Compute the unbiased covariance matrix 35 | # It is unbiased, hence the 15, else we could use 15-1 36 | # (which doesn't make a difference). 37 | # 38 | m = np.array([[np.dot(x[0], x[0]), np.dot(x[0], x[1])], 39 | [np.dot(x[1], x[0]), np.dot(x[1], x[1])]]) / (len(x[0]) - 1) 40 | 41 | # We scale down M to make it a density matrix (where the trace 42 | # has to be 1). Later we must not forget to scale up the 43 | # results again. 44 | # 45 | rho = m / np.trace(m) 46 | 47 | # Construct purified state \psi. This is a bit like cheating 48 | # since in order to construct this state, we have to compute 49 | # the eigenvalues - the computation of which is the whole point of 50 | # this algorithm. On a real quantum machine, this has to be 51 | # done via state preparation. 52 | # 53 | rho_eig_val, rho_eig_vec = np.linalg.eig(rho) 54 | p_vec = np.concatenate((np.sqrt(rho_eig_val), np.sqrt(rho_eig_val))) 55 | u_vec = rho_eig_vec.reshape((4)) 56 | psi = state.State(p_vec * u_vec) 57 | 58 | # Construct swap test. The expectation value of the swap gate under 59 | # the purified state allows us to re-construct the eigenvalues. 60 | # 61 | # Here we just initialize qubits [1,2] and [3,4] with the state 62 | # as we want it to be (from above calculations). On a real quantum 63 | # computer we would have to add circuitry to actually generate 64 | # this state. 65 | # 66 | qc = circuit.qc('pca') 67 | qc.reg(1, 0) 68 | qc.state(psi) # qubits 1, 2 69 | qc.state(psi) # qubits 3, 4 70 | qc.h(0) 71 | qc.cswap(0, 1, 3) 72 | qc.h(0) 73 | 74 | # We know that for the diagonal of rho: 75 | # p0^2 + p1^2 = purity 76 | # p0 + p1 = 1 77 | # 78 | # Squaring: 79 | # (p0 + p1)^2 = p0^2 + p1^2 + 2p0p1 80 | # -> p0p1 = (1-P)/2 81 | # -> p0 = 1 - p1 82 | # p0 - p0^2 = (1-P)/2 83 | # -> p0^2 - p0 + (1-P)/2 = 0 84 | # 85 | # Quadratic formula: 86 | # p0,1 = 1 -+ sqrt(2 * P - 1) / 2 87 | # 88 | # We have to scale the results up to the original covariance 89 | # matrix by multiplying with np.trace(M). 90 | # 91 | purity = qc.pauli_expectation(idx=0) 92 | m_0 = (1 - np.sqrt(2 * purity - 1)) / 2 * np.trace(m) 93 | m_1 = (1 + np.sqrt(2 * purity - 1)) / 2 * np.trace(m) 94 | print(f'Eigenvalues Quantum PCA: {m_0:.6f}, {m_1:.6f}') 95 | 96 | # Compare to classically derived values, which must match. 97 | m, _ = np.linalg.eig(m) 98 | if (not np.isclose(m_0, m[0], atol=1e-5) or 99 | not np.isclose(m_1, m[1], atol=1e-5)): 100 | raise AssertionError('Incorrect Computation.') 101 | print(f'Eigenvalues Classically: {m[0]:.6f}, {m[1]:.6f}. Correct') 102 | 103 | 104 | def main(argv): 105 | if len(argv) > 1: 106 | raise app.UsageError('Too many command-line arguments.') 107 | 108 | print('Quantum Principal Component Analysis (PCA).') 109 | 110 | # Data set from the paper is the correlation of 111 | # - number of bedrooms 112 | # - square footage 113 | # 114 | x = [[4, 3, 4, 4, 3, 3, 3, 3, 4, 4, 4, 5, 4, 3, 4], 115 | [3028, 1365, 2726, 2538, 1318, 1693, 1412, 1632, 2875, 116 | 3564, 4412, 4444, 4278, 3064, 3857]] 117 | pca(x) 118 | 119 | for _ in range(10): 120 | for idx, _ in enumerate(x[0]): 121 | x[1][idx] = random.random() * 10000 122 | pca(x) 123 | 124 | 125 | if __name__ == '__main__': 126 | app.run(main) 127 | -------------------------------------------------------------------------------- /src/quantum_walk.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Simple quantum walk with Hadamard Coin.""" 3 | 4 | from absl import app 5 | 6 | from src.lib import circuit 7 | from src.lib import helper 8 | from src.lib import ops 9 | 10 | 11 | def incr(qc, idx: int, nbits: int, aux, controller): 12 | """Increment-by-1 circuit.""" 13 | 14 | # See "Efficient Quantum Circuit Implementation of 15 | # Quantum Walks" by Douglas, Wang. 16 | # 17 | # -X-- 18 | # -o--X-- 19 | # -o--o--X-- 20 | # -o--o--o--X-- 21 | # ... 22 | for i in range(nbits): 23 | ctl = controller.copy() 24 | for j in range(nbits - 1, i, -1): 25 | ctl.append(j + idx) 26 | qc.multi_control(ctl, i+idx, aux, ops.PauliX(), 'multi-1-X') 27 | 28 | 29 | def decr(qc, idx: int, nbits: int, aux, controller): 30 | """Decrement-by-1 circuit.""" 31 | 32 | # See "Efficient Quantum Circuit Implementation of 33 | # Quantum Walks" by Douglas, Wang. 34 | # 35 | # Similar to incr, except controlled-by-0's are being used. 36 | # 37 | # -X-- 38 | # -0--X-- 39 | # -0--0--X-- 40 | # -0--0--0--X-- 41 | # ... 42 | for i in range(nbits): 43 | ctl = controller.copy() 44 | for j in range(nbits - 1, i, -1): 45 | ctl.append([j + idx]) 46 | qc.multi_control(ctl, i+idx, aux, ops.PauliX(), 'multi-0-X') 47 | 48 | 49 | def experiment_incr(): 50 | """Run a few incr experiments.""" 51 | 52 | qc = circuit.qc('incr') 53 | qc.reg(4, 0) 54 | aux = qc.reg(4) 55 | 56 | for val in range(15): 57 | incr(qc, 0, 4, aux, []) 58 | 59 | maxbits, _ = qc.psi.maxprob() 60 | res = helper.bits2val(maxbits[0:4]) 61 | if val + 1 != res: 62 | raise AssertionError('Invalid Result') 63 | 64 | 65 | def experiment_decr(): 66 | """Run a few decr experiments.""" 67 | qc = circuit.qc('decr') 68 | qc.reg(4, 15) 69 | aux = qc.reg(4) 70 | 71 | for val in range(15, 0, -1): 72 | decr(qc, 0, 4, aux, []) 73 | 74 | maxbits, _ = qc.psi.maxprob() 75 | res = helper.bits2val(maxbits[0:4]) 76 | if val - 1 != res: 77 | raise AssertionError('Invalid Result') 78 | 79 | 80 | def incr_mod_9(qc, aux): 81 | """Increment-by-1 modulo 9 circuit.""" 82 | 83 | # We achieve this with help of an ancilla: 84 | # 85 | # -X------------ o X 0 86 | # -o--X--------- 0 | 0 87 | # -o--o--X------ 0 | 0 88 | # -o--o--o--X--- o X | 0 89 | # | | | | 90 | # needs an extra ancillary: 91 | # | | | | 92 | # ... X--o--o--X -> |0> 93 | # 94 | for i in range(4): 95 | ctl = [] 96 | for j in range(4 - 1, i, -1): 97 | ctl.append(j) 98 | qc.multi_control(ctl, i, aux, ops.PauliX(), 'multi-X') 99 | 100 | qc.multi_control([0, [1], [2], 3], aux[4], aux, ops.PauliX(), 'multi-X') 101 | qc.cx(aux[4], 0) 102 | qc.cx(aux[4], 3) 103 | qc.multi_control([[0], [1], [2], [3]], aux[4], aux, ops.PauliX(), 'multi-X') 104 | 105 | 106 | def experiment_mod_9(): 107 | """Run a few incr-mod-9 experiments.""" 108 | 109 | qc = circuit.qc('incr') 110 | qc.reg(4, 0) 111 | aux = qc.reg(5) # extra aux 112 | 113 | for val in range(18): 114 | incr_mod_9(qc, aux) 115 | maxbits, _ = qc.psi.maxprob() 116 | res = helper.bits2val(maxbits[0:4]) 117 | if ((val + 1) % 9) != res: 118 | raise AssertionError('Invalid Result') 119 | 120 | 121 | def simple_walk(): 122 | """Simple quantum walk, allowing initial experiments.""" 123 | 124 | nbits = 8 125 | qc = circuit.qc('simple_walk') 126 | qc.reg(nbits, 0b10000000) 127 | aux = qc.reg(nbits, 0) 128 | coin = qc.reg(1, 0) 129 | 130 | for _ in range(32): 131 | # Using a Hadamard coin, others are possible, of course. 132 | qc.h(coin[0]) 133 | incr(qc, 0, nbits, aux, [coin[0]]) 134 | decr(qc, 0, nbits, aux, [[coin[0]]]) 135 | 136 | # Find and print the non-zero amplitudes for all states 137 | for bits in helper.bitprod(nbits): 138 | idx_bits = bits 139 | for _ in range(nbits): 140 | idx_bits = idx_bits + (0,) 141 | idx_bits0 = idx_bits + (0,) 142 | 143 | # Printing bits0 only, this can be changed, of course. 144 | if qc.psi.ampl(*idx_bits0) > 1e-5: 145 | print( 146 | '{:5.1f} {:5.4f}'.format( 147 | float(helper.bits2val(bits)), qc.psi.ampl(*idx_bits0).real 148 | ) 149 | ) 150 | 151 | 152 | def main(argv): 153 | if len(argv) > 1: 154 | raise app.UsageError('Too many command-line arguments.') 155 | 156 | print('Increment...') 157 | experiment_incr() 158 | 159 | print('Decrement...') 160 | experiment_decr() 161 | 162 | print('Increment mod 9...') 163 | experiment_mod_9() 164 | 165 | print('Simple Walk...') 166 | simple_walk() 167 | 168 | 169 | if __name__ == '__main__': 170 | app.run(main) 171 | -------------------------------------------------------------------------------- /src/runall.sh: -------------------------------------------------------------------------------- 1 | # Run all .py targets in this directory. 2 | # 3 | # This first command build the accelerated libxgates.so. 4 | # 5 | # The script uses -c opt on the bazel command-line, 6 | # which may cause problems on some OS'es. It can be removed. 7 | # 8 | # All code will run without the library, just about 10x+ slower. 9 | # 10 | bazel build -c opt lib:libxgates.so 11 | if [[ $? != 0 ]]; then 12 | echo "*** Building libxgates failed. ***" 13 | echo "*** Try building manually with script 'make_libxgates'" 14 | fi 15 | 16 | # 17 | # Now we just iterate over all Python files and run them. 18 | # 19 | for algo in `ls -1 *.py | sort` 20 | do 21 | echo 22 | echo "--- [$algo] ------------------------" 23 | python3 $algo || exit 1 24 | done 25 | -------------------------------------------------------------------------------- /src/schmidt_decomp.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Schmidt Decomposition.""" 3 | 4 | 5 | import random 6 | from absl import app 7 | import numpy as np 8 | 9 | from src.lib import ops 10 | from src.lib import state 11 | 12 | 13 | # The Schmidt Decomposition of a (2-qubit) bipartite system (quantum state) 14 | # states that for a state \psi from a Hilbert space: 15 | # 16 | # H_1 \otimes H_2 17 | # 18 | # We can find new bases: 19 | # {u_0, u_1, ..., u_{n-1} \elem H_1 20 | # {v_0, v_1, ..., v_{n-1} \elem H_2 21 | # 22 | # such that: 23 | # \psi = \sum_i \sqrt(\alpha} u_i \otimes v_i 24 | # 25 | # Additionally, adding up the \alpha will result in 1.0. 26 | # 27 | # This method can be used for testing for entanglement. A state is 28 | # separable only if the number of nonzero coefficients \alpha is 29 | # exactly 1, else the state is entangled. 30 | # 31 | # More information is available online, eg: 32 | # https://en.wikipedia.org/wiki/Schmidt_decomposition 33 | 34 | 35 | def compute_eigvals(psi: state.State, expected: int, tolerance: float): 36 | """Compute the eigenvalues for the individial substates.""" 37 | 38 | # To find the factors \alpha and the new bases, 39 | # we trace out the subspaces and find their eigenvalues. 40 | # 41 | rho = psi.density() 42 | 43 | rho0 = ops.TraceOut(rho, [1]) 44 | eigvals0 = np.linalg.eigvalsh(rho0) 45 | 46 | rho1 = ops.TraceOut(rho, [0]) 47 | eigvals1 = np.linalg.eigvalsh(rho1) 48 | 49 | # The set of eigenvalues must be identical between the two sub states. 50 | # 51 | assert np.allclose(eigvals0, eigvals1, atol=1e-6), 'Whaa' 52 | 53 | # The eigenvalues must add up to 1.0. 54 | # 55 | assert np.allclose(np.sum(eigvals0), 1.0), 'Whaa' 56 | 57 | # Count the number of nonzero eigenvalues and match against expected. 58 | # 59 | nonzero = np.sum(eigvals0 > tolerance) 60 | if nonzero != expected: 61 | print(f'\t\tCase of unstable math: {eigvals0[0]:.4f}, {eigvals0[1]:.4f}') 62 | 63 | # Construct the state from the eigenvalues and the new bases 64 | # which we derive via SVD. Then we check whether the new state 65 | # matches the original state. 66 | # 67 | u0, d0, _ = np.linalg.svd(rho0) 68 | a1, _, _ = np.linalg.svd(rho1) 69 | newpsi = (np.sqrt(d0[0]) * np.kron(u0[:, 0], a1[0, :]) + 70 | np.sqrt(d0[1]) * np.kron(u0[:, 1], a1[1, :])) 71 | assert np.allclose(psi, newpsi, atol=1e-3), 'Incorrect Schmidt basis' 72 | 73 | return eigvals0 74 | 75 | 76 | def main(argv): 77 | if len(argv) > 1: 78 | raise app.UsageError('Too many command-line arguments.') 79 | print('Schmidt Decomposition and Entanglement Test.') 80 | 81 | iterations = 1000 82 | random.seed(7) 83 | 84 | # Test a number of separable states. 85 | # 86 | print('\tSchmidt Decomposition for seperable states.') 87 | for _ in range(iterations): 88 | psi = state.qubit(random.random()) * state.qubit(random.random()) 89 | 90 | # States are separable if they have only 1 nonzero eigenvalue. 91 | compute_eigvals(psi, 1, 1e-3) 92 | 93 | # Test a number of entangled states. 94 | # 95 | print('\tSchmidt Decomposition for entangled states.') 96 | for _ in range(iterations): 97 | psi = state.bitstring(0, 0) 98 | psi = ops.Hadamard()(psi) 99 | angle = random.random() * np.pi 100 | if abs(angle) < 1e-5: 101 | continue 102 | psi = ops.ControlledU(0, 1, ops.RotationY(angle))(psi) 103 | 104 | # For entangled 2-qubit states we expect 2 nonzero eigenvalues. 105 | compute_eigvals(psi, 2, 1e-9) 106 | 107 | # Maximally entangled state. 108 | # 109 | print('\tSchmidt Decomposition for max-entangled state.') 110 | psi = state.bitstring(0, 0) 111 | psi = ops.Hadamard()(psi) 112 | psi = ops.Cnot()(psi) 113 | eigv = compute_eigvals(psi, 2, 1e-9) 114 | if abs(eigv[0] - eigv[1]) > 0.001: 115 | raise AssertionError('Incorrect computation for max-entangled state.') 116 | 117 | print('Success') 118 | 119 | 120 | if __name__ == '__main__': 121 | app.run(main) 122 | -------------------------------------------------------------------------------- /src/shor_classic.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Shor's algorithm for factorization - using classic order finding.""" 3 | 4 | import math 5 | import random 6 | from typing import Tuple 7 | 8 | from absl import app 9 | 10 | 11 | def is_prime(num: int) -> bool: 12 | """Check to see whether num can be factored at all.""" 13 | 14 | for i in range(3, num // 2, 2): 15 | if num % i == 0: 16 | return False 17 | return True 18 | 19 | 20 | def is_coprime(num: int, larger_num: int) -> bool: 21 | """Determine if num is a coprime to larger_num.""" 22 | 23 | return math.gcd(num, larger_num) == 1 24 | 25 | 26 | def get_odd_non_prime(fr: int, to: int) -> int: 27 | """Get a non-prime number in the range.""" 28 | 29 | while True: 30 | n = random.randint(fr, to) 31 | if n % 2 == 0: 32 | continue 33 | if not is_prime(n): 34 | return n 35 | 36 | 37 | def get_coprime(larger_num: int) -> int: 38 | """Find a numnber < larger_num which is coprime to it.""" 39 | 40 | while True: 41 | val = random.randint(3, larger_num - 1) 42 | if is_coprime(val, larger_num): 43 | return val 44 | 45 | 46 | def classic_order(num: int, modulus: int) -> int: 47 | """Find the order classically via simple iteration.""" 48 | 49 | order = 1 50 | while 1 != (num ** order) % modulus: 51 | order += 1 52 | return order 53 | 54 | def run_experiment(fr: int, to: int) -> Tuple[int, int]: 55 | """Run the classical part of Shor's algorithm.""" 56 | 57 | n = get_odd_non_prime(fr, to) 58 | a = get_coprime(n) 59 | order = classic_order(a, n) 60 | 61 | factor1 = math.gcd(a ** (order // 2) + 1, n) 62 | factor2 = math.gcd(a ** (order // 2) - 1, n) 63 | if factor1 == 1 or factor2 == 1: 64 | return (0, 0) 65 | 66 | print('Found Factors: N = {:4d} = {:4d} * {:4d} (r={:4})'. 67 | format(factor1 * factor2, factor1, factor2, order)) 68 | assert factor1 * factor2 ==n, 'Invalid factoring' 69 | return (factor1, factor2) 70 | 71 | 72 | def main(argv): 73 | if len(argv) > 1: 74 | raise app.UsageError('Too many command-line arguments.') 75 | 76 | print('Classic Part of Shors Algorithm.') 77 | for _ in range(25): 78 | run_experiment(21, 9999) 79 | 80 | 81 | if __name__ == '__main__': 82 | app.run(main) 83 | -------------------------------------------------------------------------------- /src/simon.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Simon's Algorithm.""" 3 | 4 | from absl import app 5 | 6 | from src.lib import helper 7 | from src.lib import ops 8 | from src.lib import state 9 | 10 | 11 | # This is a common Oracle description for a 2-qubit (4-qubit total) 12 | # Uf operator that produces even f(x) == f(x ^ c) 13 | # 14 | # ----o--o-------- 15 | # | | 16 | # ----|--|--o--o--- 17 | # | | | | 18 | # ----X--|--X--|--- 19 | # | | 20 | # -------X-----X--- 21 | # 22 | # Truth Table will be (for secret string 11): 23 | # 24 | # f(0, 0) = 00 = f(0^1, 0^1) = f(1, 1) 25 | # f(0, 1) = 11 = f(0^1, 1^1) = f(1, 0) 26 | # f(1, 0) = 11 = ... 27 | # f(1, 1) = 00 28 | # 29 | # Thus, f(x) = f(x + c) with c being the secret string 11 30 | # (Note that module 2 xor, +, and -, are all the same). 31 | # 32 | # Reference: 33 | # https://qiskit.org/textbook/ch-algorithms/simon.html#implementation 34 | 35 | 36 | def make_u(): 37 | """Make Simon's 2 (total 4) qubit Oracle.""" 38 | 39 | # We have to properly 'pad' the various gates to 4 qubits. 40 | # 41 | ident = ops.Identity() 42 | cnot0 = ops.Cnot(0, 2) * ident 43 | cnot1 = ops.Cnot(0, 3) 44 | cnot2 = ident * ops.Cnot(0, 1) * ident 45 | cnot3 = ident * ops.Cnot(0, 2) 46 | 47 | return cnot3 @ cnot2 @ cnot1 @ cnot0 48 | 49 | 50 | def dot2(bits): 51 | """Compute dot module 2.""" 52 | 53 | return (bits[0] * bits[2] + bits[1] * bits[3]) % 2 54 | 55 | 56 | def run_experiment(): 57 | """Run single, defined experiment for secret 11.""" 58 | 59 | psi = state.zeros(4) 60 | u = make_u() 61 | 62 | psi = ops.Hadamard(2)(psi) 63 | psi = u(psi) 64 | psi = ops.Hadamard(2)(psi) 65 | 66 | # Because of the xor patterns (Yanofski 6.64) 67 | # measurement will only find those qubit strings where 68 | # the scalar product of z (lower bits) and secret string: 69 | # = 0 70 | # 71 | # This should measure |00> and |11> with equal probability. 72 | # If true, than we can derive the secret string as being 11 73 | # because f(00) = f(11) and because f(00) = f(00 ^ c) -> c = 11 74 | # 75 | print('Measure likely states (want: pairs of 00 or 11):') 76 | for bits in helper.bitprod(4): 77 | if psi.prob(*bits) > 0.01: 78 | if (bits[0] == 0 and bits[1] == 1) or (bits[0] == 1 and bits[1] == 0): 79 | raise AssertionError('Invalid Results') 80 | print('|{}{} {}{}> = 0 : {:.2f} dot % 2: {:.2f}'. 81 | format(bits[0], bits[1], 82 | bits[2], bits[3], psi.prob(*bits), 83 | dot2(bits))) 84 | 85 | 86 | def main(argv): 87 | if len(argv) > 1: 88 | raise app.UsageError('Too many command-line arguments.') 89 | run_experiment() 90 | 91 | 92 | if __name__ == '__main__': 93 | app.run(main) 94 | -------------------------------------------------------------------------------- /src/simon_general.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Simon's General Algorithm.""" 3 | 4 | from absl import app 5 | import numpy as np 6 | 7 | from src.lib import helper 8 | from src.lib import ops 9 | from src.lib import state 10 | 11 | # A general Simon Oracle can be constructed the following way: 12 | # 13 | # Assume a secret string 'c' and the msb set to 1 14 | # 15 | # For each input value x: 16 | # if msb(x) == 0: 17 | # return x 18 | # if msb(x) == 1: 19 | # return x % c 20 | # 21 | # In quantum gates, that's easy to accomplish. 22 | # First copy the input gates to the output gates: 23 | # 24 | # ----o-------- 25 | # | 26 | # ----|--o----- 27 | # | | 28 | # ----X--|----- 29 | # | 30 | # -------X----- 31 | # 32 | # Now, for each bit i in c that is 1, cxor the output gate i 33 | # controlled by bit 0. 34 | # 35 | # So for example, for the string c = 10: 36 | # 37 | # ----o------o- 38 | # | | 39 | # ----|--o---|- 40 | # | | | 41 | # ----X--|---X- 42 | # | 43 | # -------X----- 44 | # 45 | # So for example, for the string c = 11: 46 | # 47 | # ----o------o--o- 48 | # | | | 49 | # ----|--o---|--|- 50 | # | | | | 51 | # ----X--|---X--|- 52 | # | | 53 | # -------X------X- 54 | 55 | 56 | def make_c(nbits): 57 | """Make a random constant c from {0,1}. This is the c we try to find.""" 58 | 59 | constant_c = [0] * nbits 60 | for idx in range(nbits): 61 | constant_c[idx] = int(np.random.random() < 0.5) 62 | print('Magic constant: {}'.format(constant_c)) 63 | return constant_c 64 | 65 | 66 | def make_u(nbits, constant_c): 67 | """Make general Simon's Oracle.""" 68 | 69 | # Copy bits. 70 | op = ops.Identity(nbits*2) 71 | for idx in range(nbits): 72 | op = (ops.Identity(idx) * 73 | ops.Cnot(idx, idx+nbits) * 74 | ops.Identity(nbits - idx - 1)) @ op 75 | 76 | # Connect the xor's controlled by the msb(x). 77 | for idx in range(nbits): 78 | if constant_c[idx] == 1: 79 | op = (ops.Cnot(0, idx+nbits) * ops.Identity(nbits-idx-1)) @ op 80 | 81 | if not op.is_unitary(): 82 | raise AssertionError('Produced non-unitary Uf') 83 | return op 84 | 85 | 86 | def dot2(bits, nbits): 87 | """Compute dot module 2.""" 88 | 89 | accum = 0 90 | for idx in range(nbits): 91 | accum = accum + bits[idx] * bits[idx + nbits] 92 | return accum % 2 93 | 94 | 95 | def run_experiment(nbits): 96 | """Run single, defined experiment for secret 11.""" 97 | 98 | psi = state.zeros(nbits * 2) 99 | c = make_c(nbits) 100 | u = make_u(nbits, c) 101 | 102 | psi = ops.Hadamard(nbits)(psi) 103 | psi = u(psi) 104 | psi = ops.Hadamard(nbits)(psi) 105 | 106 | # Because of the xor patterns (Yanofski 6.64) 107 | # measurement will only find those qubit strings where 108 | # the scalar product of z (lower bits) and secret string: 109 | # = 0 110 | # 111 | print('Measure likely states:') 112 | for bits in helper.bitprod(nbits*2): 113 | if psi.prob(*bits) > 0.0 and dot2(bits, nbits) < 1.0: 114 | print('|{}> = 0 : {:.2f} dot % 2: {:.2f}'. 115 | format(bits, psi.prob(*bits), 116 | dot2(bits, nbits))) 117 | 118 | # Multiple rounds (nbits) are necessary to find 119 | # the system of equations that allows finding of 'c'. 120 | # ... (not implemented here) 121 | 122 | 123 | def main(argv): 124 | if len(argv) > 1: 125 | raise app.UsageError('Too many command-line arguments.') 126 | 127 | # Note: Running with 5 (10 total) qubits takes about 5 minutes. 128 | run_experiment(3) 129 | 130 | 131 | if __name__ == '__main__': 132 | app.run(main) 133 | -------------------------------------------------------------------------------- /src/solovay_kitaev.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Solovay-Kitaev Algorithm for gate approximation.""" 3 | 4 | import random 5 | 6 | from absl import app 7 | import numpy as np 8 | import scipy.linalg 9 | 10 | from src.lib import helper 11 | from src.lib import ops 12 | from src.lib import state 13 | 14 | 15 | def to_su2(u): 16 | """Convert a 2x2 unitary to a unitary with determinant 1.0.""" 17 | 18 | det = u[0][0] * u[1][1] - u[0][1] * u[1][0] 19 | return np.sqrt(1 / det) * u 20 | 21 | 22 | def trace_dist(u, v): 23 | """Compute trace distance between two 2x2 matrices.""" 24 | 25 | return np.real(0.5 * np.trace(scipy.linalg.sqrtm((u - v).adjoint() @ (u - v)))) 26 | 27 | 28 | def create_unitaries(base, limit): 29 | """Create all combinations of all base gates, up to length 'limit'.""" 30 | 31 | # Create bitstrings up to bitstring length limit-1: 32 | # 0, 1, 00, 01, 10, 11, 000, 001, 010, ... 33 | # 34 | # Multiply together the 2 base operators, according to their index. 35 | # Note: This can be optimized, by remembering the last 2^x results 36 | # and multiplying them with base gets 0, 1. 37 | # 38 | gate_list = [] 39 | for width in range(limit): 40 | for bits in helper.bitprod(width): 41 | u = ops.Identity() 42 | for bit in bits: 43 | u = u @ base[bit] 44 | gate_list.append(u) 45 | return gate_list 46 | 47 | 48 | def find_closest_u(gate_list, u): 49 | """Find the one gate in the list closest to u.""" 50 | 51 | # Linear search over list of gates - is _very_ slow. 52 | # This can be optimized by using kd-trees. 53 | # 54 | min_dist, min_u = 10, ops.Identity() 55 | for gate in gate_list: 56 | tr_dist = trace_dist(gate, u) 57 | if tr_dist < min_dist: 58 | min_dist, min_u = tr_dist, gate 59 | return min_u 60 | 61 | 62 | def u_to_bloch(u): 63 | """Compute angle and axis for a unitary.""" 64 | 65 | angle = np.real(np.arccos((u[0, 0] + u[1, 1]) / 2)) 66 | sin = np.sin(angle) 67 | if sin < 1e-10: 68 | axis = [0, 0, 1] 69 | else: 70 | nx = (u[0, 1] + u[1, 0]) / (2j * sin) 71 | ny = (u[0, 1] - u[1, 0]) / (2 * sin) 72 | nz = (u[0, 0] - u[1, 1]) / (2j * sin) 73 | axis = [nx, ny, nz] 74 | return axis, 2 * angle 75 | 76 | 77 | def gc_decomp(u): 78 | """Group commutator decomposition.""" 79 | 80 | def diagonalize(u): 81 | _, v = np.linalg.eig(u) 82 | return ops.Operator(v) 83 | 84 | # Get axis and theta for the operator. 85 | axis, theta = u_to_bloch(u) 86 | 87 | # The angle phi comes from eq 10 in 'The Solovay-Kitaev Algorithm' by 88 | # Dawson, Nielsen. It is fully derived in the book section on the 89 | # theorem and algorithm. 90 | phi = 2.0 * np.arcsin(np.sqrt(np.sqrt((0.5 - 0.5 * np.cos(theta / 2))))) 91 | 92 | v = ops.RotationX(phi) 93 | if axis[2] > 0: 94 | w = ops.RotationY(2 * np.pi - phi) 95 | else: 96 | w = ops.RotationY(phi) 97 | 98 | ud = diagonalize(u) 99 | vwvdwd = diagonalize(v @ w @ v.adjoint() @ w.adjoint()) 100 | s = ud @ vwvdwd.adjoint() 101 | 102 | v_hat = s @ v @ s.adjoint() 103 | w_hat = s @ w @ s.adjoint() 104 | return v_hat, w_hat 105 | 106 | 107 | def sk_algo(u, gates, n): 108 | """Solovay-Kitaev Algorithm.""" 109 | 110 | if n == 0: 111 | return find_closest_u(gates, u) 112 | else: 113 | u_next = sk_algo(u, gates, n - 1) 114 | v, w = gc_decomp(u @ u_next.adjoint()) 115 | v_next = sk_algo(v, gates, n - 1) 116 | w_next = sk_algo(w, gates, n - 1) 117 | return v_next @ w_next @ v_next.adjoint() @ w_next.adjoint() @ u_next 118 | 119 | 120 | def random_gates(min_length, max_length, num_experiments): 121 | """Just create random sequences, find the best.""" 122 | 123 | base = [to_su2(ops.Hadamard()), to_su2(ops.Tgate())] 124 | 125 | u = (ops.RotationX(2.0 * np.pi * random.random()) @ 126 | ops.RotationY(2.0 * np.pi * random.random()) @ 127 | ops.RotationZ(2.0 * np.pi * random.random())) 128 | 129 | min_dist = 1000 130 | min_u = ops.Identity() 131 | for _ in range(num_experiments): 132 | seq_length = min_length + random.randint(0, max_length) 133 | u_approx = ops.Identity() 134 | 135 | for _ in range(seq_length): 136 | g = random.randint(0, 1) 137 | u_approx = u_approx @ base[g] 138 | 139 | dist = trace_dist(u, u_approx) 140 | if dist < min_dist: 141 | min_dist = dist 142 | min_u = u_approx 143 | 144 | phi1 = u(state.zeros(1)) 145 | phi2 = min_u(state.zeros(1)) 146 | print('Trace distance: {:.4f}, States dot product: {:6.4f}'. 147 | format(min_dist, 148 | (np.abs(np.dot(phi1, phi2.conj()))))) 149 | 150 | 151 | def main(argv): 152 | if len(argv) > 1: 153 | raise app.UsageError('Too many command-line arguments.') 154 | 155 | num_experiments = 10 156 | depth = 8 157 | recursion = 4 158 | print('SK algorithm - depth: {}, recursion: {}, experiments: {}'. 159 | format(depth, recursion, num_experiments)) 160 | 161 | base = [to_su2(ops.Hadamard()), to_su2(ops.Tgate())] 162 | gates = create_unitaries(base, depth) 163 | sum_dist = 0.0 164 | for _ in range(num_experiments): 165 | u = (ops.RotationX(2.0 * np.pi * random.random()) @ 166 | ops.RotationY(2.0 * np.pi * random.random()) @ 167 | ops.RotationZ(2.0 * np.pi * random.random())) 168 | 169 | u_approx = sk_algo(u, gates, recursion) 170 | 171 | dist = trace_dist(u, u_approx) 172 | sum_dist += dist 173 | 174 | phi1 = u(state.zeros(1)) 175 | phi2 = u_approx(state.zeros(1)) 176 | print('Trace distance: {:.4f}, States dot product: {:6.4f}'. 177 | format(dist, 178 | (np.real(np.dot(phi1, phi2.conj()))))) 179 | 180 | print('Gates: {}, Mean Trace Dist:: {:.4f}'. 181 | format(len(gates), sum_dist / num_experiments)) 182 | 183 | min_length = 10 184 | max_delta = 50 185 | max_tries = 1000 186 | print('Random Experiment, seq length: {} - {}, tries: {}' 187 | .format(min_length, max_delta, max_tries)) 188 | for _ in range(num_experiments): 189 | random_gates(min_length, max_delta, max_tries) 190 | 191 | 192 | if __name__ == '__main__': 193 | app.run(main) 194 | -------------------------------------------------------------------------------- /src/spectral_decomp.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Spectral Decomposition.""" 3 | 4 | from absl import app 5 | import numpy as np 6 | import scipy.stats 7 | 8 | from src.lib import ops 9 | 10 | 11 | def spectral_decomp(ndim: int): 12 | """Implement and verify spectral decomposition theorem.""" 13 | 14 | # The spectral theorem says that a Hermitian matrix can be written as 15 | # the sum over eigenvalues lambda_i and eigenvectors u_i, as: 16 | # 17 | # A = sum_i {lambda_i * |u_i> 1: 85 | raise app.UsageError('Too many command-line arguments.') 86 | 87 | iterations = 100 88 | print(f'{iterations} Spectral decompositiona') 89 | for _ in range(iterations): 90 | spectral_decomp(32) 91 | print('Success') 92 | 93 | 94 | if __name__ == '__main__': 95 | app.run(main) 96 | -------------------------------------------------------------------------------- /src/state_prep_mottonen.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: State preparation with Moettoenen's algorithm.""" 3 | 4 | from absl import app 5 | import numpy as np 6 | from src.lib import circuit 7 | 8 | # Reference: 9 | # [1] Transformation of quantum states using uniformly controlled rotations 10 | # https://arxiv.org/pdf/quant-ph/0407010.pdf 11 | # 12 | # This implementation would not have been possible without looking 13 | # at existing code references: 14 | # 15 | # https://github.com/ravikumar1728/Mottonen-State-Preparation 16 | # 17 | # https://docs.pennylane.ai/en/stable/_modules/pennylane/ 18 | # templates/state_preparations/mottonen.html 19 | # 20 | # The reference takes an existing state and transforms it down to |0>. 21 | # Since we want to prepare a specific state starting from |0> we 22 | # reverse the procedure in this code snippet. 23 | 24 | 25 | def gray_code(i: int) -> int: 26 | """Return Gray code at index i.""" 27 | 28 | return i ^ (i >> 1) 29 | 30 | 31 | def compute_alpha_y(vec, k: int, j: int): 32 | """Compute the angles alpha_k for the y rotations.""" 33 | 34 | # This is the implementation of Equation (8) in the reference. 35 | # Note the off-by-1 issues (the paper is 1-based). 36 | m = 2 ** (k - 1) 37 | enumerator = sum(vec[(2 * (j + 1) - 1) * m + bit] ** 2 for bit in range(m)) 38 | m = 2**k 39 | divisor = sum(vec[j * m + bit] ** 2 for bit in range(m)) 40 | if divisor != 0: 41 | return 2 * np.arcsin(np.sqrt(enumerator / divisor)) 42 | return 0.0 43 | 44 | 45 | def compute_alpha_z(omega, k: int, j: int): 46 | """Compute the angles alpha_k for the z rotations.""" 47 | 48 | # This is the implementation of Equation (5) in the reference. 49 | # Note the off-by-1 issues (the paper is 1-based). 50 | m = 2 ** (k - 1) 51 | ind1 = [(2 * (j + 1) - 1) * m + bit for bit in range(m)] 52 | ind2 = [(2 * (j + 1) - 2) * m + bit for bit in range(m)] 53 | diff = (omega[ind1] - omega[ind2]) / m 54 | return sum(diff) 55 | 56 | 57 | def compute_m(k: int): 58 | """Compute matrix M which takes alpha -> theta.""" 59 | 60 | # This computation of M follows Equation (3) in the reference. 61 | n = 2**k 62 | m = np.zeros([n, n]) 63 | for i in range(n): 64 | for j in range(n): 65 | # Note: bit_count() only supported from Python 3.10. 66 | m[i, j] = (-1) ** bin(j & gray_code(i)).count('1') * 2 ** (-k) 67 | return m 68 | 69 | 70 | def compute_ctl(idx: int): 71 | """Compute control indices for the cx gates.""" 72 | 73 | # This code implements the control qubit indices following 74 | # Fig 2 in the reference in a recursive manner. The secret 75 | # to success is to 'kill' the last token in the recursive call. 76 | if idx == 0: 77 | return [] 78 | side = compute_ctl(idx - 1)[:-1] 79 | return side + [idx - 1] + side + [idx - 1] 80 | 81 | 82 | def controlled_rotation(qc, alpha_k, control, target, gate): 83 | """Implement the controlled rotations.""" 84 | 85 | k = len(control) 86 | thetas = compute_m(k) @ alpha_k 87 | ctl = compute_ctl(k) 88 | for i in range(2**k): 89 | gate(target, thetas[i]) 90 | if k > 0: 91 | qc.cx(control[k - 1 - ctl[i]], target) 92 | 93 | 94 | def prepare_state_mottonen(qc, qb, vector, nbits: int = 3): 95 | """Construct the Mottonen circuit based on input vector.""" 96 | 97 | # Ry gates for the absolute amplitudes. 98 | avec = abs(vector) 99 | for k in range(nbits): 100 | alpha_k = [compute_alpha_y(avec, nbits - k, j) for j in range(2**k)] 101 | controlled_rotation(qc, alpha_k, qb[:k], qb[k], qc.ry) 102 | 103 | # Rz gates to normalize up to a global phase. This is only 104 | # needed for complex values. 105 | omega = np.angle(vector) 106 | if np.allclose(omega, 0.0): 107 | return 108 | 109 | for k in range(0, nbits): 110 | alpha_z = [compute_alpha_z(omega, nbits - k, j) for j in range(2**k)] 111 | controlled_rotation(qc, alpha_z, qb[:k], qb[k], qc.rz) 112 | 113 | 114 | def run_experiment(nbits: int = 3): 115 | """Prepare a random state with nbits qubits.""" 116 | 117 | vector = np.random.random([2**nbits]) + 1j * np.random.random([2**nbits]) 118 | vector = vector / np.linalg.norm(vector) 119 | print(f' Qubits: {nbits:2d}, vector: {vector[:6]}...') 120 | 121 | qc = circuit.qc() 122 | qb = qc.reg(nbits) 123 | prepare_state_mottonen(qc, qb, vector, nbits) 124 | 125 | # For complex numbers, this algorithm introduces a global phase 126 | # which we can account for (and ignore) here: 127 | phase = vector[0] / qc.psi[0] 128 | if not np.allclose(vector, qc.psi * phase, atol=1e-5): 129 | raise AssertionError('Invalid State initialization.') 130 | 131 | 132 | def main(argv): 133 | if len(argv) > 1: 134 | raise app.UsageError('Too many command-line arguments.') 135 | print("State Preparation with Moettoenen's Algorithm...") 136 | 137 | for nbits in range(1, 11): 138 | run_experiment(nbits) 139 | 140 | 141 | if __name__ == '__main__': 142 | np.set_printoptions(precision=2) 143 | app.run(main) 144 | -------------------------------------------------------------------------------- /src/subset_sum.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Number set partitioning such set sum(A) == sum(B).""" 3 | 4 | 5 | # Based on this paper: 6 | # https://cds.cern.ch/record/467590/files/0010018.pdf 7 | # 8 | # For a set A of integers, can A be partitioned into 9 | # two sets A1 and A2, such that: 10 | # sum(A1) == sum(A2) 11 | # 12 | # For this to work, sum(A) must not be odd. 13 | # We should reach 100% consistent results. 14 | 15 | import random 16 | from typing import List 17 | 18 | from absl import app 19 | from absl import flags 20 | import numpy as np 21 | 22 | from src.lib import helper 23 | 24 | flags.DEFINE_integer('nmax', 15, 'Maximum number') 25 | flags.DEFINE_integer('nnum', 6, 26 | 'Maximum number of set elements [1-nmax]') 27 | flags.DEFINE_integer('iterations', 20, 'Number of experiments') 28 | 29 | 30 | def select_numbers(nmax: int, nnum: int) -> List[int]: 31 | """Select nnum random, unique numbers in range 1 to nmax.""" 32 | 33 | while True: 34 | sample = random.sample(range(1, nmax), nnum) 35 | if sum(sample) % 2 == 0: 36 | return sample 37 | 38 | 39 | def tensor_diag(n: int, num: int): 40 | """Construct tensor product from diagonal matrices.""" 41 | 42 | def tensor_product(w1: float, w2: float, diag): 43 | # pylint: disable=g-complex-comprehension 44 | return [j for i in zip([x * w1 for x in diag], 45 | [x * w2 for x in diag]) for j in i] 46 | 47 | assert num > 0, 'Invalid input, expected num > 0.' 48 | diag = [1, 1] 49 | for i in range(1, n): 50 | if i == num: 51 | diag = tensor_product(i, -i, diag) 52 | else: 53 | diag = tensor_product(1, 1, diag) 54 | return diag 55 | 56 | 57 | def set_to_diagonal_h(num_list: List[int], nmax: int) -> List[float]: 58 | """Construct diag(H).""" 59 | 60 | h = [0.0] * 2**nmax 61 | for num in num_list: 62 | diag = tensor_diag(nmax, num) 63 | for idx, val in enumerate(diag): 64 | h[idx] += val 65 | return h 66 | 67 | 68 | def compute_partition(num_list: List[int]): 69 | """Compute paritions that add up.""" 70 | 71 | solutions = [] 72 | for bits in helper.bitprod(len(num_list)): 73 | iset = [] 74 | oset = [] 75 | for idx, val in enumerate(bits): 76 | if val == 0: 77 | iset.append(num_list[idx]) 78 | else: 79 | oset.append(num_list[idx]) 80 | if sum(iset) == sum(oset): 81 | solutions.append(bits) 82 | return solutions 83 | 84 | 85 | def dump_solution(bits: List[int], num_list: List[int]): 86 | """Simply print a solution.""" 87 | 88 | iset = [] 89 | oset = [] 90 | for idx, val in enumerate(bits): 91 | if val == 0: 92 | iset.append(f'{num_list[idx]:d}') 93 | else: 94 | oset.append(f'{num_list[idx]:d}') 95 | return '+'.join(iset) + ' == ' + '+'.join(oset) 96 | 97 | 98 | def run_experiment(num_list: List[int]) -> bool: 99 | """Run an experiment, compute H, match against 0.""" 100 | 101 | nmax = flags.FLAGS.nmax 102 | if not num_list: 103 | num_list = select_numbers(nmax, flags.FLAGS.nnum) 104 | solutions = compute_partition(num_list) 105 | 106 | diag = set_to_diagonal_h(num_list, nmax) 107 | 108 | non_zero = np.count_nonzero(diag) 109 | if non_zero != 2**nmax: 110 | print(' Solution should exist...', end='') 111 | if solutions: 112 | print(' Found Solution:', 113 | dump_solution(solutions[0], num_list)) 114 | return True 115 | assert False, 'False positive found.' 116 | 117 | print(' No Solution Found.', sorted(num_list)) 118 | assert not solutions, 'False negative found.' 119 | return False 120 | 121 | 122 | def main(argv): 123 | if len(argv) > 1: 124 | raise app.UsageError('Too many command-line arguments.') 125 | 126 | print(f'Test random sets [1..{flags.FLAGS.nmax}]') 127 | for _ in range(flags.FLAGS.iterations): 128 | run_experiment(None) 129 | 130 | # A few negative tests. 131 | print('Test known-negative sets...') 132 | sets = [ 133 | [1, 2, 3, 7], 134 | [1, 3, 5, 10], 135 | [2, 7, 8, 10, 12, 13], 136 | [1, 6, 8, 9, 12, 14], 137 | [3, 8, 9, 11, 13, 14], 138 | [7, 9, 12, 14, 15, 17], 139 | ] 140 | for s in sets: 141 | if run_experiment(s): 142 | raise AssertionError('Incorrect Classification') 143 | 144 | 145 | if __name__ == '__main__': 146 | app.run(main) 147 | -------------------------------------------------------------------------------- /src/superdense.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Superdense Coding.""" 3 | 4 | import math 5 | 6 | from absl import app 7 | 8 | from src.lib import bell 9 | from src.lib import ops 10 | from src.lib import state 11 | 12 | 13 | def alice_manipulates(psi: state.State, bit0: int, bit1: int) -> state.State: 14 | """Alice encodes 2 classical bits in her 1 qubit.""" 15 | 16 | ret = ops.Identity(2)(psi) 17 | if bit0: 18 | ret = ops.PauliX()(ret) 19 | if bit1: 20 | ret = ops.PauliZ()(ret) 21 | return ret 22 | 23 | 24 | def bob_measures(psi: state.State, expect0: int, expect1: int): 25 | """Bob measures both bits (in computational basis).""" 26 | 27 | # Change Hadamard basis back to computational basis. 28 | psi = ops.Cnot(0, 1)(psi) 29 | psi = ops.Hadamard()(psi) 30 | 31 | p0, _ = ops.Measure(psi, 0, tostate=expect1) 32 | p1, _ = ops.Measure(psi, 1, tostate=expect0) 33 | 34 | if (not math.isclose(p0, 1.0, abs_tol=1e-6) or 35 | not math.isclose(p1, 1.0, abs_tol=1e-6)): 36 | raise AssertionError(f'Invalid Result p0 {p0} p1 {p1}') 37 | 38 | print(f'Expected/matched: |{expect0}{expect1}>.') 39 | 40 | 41 | def main(argv): 42 | if len(argv) > 1: 43 | raise app.UsageError('Too many command-line arguments.') 44 | 45 | # Step 1: Alice and Bob share an entangled pair, and separate. 46 | psi = bell.bell_state(0, 0) 47 | 48 | # Alices manipulates her qubit and sends her 1 qubit back to Bob, 49 | # who measures. In the Hadamard basis he would get b00, b01, etc. 50 | # but we're measuring in the computational basis by reverse 51 | # applying Hadamard and Cnot. 52 | for bit0 in range(2): 53 | for bit1 in range(2): 54 | psi_alice = alice_manipulates(psi, bit0, bit1) 55 | bob_measures(psi_alice, bit0, bit1) 56 | 57 | 58 | if __name__ == '__main__': 59 | app.run(main) 60 | -------------------------------------------------------------------------------- /src/swap_test.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Swap Test.""" 3 | 4 | 5 | # The Swap test is a circuit to measure how close to each other 2 states are. 6 | # It doesn't tell what those states are, only if they are close. For example, 7 | # the states |0> and |1> are maximally different. The pairs |0>, |0> and 8 | # |1>, |1> are maximally similar. This applies also to other states than the 9 | # basis states. For example, the two qubits (states): 10 | # 11 | # psi = 0.2|0> + x|1> # 0.2^2 + x^2 = 1.0 12 | # phi = 0.2|0> + x|1> 13 | # 14 | # are also maximally equal. These states are somewhere inbetween (80%): 15 | # 16 | # psi = 0.2|0> + x|1> # 0.2^2 + x^2 = 1.0 17 | # phi = 0.8|0> + y|1> # 0.8^2 + y^2 = 1.0 18 | # 19 | # The measurement probability of the ancillary qubit to be in state |0> is 20 | # 1/2 + 1/2 * ||^2 21 | # 22 | # Because of the dot product: 23 | # The probability found for maximally equal is 1.0 24 | # The probability found for maximally different is 0.5 25 | # 26 | # A good overview can be found here: 27 | # https://en.wikipedia.org/wiki/Swap_test 28 | 29 | from absl import app 30 | import numpy as np 31 | 32 | from src.lib import ops 33 | from src.lib import state 34 | 35 | 36 | def run_experiment_single(a1: np.complexfloating, a2: np.complexfloating, 37 | target: float) -> None: 38 | """Construct swap test circuit and measure.""" 39 | 40 | # The circuit is quite simple: 41 | # 42 | # |0> --- H --- o --- H --- Measure 43 | # | 44 | # a1 --------- x --------- 45 | # | 46 | # a2 ----------x --------- 47 | 48 | psi = state.bitstring(0) * state.qubit(a1) * state.qubit(a2) 49 | psi = ops.Hadamard()(psi, 0) 50 | psi = ops.ControlledU(0, 1, ops.Swap(1, 2))(psi) 51 | psi = ops.Hadamard()(psi, 0) 52 | 53 | # Measure once. 54 | p0, _ = ops.Measure(psi, 0) 55 | if abs(p0 - target) > 0.05: 56 | raise AssertionError( 57 | f'Probability {p0:.2f} off more than 5% from target {target:.2f}') 58 | print(f'Similarity of a1: {a1:.2f}, a2: {a2:.2f} => %: {100 * p0:.2f}') 59 | 60 | 61 | def run_experiment_double(a0: np.complexfloating, a1: np.complexfloating, 62 | b0: np.complexfloating, b1: np.complexfloating, 63 | target: float) -> None: 64 | """Construct multi-qubit swap test circuit and measure.""" 65 | 66 | # The circuit is quite simple: 67 | # 68 | # |0> --- H --- o --- o --- H --- Measure 69 | # | | 70 | # a0 --------- x --- | ---- 71 | # | | 72 | # a1 ----------| --- x ---- 73 | # | | 74 | # b0 --------- x --- | ---- 75 | # | 76 | # b1 ----------------x ---- 77 | 78 | psi_a = state.qubit(a0) * state.qubit(a1) 79 | psi_a = ops.Cnot(0, 1)(psi_a) 80 | psi_b = state.qubit(b0) * state.qubit(b1) 81 | psi_b = ops.Cnot(0, 1)(psi_b) 82 | 83 | psi = state.bitstring(0) * psi_a * psi_b 84 | 85 | psi = ops.Hadamard()(psi, 0) 86 | psi = ops.ControlledU(0, 1, ops.Swap(1, 3))(psi) 87 | psi = ops.ControlledU(0, 2, ops.Swap(2, 4))(psi) 88 | psi = ops.Hadamard()(psi, 0) 89 | 90 | # Measure once. 91 | p0, _ = ops.Measure(psi, 0) 92 | print(f'Sim of (a0: {a0:.2f}, a1: {a1:.2f}) ' + 93 | f'(b0: {b0:.2f}, b1: {b1:.2f}) => %: {100 * p0:.2f}') 94 | if abs(p0 - target) > 0.05: 95 | raise AssertionError( 96 | 'Probability {:.2f} off more than 5% from target {:.2f}' 97 | .format(p0, target)) 98 | 99 | 100 | def main(argv): 101 | if len(argv) > 1: 102 | raise app.UsageError('Too many command-line arguments.') 103 | 104 | print('Swap test. 0.5 means different, 1.0 means similar') 105 | run_experiment_single(1.0, 0.0, 0.5) 106 | run_experiment_single(0.0, 1.0, 0.5) 107 | run_experiment_single(1.0, 1.0, 1.0) 108 | run_experiment_single(0.0, 0.0, 1.0) 109 | run_experiment_single(0.1, 0.9, 0.65) 110 | run_experiment_single(0.2, 0.8, 0.8) 111 | run_experiment_single(0.3, 0.7, 0.9) 112 | run_experiment_single(0.4, 0.6, 0.95) 113 | run_experiment_single(0.5, 0.5, 0.97) 114 | run_experiment_single(0.1, 0.1, 1.0) 115 | run_experiment_single(0.8, 0.8, 1.0) 116 | 117 | # 2 qubits: 118 | probs = [0.5, 0.5, 0.5, 0.52, 0.55, 0.59, 0.65, 0.72, 0.80, 0.90] 119 | for i in range(10): 120 | run_experiment_double(1.0, 0.0, 0.0 + i * 0.1, 1.0 - i * 0.1, probs[i]) 121 | 122 | # An experiment with superposition, as mentioned in the literature: 123 | psi = state.bitstring(0, 0, 0, 0, 0) 124 | 125 | psi = ops.Hadamard()(psi, 0) 126 | psi = ops.Hadamard()(psi, 1) 127 | psi = ops.Hadamard()(psi, 2) 128 | psi = ops.ControlledU(0, 1, ops.Swap(1, 3))(psi) 129 | psi = ops.ControlledU(0, 2, ops.Swap(2, 4))(psi) 130 | psi = ops.Hadamard()(psi, 0) 131 | p0, _ = ops.Measure(psi, 0) 132 | 133 | # P(|0>) = 1/2 + 1/2^2. 134 | # Hence (dot product)^2 = 2 * (p0 - 0.5) 135 | # 136 | if abs(p0 - 0.624999) > 0.5: 137 | raise AssertionError('Incorrect math on example.') 138 | print(f'Similarity from literature: p={p0:.3f}, dot={2*(p0-0.5):.3f} (ok)') 139 | 140 | 141 | if __name__ == '__main__': 142 | app.run(main) 143 | -------------------------------------------------------------------------------- /src/teleportation.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Quantum Teleportation.""" 3 | 4 | import math 5 | 6 | from absl import app 7 | import numpy as np 8 | 9 | from src.lib import bell 10 | from src.lib import ops 11 | from src.lib import state 12 | 13 | 14 | def alice_measures(alice: state.State, 15 | expect0: np.complexfloating, expect1: np.complexfloating, 16 | qubit0: np.complexfloating, qubit1: np.complexfloating): 17 | """Force measurement and get teleported qubit.""" 18 | 19 | # Alice measures her state and gets a collapsed |qubit0 qubit1>. 20 | # She lets Bob know which one of the 4 combinations she obtained. 21 | 22 | # We force a measurement here, collapsing to a state with the 23 | # first two qubits collapsed. Bob's qubit is still unmeasured. 24 | _, alice0 = ops.Measure(alice, 0, tostate=qubit0) 25 | _, alice1 = ops.Measure(alice0, 1, tostate=qubit1) 26 | 27 | # Depending on what was measured and communicated, Bob has to 28 | # one of these things to his qubit2: 29 | if qubit0 == 0 and qubit1 == 0: 30 | pass 31 | if qubit0 == 0 and qubit1 == 1: 32 | alice1 = ops.PauliX()(alice1, idx=2) 33 | if qubit0 == 1 and qubit1 == 0: 34 | alice1 = ops.PauliZ()(alice1, idx=2) 35 | if qubit0 == 1 and qubit1 == 1: 36 | alice1 = ops.PauliX()(ops.PauliZ()(alice1, idx=2), idx=2) 37 | 38 | # Now Bob measures his qubit (2) (without collapse, so we can 39 | # 'measure' it twice. This is not necessary, but good to double check 40 | # the math). 41 | p0, _ = ops.Measure(alice1, 2, tostate=0, collapse=False) 42 | p1, _ = ops.Measure(alice1, 2, tostate=1, collapse=False) 43 | 44 | # Alice should now have 'teleported' the qubit in state 'x'. 45 | # We sqrt() the probability, we want to show (original) amplitudes. 46 | bob_a = math.sqrt(p0.real) 47 | bob_b = math.sqrt(p1.real) 48 | print('Teleported (|{:d}{:d}>) a={:.2f}, b={:.2f}'.format( 49 | int(qubit0), int(qubit1), bob_a, bob_b)) 50 | 51 | if (not math.isclose(expect0, bob_a, abs_tol=1e-6) or 52 | not math.isclose(expect1, bob_b, abs_tol=1e-6)): 53 | raise AssertionError('Invalid result.') 54 | 55 | 56 | def main(argv): 57 | if len(argv) > 1: 58 | raise app.UsageError('Too many command-line arguments.') 59 | 60 | # Step 1: Alice and Bob share an entangled pair and separate. 61 | psi = bell.bell_state(0, 0) 62 | 63 | # Step 2: Alice wants to teleport a qubit |psi> to Bob 64 | # with |phi> = a|0> + b|1>, a^2 + b^2 == 1: 65 | a = 0.6 66 | b = math.sqrt(1.0 - a * a) 67 | phi = state.qubit(a, b) 68 | print('Quantum Teleportation') 69 | print(f'Start with EPR Pair a={a:.2f}, b={b:.2f}') 70 | 71 | # Produce combined state. 72 | alice = phi * psi 73 | 74 | # Alice lets the 1st qubit interact with the 2nd qubit, which is her 75 | # part of the entangle state with Bob. 76 | alice = ops.Cnot(0, 1)(alice) 77 | 78 | # Now she applies a Hadamard to qubit 0. Bob still owns qubit 2. 79 | alice = ops.Hadamard()(alice, idx=0) 80 | 81 | # Alice measures and communicates the result (|00>, |01>, ...) to Bob. 82 | alice_measures(alice, a, b, 0, 0) 83 | alice_measures(alice, a, b, 0, 1) 84 | alice_measures(alice, a, b, 1, 0) 85 | alice_measures(alice, a, b, 1, 1) 86 | 87 | if __name__ == '__main__': 88 | app.run(main) 89 | -------------------------------------------------------------------------------- /src/tools/BUILD: -------------------------------------------------------------------------------- 1 | py_binary( 2 | name = "random_walk", 3 | srcs = ["random_walk.py"], 4 | python_version = "PY3", 5 | srcs_version = "PY3", 6 | ) 7 | -------------------------------------------------------------------------------- /src/tools/random_walk.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Simple classic random walk.""" 3 | 4 | import random 5 | from absl import app 6 | 7 | # This little hacktastic tool simulates a classical 8 | # walk. While it is fun to play around with the parameters 9 | # it essentially produces a result that mirrors the 10 | # distribution of the random number generator, which 11 | # is random.gauss at this point. 12 | 13 | 14 | def main(argv): 15 | if len(argv) > 1: 16 | raise app.UsageError('Too many command-line arguments.') 17 | 18 | n_steps = 50 19 | n_collections = 10000 20 | final_pos = [0] * 2 * n_steps 21 | print('Simulate random walk, collect distribution...') 22 | for i in range(n_collections): 23 | for step in range(n_steps): 24 | direction = random.gauss(0, 10) 25 | step += int(direction) 26 | if step > 0 and step < 2 * n_steps: 27 | final_pos[step] += 1 28 | 29 | max_elem = 0 30 | for i in range(len(final_pos)): 31 | if final_pos[i] > max_elem: 32 | max_elem = final_pos[i] 33 | 34 | index = 0 35 | for index in range(n_steps * 2): 36 | if index == n_steps-1: 37 | continue 38 | print('{} {:.6f}'.format(index, 1.0 / max_elem * final_pos[index])) 39 | index = index + 1 40 | 41 | 42 | if __name__ == '__main__': 43 | app.run(main) 44 | -------------------------------------------------------------------------------- /src/zy_decomp.py: -------------------------------------------------------------------------------- 1 | # python3 2 | """Example: Z-Y dcomposition of a unitary U.""" 3 | 4 | import cmath 5 | 6 | from absl import app 7 | import numpy as np 8 | import scipy.stats 9 | 10 | from src.lib import ops 11 | 12 | # The "Z-Y decomposition for a single qubit" shows that 13 | # any unitary U can be decomposed into Rz and Ry rotations 14 | # with 4 parameters alpha, beta, gamma, delta as follows: 15 | # 16 | # U = e^(i*alpha) * Rz(beta) * Ry(gamma) * Rz(delta) 17 | # 18 | # The question is how to find the 4 parameters for 19 | # a given U? 20 | # 21 | # One answer was provided here - it solves correctly for alpha and gamma: 22 | # https://threeplusone.com/pubs/on_gates.pdf 23 | # 24 | # The approach from this paper can be found in code, here: 25 | # https://github.com/gecrooks/quantumflow-dev/blob/master/quantumflow/decompositions.py 26 | # 27 | # Another publication solves correctly for beta and delta: 28 | # https://quantumcomputing.stackexchange.com/questions/\ 29 | # 16256/what-is-the-procedure-of-finding-z-y-decomposition-of-unitary-matrices 30 | # 31 | # Let's try and implement this here! 32 | 33 | 34 | def make_u_zy(alpha, beta, gamma, delta): 35 | """Construct unitary via Z-Y from the 4 parameters.""" 36 | 37 | return ( 38 | ops.RotationZ(beta) @ ops.RotationY(gamma) @ ops.RotationZ(delta) 39 | ) * cmath.exp(1.0j * alpha) 40 | 41 | 42 | def make_u_xy(alpha, beta, gamma, delta): 43 | """Construct unitary via X-Y from the 4 parameters.""" 44 | 45 | return ( 46 | ops.RotationX(beta) @ ops.RotationY(gamma) @ ops.RotationX(delta) 47 | ) * cmath.exp(1.0j * alpha) 48 | 49 | 50 | def zy_decompose(umat): 51 | """Perform Z-Y decomposition of unitary operator in SU(2).""" 52 | 53 | a = umat[0][0] 54 | b = umat[0][1] 55 | c = umat[1][0] 56 | 57 | det = np.linalg.det(umat) 58 | alpha = 0.5 * np.arctan2(det.imag, det.real) 59 | 60 | if a >= b: 61 | gamma = 2 * np.arccos(abs(a)) 62 | else: 63 | gamma = 2 * np.arcsin(abs(b)) 64 | 65 | # TODO(rhundt): Handle cases with gamma very close to 0 or Pi 66 | 67 | beta = cmath.phase(c) - cmath.phase(a) 68 | delta = cmath.phase(-b) - cmath.phase(a) 69 | 70 | return alpha, beta, gamma, delta 71 | 72 | 73 | def main(argv): 74 | if len(argv) > 1: 75 | raise app.UsageError('Too many command-line arguments.') 76 | 77 | iterations = 1000 78 | print(f'Perform {iterations} random Z-Y and X-Y decompositions.') 79 | 80 | for i in range(iterations): 81 | # 82 | # Construct a random unitary operator and put into SU(2). 83 | # 84 | u = scipy.stats.unitary_group.rvs(2) 85 | umat = np.sqrt(1 / np.linalg.det(u)) * u 86 | 87 | # Now decompose this operator and find the four parameters. 88 | # 89 | alpha, beta, gamma, delta = zy_decompose(umat) 90 | 91 | # Construct another operator from these newly found 92 | # parameters and make sure that the resulting 93 | # operator matches the one from above. 94 | # 95 | unew = make_u_zy(alpha, beta, gamma, delta) 96 | 97 | if not np.allclose(umat, unew, atol=1e-4): 98 | print(f'decomp : {i:2d}: {alpha:.3f} {beta:.3f} {gamma:.3f} {delta:.3f}') 99 | raise AssertionError('Z-Y decomposition failed') 100 | 101 | # According to Problem 4.10 in Nielsen/Chuang, we can also derive 102 | # an XY-decomposition. How? See: 103 | # 104 | # https://quantumcomputing.stackexchange.com/a/32088/11582 105 | # 106 | # In essence, we change the axes by rotating U to U' = HUH. 107 | # We compute the Y-Z decomposition for U' and note that: 108 | # U = HU'H 109 | # and correspondingly: 110 | # H Rz(beta') H -> Rx(beta) 111 | # H Rz(delta') H -> Rx(delta) 112 | # and 113 | # H Ry(gamma') H -> Ry (-gamma) 114 | # 115 | udash = ops.Hadamard() @ umat @ ops.Hadamard() 116 | 117 | alpha, beta, gamma, delta = zy_decompose(udash) 118 | unew = make_u_xy(alpha, beta, -gamma, delta) 119 | 120 | if not np.allclose(umat, unew, atol=1e-4): 121 | print(f'decomp : {i:2d}: {alpha:.3f} {beta:.3f} {gamma:.3f} {delta:.3f}') 122 | raise AssertionError('X-Y decomposition failed') 123 | 124 | print('Success') 125 | 126 | 127 | if __name__ == '__main__': 128 | np.set_printoptions(precision=3) 129 | app.run(main) 130 | --------------------------------------------------------------------------------