diff --git a/CHANGELOG.md b/CHANGELOG.md index 20c00d49..270a1a18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Improvements and Changes: - Changed qubit tensor factor ordering of state tomography estimates to match that of process tomography, e.g. tomographizing the plus eigenstate of `X0 * Z1` and passing in `qubits = [0,1]` will yield the state estimate corresponding to `|+0> = (1, 0, 1, 0)/sqrt(2)` rather than `|0+>` (gh-142) +- Improvements to Ripple carry adder notebook, added tests for non parametric bit string +prep program in utils (gh-98) v0.6 (June 11, 2019) diff --git a/examples/ripple_adder_benchmark.ipynb b/examples/ripple_adder_benchmark.ipynb index fd934f08..798907b4 100644 --- a/examples/ripple_adder_benchmark.ipynb +++ b/examples/ripple_adder_benchmark.ipynb @@ -6,13 +6,20 @@ "source": [ "# A simple ripple carry adder on the QPU\n", "\n", - "In this notebook we implement a \"simple\" reversible binary adder. It is based on\n", + "In this notebook use a \"simple\" reversible binary adder to benchmark a quantum computer. The code is contained in the module `classical_logic`.\n", "\n", - "*A new quantum ripple-carry addition circuit*, by \n", - "Cuccaro, Draper, Kutin, and Moulton. See\n", - "https://arxiv.org/abs/quant-ph/0410184v1 .\n", + "The benchmark is simplistic and not very rigorous as it does not test any specific feature of the hardware. Further the whole circuit is classical in the sense that we start and end in computational basis states and all gates simply perform classical not, controlled not (`CNOT`), or doubly controlled not (`CCNOT` aka a [Toffoli gate](https://en.wikipedia.org/wiki/Toffoli_gate)). Finally, even for the modest task of adding two one bit numbers, the `CZ` gate (our fundamental two qubit gate) count is very high for the circuit. This in turn implies a low probablity of the entire circuit working.\n", "\n", - "The whole circuit is classical in the sense that we start and end in computational basis states and all gates simply perform classical not, controlled not, or doublely controled not." + "However it is very easy to explain the performance of hardware to non-experts, e.g. *\"At the moment quantum hardware is pretty noisy, so much so that when we run circuits to add two classical bits it gives the correct answer 40% of the time.\"*\n", + "\n", + "Moreover the simplicity of the benchmark is also its strength. At the bottom of this notebook we provide code for examining the \"error distribution\". When run on hardware we can observe that low weight errors dominate which gives some insight that the hardware is approximately doing the correct thing.\n", + "\n", + "The module `classical_logic` is based on the circuits found in \n", + "\n", + "*A new quantum ripple-carry addition circuit*, \n", + "Cuccaro, Draper, Kutin, and Moulton. \n", + "https://arxiv.org/abs/quant-ph/0410184v1. \n", + "\n" ] }, { @@ -46,7 +53,7 @@ "outputs": [], "source": [ "# noiseless QVM\n", - "qc = get_qc(\"Aspen-1-15Q-A\", as_qvm=True, noisy=False)\n", + "qc = get_qc(\"9q-generic\", as_qvm=True, noisy=False)\n", "\n", "# noisy QVM\n", "noisy_qc = get_qc(\"9q-generic-noisy-qvm\", as_qvm=True, noisy=True)" @@ -68,15 +75,6 @@ "nx.draw(qc.qubit_topology(),with_labels=True)" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "nx.draw(qc.qubit_topology().subgraph([17,10,11,12]),with_labels=True)" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -88,7 +86,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Suppose you want to use Alexa's favorite qubits on Aspen 1 [17,10,11,12] to do one bit addtion. " + "There is a small bit of setup that needs to happen before creating the program for the circuit. Specifically you have to pick registers of qubits for the two input numbers `reg_a` and `reg_b`, a carry bit `c`, and an extra digit `z` that will hold the most significant bit of the answer.\n", + "\n", + "If you have a specific line of qubits in mind for the registers there is a helper `assign_registers_to_line_or_cycle()` which will provide these registers for you--`c` is assigned to the provided start qubit and assignments go down the line in the circuit diagram above; however, you have to provide a subgraph that is sufficiently simple so that the assignment can be done by simpling moving to the next neighbor--e.g. the graph is a line or a cycle. \n", + "\n", + "If you don't care about the particular arrangment of qubits then you can instead use `get_qubit_registers_for_adder()` which will find a suitable assignment for you if one exists." ] }, { @@ -97,19 +99,34 @@ "metadata": {}, "outputs": [], "source": [ + "\n", "# the input numbers\n", "num_a = [1]\n", "num_b = [1]\n", "\n", - "reg_a, reg_b, c, z = assign_registers_to_line_or_cycle(17, qc.qubit_topology().subgraph([17,10,11,12]), len(num_a))\n", + "# There are two easy routes to assign registers\n", + "\n", + "# 1) if you have particular target qubits in mind\n", + "target_qubits = [3,6,7,4,1]\n", + "start = 3\n", + "reg_a, reg_b, c, z = assign_registers_to_line_or_cycle(start, \n", + " qc.qubit_topology().subgraph(target_qubits), \n", + " len(num_a))\n", + "print('Registers c, a, b, z on target qubits', target_qubits,': ', c, reg_a, reg_b, z)\n", + "\n", + "# 2) if you don't care about a particular arrangement\n", + "# you can still exclude qubits. Here we exclude 0.\n", + "reg_a, reg_b, c, z = get_qubit_registers_for_adder(qc, len(num_a), qubits = list(range(1,10)))\n", + "print('Registers c, a, b, z on any qubits excluding q0: ', c, reg_a, reg_b, z)\n", + "\n", "\n", "# given the numbers and registers construct the circuit to add\n", "ckt = adder(num_a, num_b, reg_a, reg_b, c, z)\n", "exe = qc.compile(ckt)\n", "result = qc.run(exe)\n", "\n", - "print('The answer of 1+1 is 10')\n", - "print('The circuit gave: ', result)" + "print('\\nThe answer of 1+1 is 10')\n", + "print('The circuit on an ideal qc gave: ', result)" ] }, { @@ -130,7 +147,8 @@ "01 + 01 = 010 \n", "\n", "where the bits are ordered from most significant to least i.e. (MSB...LSB).\n", - "The MSB is the carry bit.\n" + "\n", + "The MSB is necessary for representing other two bit additions e.g. 2 + 2 = 4 -> 10 + 10 = 100\n" ] }, { @@ -144,12 +162,15 @@ "num_b = [0,1]\n", "\n", "# \n", - "reg_a, reg_b, c, z = assign_registers_to_line_or_cycle(17, qc.qubit_topology(), len(num_a))\n", + "reg_a, reg_b, c, z = get_qubit_registers_for_adder(qc, len(num_a))\n", "\n", "# given the numbers and registers construct the circuit to add\n", "ckt = adder(num_a, num_b, reg_a, reg_b, c, z)\n", "exe = qc.compile(ckt)\n", - "qc.run(exe)" + "result = qc.run(exe)\n", + "\n", + "print('The answer of 01+01 is 010')\n", + "print('The circuit on an ideal qc gave: ', result)" ] }, { @@ -172,7 +193,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Now try 1+1=2 on a noisy qc" + "## Now try 1+1=2 on a noisy qc\n", + "The output is now stochastic--try re-running this cell multiple times!\n", + "Note in particular that the MSB is sometimes (rarely) 1 due to some combination of readout error and error propogation through the CNOT" ] }, { @@ -198,8 +221,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Because binary addition is easy we can caculate the output of the circuit. In order to see how well the QPU excutes the circuit we average the circuit over all possible input strings. Here we look at two bit strings e.g.\n", + "Because classical binary addition is easy we can caculate the ideal output of the circuit. In order to see how well the QPU excutes the circuit we average the circuit over all possible input strings. Here we look at two bit strings e.g.\n", "\n", + "\n", + " \n", "| Register a| Register b| a + b + carry|\n", "|-----------|-----------|--------------|\n", "| 00 | 00 | 000 |\n", @@ -210,15 +235,14 @@ "| $\\vdots$ | $\\vdots$ | $\\vdots$ |\n", "| 11 | 11 | 110 |\n", "\n", - "\n", "The rough measure of goodness is the success probablity, which we define as number of times the QPU correctly returns the string listed in the (a+b+carry) column divided by the total number of trials.\n", "\n", "You might wonder how well you can do just by generating a random binary number and reporting that as the answer.\n", "Well if you are doing addition of two $n$ bit strings the probablity that you can get the correct answer by guessing \n", "\n", - "$\\Pr({\\rm correct}|n)= 1/ 2^{n +1}$,\n", + "$\\Pr({\\rm correct}\\, |\\, n)= 1/ 2^{n +1}$,\n", "\n", - "explicilty $\\Pr({\\rm correct}|1)= 0.25$ and $\\Pr({\\rm correct}|2)= 0.125$.\n", + "explicilty $\\Pr({\\rm correct}\\, |\\, 1)= 0.25$ and $\\Pr({\\rm correct}\\, |\\, 2)= 0.125$.\n", "\n", "A zeroth order performance criterion is to do better than these numbers.\n" ] @@ -242,7 +266,10 @@ "source": [ "# sucess probabilities of different input strings\n", "pr_correct = get_success_probabilities_from_results(results)\n", - "print(pr_correct)" + "\n", + "print('The probablity of getting the correct answer for each output in the above table is:')\n", + "print(np.round(pr_correct, 4),'\\n')\n", + "print('The sucess probality averaged over all inputs is', np.round(np.mean(pr_correct), 5))" ] }, { @@ -251,7 +278,7 @@ "metadata": {}, "outputs": [], "source": [ - "# did we do better than random ?\n", + "# For which outputs did we do better than random ?\n", "np.asarray(pr_correct)> 1/2**(n_bits+1)" ] }, @@ -266,21 +293,25 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Even if the sucess probablity of the circuit is worse than random there might be a way in which the circuit is not absolutely random. That is the computation is actualling doing something. To look for such situations we consider the full distribution of errors.\n", + "Even if the sucess probablity of the circuit is worse than random there might be a way in which the circuit is not absolutely random. This could indicate that the computation is actualling doing something 'close' to what is desired. To look for such situations we consider the full distribution of errors in our outputs.\n", + "\n", + "The output of our circuit is in the computational basis so all errors manifest as bit flips from the actual answer. The number of bits you need to flip to transform one binary string $B_1$ to another binary string $B_2$ is called the Hamming distance. We are interested in the distance ${\\rm dist}(B_t, B_o)$ between the true ideal answer $B_{t}$ and the noisy output answer $B_{o}$, which is equivalent to the Hamming weight ${\\rm wt}(\\cdot) $ of the error in our output. \n", + "\n", + "For example, for various ideal answers and measured outputs for 4 bit addition (remember there's an extra fifth MSB for the answer) we have\n", + "\n", + "${\\rm dist}(00000,00001) = {\\rm wt}(00001) = 1$\n", "\n", - "As an example consider 2-bit addition: 00 + 00 = 000 (including the carry bit).\n", "\n", - "The output of our circuit is in the computational baiss so all errors manifest as bit flips from the actual answer. The number of bit you need to flip to transform one binary string $B_1$ to another binary string $B_2$ is called the Hamming distance or Hamming weight. The Hamming weight is denoted by ${\\rm wt}(B_1,B_2)$. E.g.\n", + "${\\rm dist}(00000,10001) = {\\rm wt}(10001) = 2$\n", "\n", - "${\\rm wt}(00000,00001) = 1$\n", "\n", - "${\\rm wt}(00000,10001) = 2$\n", + "${\\rm dist}(11000,10101) = {\\rm wt}(01101) = 3$\n", "\n", - "${\\rm wt}(00000,10101) = 3$\n", "\n", - "${\\rm wt}(00000,11111) = 5$\n", + "${\\rm dist}(00001,11110) = {\\rm wt}(11111) = 5$\n", "\n", - "In order to see if our near term devices are doing interesting things we calculate the Hamming weight distribution between the answer and data from the QPU. The entry corresponding to zero Hamming weight is the sucess probablity." + "\n", + "In order to see if our near term devices are doing interesting things we calculate the distrubition of the Hamming weight of the errors observed in our QPU data with respect to the known ideal output. The entry corresponding to zero Hamming weight is the sucess probablity." ] }, { @@ -427,7 +458,6 @@ "plt.grid(axis='y', alpha=0.75)\n", "plt.legend(['data','random'])\n", "plt.title('X basis Error Hamming Wt Distr Avgd Over {}-bit Strings'.format(n_bits))\n", - "#name = 'numbits'+str(n_bits) + '_basisX' + '_shots' + str(nshots)\n", "#plt.savefig(name)\n", "plt.show()" ] @@ -484,7 +514,7 @@ "plt.scatter(summand_lengths, rand_n, c='m', marker='D', label='random')\n", "plt.scatter(summand_lengths, min_n, c='r', marker='_', label='min/max')\n", "plt.scatter(summand_lengths, max_n, c='r', marker='_')\n", - "plt.xticks(summand_lengths) #, [str(n_bits) for n_bits in summand_lengths])\n", + "plt.xticks(summand_lengths)\n", "plt.xlabel('Number of bits added n (n+1 including carry bit)')\n", "plt.ylabel('Probablity of working')\n", "plt.legend()\n", @@ -499,20 +529,23 @@ "metadata": {}, "outputs": [], "source": [ - "print('n bit:', summand_lengths)\n", - "print('average:', avg_n)\n", - "print('median:', med_n)\n", - "print('min:', min_n)\n", - "print('max:', max_n)\n", - "print('rand:', rand_n)" + "print('n bit:', np.round(summand_lengths, 5))\n", + "print('average:', np.round(avg_n, 5))\n", + "print('median:', np.round(med_n, 5))\n", + "print('min:', np.round(min_n, 5))\n", + "print('max:', np.round(max_n, 5))\n", + "print('rand:', np.round(rand_n, 5))" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { - "kernelspec": { - "display_name": "", - "name": "" - }, "language_info": { "name": "python", "pygments_lexer": "ipython3" diff --git a/forest/benchmarking/classical_logic/ripple_carry_adder.py b/forest/benchmarking/classical_logic/ripple_carry_adder.py index 62adfd16..f2562e5e 100644 --- a/forest/benchmarking/classical_logic/ripple_carry_adder.py +++ b/forest/benchmarking/classical_logic/ripple_carry_adder.py @@ -108,7 +108,7 @@ def get_qubit_registers_for_adder(qc: QuantumComputer, num_length: int, else: unavailable = [qubit for qubit in qc.qubits() if qubit not in qubits] - graph = qc.qubit_topology() + graph = qc.qubit_topology().copy() for qubit in unavailable: graph.remove_node(qubit) @@ -249,7 +249,7 @@ def adder(num_a: Sequence[int], num_b: Sequence[int], register_a: Sequence[int], def get_n_bit_adder_results(qc: QuantumComputer, n_bits: int, registers: Tuple[Sequence[int], Sequence[int], int, int] = None, qubits: Sequence[int] = None, in_x_basis: bool = False, - num_shots: int = 100, use_param_program: bool = True, + num_shots: int = 100, use_param_program: bool = False, use_active_reset: bool = True) -> Sequence[Sequence[Sequence[int]]]: """ Convenient wrapper for collecting the results of addition for every possible pair of n_bits diff --git a/forest/benchmarking/tests/test_utils.py b/forest/benchmarking/tests/test_utils.py index 4e9f1b66..5c07f04b 100755 --- a/forest/benchmarking/tests/test_utils.py +++ b/forest/benchmarking/tests/test_utils.py @@ -58,3 +58,30 @@ def test_partial_trace(): rho = np.kron(I, I) / 4 np.testing.assert_array_equal(I / 2, partial_trace(rho, [1], [2, 2])) np.testing.assert_array_equal(I / 2, partial_trace(rho, [0], [2, 2])) + + +def test_bitstring_prep(): + # no flips + flip_prog = bitstring_prep([0, 1, 2, 3, 4, 5], [0, 0, 0, 0, 0, 0]) + assert flip_prog.out().splitlines() == ['RX(0) 0', + 'RX(0) 1', + 'RX(0) 2', + 'RX(0) 3', + 'RX(0) 4', + 'RX(0) 5'] + # mixed flips + flip_prog = bitstring_prep([0, 1, 2, 3, 4, 5], [1, 1, 0, 1, 0, 1]) + assert flip_prog.out().splitlines() == ['RX(pi) 0', + 'RX(pi) 1', + 'RX(0) 2', + 'RX(pi) 3', + 'RX(0) 4', + 'RX(pi) 5'] + # flip all + flip_prog = bitstring_prep([0, 1, 2, 3, 4, 5], [1, 1, 1, 1, 1, 1]) + assert flip_prog.out().splitlines() == ['RX(pi) 0', + 'RX(pi) 1', + 'RX(pi) 2', + 'RX(pi) 3', + 'RX(pi) 4', + 'RX(pi) 5']