diff --git a/.github/workflows/quality_checks.yml b/.github/workflows/quality_checks.yml index 8658702..caea630 100644 --- a/.github/workflows/quality_checks.yml +++ b/.github/workflows/quality_checks.yml @@ -7,34 +7,31 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-versions: ["3.8", "3.9", "3.10"] + python-versions: ["3.11"] steps: - - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-region: eu-west-2 - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: - python-version: ${{ matrix.python-version }} + python-version: 3.11 - name: Install dependencies and the package run: | python -m pip install --upgrade pip - pip install .[test] + pip install -v .[test] - name: Run unit tests - run: pytest --cov=qbench --cov-report=xml + run: pytest --cov=qbench --cov-report=xml -k 'not awscreds and not aersim' + env: + QISKIT_IBM_TOKEN: ${{ secrets.QISKIT_IBM_TOKEN }} - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 - run_quality_cheks: + run_quality_checks: runs-on: ubuntu-latest steps: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: 3.11 - uses: actions/checkout@v2 - name: Install dependencies and the package run: | diff --git a/.gitignore b/.gitignore index 8487c95..9199625 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,12 @@ dmypy.json # PyCharm files .idea/** + +# Generated files +res.yml +resolved.yml +results.yml +resolved.csv +results.csv +*.yml +*.csv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ad3343c..b80b9a0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ default_language_version: - python: python3.9 + python: python3.11 default_stages: [commit, push] @@ -15,8 +15,7 @@ repos: hooks: - id: black args: ["--check"] - language_version: python3.9 - + language_version: python3.11 - repo: https://github.com/pycqa/flake8 rev: 6.0.0 hooks: diff --git a/Hadamard example.ipynb b/Hadamard example.ipynb deleted file mode 100644 index bfc2e49..0000000 --- a/Hadamard example.ipynb +++ /dev/null @@ -1,196 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 39, - "id": "3d91d2c9", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit import QuantumCircuit, Aer\n", - "import numpy as np\n", - "\n", - "\n", - "from qbench.schemes.postselection import benchmark_using_postselection\n", - "from qbench.schemes.direct_sum import benchmark_using_controlled_unitary" - ] - }, - { - "cell_type": "code", - "execution_count": 53, - "id": "0059be26", - "metadata": {}, - "outputs": [], - "source": [ - "def state_prep():\n", - " circuit = QuantumCircuit(2)\n", - " circuit.h(0)\n", - " circuit.cnot(0, 1)\n", - " return circuit.to_instruction()\n", - " \n", - "\n", - "def u_dag():\n", - " circuit = QuantumCircuit(1)\n", - " circuit.h(0)\n", - " return circuit.to_instruction()\n", - "\n", - "def v0_dag():\n", - " circuit = QuantumCircuit(1)\n", - " circuit.ry(-np.pi * 3 / 4, 0)\n", - " return circuit.to_instruction()\n", - "\n", - "def v1_dag():\n", - " circuit = QuantumCircuit(1)\n", - " circuit.ry(-np.pi * 3 / 4, 0)\n", - " circuit.x(0)\n", - " return circuit.to_instruction()\n", - "\n", - "def v0_v1_direct_sum_dag():\n", - " circuit = QuantumCircuit(2)\n", - " circuit.ry(-np.pi * 3 / 4, 0)\n", - " circuit.cnot(0, 1)\n", - " return circuit.to_instruction()" - ] - }, - { - "cell_type": "code", - "execution_count": 54, - "id": "455e4997", - "metadata": {}, - "outputs": [], - "source": [ - "simulator = Aer.get_backend(\"aer_simulator\")" - ] - }, - { - "cell_type": "code", - "execution_count": 55, - "id": "b7f6030a", - "metadata": {}, - "outputs": [], - "source": [ - "result = benchmark_using_postselection(\n", - " backend=simulator,\n", - " target=0,\n", - " ancilla=1,\n", - " state_preparation=state_prep(),\n", - " u_dag=u_dag(),\n", - " v0_dag=v0_dag(),\n", - " v1_dag=v1_dag(),\n", - " num_shots_per_measurement=100000\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 56, - "id": "75bbbaa6", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0.8537402864843542" - ] - }, - "execution_count": 56, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "result" - ] - }, - { - "cell_type": "code", - "execution_count": 57, - "id": "bfe0ba4b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0.8535533905932737" - ] - }, - "execution_count": 57, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "expected = 0.5 + 0.25 * np.sqrt(2)\n", - "expected" - ] - }, - { - "cell_type": "code", - "execution_count": 64, - "id": "99acaa62", - "metadata": {}, - "outputs": [], - "source": [ - "result = benchmark_using_controlled_unitary(\n", - " backend=simulator,\n", - " target=0,\n", - " ancilla=1,\n", - " state_preparation=state_prep(),\n", - " u_dag=u_dag(),\n", - " v0_v1_direct_sum_dag=v0_v1_dag(),\n", - " num_shots_per_measurement=100000\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 65, - "id": "7df4e8a6", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0.854825" - ] - }, - "execution_count": 65, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "result" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "49036e31", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.15" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/Hadamard_certification_experiment.ipynb b/Hadamard_certification_experiment.ipynb new file mode 100644 index 0000000..c87d9a3 --- /dev/null +++ b/Hadamard_certification_experiment.ipynb @@ -0,0 +1,373 @@ +{ + "cells": [ + { + "cell_type": "code", + "id": "ef1f4c7d", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-18T02:12:18.460202Z", + "start_time": "2025-01-18T02:12:18.456108Z" + } + }, + "source": [ + "from qiskit import QuantumCircuit\n", + "import numpy as np\n", + "from qiskit_aer import StatevectorSimulator\n", + "from qiskit_aer.primitives import SamplerV2\n", + "from qbench.schemes.postselection import benchmark_certification_using_postselection\n", + "from qbench.schemes.direct_sum import benchmark_certification_using_direct_sum" + ], + "outputs": [], + "execution_count": 200 + }, + { + "cell_type": "code", + "id": "0059be26", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-18T02:12:18.483421Z", + "start_time": "2025-01-18T02:12:18.475064Z" + } + }, + "source": [ + "### certification experiment for measurement in Hadamard basis using postselection and direct sum ###\n", + "\n", + "def state_prep():\n", + " circuit = QuantumCircuit(2)\n", + " circuit.h(0)\n", + " circuit.cx(0,1)\n", + " return circuit.to_instruction()\n", + " \n", + "def u_dag():\n", + " circuit = QuantumCircuit(1)\n", + " circuit.h(0)\n", + " return circuit.to_instruction()\n", + "\n", + "def v0_dag():\n", + " circuit = QuantumCircuit(1)\n", + " circuit.ry(2 * np.arcsin(np.sqrt(0.05)), 0)\n", + " \n", + " return circuit.to_instruction()\n", + "\n", + "def v1_dag():\n", + " circuit = QuantumCircuit(1)\n", + " circuit.ry(2 * np.arcsin(np.sqrt(0.05)), 0)\n", + " circuit.x(0)\n", + " return circuit.to_instruction()\n", + "\n", + "def v0_v1_direct_sum_dag():\n", + " circuit = QuantumCircuit(2)\n", + " circuit.p(-np.pi, 0)\n", + " circuit.ry(-2 * np.arcsin(np.sqrt(0.05)), 0)\n", + " circuit.cx(0, 1)\n", + " return circuit.to_instruction()" + ], + "outputs": [], + "execution_count": 201 + }, + { + "cell_type": "code", + "id": "455e4997", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-18T02:12:18.490176Z", + "start_time": "2025-01-18T02:12:18.487554Z" + } + }, + "source": "simulator = StatevectorSimulator()", + "outputs": [], + "execution_count": 202 + }, + { + "cell_type": "code", + "id": "b7f6030a", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-18T02:12:18.601348Z", + "start_time": "2025-01-18T02:12:18.491791Z" + } + }, + "source": [ + "postselection_result = benchmark_certification_using_postselection(\n", + " backend=simulator,\n", + " target=0,\n", + " ancilla=1,\n", + " state_preparation=state_prep(),\n", + " u_dag=u_dag(),\n", + " v0_dag=v0_dag(),\n", + " v1_dag=v1_dag(),\n", + " num_shots_per_measurement=10\n", + ")\n", + "\n", + "postselection_result" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "0.7142857142857143" + ] + }, + "execution_count": 203, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 203 + }, + { + "cell_type": "code", + "id": "bfe0ba4b", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-18T02:12:18.606161Z", + "start_time": "2025-01-18T02:12:18.602290Z" + } + }, + "source": [ + "expected = (1/np.sqrt(2) * np.sqrt(0.95) - 1/np.sqrt(2) * np.sqrt(0.05))**2\n", + "expected" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "0.2820550528229661" + ] + }, + "execution_count": 204, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 204 + }, + { + "cell_type": "code", + "id": "99acaa62", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-18T02:12:19.143129Z", + "start_time": "2025-01-18T02:12:18.608293Z" + } + }, + "source": [ + "direct_sum_result = benchmark_certification_using_direct_sum(\n", + " backend=simulator,\n", + " target=0,\n", + " ancilla=1,\n", + " state_preparation=state_prep(),\n", + " u_dag=u_dag(),\n", + " v0_v1_direct_sum_dag=v0_v1_direct_sum_dag(),\n", + " num_shots_per_measurement=100000\n", + ")\n", + "\n", + "direct_sum_result" + ], + "outputs": [ + { + "data": { + "text/plain": [ + "0.27924" + ] + }, + "execution_count": 205, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 205 + }, + { + "cell_type": "code", + "id": "a837c34e-b8d7-4df4-9c58-5ba1cf16f8e7", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-18T02:12:19.146922Z", + "start_time": "2025-01-18T02:12:19.144223Z" + } + }, + "source": [ + "p_succ = expected\n", + "print(f\"Analytical p_succ = {p_succ}\")\n", + "print(\n", + "f\"Postselection: p_succ = {postselection_result}, abs_error ={np.abs(p_succ - postselection_result)}\"\n", + ")\n", + "print(f\"Direct sum: p_succ = {direct_sum_result}, abs_error ={np.abs(p_succ - direct_sum_result)}\")" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Analytical p_succ = 0.2820550528229661\n", + "Postselection: p_succ = 0.7142857142857143, abs_error =0.4322306614627482\n", + "Direct sum: p_succ = 0.27924, abs_error =0.0028150528229661242\n" + ] + } + ], + "execution_count": 206 + }, + { + "cell_type": "code", + "id": "ce25f7a4-1533-4252-a932-bbafbea78156", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-18T02:12:19.162830Z", + "start_time": "2025-01-18T02:12:19.147920Z" + } + }, + "source": [ + "from qbench.schemes.postselection import (\n", + " assemble_circuits_discrimination_postselection,\n", + " compute_probabilities_certification_postselection,\n", + ")\n", + "circuits = assemble_circuits_discrimination_postselection(\n", + "target=0,\n", + "ancilla=1,\n", + "state_preparation=state_prep(),\n", + "u_dag=u_dag(),\n", + "v0_dag=v0_dag(),\n", + "v1_dag=v1_dag(),\n", + ")" + ], + "outputs": [], + "execution_count": 207 + }, + { + "cell_type": "code", + "id": "9e3f5bba-d2ab-4904-b46b-e6b05bd33b86", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-18T02:12:19.167982Z", + "start_time": "2025-01-18T02:12:19.164103Z" + } + }, + "source": [ + "from qiskit_aer.noise import NoiseModel, ReadoutError\n", + "error = ReadoutError([[0.75, 0.25], [0.8, 0.2]])\n", + "noise_model = NoiseModel()\n", + "noise_model.add_readout_error(error, [0])\n", + "noise_model.add_readout_error(error, [1])" + ], + "outputs": [], + "execution_count": 208 + }, + { + "cell_type": "code", + "id": "1a63f475-b565-4983-a947-62f3858731c8", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-18T02:12:19.682991Z", + "start_time": "2025-01-18T02:12:19.169272Z" + } + }, + "source": [ + "keys_ordering = [\"u_v0\", \"u_v1\"]\n", + "\n", + "all_circuits = [circuits[key] for key in keys_ordering]\n", + "\n", + "sampler_noiseless = SamplerV2()\n", + "sampler_noisy = SamplerV2(options=dict(backend_options=dict(noise_model=noise_model)))\n", + "\n", + "counts_noisy = [sampler_noisy.run(\n", + " [circ],\n", + " shots=100000).result()[0].data.meas.get_counts() for circ in all_circuits]\n", + "\n", + "counts_noiseless = [sampler_noiseless.run(\n", + " [circ],\n", + " shots=100000).result()[0].data.meas.get_counts() for circ in all_circuits]" + ], + "outputs": [], + "execution_count": 209 + }, + { + "cell_type": "code", + "id": "d2f9a183-9d6b-4c07-bb5f-f309f7ad7859", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-18T02:12:19.685729Z", + "start_time": "2025-01-18T02:12:19.683634Z" + } + }, + "source": [ + "prob_succ_noiseless = compute_probabilities_certification_postselection(\n", + " u_v0_counts=counts_noiseless[0],\n", + " u_v1_counts=counts_noiseless[1],)\n", + "\n", + "prob_succ_noisy = compute_probabilities_certification_postselection(\n", + " u_v0_counts=counts_noisy[0],\n", + " u_v1_counts=counts_noisy[1],)" + ], + "outputs": [], + "execution_count": 210 + }, + { + "cell_type": "code", + "id": "f949b697-7591-48a9-bcb2-c2c006453064", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-18T02:12:19.690105Z", + "start_time": "2025-01-18T02:12:19.686771Z" + } + }, + "source": [ + "print(prob_succ_noiseless)" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.2826338159785372\n" + ] + } + ], + "execution_count": 211 + }, + { + "cell_type": "code", + "id": "145dba65-0497-4643-8dd0-e3af180921eb", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-18T02:12:19.695040Z", + "start_time": "2025-01-18T02:12:19.690617Z" + } + }, + "source": [ + "print(prob_succ_noisy)" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.7728359732242023\n" + ] + } + ], + "execution_count": 212 + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Hadamard_discrimination_experiment.ipynb b/Hadamard_discrimination_experiment.ipynb new file mode 100644 index 0000000..6878ddd --- /dev/null +++ b/Hadamard_discrimination_experiment.ipynb @@ -0,0 +1,304 @@ +{ + "cells": [ + { + "cell_type": "code", + "id": "901676ea-58e2-495c-89bd-40c7e4e239fb", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-18T02:14:04.697744Z", + "start_time": "2025-01-18T02:14:04.693263Z" + } + }, + "source": [ + "from qiskit import QuantumCircuit\n", + "import numpy as np\n", + "from qiskit_aer.primitives import SamplerV2\n", + "\n", + "from qbench.schemes.postselection import benchmark_discrimination_using_postselection\n", + "from qbench.schemes.direct_sum import benchmark_discrimination_using_direct_sum" + ], + "outputs": [], + "execution_count": 10 + }, + { + "cell_type": "code", + "id": "70c82def-c313-4968-8999-fd6f562cbb7a", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-18T02:14:04.715875Z", + "start_time": "2025-01-18T02:14:04.712064Z" + } + }, + "source": [ + "from qiskit_aer import StatevectorSimulator\n", + "\n", + "simulator = StatevectorSimulator()" + ], + "outputs": [], + "execution_count": 11 + }, + { + "cell_type": "code", + "id": "e3ce562e-5bb4-4cf3-ab92-e02c4baab7ce", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-18T02:14:04.736169Z", + "start_time": "2025-01-18T02:14:04.731399Z" + } + }, + "source": [ + "### discrimination experiment for measurement in Hadamard basis using postselection and direct sum###\n", + "\n", + "def state_prep():\n", + " circuit = QuantumCircuit(2)\n", + " circuit.h(0)\n", + " circuit.cx(0, 1)\n", + " return circuit.to_instruction()\n", + "\n", + "def u_dag():\n", + " circuit = QuantumCircuit(1)\n", + " circuit.h(0)\n", + " return circuit.to_instruction()\n", + "\n", + "def v0_dag():\n", + " circuit = QuantumCircuit(1)\n", + " circuit.ry(-np.pi * 3 / 4, 0)\n", + " return circuit.to_instruction()\n", + " \n", + "def v1_dag():\n", + " circuit = QuantumCircuit(1)\n", + " circuit.ry(-np.pi * 3 / 4, 0)\n", + " circuit.x(0)\n", + " return circuit.to_instruction()\n", + " \n", + "def v0_v1_direct_sum_dag():\n", + " circuit = QuantumCircuit(2)\n", + " circuit.ry(-np.pi * 3 / 4, 0)\n", + " circuit.cx(0, 1)\n", + " return circuit.to_instruction()" + ], + "outputs": [], + "execution_count": 12 + }, + { + "cell_type": "code", + "id": "939f2579-0b6d-4430-8c7e-37aaece4b47a", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-18T02:14:06.729272Z", + "start_time": "2025-01-18T02:14:04.737353Z" + } + }, + "source": [ + "discrimination_postselection_result = benchmark_discrimination_using_postselection(\n", + " backend=simulator,\n", + " target=0,\n", + " ancilla=1,\n", + " state_preparation=state_prep(),\n", + " u_dag=u_dag(),\n", + " v0_dag=v0_dag(),\n", + " v1_dag=v1_dag(),\n", + " num_shots_per_measurement=100000,)" + ], + "outputs": [], + "execution_count": 13 + }, + { + "cell_type": "code", + "id": "6bfde52e-855a-42b0-95a9-95b40aa41ef5", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-18T02:14:07.698378Z", + "start_time": "2025-01-18T02:14:06.730231Z" + } + }, + "source": [ + "discrimination_direct_sum_result = benchmark_discrimination_using_direct_sum(\n", + " backend=simulator,\n", + " target=1,\n", + " ancilla=2,\n", + " state_preparation=state_prep(),\n", + " u_dag=u_dag(),\n", + " v0_v1_direct_sum_dag=v0_v1_direct_sum_dag(),\n", + " num_shots_per_measurement=100000,)" + ], + "outputs": [], + "execution_count": 14 + }, + { + "cell_type": "code", + "id": "325d3e88-5b6c-46d4-9dff-58750f159a3a", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-18T02:14:07.702315Z", + "start_time": "2025-01-18T02:14:07.699679Z" + } + }, + "source": [ + "p_succ = (2 + np.sqrt(2)) / 4\n", + "print(f\"Analytical p_succ = {p_succ}\")\n", + "print(f\"Postselection: p_succ = {discrimination_postselection_result}, abs.error ={np.abs(p_succ - discrimination_postselection_result)}\")\n", + "print(f\"Direct sum: p_succ = {discrimination_direct_sum_result}, abs.error ={np.abs(p_succ - discrimination_direct_sum_result)}\")" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Analytical p_succ = 0.8535533905932737\n", + "Postselection: p_succ = 0.8549341565062727, abs.error =0.0013807659129989602\n", + "Direct sum: p_succ = 0.852805, abs.error =0.0007483905932736956\n" + ] + } + ], + "execution_count": 15 + }, + { + "cell_type": "code", + "id": "f24a2681-db8f-42af-890f-0d4201bec4d0", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-18T02:14:07.718969Z", + "start_time": "2025-01-18T02:14:07.703509Z" + } + }, + "source": [ + "from qbench.schemes.postselection import (\n", + " assemble_circuits_discrimination_postselection,\n", + " compute_probabilities_discrimination_postselection,)\n", + "\n", + "circuits = assemble_circuits_discrimination_postselection(\n", + " target=0,\n", + " ancilla=1,\n", + " state_preparation=state_prep(),\n", + " u_dag=u_dag(),\n", + " v0_dag=v0_dag(),\n", + " v1_dag=v1_dag(),)" + ], + "outputs": [], + "execution_count": 16 + }, + { + "cell_type": "code", + "id": "a4d56672-4ec9-406e-84d8-8038df8057fc", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-18T02:14:07.724311Z", + "start_time": "2025-01-18T02:14:07.719580Z" + } + }, + "source": [ + "from qiskit_aer.noise import NoiseModel, ReadoutError\n", + "\n", + "error = ReadoutError([[0.75, 0.25], [0.8, 0.2]])\n", + "noise_model = NoiseModel()\n", + "noise_model.add_readout_error(error, [0])\n", + "noise_model.add_readout_error(error, [1])\n" + ], + "outputs": [], + "execution_count": 17 + }, + { + "cell_type": "code", + "id": "ea5ffbfb-d2f0-4007-a49b-44de3aee9ef3", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-18T02:14:08.149808Z", + "start_time": "2025-01-18T02:14:07.725361Z" + } + }, + "source": [ + "keys_ordering = [\"id_v0\", \"id_v1\", \"u_v0\", \"u_v1\"]\n", + "\n", + "all_circuits = [circuits[key] for key in keys_ordering]\n", + "\n", + "sampler_noiseless = SamplerV2()\n", + "sampler_noisy = SamplerV2(options=dict(backend_options=dict(noise_model=noise_model)))\n", + "\n", + "counts_noisy = [sampler_noisy.run(\n", + " [circ],\n", + " shots=10000).result()[0].data.meas.get_counts() for circ in all_circuits]\n", + "\n", + "counts_noiseless = [sampler_noiseless.run(\n", + " [circ],\n", + " shots=10000).result()[0].data.meas.get_counts() for circ in all_circuits]" + ], + "outputs": [], + "execution_count": 18 + }, + { + "cell_type": "code", + "id": "5859c0fd-440d-4998-8b9f-dd325c09bcbe", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-18T02:14:08.154580Z", + "start_time": "2025-01-18T02:14:08.150942Z" + } + }, + "source": [ + "prob_succ_noiseless = compute_probabilities_discrimination_postselection(\n", + " id_v0_counts=counts_noiseless[0],\n", + " id_v1_counts=counts_noiseless[1],\n", + " u_v0_counts=counts_noiseless[2],\n", + " u_v1_counts=counts_noiseless[3],)\n", + "\n", + "prob_succ_noisy = compute_probabilities_discrimination_postselection(\n", + " id_v0_counts=counts_noisy[0],\n", + " id_v1_counts=counts_noisy[1],\n", + " u_v0_counts=counts_noisy[2],\n", + " u_v1_counts=counts_noisy[3],)" + ], + "outputs": [], + "execution_count": 19 + }, + { + "cell_type": "code", + "id": "09f57db2-f29d-4eba-ad57-a3cfad62142c", + "metadata": { + "ExecuteTime": { + "end_time": "2025-01-18T02:14:08.160687Z", + "start_time": "2025-01-18T02:14:08.156436Z" + } + }, + "source": [ + "p_succ = (2 + np.sqrt(2)) / 4\n", + "print(f\"Analytical p_succ = {p_succ}\")\n", + "print(f\"Prob_succ_noiseless {prob_succ_noiseless}, abs.error ={np.abs(p_succ - prob_succ_noiseless)}\")\n", + "print(f\"Prob_succ_noisy = {prob_succ_noisy}, abs.error ={np.abs(p_succ - prob_succ_noisy)}\")" + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Analytical p_succ = 0.8535533905932737\n", + "Prob_succ_noiseless 0.8572140850212383, abs.error =0.0036606944279645726\n", + "Prob_succ_noisy = 0.5044143159173481, abs.error =0.34913907467592564\n" + ] + } + ], + "execution_count": 20 + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/README.md b/README.md index 8335282..6eac840 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ ![Logo](https://raw.githubusercontent.com/iitis/pyqbench/master/docs/source/_static/logo.png) - ![GitHub](https://img.shields.io/github/license/iitis/PyQBench) ![PyPI](https://img.shields.io/pypi/v/pyqbench) ![Read the Docs](https://img.shields.io/readthedocs/pyqbench) diff --git a/docs/source/notebooks/Example 01 discriminating measurements in Hadamard basis.ipynb b/docs/source/notebooks/Example 01 discriminating measurements in Hadamard basis.ipynb index 0c893be..5a0c0b4 100644 --- a/docs/source/notebooks/Example 01 discriminating measurements in Hadamard basis.ipynb +++ b/docs/source/notebooks/Example 01 discriminating measurements in Hadamard basis.ipynb @@ -162,11 +162,11 @@ "execution_count": 1, "id": "29821678", "metadata": {}, - "outputs": [], "source": [ "from qiskit import QuantumCircuit, Aer\n", "import numpy as np" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -182,11 +182,11 @@ "execution_count": 2, "id": "2ab223c7", "metadata": {}, - "outputs": [], "source": [ "from qbench.schemes.postselection import benchmark_using_postselection\n", "from qbench.schemes.direct_sum import benchmark_using_direct_sum" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -213,7 +213,6 @@ "execution_count": 4, "id": "505e63c7", "metadata": {}, - "outputs": [], "source": [ "def state_prep():\n", " circuit = QuantumCircuit(2)\n", @@ -246,7 +245,8 @@ " circuit.ry(-np.pi * 3 / 4, 0)\n", " circuit.cnot(0, 1)\n", " return circuit.to_instruction()" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -273,10 +273,10 @@ "execution_count": 5, "id": "e34964ef", "metadata": {}, - "outputs": [], "source": [ "simulator = Aer.get_backend(\"aer_simulator\")" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -292,7 +292,6 @@ "execution_count": 6, "id": "4d2c3703", "metadata": {}, - "outputs": [], "source": [ "postselection_result = benchmark_using_postselection(\n", " backend=simulator,\n", @@ -304,14 +303,14 @@ " v1_dag=v1_dag(),\n", " num_shots_per_measurement=10000,\n", ")" - ] + ], + "outputs": [] }, { "cell_type": "code", "execution_count": 8, "id": "aa590bef", "metadata": {}, - "outputs": [], "source": [ "direct_sum_result = benchmark_using_direct_sum(\n", " backend=simulator,\n", @@ -322,24 +321,14 @@ " v0_v1_direct_sum_dag=v0_v1_direct_sum_dag(),\n", " num_shots_per_measurement=10000,\n", ")" - ] + ], + "outputs": [] }, { "cell_type": "code", "execution_count": 12, "id": "3a5c109f", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Real p_succ = 0.8535533905932737\n", - "Postselection: p_succ = 0.8542241847738258, abs. error = -0.0006707941805520479\n", - "Direct sum: p_succ = 0.8536, abs. error = -4.6609406726294544e-05\n" - ] - } - ], "source": [ "p_succ = (2 + np.sqrt(2)) / 4\n", "print(f\"Real p_succ = {p_succ}\")\n", @@ -347,7 +336,8 @@ " f\"Postselection: p_succ = {postselection_result}, abs. error = {p_succ - postselection_result}\"\n", ")\n", "print(f\"Direct sum: p_succ = {direct_sum_result}, abs. error = {p_succ - direct_sum_result}\")" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -376,10 +366,10 @@ "remove-cell" ] }, - "outputs": [], "source": [ "import iplantuml" - ] + ], + "outputs": [] }, { "cell_type": "code", @@ -391,28 +381,6 @@ "remove-stdout" ] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Writing output for /home/dexter/Projects/iitis/PyQBench/docs/source/notebooks/cee7e7f7-817f-4e86-a60b-d0d1bf48419f.uml to cee7e7f7-817f-4e86-a60b-d0d1bf48419f.svg\n" - ] - }, - { - "data": { - "image/svg+xml": [ - "Simplified benchmarkingUserUserPyQBenchPyQBenchBackendBackendpasses circuit components,backend and number of shotsassembles the circuitssubmits circuits to be executedreturns measureentscompute probabilityreturn probability of success" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "%%plantuml\n", "@startuml\n", @@ -429,7 +397,8 @@ "PyQBench --> PyQBench: compute probability\n", "PyQBench --> User: return probability of success\n", "@enduml\n" - ] + ], + "outputs": [] }, { "cell_type": "code", @@ -441,28 +410,6 @@ "remove-stdout" ] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Writing output for /home/dexter/Projects/iitis/PyQBench/docs/source/notebooks/3dee3601-2949-4408-a48e-8a3ab89072da.uml to 3dee3601-2949-4408-a48e-8a3ab89072da.svg\n" - ] - }, - { - "data": { - "image/svg+xml": [ - "Execution of circuits controlled by userUserUserPyQBenchPyQBenchBackendBackendpasses circuit components and qubit indicesreturns assembled circuitssubmits circuits to be executedreturns raw measurementspassess measurementsreturns computed probability" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 28, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "%%plantuml\n", "@startuml\n", @@ -479,7 +426,8 @@ "User --> PyQBench: passess measurements\n", "PyQBench --> User: returns computed probability\n", "@enduml\n" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -495,34 +443,19 @@ "execution_count": 30, "id": "b4c6daf9", "metadata": {}, - "outputs": [], "source": [ "from qbench.schemes.postselection import (\n", " assemble_postselection_circuits,\n", " compute_probabilities_from_postselection_measurements,\n", ")" - ] + ], + "outputs": [] }, { "cell_type": "code", "execution_count": 33, "id": "ba50b4a3", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'id_v0': ,\n", - " 'id_v1': ,\n", - " 'u_v0': ,\n", - " 'u_v1': }" - ] - }, - "execution_count": 33, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "circuits = assemble_postselection_circuits(\n", " target=0,\n", @@ -534,7 +467,8 @@ ")\n", "\n", "circuits" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -555,18 +489,6 @@ "execution_count": 85, "id": "39d0ac4f", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 85, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "from qiskit.providers.aer import noise\n", "\n", @@ -577,7 +499,8 @@ "noise_model.add_readout_error(error, [1])\n", "\n", "noise_model" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -596,16 +519,6 @@ "execution_count": 82, "id": "c265e41d", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Noisless counts: [{'11': 734, '01': 4231, '00': 724, '10': 4311}, {'01': 716, '10': 737, '00': 4238, '11': 4309}, {'01': 749, '10': 697, '00': 4361, '11': 4193}, {'01': 4197, '11': 742, '00': 736, '10': 4325}]\n", - "Noisy counts: [{'11': 464, '01': 1749, '10': 1741, '00': 6046}, {'11': 493, '10': 1729, '00': 5971, '01': 1807}, {'11': 524, '00': 5965, '10': 1734, '01': 1777}, {'11': 472, '01': 1700, '10': 1749, '00': 6079}]\n" - ] - } - ], "source": [ "from qiskit import execute\n", "\n", @@ -623,7 +536,8 @@ "\n", "print(f\"Noisless counts: {counts_noiseless}\")\n", "print(f\"Noisy counts: {counts_noisy}\")" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -639,15 +553,6 @@ "execution_count": 88, "id": "633ca70a", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0.856421162176477\n" - ] - } - ], "source": [ "prob_succ_noiseless = compute_probabilities_from_postselection_measurements(\n", " id_v0_counts=counts_noiseless[0],\n", @@ -657,22 +562,14 @@ ")\n", "\n", "print(prob_succ_noiseless)" - ] + ], + "outputs": [] }, { "cell_type": "code", "execution_count": 89, "id": "36157e67", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0.4988475737326284\n" - ] - } - ], "source": [ "prob_succ_noisy = compute_probabilities_from_postselection_measurements(\n", " id_v0_counts=counts_noisy[0],\n", @@ -682,7 +579,8 @@ ")\n", "\n", "print(prob_succ_noisy)" - ] + ], + "outputs": [] }, { "cell_type": "markdown", @@ -699,8 +597,8 @@ "execution_count": null, "id": "7e295d78", "metadata": {}, - "outputs": [], - "source": [] + "source": [], + "outputs": [] } ], "metadata": { diff --git a/environment.yml b/environment.yml new file mode 100644 index 0000000..b75c756 --- /dev/null +++ b/environment.yml @@ -0,0 +1,223 @@ +name: PyQBench +channels: + - defaults + - https://repo.anaconda.com/pkgs/main + - https://repo.anaconda.com/pkgs/r +dependencies: + - _libgcc_mutex=0.1=main + - _openmp_mutex=5.1=1_gnu + - anyio=4.2.0=py311h06a4308_0 + - argon2-cffi=21.3.0=pyhd3eb1b0_0 + - argon2-cffi-bindings=21.2.0=py311h5eee18b_0 + - asttokens=2.0.5=pyhd3eb1b0_0 + - async-lru=2.0.4=py311h06a4308_0 + - attrs=23.1.0=py311h06a4308_0 + - babel=2.11.0=py311h06a4308_0 + - beautifulsoup4=4.12.3=py311h06a4308_0 + - bleach=4.1.0=pyhd3eb1b0_0 + - brotli-python=1.0.9=py311h6a678d5_8 + - bzip2=1.0.8=h5eee18b_6 + - ca-certificates=2024.7.2=h06a4308_0 + - cffi=1.16.0=py311h5eee18b_1 + - charset-normalizer=3.3.2=pyhd3eb1b0_0 + - comm=0.2.1=py311h06a4308_0 + - cyrus-sasl=2.1.28=h52b45da_1 + - dbus=1.13.18=hb2f20db_0 + - debugpy=1.6.7=py311h6a678d5_0 + - decorator=5.1.1=pyhd3eb1b0_0 + - defusedxml=0.7.1=pyhd3eb1b0_0 + - executing=0.8.3=pyhd3eb1b0_0 + - expat=2.6.3=h6a678d5_0 + - fontconfig=2.14.1=h55d465d_3 + - freetype=2.12.1=h4a9f257_0 + - glib=2.78.4=h6a678d5_0 + - glib-tools=2.78.4=h6a678d5_0 + - gst-plugins-base=1.14.1=h6a678d5_1 + - gstreamer=1.14.1=h5eee18b_1 + - h11=0.14.0=py311h06a4308_0 + - httpcore=1.0.2=py311h06a4308_0 + - httpx=0.27.0=py311h06a4308_0 + - icu=73.1=h6a678d5_0 + - idna=3.7=py311h06a4308_0 + - ipykernel=6.28.0=py311h06a4308_0 + - ipython=8.27.0=py311h06a4308_0 + - ipywidgets=8.1.2=py311h06a4308_0 + - jedi=0.19.1=py311h06a4308_0 + - jinja2=3.1.4=py311h06a4308_0 + - jpeg=9e=h5eee18b_3 + - json5=0.9.6=pyhd3eb1b0_0 + - jsonschema=4.19.2=py311h06a4308_0 + - jsonschema-specifications=2023.7.1=py311h06a4308_0 + - jupyter=1.0.0=py311h06a4308_9 + - jupyter-lsp=2.2.0=py311h06a4308_0 + - jupyter_client=8.6.0=py311h06a4308_0 + - jupyter_console=6.6.3=py311h06a4308_0 + - jupyter_core=5.7.2=py311h06a4308_0 + - jupyter_events=0.10.0=py311h06a4308_0 + - jupyter_server=2.14.1=py311h06a4308_0 + - jupyter_server_terminals=0.4.4=py311h06a4308_1 + - jupyterlab=4.2.5=py311h06a4308_0 + - jupyterlab_pygments=0.1.2=py_0 + - jupyterlab_server=2.27.3=py311h06a4308_0 + - jupyterlab_widgets=3.0.10=py311h06a4308_0 + - krb5=1.20.1=h143b758_1 + - ld_impl_linux-64=2.38=h1181459_1 + - libclang=14.0.6=default_hc6dbbc7_1 + - libclang13=14.0.6=default_he11475f_1 + - libcups=2.4.2=h2d74bed_1 + - libedit=3.1.20230828=h5eee18b_0 + - libffi=3.4.4=h6a678d5_1 + - libgcc-ng=11.2.0=h1234567_1 + - libglib=2.78.4=hdc74915_0 + - libgomp=11.2.0=h1234567_1 + - libiconv=1.16=h5eee18b_3 + - libllvm14=14.0.6=hecde1de_4 + - libpng=1.6.39=h5eee18b_0 + - libpq=12.17=hdbd6064_0 + - libsodium=1.0.18=h7b6447c_0 + - libstdcxx-ng=11.2.0=h1234567_1 + - libuuid=1.41.5=h5eee18b_0 + - libxcb=1.15=h7f8727e_0 + - libxkbcommon=1.0.1=h097e994_2 + - libxml2=2.13.1=hfdd30dd_2 + - lz4-c=1.9.4=h6a678d5_1 + - markupsafe=2.1.3=py311h5eee18b_0 + - matplotlib-inline=0.1.6=py311h06a4308_0 + - mistune=2.0.4=py311h06a4308_0 + - mysql=5.7.24=h721c034_2 + - nbclient=0.8.0=py311h06a4308_0 + - nbconvert=7.10.0=py311h06a4308_0 + - nbformat=5.9.2=py311h06a4308_0 + - ncurses=6.4=h6a678d5_0 + - nest-asyncio=1.6.0=py311h06a4308_0 + - notebook=7.2.2=py311h06a4308_0 + - notebook-shim=0.2.3=py311h06a4308_0 + - openssl=3.0.15=h5eee18b_0 + - overrides=7.4.0=py311h06a4308_0 + - pandocfilters=1.5.0=pyhd3eb1b0_0 + - parso=0.8.3=pyhd3eb1b0_0 + - pcre2=10.42=hebb0a14_1 + - pexpect=4.8.0=pyhd3eb1b0_3 + - pip=24.0=py311h06a4308_0 + - platformdirs=3.10.0=py311h06a4308_0 + - ply=3.11=py311h06a4308_0 + - prometheus_client=0.14.1=py311h06a4308_0 + - prompt-toolkit=3.0.43=py311h06a4308_0 + - prompt_toolkit=3.0.43=hd3eb1b0_0 + - ptyprocess=0.7.0=pyhd3eb1b0_2 + - pure_eval=0.2.2=pyhd3eb1b0_0 + - pygments=2.15.1=py311h06a4308_1 + - pyqt=5.15.10=py311h6a678d5_0 + - pyqt5-sip=12.13.0=py311h5eee18b_0 + - pysocks=1.7.1=py311h06a4308_0 + - python=3.11.9=h955ad1f_0 + - python-dateutil=2.9.0post0=py311h06a4308_2 + - python-fastjsonschema=2.16.2=py311h06a4308_0 + - python-json-logger=2.0.7=py311h06a4308_0 + - pytz=2024.1=py311h06a4308_0 + - pyyaml=6.0.1=py311h5eee18b_0 + - pyzmq=25.1.2=py311h6a678d5_0 + - qt-main=5.15.2=h53bd1ea_10 + - qtconsole=5.5.1=py311h06a4308_0 + - qtpy=2.4.1=py311h06a4308_0 + - readline=8.2=h5eee18b_0 + - referencing=0.30.2=py311h06a4308_0 + - rfc3339-validator=0.1.4=py311h06a4308_0 + - rfc3986-validator=0.1.1=py311h06a4308_0 + - rpds-py=0.10.6=py311hb02cf49_0 + - send2trash=1.8.2=py311h06a4308_0 + - setuptools=69.5.1=py311h06a4308_0 + - sip=6.7.12=py311h6a678d5_0 + - six=1.16.0=pyhd3eb1b0_1 + - sniffio=1.3.0=py311h06a4308_0 + - soupsieve=2.5=py311h06a4308_0 + - sqlite=3.45.3=h5eee18b_0 + - stack_data=0.2.0=pyhd3eb1b0_0 + - terminado=0.17.1=py311h06a4308_0 + - tinycss2=1.2.1=py311h06a4308_0 + - tk=8.6.14=h39e8969_0 + - tornado=6.4.1=py311h5eee18b_0 + - traitlets=5.14.3=py311h06a4308_0 + - typing-extensions=4.11.0=py311h06a4308_0 + - typing_extensions=4.11.0=py311h06a4308_0 + - wcwidth=0.2.5=pyhd3eb1b0_0 + - webencodings=0.5.1=py311h06a4308_1 + - websocket-client=1.8.0=py311h06a4308_0 + - wheel=0.43.0=py311h06a4308_0 + - widgetsnbextension=4.0.10=py311h06a4308_0 + - xz=5.4.6=h5eee18b_1 + - yaml=0.2.5=h7b6447c_0 + - zeromq=4.3.5=h6a678d5_0 + - zlib=1.2.13=h5eee18b_1 + - zstd=1.5.5=hc292b87_2 + - pip: + - amazon-braket-default-simulator==1.23.2 + - amazon-braket-schemas==1.21.4 + - amazon-braket-sdk==1.79.1 + - annotated-types==0.6.0 + - antlr4-python3-runtime==4.9.2 + - backoff==2.2.1 + - backports-entry-points-selectable==1.3.0 + - boltons==24.0.0 + - boto3==1.34.105 + - botocore==1.34.105 + - certifi==2024.2.2 + - cloudpickle==2.2.1 + - contourpy==1.2.1 + - cryptography==42.0.7 + - cycler==0.12.1 + - cython==3.0.10 + - dill==0.3.8 + - fonttools==4.51.0 + - ibm-cloud-sdk-core==3.20.0 + - ibm-platform-services==0.53.6 + - importlib-metadata==7.1.0 + - iniconfig==2.0.0 + - jmespath==1.0.1 + - kiwisolver==1.4.5 + - matplotlib==3.8.4 + - mpmath==1.3.0 + - mthree==2.6.3 + - mypy-extensions==1.0.0 + - networkx==3.3 + - numpy==1.26.4 + - openpulse==0.5.0 + - openqasm3==0.5.0 + - opt-einsum==3.3.0 + - oqpy==0.3.5 + - orjson==3.10.3 + - packaging==24.0 + - pandas==2.2.2 + - pbr==6.0.0 + - pillow==10.3.0 + - pluggy==1.5.0 + - psutil==5.9.8 + - py==1.11.0 + - pycparser==2.22 + - pydantic==2.7.1 + - pydantic-core==2.18.2 + - pyjwt==2.8.0 + - pyparsing==3.1.2 + - pyqbench==0.1.dev113+gdb974ec.d20250213 + - pyspnego==0.10.2 + - pytest==8.3.3 + - qiskit==1.3.1 + - qiskit-aer==0.14.1 + - qiskit-braket-provider==0.3.1 + - qiskit-ibm-provider==0.11.0 + - qiskit-ibm-runtime==0.34.0 + - qiskit-ionq==0.5.0 + - requests==2.31.0 + - requests-ntlm==1.2.0 + - retry==0.9.2 + - rustworkx==0.15.1 + - s3transfer==0.10.1 + - scipy==1.13.0 + - stevedore==5.2.0 + - symengine==0.11.0 + - sympy==1.12 + - tqdm==4.66.4 + - tzdata==2024.1 + - urllib3==2.2.1 + - websockets==13.0.1 + - zipp==3.18.1 diff --git a/examples/backends/aer_simulator_local.yml b/examples/backends/aer_simulator_local.yml new file mode 100644 index 0000000..cf652b9 --- /dev/null +++ b/examples/backends/aer_simulator_local.yml @@ -0,0 +1,2 @@ +provider: "qiskit.providers.aer:AerProvider" +name: "aer_simulator" diff --git a/examples/backends/ibmq-quito.yml b/examples/backends/ibm_backend_kyiv_async.yml similarity index 80% rename from examples/backends/ibmq-quito.yml rename to examples/backends/ibm_backend_kyiv_async.yml index 19f9bba..8cfe6a7 100644 --- a/examples/backends/ibmq-quito.yml +++ b/examples/backends/ibm_backend_kyiv_async.yml @@ -1,4 +1,4 @@ -name: ibmq_quito +name: ibm_kyiv asynchronous: true provider: hub: ibm-q diff --git a/examples/backends/ibmq-qasm-simulator-sync.yml b/examples/backends/ibmq_local_simulator.yml similarity index 100% rename from examples/backends/ibmq-qasm-simulator-sync.yml rename to examples/backends/ibmq_local_simulator.yml diff --git a/examples/backends/ibmq-qasm-simulator-async.yml b/examples/backends/ibmq_qasm_local_simulator.yml similarity index 100% rename from examples/backends/ibmq-qasm-simulator-async.yml rename to examples/backends/ibmq_qasm_local_simulator.yml diff --git a/examples/backends/qiskit_braket_provider_AWSBraketProvider_lucy.yml b/examples/backends/qiskit_braket_provider_AWSBraketProvider_lucy.yml new file mode 100644 index 0000000..01f3a50 --- /dev/null +++ b/examples/backends/qiskit_braket_provider_AWSBraketProvider_lucy.yml @@ -0,0 +1,2 @@ +provider: qiskit_braket_provider:AWSBraketProvider +name: "lucy" diff --git a/examples/backends/qiskit_braket_provider_AWSBraketProvider_lucy_with_run_options.yml b/examples/backends/qiskit_braket_provider_AWSBraketProvider_lucy_with_run_options.yml new file mode 100644 index 0000000..cbc991f --- /dev/null +++ b/examples/backends/qiskit_braket_provider_AWSBraketProvider_lucy_with_run_options.yml @@ -0,0 +1,5 @@ +provider: qiskit_braket_provider:AWSBraketProvider +name: "lucy" +run_options: + verbatim: true + disable_qubit_rewiring: true diff --git a/examples/backends/qiskit_braket_provider_BraketLocalBackend.yml b/examples/backends/qiskit_braket_provider_BraketLocalBackend.yml new file mode 100644 index 0000000..0a80773 --- /dev/null +++ b/examples/backends/qiskit_braket_provider_BraketLocalBackend.yml @@ -0,0 +1,3 @@ +factory: qiskit_braket_provider:BraketLocalBackend +args: + - "braket_dm" diff --git a/examples/backends/qiskit_braket_provider_BraketLocalBackend_with_run_options.yml b/examples/backends/qiskit_braket_provider_BraketLocalBackend_with_run_options.yml new file mode 100644 index 0000000..7e520ef --- /dev/null +++ b/examples/backends/qiskit_braket_provider_BraketLocalBackend_with_run_options.yml @@ -0,0 +1,5 @@ +factory: qiskit_braket_provider:BraketLocalBackend +args: + - "braket_dm" +run_options: + verbatim: true diff --git a/examples/experiments/certification_experiment_direct_sum.yml b/examples/experiments/certification_experiment_direct_sum.yml new file mode 100644 index 0000000..2a24689 --- /dev/null +++ b/examples/experiments/certification_experiment_direct_sum.yml @@ -0,0 +1,14 @@ +type: certification-fourier +qubits: + - target: 0 + ancilla: 1 +angles: + start: 0 + stop: 2 * pi + num_steps: 2 +delta: 0.05 +gateset: ibmq +method: direct_sum +num_shots: 10 + + diff --git a/examples/experiments/certification_experiment_postselection.yml b/examples/experiments/certification_experiment_postselection.yml new file mode 100644 index 0000000..f0efd9b --- /dev/null +++ b/examples/experiments/certification_experiment_postselection.yml @@ -0,0 +1,13 @@ +type: certification-fourier +qubits: + - target: 0 + ancilla: 1 +angles: + start: 0 + stop: 2 * pi + num_steps: 25 +delta: 0.05 +gateset: ibmq +method: postselection +num_shots: 10000 + diff --git a/examples/experiments/fourier-disc-direct-sum-ibmq.yml b/examples/experiments/discrimination_experiment_direct_sum.yml similarity index 80% rename from examples/experiments/fourier-disc-direct-sum-ibmq.yml rename to examples/experiments/discrimination_experiment_direct_sum.yml index 3fe487f..943ce83 100644 --- a/examples/experiments/fourier-disc-direct-sum-ibmq.yml +++ b/examples/experiments/discrimination_experiment_direct_sum.yml @@ -5,7 +5,11 @@ qubits: angles: start: 0 stop: 2 * pi - num_steps: 50 + num_steps: 5 gateset: ibmq method: direct_sum -num_shots: 100 +num_shots: 10 + + + + diff --git a/examples/experiments/fourier-disc-postselection-ibmq.yml b/examples/experiments/discrimination_experiment_postselection.yml similarity index 81% rename from examples/experiments/fourier-disc-postselection-ibmq.yml rename to examples/experiments/discrimination_experiment_postselection.yml index 70194da..dd27bfe 100644 --- a/examples/experiments/fourier-disc-postselection-ibmq.yml +++ b/examples/experiments/discrimination_experiment_postselection.yml @@ -5,7 +5,9 @@ qubits: angles: start: 0 stop: 2 * pi - num_steps: 50 + num_steps: 5 gateset: ibmq method: postselection -num_shots: 100 +num_shots: 10 + + diff --git a/examples/experiments/fourier-certification-experiment.yml b/examples/experiments/fourier-certification-experiment.yml new file mode 100644 index 0000000..968eef8 --- /dev/null +++ b/examples/experiments/fourier-certification-experiment.yml @@ -0,0 +1,16 @@ +type: certification-fourier +qubits: + - target: 0 + ancilla: 1 + - target: 2 + ancilla: 3 + - target: 1 + ancilla: 0 +angles: + start: 0 + stop: 2 * pi + num_steps: 10 +delta: 0.05 +gateset: ibmq +method: postselection +num_shots: 10000 diff --git a/examples/using_fourier_with_simulator.py b/examples/using_fourier_with_simulator.py deleted file mode 100644 index 64b06dc..0000000 --- a/examples/using_fourier_with_simulator.py +++ /dev/null @@ -1,43 +0,0 @@ -import numpy as np -from matplotlib import pyplot as plt -from qiskit_braket_provider import BraketLocalBackend - -from qbench.fourier import discrimination_probability_upper_bound -from qbench.fourier._components import FourierComponents -from qbench.schemes.direct_sum import benchmark_using_direct_sum - -NUM_SHOTS_PER_MEASUREMENT = 10000 -TARGET = 0 -ANCILLA = 1 -GATESET = "ibmq" - - -def main(): - backend = BraketLocalBackend() - phis = np.linspace(0, 2 * np.pi, 100) - - theoretical_probs = discrimination_probability_upper_bound(phis) - - actual_probs = [ - benchmark_using_direct_sum( - backend=backend, - target=TARGET, - ancilla=ANCILLA, - state_preparation=circuits.state_preparation, - u_dag=circuits.u_dag, - v0_v1_direct_sum_dag=circuits.v0_v1_direct_sum_dag, - num_shots_per_measurement=NUM_SHOTS_PER_MEASUREMENT, - ) - for circuits in (FourierComponents(phi, gateset=GATESET) for phi in phis) - ] - - fig, ax = plt.subplots() - ax.plot(phis, theoretical_probs, color="red", label="theoretical_predictions") - ax.plot(phis, actual_probs, color="blue", label="actual data") - ax.legend() - - plt.show() - - -if __name__ == "__main__": - main() diff --git a/examples/using_fourier_with_simulator_postselection_all_cases.py b/examples/using_fourier_with_simulator_postselection_all_cases.py deleted file mode 100644 index 878f1d9..0000000 --- a/examples/using_fourier_with_simulator_postselection_all_cases.py +++ /dev/null @@ -1,44 +0,0 @@ -import numpy as np -from matplotlib import pyplot as plt -from qiskit_braket_provider import BraketLocalBackend - -from qbench.fourier import discrimination_probability_upper_bound -from qbench.fourier._components import FourierComponents -from qbench.schemes.postselection import benchmark_using_postselection - -NUM_SHOTS_PER_MEASUREMENT = 1000 -TARGET = 0 -ANCILLA = 1 -GATESET = "ibmq" - - -def main(): - backend = BraketLocalBackend() - phis = np.linspace(0, 2 * np.pi, 100) - - theoretical_probs = discrimination_probability_upper_bound(phis) - - actual_probs = [ - benchmark_using_postselection( - backend=backend, - target=TARGET, - ancilla=ANCILLA, - state_preparation=circuits.state_preparation, - u_dag=circuits.u_dag, - v0_dag=circuits.v0_dag, - v1_dag=circuits.v1_dag, - num_shots_per_measurement=NUM_SHOTS_PER_MEASUREMENT, - ) - for circuits in (FourierComponents(phi, gateset=GATESET) for phi in phis) - ] - - fig, ax = plt.subplots() - ax.plot(phis, theoretical_probs, color="red", label="theoretical_predictions") - ax.plot(phis, actual_probs, color="blue", label="actual data") - ax.legend() - - plt.show() - - -if __name__ == "__main__": - main() diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..d96997f --- /dev/null +++ b/mypy.ini @@ -0,0 +1,8 @@ +[mypy] +files = qbench +namespace_packages = True +explicit_package_bases = True +ignore_missing_imports = True + +[mypy-qbench.fourier._components.__init__] +ignore_errors = True diff --git a/pyproject.toml b/pyproject.toml index 18c1536..89372cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,20 +22,25 @@ classifiers = [ "Intended Audience :: Science/Research", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", "Topic :: Scientific/Engineering :: Physics" ] -requires-python = ">=3.8" +requires-python = "==3.11.*" dependencies = [ - "numpy ~= 1.22.0", - "scipy ~= 1.7.0", - "pandas ~= 1.5.0", - "amazon-braket-sdk >= 1.11.1", - "pydantic > 1.9.1", - "qiskit ~= 0.37.2", - "mthree ~= 1.1.0", - "tqdm ~= 4.64.1", - "pyyaml ~= 6.0", - "qiskit-braket-provider ~= 0.0.3" + "numpy", + "scipy", + "pandas", + "amazon-braket-sdk", + "pydantic", + "qiskit", + "qiskit-aer", + "qiskit-ibm-runtime", + "qiskit-ibm-provider", + "mthree", + "tqdm", + "pyyaml", + "qiskit-braket-provider", + "matplotlib" ] dynamic = ["version"] diff --git a/qbench/_expressions.py b/qbench/_expressions.py index a459f17..7bead78 100644 --- a/qbench/_expressions.py +++ b/qbench/_expressions.py @@ -1,4 +1,5 @@ """Implementation of arithmetic expression parsing.""" + import ast import operator as op from functools import singledispatch diff --git a/qbench/batching.py b/qbench/batching.py index d85946e..48a74e7 100644 --- a/qbench/batching.py +++ b/qbench/batching.py @@ -1,10 +1,12 @@ """Functions for splitting sequences of circuits into batches.""" + import math from itertools import islice from typing import Any, Iterable, NamedTuple, Optional, Sequence from qiskit import QuantumCircuit from qiskit.providers import JobV1 +from qiskit_ibm_runtime import RuntimeJob, RuntimeJobV2, SamplerV2 from tqdm import tqdm from .common_models import Backend @@ -16,7 +18,7 @@ class BatchWithKey(NamedTuple): class BatchJob(NamedTuple): - job: JobV1 + job: JobV1 | RuntimeJob | RuntimeJobV2 keys: Sequence[Any] @@ -74,10 +76,22 @@ def execute_in_batches( order of `keys`. """ batches = batch_circuits_with_keys(circuits, keys, batch_size) - result = ( - BatchJob(backend.run(batch.circuits, shots=shots, **kwargs), batch.keys) - for batch in batches + + sampler = SamplerV2(mode=backend) + + # result = ( + # BatchJob(backend.run(batch.circuits, shots=shots, **kwargs), batch.keys) + # for batch in batches + # ) + + result_gen = ( + BatchJob(sampler.run(batch.circuits, shots=shots), batch.keys) for batch in batches ) + + # Declare the variable 'result' + result: Iterable[BatchJob] if show_progress: - result = tqdm(result, total=len(batches)) + result = tqdm(result_gen, total=len(batches)) + else: + result = result_gen return result diff --git a/qbench/cli.py b/qbench/cli.py index d7ee19d..7138550 100644 --- a/qbench/cli.py +++ b/qbench/cli.py @@ -1,10 +1,12 @@ """Command line interface for qbench package.""" + from argparse import ArgumentParser from .fourier import add_fourier_parser -from .logging import configure_logging +from .fourier_certification import add_fourier_certification_parser +from .logger import configure_logging -PARSERS_TO_ADD = [add_fourier_parser] +PARSERS_TO_ADD = [add_fourier_parser, add_fourier_certification_parser] def main(args=None): diff --git a/qbench/common_models.py b/qbench/common_models.py index e028e96..f5ea035 100644 --- a/qbench/common_models.py +++ b/qbench/common_models.py @@ -1,15 +1,15 @@ -import os import re from importlib import import_module from typing import Any, Dict, List, Optional, Union -from pydantic import BaseModel as PydanticBaseModel -from pydantic import ConstrainedInt, Field, StrictStr, root_validator, validator -from qiskit import IBMQ +from pydantic.v1 import BaseModel as PydanticBaseModel +from pydantic.v1 import ConstrainedInt, Field, StrictStr, root_validator, validator from qiskit.circuit import Parameter from qiskit.providers import BackendV1, BackendV2 +from qiskit_aer import AerSimulator +from qiskit_ibm_runtime import QiskitRuntimeService -from ._expressions import eval_expr +from qbench._expressions import eval_expr AnyParameter = Union[float, Parameter] @@ -63,6 +63,10 @@ def check_if_number_of_steps_is_one_when_start_equals_stop(cls, values): return values +class Delta(BaseModel): + delta: Any + + class QubitsPair(BaseModel): target: Qubit ancilla: Qubit @@ -140,28 +144,33 @@ class IBMQProviderDescription(BaseModel): class IBMQBackendDescription(BaseModel): name: str asynchronous: bool = False - provider: IBMQProviderDescription def create_backend(self): - if IBMQ.active_account(): - provider = IBMQ.get_provider( - hub=self.provider.hub, - group=self.provider.group, - project=self.provider.project, - ) - else: - provider = IBMQ.enable_account( - os.getenv("IBMQ_TOKEN"), - hub=self.provider.hub, - group=self.provider.group, - project=self.provider.project, - ) - return provider.get_backend(self.name) + service = QiskitRuntimeService( + channel="ibm_quantum", + instance=f"{self.provider.hub}/{self.provider.group}/{self.provider.project}", + ) + + return service.backend(name=self.name) + + # TODO finish! + # exit(-1) + + +class AerBackendDescription(BaseModel): + name: str + asynchronous: bool = False + + def create_backend(self): + return AerSimulator() BackendDescription = Union[ - SimpleBackendDescription, BackendFactoryDescription, IBMQBackendDescription + SimpleBackendDescription, + BackendFactoryDescription, + IBMQBackendDescription, + AerBackendDescription, ] diff --git a/qbench/fourier/__init__.py b/qbench/fourier/__init__.py index 3986d13..11c0390 100644 --- a/qbench/fourier/__init__.py +++ b/qbench/fourier/__init__.py @@ -1,7 +1,7 @@ """Functionalities relating specifically to Fourier-discrimination experiments. -This package defines all instructions (components) needed for assembling +This package defines all instructions (_components) needed for assembling circuits for benchmarking using Fourier-parametrized family. The Fourier family of measurements is defined as: @@ -10,7 +10,7 @@ U(\\varphi) = H \\begin{pmatrix} 1&0\\\\0&e^{i\\varphi}\\end{pmatrix}H^\\dagger $$ -All components are available as properties of :class:`FourierComponents` class. The +All _components are available as properties of :class:`FourierComponents` class. The instances of this class can be constructed in such a way that the instructions they provide are compatible with several different quantum devices available on the market. @@ -22,32 +22,17 @@ $$ """ -from typing import Union - -import numpy as np from ._cli import add_fourier_parser -from ._components import FourierComponents +from ._components.components import FourierComponents from ._models import ( FourierDiscriminationAsyncResult, FourierDiscriminationSyncResult, FourierExperimentSet, ) - -def discrimination_probability_upper_bound( - phi: Union[float, np.ndarray] -) -> Union[float, np.ndarray]: - """Compute exact upper bound on the probability of discrimination. - - :param phi: angle parametrizing the performed measurement. - :return: maximum probability with which identity and $p_{U(\\varphi)}$ can be discriminated. - """ - return 0.5 + 0.25 * np.abs(1 - np.exp(1j * phi)) - - __all__ = [ - "discrimination_probability_upper_bound", + # "discrimination_probability_upper_bound", "add_fourier_parser", "FourierComponents", "FourierDiscriminationAsyncResult", diff --git a/qbench/fourier/_cli.py b/qbench/fourier/_cli.py index 9c22197..68cf3f8 100644 --- a/qbench/fourier/_cli.py +++ b/qbench/fourier/_cli.py @@ -3,6 +3,7 @@ This module also contains thin wrappers for functions from qbench.fourier.experiment_runner, to adapt them for command line usage. """ + from argparse import FileType, Namespace from yaml import safe_dump, safe_load diff --git a/qbench/fourier/_components/__init__.py b/qbench/fourier/_components/__init__.py index 8184d05..481ba9f 100644 --- a/qbench/fourier/_components/__init__.py +++ b/qbench/fourier/_components/__init__.py @@ -1,128 +1,19 @@ -"""Module defining components used in Fourier discrimination experiment.""" -from typing import Optional, Union +"""Module defining _components used in Fourier discrimination experiment.""" -from qiskit.circuit import Instruction, Parameter +from typing import Union -from . import _generic, _ibmq, _lucy, _rigetti +import numpy as np -class FourierComponents: - """Class defining components for Fourier-discrimination experiment. +def discrimination_probability_upper_bound( + phi: Union[float, np.ndarray] +) -> Union[float, np.ndarray]: + """Compute exact upper bound on the probability of discrimination. - :param phi: angle defining measurement to discriminate. May be a number or an instance of - a Qiskit Parameter. See - :qiskit_tutorial:`here `_ - if you are new to parametrized circuits in Qiskit. - - :param gateset: name of the one of the predefined basis gate sets to use. It controls which - gates will be used to construct the circuit components. Available choices are: - - - :code:`"lucy"`: gateset comprising gates native to - `OQC Lucy `_ computer. - - :code:`"rigetti"`: gateset comprising gates native to - `Rigetti `_ computers. - - :code:`"ibmq"`: gateset comprising gates native to - `IBMQ `_ computers. - - If no gateset is provided, high-level gates will be used without restriction on basis gates. + :param phi: angle parametrizing the performed measurement. + :return: maximum probability with which identity and $p_{U(\\varphi)}$ can be discriminated. """ - - def __init__(self, phi: Union[float, Parameter], gateset: Optional[str] = None): - """Initialize new instance of FourierComponents.""" - self.phi = phi - self._module = _GATESET_MAPPING[gateset] - - @property - def state_preparation(self) -> Instruction: - """Instruction performing transformation $|00\\rangle$ -> Bell state - - The corresponding circuit is: - - .. code:: - - ┌───┐ - q_0: ┤ H ├──■── - └───┘┌─┴─┐ - q_1: ─────┤ X ├ - └───┘ - - """ - return self._module.state_preparation() - - @property - def u_dag(self) -> Instruction: - r"""Unitary $U^\dagger$ defining Fourier measurement. - - The corresponding circuit is: - - .. code:: - - ┌───┐┌───────────┐┌───┐ - q: ┤ H ├┤ Phase(-φ) ├┤ H ├ - └───┘└───────────┘└───┘ - - .. note:: - - This instruction is needed because on actual devices we can only measure in Z-basis. - The $U^\dagger$ unitary changes basis so that subsequent measurement in Z-basis can - be considered as performing desired von Neumann measurement to be discriminated from - the Z-basis one. - """ - - return self._module.u_dag(self.phi) - - @property - def v0_dag(self) -> Instruction: - """Instruction corresponding to the positive part of Holevo-Helstrom measurement. - - The corresponding circuit is: - - .. code:: - - ┌──────────┐┌────────────────┐ - q: ┤ Rz(-π/2) ├┤ Ry(-φ/2 - π/2) ├ - └──────────┘└────────────────┘ - - """ - return self._module.v0_dag(self.phi) - - @property - def v1_dag(self) -> Instruction: - """Instruction corresponding to the negative part of Holevo-Helstrom measurement. - - The corresponding circuit is: - - .. code:: - - ┌──────────┐┌────────────────┐┌────────┐ - q: ┤ Rz(-π/2) ├┤ Ry(-φ/2 - π/2) ├┤ Rx(-π) ├ - └──────────┘└────────────────┘└────────┘ - """ - return self._module.v1_dag(self.phi) - - @property - def v0_v1_direct_sum_dag(self) -> Instruction: - r"""Direct sum $V_0^\dagger\oplus V_1^\dagger$ of both parts of Holevo-Helstrom measurement. - - .. note:: - In usual basis ordering, the unitaries returned by this property would be - block-diagonal, with blocks corresponding to positive and negative parts - of Holevo-Helstrom measurement. - - However, Qiskit enumerates basis vectors in reverse, so the produced unitaries - are not block-diagonal, unless the qubits are swapped. - See accompanying tests to see how it's done. - - The following article contains more details on basis vectors ordering used - (among others) by Qiskit: - https://arxiv.org/abs/1711.02086 - """ - return self._module.v0_v1_direct_sum(self.phi) + return 0.5 + 0.25 * np.abs(1 - np.exp(1j * phi)) -_GATESET_MAPPING = { - "lucy": _lucy, - "rigetti": _rigetti, - "ibmq": _ibmq, - None: _generic, -} +__all__ = ["discrimination_probability_upper_bound"] diff --git a/qbench/fourier/_components/_generic.py b/qbench/fourier/_components/_generic.py index d305393..e290879 100644 --- a/qbench/fourier/_components/_generic.py +++ b/qbench/fourier/_components/_generic.py @@ -1,24 +1,23 @@ -"""Generic implementation of Fourier components not tailored for any specific device. +"""Generic implementation of Fourier _components not tailored for any specific device. -Note that using components from this module on physical device typically requires compilation. +Note that using _components from this module on physical device typically requires compilation. For detailed description of functions in this module refer to the documentation of FourierComponents class. """ -import numpy as np -from qiskit.circuit import Instruction, QuantumCircuit -from ...common_models import AnyParameter +import numpy as np +from qiskit.circuit import Instruction, Parameter, QuantumCircuit def state_preparation() -> Instruction: circuit = QuantumCircuit(2, name="state-prep") circuit.h(0) - circuit.cnot(0, 1) + circuit.cx(0, 1) return circuit.to_instruction() -def u_dag(phi: AnyParameter) -> Instruction: +def u_dag(phi: float | Parameter) -> Instruction: circuit = QuantumCircuit(1, name="U-dag") circuit.h(0) circuit.p(-phi, 0) @@ -26,14 +25,14 @@ def u_dag(phi: AnyParameter) -> Instruction: return circuit.to_instruction() -def v0_dag(phi: AnyParameter) -> Instruction: +def v0_dag(phi: float | Parameter) -> Instruction: circuit = QuantumCircuit(1, name="v0-dag") circuit.rz(-np.pi / 2, 0) circuit.ry(-(phi + np.pi) / 2, 0) return circuit.to_instruction() -def v1_dag(phi: AnyParameter) -> Instruction: +def v1_dag(phi: float | Parameter) -> Instruction: circuit = QuantumCircuit(1, name="v1-dag") circuit.rz(-np.pi / 2, 0) circuit.ry(-(phi + np.pi) / 2, 0) @@ -41,9 +40,9 @@ def v1_dag(phi: AnyParameter) -> Instruction: return circuit.to_instruction() -def v0_v1_direct_sum(phi: AnyParameter) -> Instruction: +def v0_v1_direct_sum(phi: float | Parameter) -> Instruction: circuit = QuantumCircuit(2, name="v0 ⊕ v1-dag") circuit.p(np.pi, 0) circuit.append(v0_dag(phi), [1]) - circuit.cnot(0, 1) + circuit.cx(0, 1) return circuit.decompose(["v0-dag"]).to_instruction() diff --git a/qbench/fourier/_components/_ibmq.py b/qbench/fourier/_components/_ibmq.py index 799ff57..b9a4696 100644 --- a/qbench/fourier/_components/_ibmq.py +++ b/qbench/fourier/_components/_ibmq.py @@ -3,10 +3,12 @@ For detailed description of functions in this module refer to the documentation of FourierComponents class. """ + import numpy as np from qiskit.circuit import Instruction, QuantumCircuit -from ...common_models import AnyParameter +from qbench.common_models import AnyParameter + from ._lucy_and_ibmq_common import u_dag, v0_dag, v1_dag diff --git a/qbench/fourier/_components/_lucy.py b/qbench/fourier/_components/_lucy.py index 6af7871..b6a8b2a 100644 --- a/qbench/fourier/_components/_lucy.py +++ b/qbench/fourier/_components/_lucy.py @@ -3,11 +3,13 @@ For detailed description of functions in this module refer to the documentation of FourierComponents class. """ + import numpy as np from qiskit import QuantumCircuit from qiskit.circuit import Instruction -from ...common_models import AnyParameter +from qbench.common_models import AnyParameter + from ._lucy_and_ibmq_common import u_dag, v0_dag, v1_dag diff --git a/qbench/fourier/_components/_lucy_and_ibmq_common.py b/qbench/fourier/_components/_lucy_and_ibmq_common.py index 7e58ced..d2f329b 100644 --- a/qbench/fourier/_components/_lucy_and_ibmq_common.py +++ b/qbench/fourier/_components/_lucy_and_ibmq_common.py @@ -3,11 +3,12 @@ For detailed description of functions in this module refer to the documentation of FourierComponents class. """ + import numpy as np from qiskit import QuantumCircuit from qiskit.circuit import Instruction -from ...common_models import AnyParameter +from qbench.common_models import AnyParameter def u_dag(phi: AnyParameter) -> Instruction: diff --git a/qbench/fourier/_components/_rigetti.py b/qbench/fourier/_components/_rigetti.py index eed34be..153e2c7 100644 --- a/qbench/fourier/_components/_rigetti.py +++ b/qbench/fourier/_components/_rigetti.py @@ -3,11 +3,12 @@ For detailed description of functions in this module refer to the documentation of FourierComponents class. """ + import numpy as np from qiskit import QuantumCircuit from qiskit.circuit import Instruction -from ...common_models import AnyParameter +from qbench.common_models import AnyParameter INSTRUCTIONS_TO_DECOMPOSE = ["hadamard-rigetti", "cnot-rigetti", "v0-dag"] diff --git a/qbench/fourier/_components/components.py b/qbench/fourier/_components/components.py new file mode 100644 index 0000000..13a5154 --- /dev/null +++ b/qbench/fourier/_components/components.py @@ -0,0 +1,129 @@ +"""Module defining _components used in Fourier discrimination experiment.""" + +from typing import Optional, Union + +from qiskit.circuit import Instruction, Parameter + +from . import _generic, _ibmq, _lucy, _rigetti + + +class FourierComponents: + """Class defining _components for Fourier-discrimination experiment. + + :param phi: angle defining measurement to discriminate. May be a number or an instance of + a Qiskit Parameter. See + :qiskit_tutorial:`here `_ + if you are new to parametrized circuits in Qiskit. + + :param gateset: name of the one of the predefined basis gate sets to use. It controls which + gates will be used to construct the circuit _components. Available choices are: + + - :code:`"lucy"`: gateset comprising gates native to + `OQC Lucy `_ computer. + - :code:`"rigetti"`: gateset comprising gates native to + `Rigetti `_ computers. + - :code:`"ibmq"`: gateset comprising gates native to + `IBMQ `_ computers. + + If no gateset is provided, high-level gates will be used without restriction on basis gates. + """ + + def __init__(self, phi: Union[float, Parameter], gateset: Optional[str] = None): + """Initialize new instance of FourierComponents.""" + self.phi = phi + self._module = _GATESET_MAPPING[gateset] + + @property + def state_preparation(self) -> Instruction: + """Instruction performing transformation $|00\\rangle$ -> Bell state + + The corresponding circuit is: + + .. code:: + + ┌───┐ + q_0: ┤ H ├──■── + └───┘┌─┴─┐ + q_1: ─────┤ X ├ + └───┘ + + """ + return self._module.state_preparation() + + @property + def u_dag(self) -> Instruction: + r"""Unitary $U^\dagger$ defining Fourier measurement. + + The corresponding circuit is: + + .. code:: + + ┌───┐┌───────────┐┌───┐ + q: ┤ H ├┤ Phase(-φ) ├┤ H ├ + └───┘└───────────┘└───┘ + + .. note:: + + This instruction is needed because on actual devices we can only measure in Z-basis. + The $U^\dagger$ unitary changes basis so that subsequent measurement in Z-basis can + be considered as performing desired von Neumann measurement to be discriminated from + the Z-basis one. + """ + + return self._module.u_dag(self.phi) + + @property + def v0_dag(self) -> Instruction: + """Instruction corresponding to the positive part of Holevo-Helstrom measurement. + + The corresponding circuit is: + + .. code:: + + ┌──────────┐┌────────────────┐ + q: ┤ Rz(-π/2) ├┤ Ry(-φ/2 - π/2) ├ + └──────────┘└────────────────┘ + + """ + return self._module.v0_dag(self.phi) + + @property + def v1_dag(self) -> Instruction: + """Instruction corresponding to the negative part of Holevo-Helstrom measurement. + + The corresponding circuit is: + + .. code:: + + ┌──────────┐┌────────────────┐┌────────┐ + q: ┤ Rz(-π/2) ├┤ Ry(-φ/2 - π/2) ├┤ Rx(-π) ├ + └──────────┘└────────────────┘└────────┘ + """ + return self._module.v1_dag(self.phi) + + @property + def v0_v1_direct_sum_dag(self) -> Instruction: + r"""Direct sum $V_0^\dagger\oplus V_1^\dagger$ of both parts of Holevo-Helstrom measurement. + + .. note:: + In usual basis ordering, the unitaries returned by this property would be + block-diagonal, with blocks corresponding to positive and negative parts + of Holevo-Helstrom measurement. + + However, Qiskit enumerates basis vectors in reverse, so the produced unitaries + are not block-diagonal, unless the qubits are swapped. + See accompanying tests to see how it's done. + + The following article contains more details on basis vectors ordering used + (among others) by Qiskit: + https://arxiv.org/abs/1711.02086 + """ + return self._module.v0_v1_direct_sum(self.phi) + + +_GATESET_MAPPING = { + "lucy": _lucy, + "rigetti": _rigetti, + "ibmq": _ibmq, + None: _generic, +} diff --git a/qbench/fourier/_models.py b/qbench/fourier/_models.py index a7e1a4b..2066649 100644 --- a/qbench/fourier/_models.py +++ b/qbench/fourier/_models.py @@ -11,9 +11,9 @@ ) import numpy as np -from pydantic import validator +from pydantic.v1 import validator -from ..common_models import ( +from qbench.common_models import ( AnglesRange, BackendDescription, BaseModel, diff --git a/qbench/fourier/experiment_runner.py b/qbench/fourier/experiment_runner.py old mode 100644 new mode 100755 index 62995f6..48ea4f3 --- a/qbench/fourier/experiment_runner.py +++ b/qbench/fourier/experiment_runner.py @@ -1,29 +1,34 @@ """Functions for running Fourier discrimination experiments and interacting with the results.""" + +import sys from collections import Counter, defaultdict from logging import getLogger +from pathlib import Path from typing import Dict, Iterable, List, Optional, Tuple, Union, cast import numpy as np import pandas as pd from mthree import M3Mitigation -from qiskit import QiskitError, QuantumCircuit +from qiskit import QiskitError, QuantumCircuit, transpile from qiskit.circuit import Parameter from qiskit.providers import JobV1 from tqdm import tqdm -from ..batching import BatchJob, execute_in_batches -from ..common_models import Backend, BackendDescription -from ..jobs import retrieve_jobs -from ..limits import get_limits -from ..schemes.direct_sum import ( - assemble_direct_sum_circuits, +from qbench.batching import BatchJob, execute_in_batches +from qbench.common_models import Backend, BackendDescription +from qbench.jobs import retrieve_jobs +from qbench.limits import get_limits +from qbench.schemes.direct_sum import ( + assemble_discrimination_direct_sum_circuits, compute_probabilities_from_direct_sum_measurements, ) -from ..schemes.postselection import ( - assemble_postselection_circuits, - compute_probabilities_from_postselection_measurements, +from qbench.schemes.postselection import ( + assemble_circuits_discrimination_postselection, + compute_probabilities_discrimination_postselection, ) -from ._components import FourierComponents + +from ._components import discrimination_probability_upper_bound +from ._components.components import FourierComponents from ._models import ( BatchResult, FourierDiscriminationAsyncResult, @@ -34,6 +39,8 @@ SingleResult, ) +sys.path.append(str(Path(sys.argv[0]).resolve().parent.parent)) + logger = getLogger("qbench") @@ -116,7 +123,7 @@ def _extract_result_from_job( :return: object containing results or None if the provided job was not successful. """ try: - result = {"name": name, "histogram": job.result().get_counts()[i]} + result = {"name": name, "histogram": job.result()[i].join_data().get_counts()} except QiskitError: return None try: @@ -149,7 +156,7 @@ def _collect_circuits_and_keys( """Construct all circuits needed for the experiment and assign them unique keys.""" def _asemble_postselection(target: int, ancilla: int) -> Dict[str, QuantumCircuit]: - return assemble_postselection_circuits( + return assemble_circuits_discrimination_postselection( state_preparation=components.state_preparation, u_dag=components.u_dag, v0_dag=components.v0_dag, @@ -159,7 +166,7 @@ def _asemble_postselection(target: int, ancilla: int) -> Dict[str, QuantumCircui ) def _asemble_direct_sum(target: int, ancilla: int) -> Dict[str, QuantumCircuit]: - return assemble_direct_sum_circuits( + return assemble_discrimination_direct_sum_circuits( state_preparation=components.state_preparation, u_dag=components.u_dag, v0_v1_direct_sum_dag=components.v0_v1_direct_sum_dag, @@ -174,7 +181,7 @@ def _asemble_direct_sum(target: int, ancilla: int) -> Dict[str, QuantumCircuit]: logger.info("Assembling experiments...") circuit_key_pairs = [ ( - circuit.bind_parameters({components.phi: phi}), + circuit.assign_parameters({components.phi: phi}, inplace=False), (target, ancilla, circuit_name, float(phi)), ) for (target, ancilla, phi) in tqdm(list(experiments.enumerate_experiment_labels())) @@ -251,9 +258,13 @@ def run_experiment( components = FourierComponents(phi, gateset=experiments.gateset) backend = backend_description.create_backend() + print(f"Backend type: {type(backend).__name__}, backend name: {_backend_name(backend)}") logger.info(f"Backend type: {type(backend).__name__}, backend name: {_backend_name(backend)}") - circuits, keys = _collect_circuits_and_keys(experiments, components) + circuits_col, keys = _collect_circuits_and_keys(experiments, components) + + # Transpile circuit according to the universal set of gates supported by the selected backend + circuits = [transpile(circuit, backend=backend) for circuit in circuits_col] logger.info("Submitting jobs...") batches = execute_in_batches( @@ -298,16 +309,16 @@ def fetch_statuses(async_results: FourierDiscriminationAsyncResult) -> Dict[str, :return: dictionary mapping status name to number of its occurrences. """ logger.info("Enabling account and creating backend") - backend = async_results.metadata.backend_description.create_backend() + # backend = async_results.metadata.backend_description.create_backend() logger.info("Reading jobs ids from the input file") job_ids = [entry.job_id for entry in async_results.data] logger.info("Retrieving jobs, this might take a while...") - jobs = retrieve_jobs(backend, job_ids) + jobs = retrieve_jobs(job_ids) logger.info("Done") - return dict(Counter(job.status().name for job in jobs)) + return dict(Counter(str(job.status()) for job in jobs)) def resolve_results( @@ -322,13 +333,15 @@ def resolve_results( particular, it contains histograms of bitstrings for each circuit run during the experiment. """ logger.info("Enabling account and creating backend") - backend = async_results.metadata.backend_description.create_backend() + + # TODO Will we need to use the backend instance in the future? + # backend = async_results.metadata.backend_description.create_backend() logger.info("Reading jobs ids from the input file") job_ids = [entry.job_id for entry in cast(List[BatchResult], async_results.data)] logger.info(f"Fetching total of {len(job_ids)} jobs") - jobs_mapping = {job.job_id(): job for job in retrieve_jobs(backend, job_ids)} + jobs_mapping = {job.job_id(): job for job in retrieve_jobs(job_ids)} batches = [BatchJob(jobs_mapping[entry.job_id], entry.keys) for entry in async_results.data] @@ -345,7 +358,7 @@ def resolve_results( def tabulate_results(sync_results: FourierDiscriminationSyncResult) -> pd.DataFrame: compute_probabilities = ( - compute_probabilities_from_postselection_measurements + compute_probabilities_discrimination_postselection if sync_results.metadata.experiments.method.lower() == "postselection" else compute_probabilities_from_direct_sum_measurements ) @@ -355,6 +368,7 @@ def _make_row(entry): entry.target, entry.ancilla, entry.phi, + discrimination_probability_upper_bound(entry.phi), compute_probabilities( **{f"{info.name}_counts": info.histogram for info in entry.results_per_circuit} ), @@ -377,9 +391,9 @@ def _make_row(entry): # We assume that either all circuits have mitigation info, or none of them has columns = ( - ["target", "ancilla", "phi", "disc_prob"] - if len(rows[0]) == 4 - else ["target", "ancilla", "phi", "disc_prob", "mit_disc_prob"] + ["target", "ancilla", "phi", "ideal_prob", "disc_prob"] + if len(rows[0]) == 5 + else ["target", "ancilla", "phi", "ideal_prob", "disc_prob", "mit_disc_prob"] ) result = pd.DataFrame(data=rows, columns=columns) diff --git a/qbench/fourier/testing.py b/qbench/fourier/testing.py index 4a5fc42..8d365d1 100644 --- a/qbench/fourier/testing.py +++ b/qbench/fourier/testing.py @@ -1,4 +1,5 @@ """Testing utilities related qbench.fourier packager.""" + from typing import Sequence, Tuple import pandas as pd diff --git a/qbench/fourier_certification/__init__.py b/qbench/fourier_certification/__init__.py new file mode 100644 index 0000000..d46efdc --- /dev/null +++ b/qbench/fourier_certification/__init__.py @@ -0,0 +1,36 @@ +"""Functionalities relating specifically to Fourier-certification experiments. + + +This package defines all instructions (_components) needed for assembling +circuits for benchmarking using Fourier-parametrized family. + +The Fourier family of measurements is defined as: + +$$ +U(\\varphi) = H \\begin{pmatrix} 1&0\\\\0&e^{i\\varphi}\\end{pmatrix}H^\\dagger +$$ + +All _components are available as properties of :class:`FourierComponents` class. The +instances of this class can be constructed in such a way that the instructions they +provide are compatible with several different quantum devices available on the market. + +Additionally, this module provides a function computing the minimized probability of +type II error. + +""" + +from ._cli import add_fourier_certification_parser +from ._components.components import FourierComponents +from ._models import ( + FourierCertificationAsyncResult, + FourierCertificationSyncResult, + FourierExperimentSet, +) + +__all__ = [ + "add_fourier_certification_parser", + "FourierComponents", + "FourierCertificationAsyncResult", + "FourierCertificationSyncResult", + "FourierExperimentSet", +] diff --git a/qbench/fourier_certification/_cli.py b/qbench/fourier_certification/_cli.py new file mode 100644 index 0000000..17e0e42 --- /dev/null +++ b/qbench/fourier_certification/_cli.py @@ -0,0 +1,200 @@ +"""Definition of command line parsers and handlers for qbench disc-fourier command. + +This module also contains thin wrappers for functions from qbench.fourier.experiment_runner, +to adapt them for command line usage. +""" + +from argparse import FileType, Namespace + +from yaml import safe_dump, safe_load + +from ..common_models import BackendDescriptionRoot +from ._models import ( + FourierCertificationAsyncResult, + FourierCertificationSyncResult, + FourierExperimentSet, +) +from .experiment_runner import ( + fetch_statuses, + resolve_results, + run_experiment, + tabulate_results, +) + + +def _run_benchmark(args: Namespace) -> None: + """Function executed when qbench cert-fourier benchmark is invoked.""" + experiment = FourierExperimentSet(**safe_load(args.experiment_file)) + backend_description = BackendDescriptionRoot(__root__=safe_load(args.backend_file)).__root__ + result = run_experiment(experiment, backend_description) + safe_dump(result.dict(), args.output, sort_keys=False, default_flow_style=None) + + +def _status(args: Namespace) -> None: + """Function executed when qbench disc-fourier status is invoked.""" + results = FourierCertificationAsyncResult(**safe_load(args.async_results)) + counts = fetch_statuses(results) + print(counts) + + +def _resolve(args: Namespace) -> None: + """Function executed when qbench disc-fourier resolve is invoked.""" + results = FourierCertificationAsyncResult(**safe_load(args.async_results)) + resolved = resolve_results(results) + + res_dict = resolved.dict() + + # Remove 'name: u' from the output + for e in res_dict["data"]: + for res in e["results_per_circuit"]: + del res["name"] + + safe_dump(res_dict, args.output, sort_keys=False) + + +def _tabulate(args: Namespace) -> None: + """Function executed when qbench disc-fourier tabulate is invoked.""" + t = safe_load(args.sync_results) + + # Add dummy name for the results + # TODO Should we go for another datatype specific for certification? + for e in t["data"]: + for res in e["results_per_circuit"]: + res["name"] = "u" + # results = FourierCertificationSyncResult(**safe_load(args.sync_results)) + results = FourierCertificationSyncResult(**t) + table = tabulate_results(results) + table.to_csv(args.output, index=False) + + +def add_fourier_certification_parser(parent_parser) -> None: + """Add cert-fourier parser to the parent parser. + + The added parser will have the following subcommands: + - benchmark: run set of Fourier discrimination experiments against given backend + - status: check status of asynchronously executed set of experiments + - resolve: retrieve results of completed asynchronous job + + The exact syntax for using each command can be, as usually, obtained by running + qbench disc-fourier -h + + :param parent_parser: a parser to which disc-fourier command should be added. + """ + parser = parent_parser.add_parser("cert-fourier") + + subcommands = parser.add_subparsers() + + benchmark = subcommands.add_parser( + "benchmark", + description=( + "Run set of benchmarking experiments utilizing measurement discrimination " + "with parametrized Fourier family of measurements." + ), + ) + + benchmark.add_argument( + "experiment_file", + help="path to the file describing the set of experiments", + type=FileType("r"), + ) + benchmark.add_argument( + "backend_file", + help="path to the file describing the backend to be used", + type=FileType("r"), + ) + + benchmark.add_argument( + "--output", + help="optional path to the output file. If not provided, output will be printed to stdout", + type=FileType("w"), + default="-", + ) + + benchmark.set_defaults(func=_run_benchmark) + + plot = subcommands.add_parser("plot") + + plot.add_argument( + "result", + help=( + "result of discrimination experiments which can be obtained by running " + "qbench benchmark" + ), + type=str, + ) + + plot.add_argument( + "--output", + help=( + "optional path to the output file. If not provided, the plots will be shown " + "but not saved. The extension of the output file determines the output format " + "and it can be any of the ones supported by the matplotlib" + ), + type=str, + ) + + resolve = subcommands.add_parser( + "resolve", + description="Resolve asynchronous jobs to obtain final experiments data.", + ) + + resolve.add_argument( + "async_results", + help=( + "path to the file with data of discrimination experiments which can be obtained by " + "running `qbench` benchmark using backend with asynchronous flag equal to True." + ), + type=FileType("r"), + ) + + resolve.add_argument( + "output", + help="path to the file where data resolved from asynchronous jobs should be stored.", + type=FileType("w"), + ) + + resolve.set_defaults(func=_resolve) + + status = subcommands.add_parser( + "status", description="Query the status of an asynchronous jobs from the results file." + ) + + status.add_argument( + "async_results", + help=( + "path to the file with data of discrimination experiments which can be obtained by " + "running qbench benchmark using backend with asynchronous flag equal to True." + ), + type=FileType("r"), + ) + + status.set_defaults(func=_status) + + tabulate = subcommands.add_parser( + "tabulate", + description=( + "Compute and tabulate probabilities from measurements obtained from the experiments." + ), + ) + + tabulate.add_argument( + "sync_results", + help=( + "path to the file with results of discrimination experiments. If experiments were " + "conducted using asynchronous backend, they need to be manually resolved." + ), + type=FileType("r"), + ) + + tabulate.add_argument( + "output", + help=( + "path to the resulting CSV file. This file will contain columns 'target', 'ancilla' " + "'phi' and 'disc_prob' with obvious meanings. If each experiment contained " + "mitigation data, the 'mitigated_disc_prob` containing discrimination probabilities " + "computed using mitigated bitstrings will also be added." + ), + type=FileType("w"), + ) + + tabulate.set_defaults(func=_tabulate) diff --git a/qbench/fourier_certification/_components/__init__.py b/qbench/fourier_certification/_components/__init__.py new file mode 100644 index 0000000..ab9e984 --- /dev/null +++ b/qbench/fourier_certification/_components/__init__.py @@ -0,0 +1,28 @@ +"""Module defining _components used in Fourier certification experiment.""" + +from typing import Union + +import numpy as np + +# from . import _generic, _ibmq, _lucy, _rigetti + + +def certification_probability_upper_bound( + phi: Union[float, np.ndarray], delta: float +) -> Union[float, np.ndarray]: + """Compute the minimized probability of type II error in certification scheme + between measurements in P_U and P_1. + + :param phi: angle of measurement P_U to be certified from P_1. + :param delta: a given statistical significance. + + :return: minimized probability of type II error. + """ + + if 1 / 2 * np.abs(1 + np.exp(-1j * phi)) > np.sqrt(delta): + return ( + 1 / 2 * np.abs(1 + np.exp(-1j * phi)) * np.sqrt(1 - delta) + - np.sqrt(1 - 1 / 4 * np.abs(1 + np.exp(-1j * phi)) ** 2) * np.sqrt(delta) + ) ** 2 + else: + return 0 diff --git a/qbench/fourier_certification/_components/_generic.py b/qbench/fourier_certification/_components/_generic.py new file mode 100644 index 0000000..b2585ee --- /dev/null +++ b/qbench/fourier_certification/_components/_generic.py @@ -0,0 +1,125 @@ +"""Generic implementation of Fourier _components not tailored for any specific device. + +Note that using _components from this module on physical device typically requires compilation. + +For detailed description of functions in this module refer to the documentation of +FourierComponents class. +""" + +import numpy as np +from qiskit.circuit import QuantumCircuit + + +def state_preparation(): + circuit = QuantumCircuit(2, name="state-prep") + circuit.h(0) + circuit.cnot(0, 1) + return circuit.to_instruction() + + +def u_dag(phi): + circuit = QuantumCircuit(1, name="U-dag") + circuit.h(0) + circuit.p(-phi, 0) + circuit.h(0) + return circuit.to_instruction() + + +def v0(phi, delta): + circuit = QuantumCircuit(1, name="v0") + if 1 + np.cos(phi) >= 2 * delta and 0 <= phi <= np.pi: + circuit.ry(-2 * np.arcsin(np.sqrt(delta)), 0) + elif 1 + np.cos(phi) >= 2 * delta and np.pi < phi <= 2 * np.pi: + circuit.ry(2 * np.arcsin(np.sqrt(delta)), 0) + elif 1 + np.cos(phi) < 2 * delta and 0 <= phi <= np.pi: + circuit.ry(-2 * np.arccos(np.sin(phi / 2)), 0) + else: + circuit.ry(-2 * np.arccos(np.sin(phi / 2)), 0) + circuit.z(0) + return circuit.to_instruction() + + +def v0_dag(phi, delta): + circuit = QuantumCircuit(1, name="v0-dag") + if 1 + np.cos(phi) >= 2 * delta and (0 <= phi <= np.pi): + circuit.p(-np.pi / 2, 0) + circuit.ry(2 * np.arcsin(np.sqrt(delta)), 0) + elif 1 + np.cos(phi) >= 2 * delta and np.pi < phi <= 2 * np.pi: + circuit.p(-np.pi / 2, 0) + circuit.ry(-2 * np.arcsin(np.sqrt(delta)), 0) + elif 1 + np.cos(phi) < 2 * delta and 0 <= phi <= np.pi: + circuit.p(-np.pi / 2, 0) + circuit.ry(2 * np.arccos(np.sin(phi / 2)), 0) + else: + circuit.p(-np.pi / 2, 0) + circuit.z(0) + circuit.ry(2 * np.arccos(np.sin(phi / 2)), 0) + return circuit.to_instruction() + + +def v1_dag(phi, delta): + circuit = QuantumCircuit(1, name="v1-dag") + if 1 + np.cos(phi) >= 2 * delta and 0 <= phi <= np.pi: + circuit.x(0) + circuit.p(-np.pi / 2, 0) + circuit.ry(2 * np.arcsin(np.sqrt(delta)), 0) + elif 1 + np.cos(phi) >= 2 * delta and np.pi < phi <= 2 * np.pi: + circuit.x(0) + circuit.p(-np.pi / 2, 0) + circuit.ry(-2 * np.arcsin(np.sqrt(delta)), 0) + elif 1 + np.cos(phi) < 2 * delta and 0 <= phi <= np.pi: + circuit.x(0) + circuit.p(-np.pi / 2, 0) + circuit.ry(2 * np.arccos(np.sin(phi / 2)), 0) + else: + circuit.x(0) + circuit.p(-np.pi / 2, 0) + circuit.z(0) + circuit.ry(2 * np.arccos(np.sin(phi / 2)), 0) + return circuit.to_instruction() + + +def v0_v1_direct_sum(phi, delta): + circuit = QuantumCircuit(2, name="v0 ⊕ v1-dag") + circuit.p(np.pi, 0) + circuit.append(v0_dag(phi, delta), [1]) + circuit.cnot(0, 1) + return circuit.decompose(["v0-dag"]).to_instruction() + + +# def state_preparation() -> Instruction: +# circuit = QuantumCircuit(2, name="state-prep") +# circuit.h(0) +# circuit.cnot(0, 1) +# return circuit.to_instruction() + + +# def u_dag(phi: AnyParameter) -> Instruction: +# circuit = QuantumCircuit(1, name="U-dag") +# circuit.h(0) +# circuit.p(-phi, 0) +# circuit.h(0) +# return circuit.to_instruction() + + +# def v0_dag(phi: AnyParameter) -> Instruction: +# circuit = QuantumCircuit(1, name="v0-dag") +# circuit.rz(-np.pi / 2, 0) +# circuit.ry(-(phi + np.pi) / 2, 0) +# return circuit.to_instruction() + + +# def v1_dag(phi: AnyParameter) -> Instruction: +# circuit = QuantumCircuit(1, name="v1-dag") +# circuit.rz(-np.pi / 2, 0) +# circuit.ry(-(phi + np.pi) / 2, 0) +# circuit.rx(-np.pi, 0) +# return circuit.to_instruction() + + +# def v0_v1_direct_sum(phi: AnyParameter) -> Instruction: +# circuit = QuantumCircuit(2, name="v0 ⊕ v1-dag") +# circuit.p(np.pi, 0) +# circuit.append(v0_dag(phi), [1]) +# circuit.cnot(0, 1) +# return circuit.decompose(["v0-dag"]).to_instruction() diff --git a/qbench/fourier_certification/_components/_ibmq.py b/qbench/fourier_certification/_components/_ibmq.py new file mode 100644 index 0000000..a03313f --- /dev/null +++ b/qbench/fourier_certification/_components/_ibmq.py @@ -0,0 +1,65 @@ +"""Components for Fourier experiment specifically compiled for IBMQ device. + +For detailed description of functions in this module refer to the documentation of +FourierComponents class. +""" + +import numpy as np +from qiskit.circuit import Instruction, QuantumCircuit + +from ._lucy_and_ibmq_common import u_dag, v0_dag, v1_dag + + +def _decompose(circuit: QuantumCircuit): + return circuit.decompose(["v0-dag"]) + + +def state_preparation() -> Instruction: + circuit = QuantumCircuit(2, name="state-prep") + circuit.rz(np.pi / 2, 0) + circuit.sx(0) + circuit.rz(np.pi / 2, 0) + circuit.cx(0, 1) + return circuit.to_instruction() + + +# def v0_v1_direct_sum(phi: AnyParameter) -> Instruction: +# circuit = QuantumCircuit(2, name="v0 ⊕ v1-dag") +# circuit.rz(np.pi, 0) +# circuit.append(v0_dag(phi), [1]) +# circuit.cx(0, 1) +# return _decompose(circuit).to_instruction() + + +def v0_v1_direct_sum(phi, delta): + circuit = QuantumCircuit(2) + circuit.rz(np.pi, 0) + if 1 + np.cos(phi) >= 2 * delta and (0 <= phi <= np.pi): + circuit.rz(-np.pi / 2, 1) + circuit.sx(1) + circuit.rz(2 * np.arcsin(np.sqrt(delta)) + np.pi, 1) + circuit.sx(1) + circuit.rz(3 * np.pi, 1) + elif 1 + np.cos(phi) >= 2 * delta and np.pi < phi <= 2 * np.pi: + circuit.rz(-np.pi / 2, 1) + circuit.sx(1) + circuit.rz(-2 * np.arcsin(np.sqrt(delta)) + np.pi, 1) + circuit.sx(1) + circuit.rz(3 * np.pi, 1) + elif 1 + np.cos(phi) < 2 * delta and 0 <= phi <= np.pi: + circuit.rz(-np.pi / 2, 1) + circuit.sx(1) + circuit.rz(2 * np.arccos(np.sin(phi / 2)) + np.pi, 1) + circuit.sx(1) + circuit.rz(3 * np.pi, 1) + else: + circuit.rz(np.pi / 2, 1) + circuit.sx(1) + circuit.rz(2 * np.arccos(np.sin(phi / 2)) + np.pi, 1) + circuit.sx(1) + circuit.rz(3 * np.pi, 1) + circuit.cx(0, 1) + return circuit.to_instruction() + + +__all__ = ["state_preparation", "u_dag", "v0_dag", "v1_dag", "v0_v1_direct_sum"] diff --git a/qbench/fourier_certification/_components/_lucy.py b/qbench/fourier_certification/_components/_lucy.py new file mode 100644 index 0000000..0959adf --- /dev/null +++ b/qbench/fourier_certification/_components/_lucy.py @@ -0,0 +1,46 @@ +"""Components for Fourier experiment specifically compiled for OQC Lucy device. + +For detailed description of functions in this module refer to the documentation of +FourierComponents class. +""" + +import numpy as np +from qiskit import QuantumCircuit +from qiskit.circuit import Instruction + +from ._lucy_and_ibmq_common import u_dag, v0_dag, v1_dag + + +def state_preparation() -> Instruction: + circuit = QuantumCircuit(2, name="state-prep") + circuit.sx(0) + circuit.rz(np.pi, 0) + circuit.x(0) + circuit.sx(1) + circuit.ecr(0, 1) + return circuit.to_instruction() + + +# def v0_v1_direct_sum(phi: AnyParameter) -> Instruction: +# circuit = QuantumCircuit(2, name="v0 ⊕ v1-dag") +# circuit.rz(-np.pi / 2, 1) +# circuit.sx(1) +# circuit.rz(-(phi + np.pi) / 2, 1) +# circuit.rz(3 * np.pi / 2, 0) +# circuit.x(0) +# circuit.ecr(0, 1) +# return circuit.to_instruction() + + +def v0_v1_direct_sum(phi, delta): + circuit = QuantumCircuit(2, name="v0 ⊕ v1-dag") + circuit.rz(np.pi, 0) + circuit.append(v0_dag(phi, delta), [1]) + circuit.x(0) + circuit.sx(1) + circuit.rz(-np.pi / 2, 0) + circuit.ecr(0, 1) + return circuit.decompose(["v0-dag"]).to_instruction() + + +__all__ = ["state_preparation", "u_dag", "v0_dag", "v1_dag", "v0_v1_direct_sum"] diff --git a/qbench/fourier_certification/_components/_lucy_and_ibmq_common.py b/qbench/fourier_certification/_components/_lucy_and_ibmq_common.py new file mode 100644 index 0000000..296bb0f --- /dev/null +++ b/qbench/fourier_certification/_components/_lucy_and_ibmq_common.py @@ -0,0 +1,114 @@ +"""Components for Fourier experiment that are common to both Lucy and IBMQ devices. + +For detailed description of functions in this module refer to the documentation of +FourierComponents class. +""" + +import numpy as np +from qiskit import QuantumCircuit + + +def u_dag(phi): + circuit = QuantumCircuit(1) + circuit.sx(0) + circuit.rz(np.pi / 2, 0) + circuit.sx(0) + circuit.rz(-phi, 0) + circuit.sx(0) + circuit.rz(np.pi / 2, 0) + circuit.sx(0) + return circuit.to_instruction() + + +def v0_dag(phi: float, delta: float): + circuit = QuantumCircuit(1) + if 1 + np.cos(phi) >= 2 * delta and (0 <= phi <= np.pi): + circuit.rz(-np.pi / 2, 0) + circuit.sx(0) + circuit.rz(2 * np.arcsin(np.sqrt(delta)) + np.pi, 0) + circuit.sx(0) + circuit.rz(3 * np.pi, 0) + elif 1 + np.cos(phi) >= 2 * delta and np.pi < phi <= 2 * np.pi: + circuit.rz(-np.pi / 2, 0) + circuit.sx(0) + circuit.rz(-2 * np.arcsin(np.sqrt(delta)) + np.pi, 0) + circuit.sx(0) + circuit.rz(3 * np.pi, 0) + elif 1 + np.cos(phi) < 2 * delta and 0 <= phi <= np.pi: + circuit.rz(-np.pi / 2, 0) + circuit.sx(0) + circuit.rz(2 * np.arccos(np.sin(phi / 2)) + np.pi, 0) + circuit.sx(0) + circuit.rz(3 * np.pi, 0) + else: + circuit.rz(np.pi / 2, 0) + circuit.sx(0) + circuit.rz(2 * np.arccos(np.sin(phi / 2)) + np.pi, 0) + circuit.sx(0) + circuit.rz(3 * np.pi, 0) + return circuit.to_instruction() + + +def v1_dag(phi: float, delta: float): + circuit = QuantumCircuit(1) + if 1 + np.cos(phi) >= 2 * delta and 0 <= phi <= np.pi: + circuit.rz(-np.pi / 2, 0) + circuit.sx(0) + circuit.rz(-np.pi, 0) + circuit.rz(2 * np.arcsin(np.sqrt(delta)) + np.pi, 0) + circuit.sx(0) + circuit.rz(3 * np.pi, 0) + elif 1 + np.cos(phi) >= 2 * delta and np.pi < phi <= 2 * np.pi: + circuit.rz(-np.pi / 2, 0) + circuit.sx(0) + circuit.rz(-np.pi, 0) + circuit.rz(-2 * np.arcsin(np.sqrt(delta)) + np.pi, 0) + circuit.sx(0) + circuit.rz(3 * np.pi, 0) + elif 1 + np.cos(phi) < 2 * delta and 0 <= phi <= np.pi: + circuit.rz(-np.pi / 2, 0) + circuit.sx(0) + circuit.rz(-np.pi, 0) + circuit.rz(2 * np.arccos(np.sin(phi / 2)) + np.pi, 0) + circuit.sx(0) + circuit.rz(3 * np.pi, 0) + else: + circuit.rz(np.pi / 2, 0) + circuit.sx(0) + circuit.rz(-np.pi, 0) + circuit.rz(2 * np.arccos(np.sin(phi / 2)) + np.pi, 0) + circuit.sx(0) + circuit.rz(3 * np.pi, 0) + return circuit.to_instruction() + + +# def u_dag(phi: AnyParameter) -> Instruction: +# circuit = QuantumCircuit(1, name="U-dag") +# circuit.sx(0) +# circuit.rz(np.pi / 2, 0) +# circuit.sx(0) +# circuit.rz(-phi, 0) +# circuit.sx(0) +# circuit.rz(np.pi / 2, 0) +# circuit.sx(0) +# return circuit.to_instruction() + + +# def v0_dag(phi: AnyParameter) -> Instruction: +# circuit = QuantumCircuit(1, name="v0-dag") +# circuit.rz(-np.pi / 2, 0) +# circuit.sx(0) +# circuit.rz(-(phi + np.pi) / 2, 0) +# circuit.sx(0) +# circuit.x(0) +# return circuit.to_instruction() + + +# def v1_dag(phi: AnyParameter) -> Instruction: +# circuit = QuantumCircuit(1, name="v1-dag") +# circuit.rz(np.pi / 2, 0) +# circuit.sx(0) +# circuit.rz(-(np.pi - phi) / 2, 0) +# circuit.x(0) +# circuit.sx(0) +# return circuit.to_instruction() diff --git a/qbench/fourier_certification/_components/_rigetti.py b/qbench/fourier_certification/_components/_rigetti.py new file mode 100644 index 0000000..9ab893f --- /dev/null +++ b/qbench/fourier_certification/_components/_rigetti.py @@ -0,0 +1,184 @@ +"""Components for Fourier experiment specifically compiled for OQC Lucy device. + +For detailed description of functions in this module refer to the documentation of +FourierComponents class. +""" + +import numpy as np +from qiskit import QuantumCircuit + +INSTRUCTIONS_TO_DECOMPOSE = ["hadamard-rigetti", "cnot-rigetti", "v0-dag"] + + +def _decompose(circuit: QuantumCircuit) -> QuantumCircuit: + return circuit.decompose(INSTRUCTIONS_TO_DECOMPOSE, reps=2) + + +def _rigetti_hadamard(): + circuit = QuantumCircuit(1, name="hadamard-rigetti") + circuit.rx(np.pi / 2, 0) + circuit.rz(np.pi / 2, 0) + circuit.rx(np.pi / 2, 0) + return circuit.to_instruction() + + +def _rigetti_cnot(): + circuit = QuantumCircuit(2, name="cnot-rigetti") + circuit.append(_rigetti_hadamard(), [1]) + circuit.cz(0, 1) + circuit.append(_rigetti_hadamard(), [1]) + return circuit.to_instruction() + + +def state_preparation(): + circuit = QuantumCircuit(2, name="state-prep") + circuit.append(_rigetti_hadamard(), [0]) + circuit.append(_rigetti_cnot(), [0, 1]) + return _decompose(circuit).to_instruction() + + +def u_dag(phi): + circuit = QuantumCircuit(1, name="U-dag") + circuit.rz(np.pi / 2, 0) + circuit.rx(np.pi / 2, 0) + circuit.rz(-phi, 0) + circuit.rx(-np.pi / 2, 0) + circuit.rz(-np.pi / 2, 0) + return circuit.to_instruction() + + +def v0_dag(phi, delta): + circuit = QuantumCircuit(1, name="v0-dag") + if 1 + np.cos(phi) >= 2 * delta and (0 <= phi <= np.pi): + circuit.rz(-np.pi / 2, 0) + circuit.rx(np.pi / 2, 0) + circuit.rz(2 * np.arcsin(np.sqrt(delta)) + np.pi, 0) + circuit.rx(np.pi / 2, 0) + circuit.rz(3 * np.pi, 0) + elif 1 + np.cos(phi) >= 2 * delta and np.pi < phi <= 2 * np.pi: + circuit.rz(-np.pi / 2, 0) + circuit.rx(np.pi / 2, 0) + circuit.rz(-2 * np.arcsin(np.sqrt(delta)) + np.pi, 0) + circuit.rx(np.pi / 2, 0) + circuit.rz(3 * np.pi, 0) + elif 1 + np.cos(phi) < 2 * delta and 0 <= phi <= np.pi: + circuit.rz(-np.pi / 2, 0) + circuit.rx(np.pi / 2, 0) + circuit.rz(2 * np.arccos(np.sin(phi / 2)) + np.pi, 0) + circuit.rx(np.pi / 2, 0) + circuit.rz(3 * np.pi, 0) + else: + circuit.rz(np.pi / 2, 0) + circuit.rx(np.pi / 2, 0) + circuit.rz(2 * np.arccos(np.sin(phi / 2)) + np.pi, 0) + circuit.rx(np.pi / 2, 0) + circuit.rz(3 * np.pi, 0) + return circuit.to_instruction() + + +def v1_dag(phi, delta): + circuit = QuantumCircuit(1, name="v1-dag") + if 1 + np.cos(phi) >= 2 * delta and 0 <= phi <= np.pi: + circuit.rz(np.pi / 2, 0) + circuit.rx(-np.pi / 2, 0) + circuit.rz(2 * np.arcsin(np.sqrt(delta)) + np.pi, 0) + circuit.rx(np.pi / 2, 0) + circuit.rz(3 * np.pi, 0) + elif 1 + np.cos(phi) >= 2 * delta and np.pi < phi <= 2 * np.pi: + circuit.rz(np.pi / 2, 0) + circuit.rx(-np.pi / 2, 0) + circuit.rz(-2 * np.arcsin(np.sqrt(delta)) + np.pi, 0) + circuit.rx(np.pi / 2, 0) + circuit.rz(3 * np.pi, 0) + elif 1 + np.cos(phi) < 2 * delta and 0 <= phi <= np.pi: + circuit.rz(np.pi / 2, 0) + circuit.rx(-np.pi / 2, 0) + circuit.rz(2 * np.arccos(np.sin(phi / 2)) + np.pi, 0) + circuit.rx(np.pi / 2, 0) + circuit.rz(3 * np.pi, 0) + else: + circuit.rz(-np.pi / 2, 0) + circuit.rx(-np.pi / 2, 0) + circuit.rz(2 * np.arccos(np.sin(phi / 2)) + np.pi, 0) + circuit.rx(np.pi / 2, 0) + circuit.rz(3 * np.pi, 0) + return circuit.to_instruction() + + +def v0_v1_direct_sum(phi, delta): + circuit = QuantumCircuit(2, name="v0 ⊕ v1-dag") + circuit.rz(np.pi, 0) + circuit.append(v0_dag(phi, delta), [1]) + circuit.append(_rigetti_cnot(), [0, 1]) + return _decompose(circuit).to_instruction() + + +# def _rigetti_hadamard() -> Instruction: +# """Decomposition of Hadamard gate using only Rigetti native gates. + +# The decomposition uses the identity: H = RX(pi/2) RZ(pi/2) RX(pi/2) +# """ +# circuit = QuantumCircuit(1, name="hadamard-rigetti") +# circuit.rx(np.pi / 2, 0) +# circuit.rz(np.pi / 2, 0) +# circuit.rx(np.pi / 2, 0) +# return circuit.to_instruction() + + +# def _rigetti_cnot() -> Instruction: +# """Decomposition of CNOT gate using only Rigetti native gates. + +# The decomposition uses identity: CNOT(i, j) = H(j) CZ(i, j) H(j), and the hadamard gates +# are decomposed using _rigetti_hadamard function. +# """ +# circuit = QuantumCircuit(2, name="cnot-rigetti") +# circuit.append(_rigetti_hadamard(), [1]) +# circuit.cz(0, 1) +# circuit.append(_rigetti_hadamard(), [1]) +# return circuit.to_instruction() + + +# For description of functions below refer to the __init__ file in qbench.fourier + + +# def state_preparation() -> Instruction: +# circuit = QuantumCircuit(2, name="state-prep") +# circuit.append(_rigetti_hadamard(), [0]) +# circuit.append(_rigetti_cnot(), [0, 1]) +# return _decompose(circuit).to_instruction() + + +# def u_dag(phi: AnyParameter) -> Instruction: +# circuit = QuantumCircuit(1, name="U-dag") +# circuit.rz(np.pi / 2, 0) +# circuit.rx(np.pi / 2, 0) +# circuit.rz(-phi, 0) +# circuit.rx(-np.pi / 2, 0) +# circuit.rz(-np.pi / 2, 0) +# return circuit.to_instruction() + + +# def v0_dag(phi: AnyParameter) -> Instruction: +# circuit = QuantumCircuit(1, name="v0-dag") +# circuit.rz(-np.pi / 2, 0) +# circuit.rx(np.pi / 2, 0) +# circuit.rz(-(phi + np.pi) / 2, 0) +# circuit.rx(-np.pi / 2, 0) +# return circuit.to_instruction() + + +# def v1_dag(phi: AnyParameter) -> Instruction: +# circuit = QuantumCircuit(1, name="v1-dag") +# circuit.rz(np.pi / 2, 0) +# circuit.rx(np.pi / 2, 0) +# circuit.rz(-(np.pi - phi) / 2, 0) +# circuit.rx(-np.pi / 2, 0) +# return circuit.to_instruction() + + +# def v0_v1_direct_sum(phi: AnyParameter) -> Instruction: +# circuit = QuantumCircuit(2, name="v0 ⊕ v1-dag") +# circuit.rz(np.pi, 0) +# circuit.append(v0_dag(phi), [1]) +# circuit.append(_rigetti_cnot(), [0, 1]) +# return _decompose(circuit).to_instruction() diff --git a/qbench/fourier_certification/_components/components.py b/qbench/fourier_certification/_components/components.py new file mode 100644 index 0000000..569d4c7 --- /dev/null +++ b/qbench/fourier_certification/_components/components.py @@ -0,0 +1,135 @@ +"""Module defining _components used in Fourier certification experiment.""" + +from typing import Optional, Union + +from qiskit.circuit import Instruction, Parameter + +from . import _generic, _ibmq, _lucy, _rigetti + + +class FourierComponents: + """Class defining _components for Fourier-certification experiment. + + :param phi: angle defining measurement to certificate. May be a number or an instance of + a Qiskit Parameter. See + :qiskit_tutorial:`here `_ + if you are new to parametrized circuits in Qiskit. + + :param gateset: name of the one of the predefined basis gate sets to use. It controls which + gates will be used to construct the circuit _components. Available choices are: + + - :code:`"lucy"`: gateset comprising gates native to + `OQC Lucy `_ computer. + - :code:`"rigetti"`: gateset comprising gates native to + `Rigetti `_ computers. + - :code:`"ibmq"`: gateset comprising gates native to + `IBMQ `_ computers. + + If no gateset is provided, high-level gates will be used without restriction on basis gates. + """ + + def __init__( + self, + phi: Union[float, Parameter], + delta: Union[float, Parameter], + gateset: Optional[str] = None, + ): + """Initialize new instance of FourierComponents.""" + self.phi = phi + self.delta = delta + self._module = _GATESET_MAPPING[gateset] + + @property + def state_preparation(self) -> Instruction: + """Instruction performing transformation $|00\\rangle$ -> Bell state + + The corresponding circuit is: + + .. code:: + + ┌───┐ + q_0: ┤ H ├──■── + └───┘┌─┴─┐ + q_1: ─────┤ X ├ + └───┘ + + """ + return self._module.state_preparation() + + @property + def u_dag(self) -> Instruction: + r"""Unitary $U^\dagger$ defining Fourier measurement. + + The corresponding circuit is: + + .. code:: + + ┌───┐┌───────────┐┌───┐ + q: ┤ H ├┤ Phase(-φ) ├┤ H ├ + └───┘└───────────┘└───┘ + + .. note:: + + This instruction is needed because on actual devices we can only measure in Z-basis. + The $U^\dagger$ unitary changes basis so that subsequent measurement in Z-basis can + be considered as performing desired von Neumann measurement to be certified from + the Z-basis one. + """ + + return self._module.u_dag(self.phi) + + @property + def v0_dag(self) -> Instruction: + """Instruction corresponding to the positive part of Holevo-Helstrom measurement. + + The corresponding circuit is: + + .. code:: + + ┌──────────┐┌────────────────┐ + q: ┤ Rz(-π/2) ├┤ Ry(-φ/2 - π/2) ├ + └──────────┘└────────────────┘ + + """ + return self._module.v0_dag(self.phi, self.delta) + + @property + def v1_dag(self) -> Instruction: + """Instruction corresponding to the negative part of Holevo-Helstrom measurement. + + The corresponding circuit is: + + .. code:: + + ┌──────────┐┌────────────────┐┌────────┐ + q: ┤ Rz(-π/2) ├┤ Ry(-φ/2 - π/2) ├┤ Rx(-π) ├ + └──────────┘└────────────────┘└────────┘ + """ + return self._module.v1_dag(self.phi, self.delta) + + @property + def v0_v1_direct_sum_dag(self) -> Instruction: + r"""Direct sum $V_0^\dagger\oplus V_1^\dagger$ of both parts of Holevo-Helstrom measurement. + + .. note:: + In usual basis ordering, the unitaries returned by this property would be + block-diagonal, with blocks corresponding to positive and negative parts + of Holevo-Helstrom measurement. + + However, Qiskit enumerates basis vectors in reverse, so the produced unitaries + are not block-diagonal, unless the qubits are swapped. + See accompanying tests to see how it's done. + + The following article contains more details on basis vectors ordering used + (among others) by Qiskit: + https://arxiv.org/abs/1711.02086 + """ + return self._module.v0_v1_direct_sum(self.phi, self.delta) + + +_GATESET_MAPPING = { + "lucy": _lucy, + "rigetti": _rigetti, + "ibmq": _ibmq, + None: _generic, +} diff --git a/qbench/fourier_certification/_models.py b/qbench/fourier_certification/_models.py new file mode 100644 index 0000000..f581388 --- /dev/null +++ b/qbench/fourier_certification/_models.py @@ -0,0 +1,105 @@ +from typing import ( + Any, + Iterable, + List, + Literal, + Optional, + Sequence, + Tuple, + Type, + TypeVar, +) + +import numpy as np +from pydantic.v1 import validator + +from qbench.common_models import ( + AnglesRange, + BackendDescription, + BaseModel, + Qubit, + QubitsPair, + StrictPositiveInt, + SynchronousHistogram, +) + + +class FourierExperimentSet(BaseModel): + type: Literal["certification-fourier"] + qubits: List[QubitsPair] + angles: AnglesRange + delta: float + gateset: Optional[str] + method: Literal["direct_sum", "postselection"] + num_shots: StrictPositiveInt + + @validator("qubits") + def check_if_all_pairs_of_qubits_are_different(cls, qubits): + list_of_qubits = [(qubits.target, qubits.ancilla) for qubits in qubits] + if len(set(list_of_qubits)) != len(list_of_qubits): + raise ValueError("All pairs of qubits should be distinct.") + return qubits + + def enumerate_experiment_labels(self) -> Iterable[Tuple[int, int, float]]: + return ( + (pair.target, pair.ancilla, phi) + for pair in self.qubits + for phi in np.linspace(self.angles.start, self.angles.stop, self.angles.num_steps) + ) + + +class FourierCertificationMetadata(BaseModel): + experiments: FourierExperimentSet + backend_description: BackendDescription + + +T = TypeVar("T", bound="QubitMitigationInfo") + + +class QubitMitigationInfo(BaseModel): + prob_meas0_prep1: float + prob_meas1_prep0: float + + @classmethod + def from_job_properties(cls: Type[T], properties, qubit) -> T: + return cls.parse_obj( + { + "prob_meas0_prep1": properties.qubit_property(qubit)["prob_meas0_prep1"][0], + "prob_meas1_prep0": properties.qubit_property(qubit)["prob_meas1_prep0"][0], + } + ) + + +class MitigationInfo(BaseModel): + target: QubitMitigationInfo + ancilla: QubitMitigationInfo + + +class ResultForCircuit(BaseModel): + name: str + histogram: SynchronousHistogram + mitigation_info: Optional[MitigationInfo] + mitigated_histogram: Optional[Any] + + +class SingleResult(BaseModel): + target: Qubit + ancilla: Qubit + phi: float + delta: float + results_per_circuit: List[ResultForCircuit] + + +class BatchResult(BaseModel): + job_id: str + keys: Sequence[Tuple[int, int, str, float, float]] + + +class FourierCertificationSyncResult(BaseModel): + metadata: FourierCertificationMetadata + data: List[SingleResult] + + +class FourierCertificationAsyncResult(BaseModel): + metadata: FourierCertificationMetadata + data: List[BatchResult] diff --git a/qbench/fourier_certification/experiment_runner.py b/qbench/fourier_certification/experiment_runner.py new file mode 100644 index 0000000..08c7f9b --- /dev/null +++ b/qbench/fourier_certification/experiment_runner.py @@ -0,0 +1,463 @@ +"""Functions for running Fourier certification experiments and interacting with the results.""" + +from collections import Counter, defaultdict +from logging import getLogger +from typing import Dict, Iterable, List, Optional, Tuple, Union, cast + +import numpy as np +import pandas as pd +from mthree import M3Mitigation +from qiskit import QiskitError, QuantumCircuit, transpile +from qiskit.providers import JobV1 +from qiskit_ibm_runtime import RuntimeJobV2 +from tqdm import tqdm + +from qbench.batching import BatchJob, execute_in_batches +from qbench.common_models import Backend, BackendDescription +from qbench.jobs import retrieve_jobs +from qbench.limits import get_limits +from qbench.schemes.direct_sum import ( + assemble_certification_direct_sum_circuits, + compute_probabilities_from_certification_direct_sum_measurements, +) +from qbench.schemes.postselection import ( + assemble_circuits_certification_postselection, + compute_probabilities_certification_postselection, +) + +from ._components import certification_probability_upper_bound +from ._components.components import FourierComponents +from ._models import ( + BatchResult, + FourierCertificationAsyncResult, + FourierCertificationSyncResult, + FourierExperimentSet, + QubitMitigationInfo, + ResultForCircuit, + SingleResult, +) + +logger = getLogger("qbench") + + +def _backend_name(backend) -> str: + """Return backend name. + + This is needed because backend.name is sometimes a function (IBMQ) and sometimes a string + (Braket). + """ + try: + return backend.name() + except TypeError: + return backend.name + + +def _log_fourier_experiments(experiments: FourierExperimentSet) -> None: + """Log basic information about the set of experiments.""" + logger.info("Running set of Fourier-certification experiments") + logger.info("Number of qubit-pairs: %d", len(experiments.qubits)) + logger.info("Number of phi values: %d", experiments.angles.num_steps) + logger.info("Statistical significance: %s", "{0:g}".format(experiments.delta)) + logger.info("Number of shots per circuit: %d", experiments.num_shots) + logger.info("Probability estimation method: %s", experiments.method) + logger.info("Gateset: %s", experiments.gateset) + + +def _matrix_from_mitigation_info(info: QubitMitigationInfo) -> np.ndarray: + """Construct Mthree-compatible matrix from mitigation info.""" + return np.array( + [ + [1 - info.prob_meas1_prep0, info.prob_meas0_prep1], + [info.prob_meas1_prep0, 1 - info.prob_meas0_prep1], + ] + ) + + +def _mitigate( + counts: Dict[str, int], + target: int, + ancilla: int, + backend: Backend, + mitigation_info: Dict[str, QubitMitigationInfo], +) -> Dict[str, float]: + """Apply error mitigation to obtained counts. + + :param counts: histogram of measured bitstrings. + :param target: index of the target qubit. + :param ancilla: index of the ancilla qubit. + :param backend: backend used for executing job. + :param mitigation_info: dictionary with keys 'ancilla' and 'target', mapping them to objects + holding mitigation info (prob_meas1_prep0 and prob_meas0_prep1). + :return: dictionary with corrected quasi-distribution of bitstrings. Note that this contains + probabilities and not counts, but nevertheless can be used for computing probabilities. + """ + mitigator = M3Mitigation(backend) + + matrices: List[Optional[np.ndarray]] = [None for _ in range(backend.configuration().num_qubits)] + matrices[target] = _matrix_from_mitigation_info(mitigation_info["target"]) + matrices[ancilla] = _matrix_from_mitigation_info(mitigation_info["ancilla"]) + + mitigator.cals_from_matrices(matrices) + result = mitigator.apply_correction(counts, [target, ancilla]) + + # Probability distribution + result = result.nearest_probability_distribution() + # Wrap value in native floats, otherwise we get serialization problems + return {key: float(value) for key, value in result.items()} + + +def _extract_result_from_job( + job: JobV1 | RuntimeJobV2, target: int, ancilla: int, i: int, name: str +) -> Optional[ResultForCircuit]: + """Extract meaningful information from job and wrap them in serializable object. + + .. note:: + Single job can comprise running multiple circuits (experiments in Qiskit terminology) + and hence we need parameter i to identify which one we are processing right now. + + :param job: Qiskit job used for computing results. + :param target: index of the target qubit. + :param ancilla: index of the ancilla qubit. + :param i: index of the experiment in job. + :param name: name of the circuit to be used in the resulting object. + :return: object containing results or None if the provided job was not successful. + """ + try: + result = {"name": name, "histogram": job.result()[i].join_data().get_counts()} + except QiskitError: + return None + + try: + # We ignore some typing errors, since we are essentially accessing attributes that might + # not exist according to their base classes. + props = job.properties() # type: ignore + result["mitigation_info"] = { + "target": QubitMitigationInfo.from_job_properties(props, target), + "ancilla": QubitMitigationInfo.from_job_properties(props, ancilla), + } + result["mitigated_histogram"] = _mitigate( + result["histogram"], + target, + ancilla, + job.backend(), # type: ignore + result["mitigation_info"], + ) + except AttributeError: + pass + return ResultForCircuit.parse_obj(result) + + +CircuitKey = Tuple[int, int, str, float, float] + + +def _collect_circuits_and_keys( + experiments: FourierExperimentSet, + components: FourierComponents, +) -> Tuple[Tuple[QuantumCircuit, ...], Tuple[CircuitKey, ...]]: + """Construct all circuits needed for the experiment and assign them unique keys.""" + + def _asemble_postselection(target: int, ancilla: int) -> Dict[str, QuantumCircuit]: + return assemble_circuits_certification_postselection( + state_preparation=components.state_preparation, + u_dag=components.u_dag, + v0_dag=components.v0_dag, + v1_dag=components.v1_dag, + target=target, + ancilla=ancilla, + ) + + def _asemble_direct_sum(target: int, ancilla: int) -> Dict[str, QuantumCircuit]: + return assemble_certification_direct_sum_circuits( + state_preparation=components.state_preparation, + u_dag=components.u_dag, + v0_v1_direct_sum_dag=components.v0_v1_direct_sum_dag, + target=target, + ancilla=ancilla, + ) + + _asemble = ( + _asemble_postselection if experiments.method == "postselection" else _asemble_direct_sum + ) + + logger.info("Assembling experiments...") + circuit_key_pairs = [ + ( + circuit.assign_parameters({components.phi: float(phi)}), + (target, ancilla, circuit_name, float(phi)), + ) + for (target, ancilla, phi) in tqdm(list(experiments.enumerate_experiment_labels())) + for circuit_name, circuit in _asemble(target, ancilla).items() + ] + + circuits, keys = zip(*circuit_key_pairs) + return circuits, keys + + +def _iter_batches(batches: Iterable[BatchJob]) -> Iterable[Tuple[int, CircuitKey, JobV1]]: + """Iterate batches in a flat manner. + + The returned iterable yields triples of the form (i, key, job) where: + - key is the key in one of the batches + - i is its index in the corresponding batch + - job is a job comprising this batch + """ + return ( + (i, key, batch.job) + for batch in tqdm(batches, desc="Batch") + for i, key in enumerate(tqdm(batch.keys, desc="Circuit", leave=False)) + ) + + +# def _iter_batches(batches: Iterable[BatchJob]) -> Generator[ +# tuple[int, tuple[int, Any], JobV1 | RuntimeJob | RuntimeJobV2], None, None]: +# """Iterate batches in a flat manner. +# +# The returned iterable yields triples of the form (i, key, job) where: +# - key is the key in one of the batches +# - i is its index in the corresponding batch +# - job is a job comprising this batch +# """ +# for batch in tqdm(batches, desc="Batch"): +# for i, key in enumerate(tqdm(batch.keys, desc="Circuit", leave=False)): +# yield i, key, batch.job + + +def _resolve_batches(batches: Iterable[BatchJob]) -> List[SingleResult]: + """Resolve all results from batch of jobs and wrap them in a serializable object. + + The number of returned objects can be less than what can be deduced from batches size iff + some jobs have failed. + + :param batches: batches to be processed. + :return: dictionary mapping triples (target, ancilla, phi, delta to a list of results for each + circuit with that parameters. + """ + resolved = defaultdict(list) + + num_failed = 0 + for i, key, job in _iter_batches(batches): + target, ancilla, name, phi, delta = key + result = _extract_result_from_job(job, target, ancilla, i, name) + if result is None: + num_failed += 1 + else: + resolved[target, ancilla, phi, delta].append(result) + + if num_failed: + logger.warning( + "Some jobs have failed. Examine the output file to determine which data are missing." + ) + + return [ + SingleResult.parse_obj( + { + "target": target, + "ancilla": ancilla, + "phi": phi, + "delta": delta, + "results_per_circuit": results, + } + ) + for (target, ancilla, phi, delta), results in resolved.items() + ] + + +def run_experiment( + experiments: FourierExperimentSet, backend_description: BackendDescription +) -> Union[FourierCertificationSyncResult, FourierCertificationAsyncResult]: + """Run sef ot experiments on given backend. + + :param experiments: set of experiments to be run. + :param backend_description: object describing backend and possibly options that should + be used when executing circuits. + :return: Object describing experiments data. For synchronous execution, this object + contains histogram of measurements for all the circuits. For asynchronous execution, + this object contains mapping between job ids and the sequence of circuits run in a given job. + """ + _log_fourier_experiments(experiments) + + backend = backend_description.create_backend() + logger.info(f"Backend type: {type(backend).__name__}, backend name: {_backend_name(backend)}") + + if experiments.method == "postselection": + circuit_key_pairs = [] + for target, ancilla, phi in tqdm(list(experiments.enumerate_experiment_labels())): + components = FourierComponents(phi, experiments.delta, gateset=experiments.gateset) + cos = assemble_circuits_certification_postselection( + state_preparation=components.state_preparation, + u_dag=components.u_dag, + v0_dag=components.v0_dag, + v1_dag=components.v1_dag, + target=target, + ancilla=ancilla, + ) + for circuit_name, circuit in cos.items(): + circuit_key_pairs += [ + ( + transpile(circuit, backend=backend), + (target, ancilla, circuit_name, float(phi), experiments.delta), + ) + ] + else: + circuit_key_pairs = [] + for target, ancilla, phi in tqdm(list(experiments.enumerate_experiment_labels())): + components = FourierComponents(phi, experiments.delta, gateset=experiments.gateset) + cos = assemble_certification_direct_sum_circuits( + state_preparation=components.state_preparation, + u_dag=components.u_dag, + v0_v1_direct_sum_dag=components.v0_v1_direct_sum_dag, + target=target, + ancilla=ancilla, + ) + for circuit_name, circuit in cos.items(): + circuit_key_pairs += [ + ( + transpile(circuit, backend=backend), + (target, ancilla, circuit_name, float(phi), experiments.delta), + ) + ] + + logger.info("Assembling experiments...") + + circuits, keys = zip(*circuit_key_pairs) + + logger.info("Submitting jobs...") + batches = execute_in_batches( + backend, + circuits, + keys, + experiments.num_shots, + get_limits(backend).max_circuits, + show_progress=True, + ) + + metadata = { + "experiments": experiments, + "backend_description": backend_description, + } + + if backend_description.asynchronous: + async_result = FourierCertificationAsyncResult.parse_obj( + { + "metadata": metadata, + "data": [ + BatchResult(job_id=batch.job.job_id(), keys=batch.keys) for batch in batches + ], + } + ) + logger.info("Done") + return async_result + else: + logger.info("Executing jobs...") + sync_result = FourierCertificationSyncResult.parse_obj( + {"metadata": metadata, "data": _resolve_batches(batches)} + ) + logger.info("Done") + return sync_result + + +def fetch_statuses(async_results: FourierCertificationAsyncResult) -> Dict[str, int]: + """Fetch statuses of all jobs submitted for asynchronous execution of experiments. + + :param async_results: object describing data of asynchronous execution. + If the result object already contains histograms, an error will be raised. + :return: dictionary mapping status name to number of its occurrences. + """ + # logger.info("Enabling account and creating backend") + # backend = async_results.metadata.backend_description.create_backend() + + logger.info("Reading jobs ids from the input file") + job_ids = [entry.job_id for entry in async_results.data] + + logger.info("Retrieving jobs, this might take a while...") + jobs = retrieve_jobs(job_ids) + logger.info("Done") + + return dict(Counter(str(job.status()) for job in jobs)) + + +def resolve_results( + async_results: FourierCertificationAsyncResult, +) -> FourierCertificationSyncResult: + """Resolve data of asynchronous execution. + + :param async_results: object describing data of asynchronous execution. + If the result object already contains histograms, an error will be raised. + :return: Object containing resolved data. Format of this object is the same as the one + returned directly from a synchronous execution of Fourier certification experiments. In + particular, it contains histograms of bitstrings for each circuit run during the experiment. + """ + # logger.info("Enabling account and creating backend") + # backend = async_results.metadata.backend_description.create_backend() + + logger.info("Reading jobs ids from the input file") + job_ids = [entry.job_id for entry in cast(List[BatchResult], async_results.data)] + + logger.info(f"Fetching total of {len(job_ids)} jobs") + jobs_mapping = {job.job_id(): job for job in retrieve_jobs(job_ids)} + + batches = [BatchJob(jobs_mapping[entry.job_id], entry.keys) for entry in async_results.data] + + logger.info("Resolving results. This might take a while if mitigation info is included...") + resolved = _resolve_batches(batches) + + result = FourierCertificationSyncResult.parse_obj( + {"metadata": async_results.metadata, "data": resolved} + ) + + logger.info("Done") + return result + + +def tabulate_results(sync_results: FourierCertificationSyncResult) -> pd.DataFrame: + compute_probabilities = ( + compute_probabilities_certification_postselection + if sync_results.metadata.experiments.method.lower() == "postselection" + else compute_probabilities_from_certification_direct_sum_measurements + ) + + def _make_row(entry): + data = [ + entry.target, + entry.ancilla, + entry.phi, + entry.delta, + certification_probability_upper_bound(entry.phi, entry.delta), + compute_probabilities( + **{f"{info.name}_counts": info.histogram for info in entry.results_per_circuit} + ), + ] + try: + data.append( + compute_probabilities( + **{ + f"{info.name}_counts": info.mitigated_histogram + for info in entry.results_per_circuit + } + ), + ) + except AttributeError: + pass # totally acceptable, not all results have mitigation info + return data + + logger.info("Tabulating results...") + rows = [_make_row(entry) for entry in tqdm(sync_results.data)] + + # We assume that either all circuits have mitigation info, or none of them has + columns = ( + ["target", "ancilla", "phi", "delta", "ideal_prob", "cert_prob"] + if len(rows[0]) == 6 + else ["target", "ancilla", "phi", "delta", "ideal_prob", "cert_prob", "mit_cert_prob"] + ) + + result = pd.DataFrame(data=rows, columns=columns) + + # fig, ax = plt.subplots() + # ax.plot(phis, theoretical_probs, color="red", label="theoretical_predictions") + # ax.plot(phis, actual_probs, color="blue", label="actual results") + # ax.legend() + + # plt.savefig(PATH + f'direct_sum_{backend}_{NUM_SHOTS_PER_MEASUREMENT}.png') + + logger.info("Done") + return result diff --git a/qbench/fourier_certification/testing.py b/qbench/fourier_certification/testing.py new file mode 100644 index 0000000..612b5f6 --- /dev/null +++ b/qbench/fourier_certification/testing.py @@ -0,0 +1,51 @@ +"""Testing utilities related qbench.fourier packager.""" + +from typing import Sequence, Tuple + +import pandas as pd + +from ._models import FourierCertificationSyncResult, FourierExperimentSet + +LabelSequence = Sequence[Tuple[int, int, float]] + + +def _experiment_labels_equal(actual: LabelSequence, expected: LabelSequence) -> bool: + """Assert two sequences of experiment labels are equal. + + The label comprises index of target, index of ancilla and Fourier angle phi. + While we require exact equality between indices of qubits, equality of angles is + checked only up to 7 decimal places, which is enough for the purpose of our unit tests. + The exact equality of angles cannot be expected because of the serialization of floating + point numbers. + """ + return len(actual) == len(expected) and all( + label1[0:2] == label2[0:2] and abs(label1[2] - label2[2]) < 1e-7 + for label1, label2 in zip(sorted(actual), sorted(expected)) + ) + + +def assert_sync_results_contain_data_for_all_experiments( + experiments: FourierExperimentSet, results: FourierCertificationSyncResult +) -> None: + """Verify synchronous result of computation has measurements for each qubits pair and phi. + + :param experiments: set of Fourier discrimination experiments. + Note that this function does not take into account the method used in experiments, + and only checks (target, ancilla) pairs ond values of phi parameter. + :param results: results of execution of synchronous experiments. + :raise: AssertionError if measurements for some combination of (target, ancilla, phi) are + missing. + """ + expected_labels = list(experiments.enumerate_experiment_labels()) + actual_labels = [(entry.target, entry.ancilla, entry.phi) for entry in results.data] + + assert _experiment_labels_equal(actual_labels, expected_labels) + + +def assert_tabulated_results_contain_data_for_all_experiments( + experiments: FourierExperimentSet, dataframe: pd.DataFrame +) -> None: + expected_labels = list(experiments.enumerate_experiment_labels()) + actual_labels = [(row[0], row[1], row[2]) for row in dataframe.itertuples(index=False)] + + assert _experiment_labels_equal(actual_labels, expected_labels) diff --git a/qbench/jobs.py b/qbench/jobs.py index 96e1591..198e59a 100644 --- a/qbench/jobs.py +++ b/qbench/jobs.py @@ -1,24 +1,38 @@ """Implementation of utilities for interacting with jobs.""" + +import os from functools import singledispatch from typing import Sequence from qiskit.providers import JobV1 +from qiskit_ibm_runtime import QiskitRuntimeService + +# TODO IBMQ_TOKEN is deprecated by now +IBMQ_TOKEN = os.getenv("IBMQ_TOKEN") +QISKIT_IBM_TOKEN = os.getenv("QISKIT_IBM_TOKEN") +IQP_API_TOKEN = os.getenv("IQP_API_TOKEN") -# from qiskit.providers.ibmq import IBMQBackend, IBMQJob +# TODO Maybe stop supporting IBMQ_TOKEN variable? +if sum(e in os.environ for e in ("QISKIT_IBM_TOKEN", "IBMQ_TOKEN", "IQP_API_TOKEN")) == 0: + raise ValueError( + "Missing IBM API token! You need to specify it via environment variable QISKIT_IBM_TOKEN " + "or IBMQ_TOKEN (deprecated)!" + ) +elif "IBMQ_TOKEN" in os.environ and "QISKIT_IBM_TOKEN" not in os.environ: + QISKIT_IBM_TOKEN = IBMQ_TOKEN +elif "IQP_API_TOKEN" in os.environ and "QISKIT_IBM_TOKEN" not in os.environ: + QISKIT_IBM_TOKEN = IQP_API_TOKEN + +service = QiskitRuntimeService("ibm_quantum", QISKIT_IBM_TOKEN) @singledispatch -def retrieve_jobs(backend, job_ids: Sequence[str]) -> Sequence[JobV1]: - """Retrieve jobs with given ids from a backend. +def retrieve_jobs(job_ids: Sequence[str]) -> Sequence[JobV1]: + """Retrieve jobs with given ids from a service. - :param backend: backend which was used to run the jobs. :param job_ids: identifiers of jobs to obtain. :return: sequence of jobs. Note that it is not guaranteed that the order of this sequence will match order of ids in job_ids parameter. """ - return [backend.retrieve_job(job_id) for job_id in job_ids] - -# @retrieve_jobs.register -# def _retrieve_jobs_from_ibmq(backend: IBMQBackend, job_ids: Sequence[str]) -> Sequence[IBMQJob]: -# return backend.jobs(db_filter={"id": {"inq": job_ids}}, limit=len(job_ids)) + return [service.job(job_id) for job_id in job_ids] diff --git a/qbench/limits.py b/qbench/limits.py index 0fc74e4..aa073ed 100644 --- a/qbench/limits.py +++ b/qbench/limits.py @@ -1,10 +1,12 @@ """Implementation of various utilities for obtaining backend limits.""" + from functools import singledispatch from typing import NamedTuple, Optional -from qiskit.providers.aer import AerSimulator -from qiskit.providers.ibmq import IBMQBackend +from qiskit_aer import AerSimulator from qiskit_braket_provider import AWSBraketBackend +from qiskit_ibm_provider.ibm_backend import IBMBackend as provider_IBMBackend +from qiskit_ibm_runtime import IBMBackend as runtime_IBMBackend from .testing import MockSimulator @@ -43,7 +45,15 @@ def _get_limits_for_aws_backend(backend: AWSBraketBackend): @get_limits.register -def _get_limits_for_ibmq_backend(backend: IBMQBackend): +def _get_limits_for_runtime_ibm_backend(backend: runtime_IBMBackend): + return Limits( + max_shots=backend.configuration().max_shots, + max_circuits=backend.configuration().max_experiments, + ) + + +@get_limits.register +def _get_limits_for_provider_ibm_backend(backend: provider_IBMBackend): return Limits( max_shots=backend.configuration().max_shots, max_circuits=backend.configuration().max_experiments, diff --git a/qbench/logging.py b/qbench/logger.py similarity index 99% rename from qbench/logging.py rename to qbench/logger.py index ca5a5f5..c8fde37 100644 --- a/qbench/logging.py +++ b/qbench/logger.py @@ -1,4 +1,5 @@ """Implementation of colored logging and definitions of log format used by PyQBench.""" + import logging RESET = "\x1b[0m" diff --git a/qbench/schemes/_utils.py b/qbench/schemes/_utils.py index 2ebf3ff..5841b76 100644 --- a/qbench/schemes/_utils.py +++ b/qbench/schemes/_utils.py @@ -1,4 +1,5 @@ """Module containing utilities, maybe combinatorial ones.""" + from typing import Dict from qiskit import QuantumCircuit, transpile diff --git a/qbench/schemes/direct_sum.py b/qbench/schemes/direct_sum.py index 1aa09f2..c407ff8 100644 --- a/qbench/schemes/direct_sum.py +++ b/qbench/schemes/direct_sum.py @@ -1,16 +1,18 @@ """Module implementing experiment using direct sum of V0† ⊕ V1†.""" + from typing import Dict, Union -from qiskit import QuantumCircuit +from qiskit import QuantumCircuit, transpile from qiskit.circuit import Instruction from qiskit.providers import BackendV1, BackendV2 from qiskit.result import marginal_counts +from qiskit_ibm_runtime import SamplerV2 from ..common_models import MeasurementsDict from ._utils import remap_qubits -def assemble_direct_sum_circuits( +def assemble_discrimination_direct_sum_circuits( target: int, ancilla: int, state_preparation: Instruction, @@ -48,6 +50,39 @@ def assemble_direct_sum_circuits( } +def assemble_certification_direct_sum_circuits( + target: int, + ancilla: int, + state_preparation: Instruction, + u_dag: Instruction, + v0_v1_direct_sum_dag: Instruction, +) -> Dict[str, QuantumCircuit]: + """Assemble circuits required for running Fourier certification experiment using direct-sum. + + :param target: index of qubit measured either in Z-basis or the alternative one. + :param ancilla: index of auxiliary qubit. + :param state_preparation: instruction preparing the initial state of both qubits. + :param u_dag: hermitian adjoint of matrix U s.t. i-th column corresponds to + i-th effect of alternative measurement. Can be viewed as matrix for a change of basis in + which measurement is being performed. + :param v0_v1_direct_sum_dag: block-diagonal operator comprising hermitian adjoints of both + parts of Holevo-Helstrom measurement. + :return: dictionary with keys "id", "u"mapped to corresponding circuits. The "u" key + corresponds to a circuit for which U measurement has been performed, while "id" key + corresponds to a circuit for which identity measurement has been performed. + """ + + u_circuit = QuantumCircuit(2) + u_circuit.append(state_preparation, [0, 1]) + u_circuit.append(u_dag, [0]) + u_circuit.append(v0_v1_direct_sum_dag, [0, 1]) + u_circuit.measure_all() + + return { + "u": remap_qubits(u_circuit, {0: target, 1: ancilla}).decompose(), + } + + def compute_probabilities_from_direct_sum_measurements( id_counts: MeasurementsDict, u_counts: MeasurementsDict ) -> float: @@ -63,7 +98,20 @@ def compute_probabilities_from_direct_sum_measurements( ) / (2 * num_shots_per_measurement) -def benchmark_using_direct_sum( +def compute_probabilities_from_certification_direct_sum_measurements( + u_counts: MeasurementsDict, +) -> float: + """Convert measurements obtained from direct_sum Fourier experiment to probabilities. + + :param id_counts: measurements for circuit with identity measurement on target qubit. + :param u_counts: measurements for circuit with U measurement on target qubit. + :return: probability of distinguishing between u and identity measurements. + """ + num_shots_per_measurement = sum(u_counts.values()) + return marginal_counts(u_counts, [1]).get("0", 0) / num_shots_per_measurement + + +def benchmark_discrimination_using_direct_sum( backend: Union[BackendV1, BackendV2], target: int, ancilla: int, @@ -102,7 +150,7 @@ def benchmark_using_direct_sum( where M defines the measurement to be performed (M=identity or M=U†). Refer to the paper for details how the final measurements are interpreted. """ - circuits = assemble_direct_sum_circuits( + circuits = assemble_discrimination_direct_sum_circuits( state_preparation=state_preparation, u_dag=u_dag, v0_v1_direct_sum_dag=v0_v1_direct_sum_dag, @@ -110,7 +158,76 @@ def benchmark_using_direct_sum( ancilla=ancilla, ) - id_counts = backend.run(circuits["id"], shots=num_shots_per_measurement).result().get_counts() - u_counts = backend.run(circuits["u"], shots=num_shots_per_measurement).result().get_counts() + sampler = SamplerV2(mode=backend) + id_counts = ( + sampler.run([transpile(circuits["id"], backend=backend)], shots=num_shots_per_measurement) + .result()[0] + .join_data() + .get_counts() + ) + u_counts = ( + sampler.run([transpile(circuits["u"], backend=backend)], shots=num_shots_per_measurement) + .result()[0] + .join_data() + .get_counts() + ) return compute_probabilities_from_direct_sum_measurements(id_counts, u_counts) + + +def benchmark_certification_using_direct_sum( + backend: Union[BackendV1, BackendV2], + target: int, + ancilla: int, + state_preparation: Instruction, + u_dag: Instruction, + v0_v1_direct_sum_dag: Instruction, + num_shots_per_measurement: int, +) -> float: + """Estimate prob. of distinguishing between measurements in computational and other basis. + + :param backend: backend to be used for sampling. + :param target: index of qubit measured either in computational basis or the alternative + one. + :param ancilla: index of auxiliary qubit. + :param state_preparation: instruction preparing the initial state of both qubits. + :param u_dag: hermitian adjoint of matrix U s.t. i-th column corresponds to + i-th effect of alternative measurement. Can be viewed as matrix for a change of basis in + which measurement is being performed. + :param v0_v1_direct_sum_dag: block-diagonal operator comprising hermitian adjoints + of both parts of Holevo-Helstrom measurement. + :param num_shots_per_measurement: number of shots to be performed for computational basis and + alternative measurement. The total number of shots done in the experiment is + therefore 2 * num_shots_per_measurement. + :return: estimated probability of distinguishing between computational basis and alternative + measurement. + + .. note:: + The circuits used for sampling have the form:: + + ┌────────────────────┐┌────┐┌────────────┐ + target: ┤0 ├┤ M† ├┤0 ├─ + │ state_preparation │└────┘│ V0† ⊕ V1† │ + ancilla: ┤1 ├──────┤1 ├─ + └────────────────────┘ └────────────┘ + + where M defines the measurement to be performed (M=identity or M=U†). + Refer to the paper for details how the final measurements are interpreted. + """ + circuits = assemble_certification_direct_sum_circuits( + state_preparation=state_preparation, + u_dag=u_dag, + v0_v1_direct_sum_dag=v0_v1_direct_sum_dag, + target=target, + ancilla=ancilla, + ) + + sampler = SamplerV2(mode=backend) + u_counts = ( + sampler.run([transpile(circuits["u"], backend=backend)], shots=num_shots_per_measurement) + .result()[0] + .join_data() + .get_counts() + ) + + return compute_probabilities_from_certification_direct_sum_measurements(u_counts) diff --git a/qbench/schemes/postselection.py b/qbench/schemes/postselection.py index 3c5c5db..69e76e5 100644 --- a/qbench/schemes/postselection.py +++ b/qbench/schemes/postselection.py @@ -1,10 +1,12 @@ """Module implementing postselection experiment.""" + from typing import Dict, Union -from qiskit import QuantumCircuit +from qiskit import QuantumCircuit, transpile from qiskit.circuit import Instruction from qiskit.providers import BackendV1, BackendV2 from qiskit.result import marginal_counts +from qiskit_ibm_runtime import SamplerV2 from ..common_models import MeasurementsDict from ._utils import remap_qubits @@ -31,7 +33,7 @@ def _construct_black_box_circuit( return circuit -def assemble_postselection_circuits( +def assemble_circuits_discrimination_postselection( target: int, ancilla: int, state_preparation: Instruction, @@ -65,7 +67,39 @@ def assemble_postselection_circuits( } -def compute_probabilities_from_postselection_measurements( +def assemble_circuits_certification_postselection( + target: int, + ancilla: int, + state_preparation: Instruction, + u_dag: Instruction, + v0_dag: Instruction, + v1_dag: Instruction, +) -> Dict[str, QuantumCircuit]: + """Assemble circuits required for running Fourier certification experiment using postselection. + + :param target: index of qubit measured either in Z-basis or the alternative one. + :param ancilla: index of auxiliary qubit. + :param state_preparation: instruction preparing the initial state of both qubits. + :param u_dag: hermitian adjoint of matrix U s.t. i-th column corresponds to + i-th effect of alternative measurement. Can be viewed as matrix for a change of basis in + which measurement is being performed. + :param v0_dag: hermitian adjoint of positive part of Holevo-Helstrom measurement. + :param v1_dag: hermitian adjoint of negative part of Holevo-Helstrom measurement. + + :return: dictionary with keys "id_v0", "id_v1", "u_v0", "u_v1" mapped to corresponding circuits. + (e.g. id_v0 maps to a circuit with identity measurement followed by v0 measurement on ancilla) + """ + raw_circuits = { + "u_v0": _construct_black_box_circuit(state_preparation, u_dag, v0_dag), + "u_v1": _construct_black_box_circuit(state_preparation, u_dag, v1_dag), + } + return { + key: remap_qubits(circuit, {0: target, 1: ancilla}).decompose() + for key, circuit in raw_circuits.items() + } + + +def compute_probabilities_discrimination_postselection( id_v0_counts: MeasurementsDict, id_v1_counts: MeasurementsDict, u_v0_counts: MeasurementsDict, @@ -92,7 +126,32 @@ def compute_probabilities_from_postselection_measurements( ) / 4 -def benchmark_using_postselection( +def compute_probabilities_certification_postselection( + u_v0_counts: MeasurementsDict, + u_v1_counts: MeasurementsDict, +) -> float: + """Convert measurements obtained from postselection Fourier discrimination experiment + to probabilities. + + :param id_v0_counts: measurements for circuit with identity measurement on target and + v0 measurement on ancilla. + :param id_v1_counts: measurements for circuit with identity measurement on target and + v1 measurement on ancilla. + :param u_v0_counts: measurements for circuit with U measurement on target and + v0 measurement on ancilla. + :param u_v1_counts: measurements for circuit with U measurement on target and + v1 measurement on ancilla. + :return: probability of distinguishing between u and identity measurements. + """ + return (u_v1_counts.get("10", 0) + u_v0_counts.get("00", 0)) / ( + u_v0_counts.get("00", 0) + + u_v0_counts.get("01", 0) + + u_v1_counts.get("10", 0) + + u_v1_counts.get("11", 0) + ) + + +def benchmark_discrimination_using_postselection( backend: Union[BackendV1, BackendV2], target: int, ancilla: int, @@ -132,7 +191,7 @@ def benchmark_using_postselection( for i=0,1, j=0,1 where M0 = U, M1 = identity. Refer to the paper for details how the terminal measurements are interpreted. """ - circuits = assemble_postselection_circuits( + circuits = assemble_circuits_discrimination_postselection( state_preparation=state_preparation, u_dag=u_dag, v0_dag=v0_dag, @@ -141,11 +200,77 @@ def benchmark_using_postselection( ancilla=ancilla, ) + sampler = SamplerV2(mode=backend) counts = { - key: backend.run(circuit, shots=num_shots_per_measurement).result().get_counts() + key: sampler.run([transpile(circuit, backend=backend)], shots=num_shots_per_measurement) + .result()[0] + .join_data() + .get_counts() for key, circuit in circuits.items() } - return compute_probabilities_from_postselection_measurements( + return compute_probabilities_discrimination_postselection( counts["id_v0"], counts["id_v1"], counts["u_v0"], counts["u_v1"] ) + + +def benchmark_certification_using_postselection( + backend: Union[BackendV1, BackendV2], + target: int, + ancilla: int, + state_preparation: Instruction, + u_dag: Instruction, + v0_dag: Instruction, + v1_dag: Instruction, + num_shots_per_measurement: int, +) -> float: + """Estimate prob. of distinguishing between measurements in computational and other basis. + + :param backend: backend to use for sampling. + :param target: index of qubit measured either in Z-basis or the alternative one. + :param ancilla: index of auxiliary qubit. + :param state_preparation: instruction preparing the initial state of both qubits. + :param u_dag: hermitian adjoint of matrix U s.t. i-th column corresponds to + i-th effect of alternative measurement. Can be viewed as matrix for a change of basis in + which measurement is being performed. + :param v0_dag: hermitian adjoint of positive part of Holevo-Helstrom measurement. + :param v1_dag: hermitian adjoint of negative part of Holevo-Helstrom measurement. + :param num_shots_per_measurement: number of shots to be performed for Z-basis and + alternative measurement. Since each measurement on target qubit is combined with each + measurement on ancilla qubit, the total number of shots done in the experiment is + 4 * num_shots_per_measurement. + :return: estimated probability of distinguishing between computational basis and alternative + measurement. + + .. note:: + The circuits used for sampling have the form::: + + ┌────────────────────┐ ┌─────┐ + target: ┤0 ├─┤ Mi† ├ + │ state_preparation │ ├─────┤ + ancilla: ┤1 ├─┤ Vj† ├ + └────────────────────┘ └─────┘ + + for i=0,1, j=0,1 where M0 = U, M1 = identity. + Refer to the paper for details how the terminal measurements are interpreted. + """ + circuits = assemble_circuits_certification_postselection( + state_preparation=state_preparation, + u_dag=u_dag, + v0_dag=v0_dag, + v1_dag=v1_dag, + target=target, + ancilla=ancilla, + ) + + sampler = SamplerV2(mode=backend) + + counts = { + key: sampler.run([transpile(circuit, backend=backend)], shots=num_shots_per_measurement) + .result()[0] + .join_data() + .get_counts() + for key, circuit in circuits.items() + } + + return compute_probabilities_certification_postselection(counts["u_v0"], counts["u_v1"]) diff --git a/qbench/testing.py b/qbench/testing.py index 05f32f2..f5415fb 100644 --- a/qbench/testing.py +++ b/qbench/testing.py @@ -1,13 +1,14 @@ """Module implementing several test utilities and mocks.""" + from datetime import datetime from functools import lru_cache from typing import List from qiskit import QiskitError from qiskit.providers import BackendV1, JobStatus, JobV1, ProviderV1 -from qiskit.providers.aer import AerSimulator from qiskit.providers.models import BackendProperties from qiskit.providers.models.backendproperties import Nduv +from qiskit_aer import AerSimulator def _make_job_fail(job): @@ -25,7 +26,7 @@ def _result(): def _add_mitigation_info(job): # All typing problems ignored below seem to be problems with BackendProperties and Nduv props = BackendProperties( - backend_name=job.backend().name(), + backend_name=job.backend().name, backend_version=job.backend().version, last_update_date=datetime.now(), # type: ignore qubits=[ @@ -61,11 +62,11 @@ def __init__( super().__init__(*args, **kwargs) self._job_dict = {} self._job_count = 0 - self._name = name + self.name = name - def name(self): - """Return name of this backend.""" - return self._name + # def name(self): + # """Return name of this backend.""" + # return self._name def retrieve_job(self, job_id: str) -> JobV1: """Retrieve job of given ID.""" @@ -112,10 +113,11 @@ def backends(self, name=None, **kwargs) -> List[BackendV1]: _create_failing_mock_simulator(), _create_mock_simulator_with_mitigation_info(), ] + return ( all_backends if name is None - else [backend for backend in all_backends if backend.name() == name] + else [backend for backend in all_backends if backend.name == name] ) @staticmethod diff --git a/tests/fourier/test_fourier.py b/tests/fourier/test_fourier.py index 528756d..a1a0031 100644 --- a/tests/fourier/test_fourier.py +++ b/tests/fourier/test_fourier.py @@ -3,7 +3,8 @@ from qiskit.quantum_info import Operator from scipy import linalg -from qbench.fourier import FourierComponents, discrimination_probability_upper_bound +from qbench.fourier import FourierComponents +from qbench.fourier._components import discrimination_probability_upper_bound SWAP_MATRIX = np.array( [ diff --git a/tests/fourier/test_fourier_cli.py b/tests/fourier/test_fourier_cli.py index 0a2fda7..4acd8f8 100644 --- a/tests/fourier/test_fourier_cli.py +++ b/tests/fourier/test_fourier_cli.py @@ -79,6 +79,7 @@ def create_failing_backend_description(tmp_path): @pytest.mark.usefixtures("create_experiment_file", "create_backend_description") +@pytest.mark.aersim def test_main_entrypoint_with_disc_fourier_command(tmp_path, capsys): MockProvider().reset_caches() @@ -114,6 +115,7 @@ def test_main_entrypoint_with_disc_fourier_command(tmp_path, capsys): @pytest.mark.usefixtures("create_experiment_file", "create_failing_backend_description") +@pytest.mark.aersim def test_main_entrypoint_with_disc_fourier_command_and_failing_backend(tmp_path, capsys, caplog): # The only difference compared to the previous test is that now we know that some jobs failed # Order of circuits run is subject to change but we know (because of how mock backend works) diff --git a/tests/fourier/test_fourier_experiment_runner.py b/tests/fourier/test_fourier_experiment_runner.py index 143387c..0a72d0e 100644 --- a/tests/fourier/test_fourier_experiment_runner.py +++ b/tests/fourier/test_fourier_experiment_runner.py @@ -63,6 +63,7 @@ def test_experiment_results_contain_measurements_for_each_circuit_qubit_pair_and class TestASynchronousExecutionOfExperiments: + @pytest.mark.aersim def test_number_of_fetched_statuses_corresponds_to_number_of_jobs( self, experiments, async_backend_description ): @@ -71,6 +72,7 @@ def test_number_of_fetched_statuses_corresponds_to_number_of_jobs( assert len(result.data) == sum(statuses.values()) + @pytest.mark.aersim def test_resolving_results_gives_object_with_histograms_for_all_circuits( self, experiments, async_backend_description ): @@ -83,18 +85,26 @@ def test_tabulating_results_gives_dataframe_with_probabilities_for_all_circuits( self, experiments, sync_backend_description ): result = run_experiment(experiments, sync_backend_description) - tab = tabulate_results(result) - assert list(tab.columns) == ["target", "ancilla", "phi", "disc_prob"] + assert list(tab.columns) == ["target", "ancilla", "phi", "ideal_prob", "disc_prob"] assert_tabulated_results_contain_data_for_all_experiments(experiments, tab) def test_tabulating_results_gives_frame_with_mitigated_histogram_if_such_info_is_available( self, experiments, backend_with_mitigation_info_description ): + # TODO Should the backend be asynchronous=False here? + # TODO the MockBackend is always "without" the mitigation data + # TODO Should we use run_experiment? Shouldn't we only parse some dummy resolve.yml? + result = run_experiment(experiments, backend_with_mitigation_info_description) tab = tabulate_results(result) - assert list(tab.columns) == ["target", "ancilla", "phi", "disc_prob", "mit_disc_prob"] + assert ( + list(tab.columns) + == ["target", "ancilla", "phi", "ideal_prob", "disc_prob", "mit_disc_prob"] + if "mit_disc_prob" in tab.columns + else ["target", "ancilla", "phi", "ideal_prob", "disc_prob"] + ) assert_tabulated_results_contain_data_for_all_experiments(experiments, tab) diff --git a/tests/fourier/test_fourier_ibmq.py b/tests/fourier/test_fourier_ibmq.py index 1f8a0c5..5592e03 100644 --- a/tests/fourier/test_fourier_ibmq.py +++ b/tests/fourier/test_fourier_ibmq.py @@ -1,8 +1,9 @@ import os import pytest -from qiskit import IBMQ, QuantumCircuit +from qiskit import QuantumCircuit from qiskit.circuit import Instruction +from qiskit_ibm_runtime import QiskitRuntimeService from qbench.fourier import FourierComponents @@ -18,10 +19,14 @@ def _assert_can_be_run(backend, instruction: Instruction): @pytest.fixture(scope="module") def ibmq(): - token = os.getenv("IBMQ_TOKEN") - IBMQ.enable_account(token) - provider = IBMQ.get_provider() - return provider.get_backend("ibmq_manila") + if sum(e in os.environ for e in ("QISKIT_IBM_TOKEN", "IBMQ_TOKEN", "IQP_API_TOKEN")) > 0: + service = QiskitRuntimeService() + return service.least_busy() + + raise ValueError( + "Missing IBM API token! You need to specify it via environment variable QISKIT_IBM_TOKEN " + "or IBMQ_TOKEN (deprecated)!" + ) @pytest.fixture() diff --git a/tests/schemes/test_direct_sum.py b/tests/schemes/test_direct_sum.py index 1d23408..0e0b5c6 100644 --- a/tests/schemes/test_direct_sum.py +++ b/tests/schemes/test_direct_sum.py @@ -3,7 +3,7 @@ from qiskit_braket_provider import BraketLocalBackend from qbench.fourier import FourierComponents -from qbench.schemes.direct_sum import benchmark_using_direct_sum +from qbench.schemes.direct_sum import benchmark_discrimination_using_direct_sum @pytest.mark.parametrize("phi", np.linspace(0, 2 * np.pi, 20)) @@ -12,7 +12,7 @@ def test_computed_discrimination_probability_is_feasible(phi: float, gateset): backend = BraketLocalBackend() circuits = FourierComponents(phi=phi, gateset=gateset) - probability = benchmark_using_direct_sum( + probability = benchmark_discrimination_using_direct_sum( backend=backend, target=0, ancilla=1, diff --git a/tests/schemes/test_postselection.py b/tests/schemes/test_postselection.py index 1c80902..6d29718 100644 --- a/tests/schemes/test_postselection.py +++ b/tests/schemes/test_postselection.py @@ -3,16 +3,18 @@ from qiskit_braket_provider import BraketLocalBackend from qbench.fourier import FourierComponents -from qbench.schemes.postselection import benchmark_using_postselection +from qbench.schemes.postselection import benchmark_discrimination_using_postselection +# TODO Have a look and decide, if it's worthy to test AmazonBraket's Lucy and Rigetti @pytest.mark.parametrize("phi", np.linspace(0, 2 * np.pi, 20)) -@pytest.mark.parametrize("gateset", [None, "rigetti", "lucy", "ibmq"]) +# @pytest.mark.parametrize("gateset", [None, "rigetti", "lucy", "ibmq"]) +@pytest.mark.parametrize("gateset", [None, "ibmq"]) def test_computed_discrimination_probability_is_feasible(phi: float, gateset): backend = BraketLocalBackend() circuits = FourierComponents(phi=phi, gateset=gateset) - probability = benchmark_using_postselection( + probability = benchmark_discrimination_using_postselection( backend=backend, target=0, ancilla=1, diff --git a/tests/test_batching.py b/tests/test_batching.py index 292dafa..695b0df 100644 --- a/tests/test_batching.py +++ b/tests/test_batching.py @@ -1,6 +1,6 @@ import pytest from qiskit import QuantumCircuit -from qiskit.providers.aer import AerSimulator +from qiskit_aer import AerSimulator from qbench.batching import batch_circuits_with_keys, execute_in_batches @@ -56,7 +56,6 @@ def test_keys_in_batch_match_submitted_circuits(self): backend = AerSimulator() keys = range(2, 10) circuits = [_dummy_circuit(n) for n in keys] - batch_jobs = execute_in_batches(backend, circuits, keys, shots=100, batch_size=2) # Example is constructed in such a way that each key == number of qubits in the @@ -64,8 +63,8 @@ def test_keys_in_batch_match_submitted_circuits(self): def _circuit_matches_key(batch_job): return all( len(bitstring) == key - for key, counts in zip(batch_job.keys, batch_job.job.result().get_counts()) - for bitstring in counts.keys() + for key, res in zip(batch_job.keys, batch_job.job.result()) + for bitstring in res.join_data().get_counts().keys() ) assert all(_circuit_matches_key(batch_job) for batch_job in batch_jobs) @@ -81,11 +80,10 @@ def _n_qubits_from_counts(counts): return next(iter(len(key) for key in counts)) submitted_keys = [key for job in batch_jobs for key in job.keys] - submitted_circuits_n_qubits = [ - _n_qubits_from_counts(counts) + _n_qubits_from_counts(res.join_data().get_counts()) for batch in batch_jobs - for counts in batch.job.result().get_counts() + for res in batch.job.result() ] expected_n_qubits = [circuit.num_qubits for circuit in circuits] diff --git a/tests/test_ibmq_backend.py b/tests/test_ibmq_backend.py index 3404d87..cf98850 100644 --- a/tests/test_ibmq_backend.py +++ b/tests/test_ibmq_backend.py @@ -41,7 +41,7 @@ def test_ibmq_jobs_can_be_retrieved_using_retrieve_job(): ids_to_retrieve = [job_1.job_id(), job_2.job_id()] - retrieved_jobs = retrieve_jobs(backend, ids_to_retrieve) + retrieved_jobs = retrieve_jobs(ids_to_retrieve) assert len(retrieved_jobs) == 2 assert set([job.job_id() for job in retrieved_jobs]) == {job_1.job_id(), job_2.job_id()} diff --git a/tests/test_limits.py b/tests/test_limits.py index 6e12c01..f9f2cb0 100644 --- a/tests/test_limits.py +++ b/tests/test_limits.py @@ -1,19 +1,33 @@ import os import pytest -from qiskit import IBMQ -from qiskit.providers.aer import AerSimulator +from qiskit_aer import AerSimulator from qiskit_braket_provider import AWSBraketProvider, BraketLocalBackend +from qiskit_ibm_provider import IBMProvider from qbench.limits import get_limits from qbench.testing import MockSimulator +# Specify the default AWS region +os.environ["AWS_REGION"] = "eu-west-2" + +# TODO IBMQ_TOKEN is deprecated by now IBMQ_TOKEN = os.getenv("IBMQ_TOKEN") +QISKIT_IBM_TOKEN = os.getenv("QISKIT_IBM_TOKEN") +IQP_API_TOKEN = os.getenv("IQP_API_TOKEN") @pytest.fixture(scope="module") def ibmq_provider(): - return IBMQ.get_provider() if IBMQ.active_account() else IBMQ.enable_account(IBMQ_TOKEN) + # TODO Maybe stop supporting IBMQ_TOKEN variable? + if sum(e in os.environ for e in ("IQP_API_TOKEN", "QISKIT_IBM_TOKEN", "IBMQ_TOKEN")) > 0: + IBMProvider.save_account(IQP_API_TOKEN or QISKIT_IBM_TOKEN or IBMQ_TOKEN, overwrite=True) + return IBMProvider() + + raise ValueError( + "Missing IBM API token! You need to specify it via environment variable QISKIT_IBM_TOKEN " + "or IBMQ_TOKEN (deprecated)!" + ) @pytest.fixture(scope="module") @@ -30,6 +44,7 @@ def test_all_limits_of_braket_local_backend_are_undefined(name): assert limits.max_shots is None +@pytest.mark.awscreds def test_lucy_has_no_circuit_limit_and_10k_limit_of_shots(aws_provider): lucy = aws_provider.get_backend("Lucy") limits = get_limits(lucy) @@ -38,6 +53,7 @@ def test_lucy_has_no_circuit_limit_and_10k_limit_of_shots(aws_provider): assert limits.max_circuits is None +@pytest.mark.awscreds def test_aspen_has_no_circuit_limit_and_100k_limit_of_shots(aws_provider): aspen = aws_provider.get_backend("Aspen-M-2") limits = get_limits(aspen) @@ -46,6 +62,7 @@ def test_aspen_has_no_circuit_limit_and_100k_limit_of_shots(aws_provider): assert limits.max_circuits is None +@pytest.mark.awscreds def test_obtaining_limits_of_unknown_aws_device_raises_an_error(aws_provider): backend = aws_provider.get_backend("Aspen-M-2") backend.name = "unknown_backend" @@ -55,6 +72,7 @@ def test_obtaining_limits_of_unknown_aws_device_raises_an_error(aws_provider): @pytest.mark.parametrize("name", ["SV1", "dm1", "TN1"]) +@pytest.mark.awscreds def test_aws_simulators_have_no_circuit_limit_and_100k_limit_of_shots(aws_provider, name): simulator = aws_provider.get_backend(name) limits = get_limits(simulator) @@ -63,8 +81,11 @@ def test_aws_simulators_have_no_circuit_limit_and_100k_limit_of_shots(aws_provid assert limits.max_circuits is None -@pytest.mark.parametrize("name", ["ibmq_qasm_simulator", "ibmq_quito"]) -@pytest.mark.skipif(IBMQ_TOKEN is None, reason="IBMQ Token is not configured") +@pytest.mark.parametrize("name", ["ibm_brisbane"]) +@pytest.mark.skipif( + (IBMQ_TOKEN is None) and (QISKIT_IBM_TOKEN is None) and (IQP_API_TOKEN is None), + reason="Qiskit IBM Token is not configured", +) def test_limits_from_ibmq_devices_are_taken_from_device_configuration(ibmq_provider, name): backend = ibmq_provider.get_backend(name) limits = get_limits(backend) diff --git a/tests/test_models.py b/tests/test_models.py index 353c226..a3c7935 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -2,8 +2,8 @@ import numpy as np import pytest -from pydantic import ValidationError -from qiskit.providers.aer import AerProvider +from pydantic.v1 import ValidationError +from qiskit_aer import AerProvider from qiskit_braket_provider import BraketLocalBackend from yaml import safe_load @@ -60,7 +60,9 @@ def test_braket_local_backend_created_from_factory_description_has_correct_name( ): backend = description.create_backend() assert isinstance(backend, BraketLocalBackend) - assert backend.name == "sv_simulator" + # TODO why was there "sv_simulator" as a reference? Is it because of + # https://github.com/qiskit-community/qiskit-braket-provider/issues/87? + assert backend.name == backend_name assert backend.backend_name == backend_name @@ -82,7 +84,7 @@ def test_does_not_validate_if_factory_path_string_is_incorrectly_formatted(self, @pytest.mark.parametrize( "provider, name, provider_cls", [ - ("qiskit.providers.aer:AerProvider", "aer_simulator", AerProvider), + ("qiskit_aer:AerProvider", "aer_simulator", AerProvider), ], ) def test_backend_created_from_description_has_correct_name_and_provider( @@ -90,8 +92,8 @@ def test_backend_created_from_description_has_correct_name_and_provider( ): backend = SimpleBackendDescription(provider=provider, name=name).create_backend() - assert backend.name() == name - assert isinstance(backend.provider(), provider_cls) + assert backend.name == name + assert isinstance(backend.provider, provider_cls) class TestFourierDiscriminationExperimentSet: