diff --git a/docs/guides/access-groups.mdx b/docs/guides/access-groups.mdx index 23572ae1737..94a0fad04ee 100644 --- a/docs/guides/access-groups.mdx +++ b/docs/guides/access-groups.mdx @@ -39,7 +39,8 @@ When you create this access group, include the following policies: Users with the `viewer` platform management role on "all account management services" can also view services such as billing. If you want to prevent this extra view access, use the IBM Cloud CLI to give them access to just Resource groups: ```cli - ibmcloud iam access-group-policy-create --roles Viewer --resource-type resource-group + ibmcloud iam access-group-policy-create ( --roles Viewer + --resource-type resource-group) ``` @@ -57,7 +58,11 @@ ibmcloud iam access-group-create GROUP_NAME [-d, --description DESCRIPTION] To create an access group _policy_ by using the CLI, use the [`ibmcloud iam access-group-policy-create`](https://cloud.ibm.com/docs/cli?topic=cli-ibmcloud_commands_iam#ibmcloud_iam_access_group_policy_create) command. ```cli -ibmcloud iam access-group-policy-create GROUP_NAME {-f, --file @JSON_FILE | --roles ROLE_NAME1,ROLE_NAME2... [--service-name SERVICE_NAME] [--service-instance SERVICE_INSTANCE] [--region REGION] [--resource-type RESOURCE_TYPE] [--resource RESOURCE] [--resource-group-name RESOURCE_GROUP_NAME] [--resource-group-id RESOURCE_GROUP_ID]} +ibmcloud iam access-group-policy-create GROUP_NAME {-f, --file @JSON_FILE | --roles ROLE_NAME1,ROLE_NAME2... + [--service-name SERVICE_NAME] [--service-instance SERVICE_INSTANCE] [--region REGION] + [--resource-type RESOURCE_TYPE] [--resource RESOURCE] [--resource-group-name RESOURCE_GROUP_NAME] + [--resource-group-id RESOURCE_GROUP_ID] + } ``` You can use the following JSON code to create policies for an Administrators access group: diff --git a/docs/guides/cloud-setup-rest-api.mdx b/docs/guides/cloud-setup-rest-api.mdx index f27b12e09b6..537276d5e80 100644 --- a/docs/guides/cloud-setup-rest-api.mdx +++ b/docs/guides/cloud-setup-rest-api.mdx @@ -67,7 +67,9 @@ Choose the appropriate authentication method, depending on your working environm 1. To set the IQP_API_TOKEN environment variable in your system, you can add the following line to your shell profile (for example, .bashrc or .zshrc) or by setting it directly in your terminal: ```shell - export IQP_API_TOKEN= # Use the 44-character API_KEY you created and saved from the IBM Quantum Platform Home dashboard + export IQP_API_TOKEN= + # Use the 44-character API_KEY you created and saved + # from the IBM Quantum Platform Home dashboard ``` When you invoke the environment variable in your code, include `import os`, as in this example: diff --git a/docs/guides/debug-qiskit-runtime-jobs.ipynb b/docs/guides/debug-qiskit-runtime-jobs.ipynb index 2a9969ccd60..7aca9ff1a30 100644 --- a/docs/guides/debug-qiskit-runtime-jobs.ipynb +++ b/docs/guides/debug-qiskit-runtime-jobs.ipynb @@ -104,7 +104,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "a45a6d9e-de39-4586-8395-a7f580f0e0dc", "metadata": {}, "outputs": [], @@ -114,7 +114,9 @@ "backend = service.least_busy(operational=True, simulator=False)\n", "\n", "# Generate a preset pass manager\n", - "# This will be used to convert the abstract circuit to an equivalent Instruction Set Architecture (ISA) circuit.\n", + "# This will be used to convert the abstract circuit to an equivalent\n", + "# Instruction Set Architecture (ISA) circuit.\n", + "\n", "pm = generate_preset_pass_manager(backend=backend, optimization_level=0)\n", "\n", "# Set the random seed\n", diff --git a/docs/guides/estimator-input-output.ipynb b/docs/guides/estimator-input-output.ipynb index db3d978eb85..b662dbfc542 100644 --- a/docs/guides/estimator-input-output.ipynb +++ b/docs/guides/estimator-input-output.ipynb @@ -222,7 +222,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "defbebd5-f09f-4596-aff0-ae88cbf2555c", "metadata": {}, "outputs": [ @@ -252,19 +252,23 @@ ], "source": [ "print(\n", - " f\"The result of the submitted job had {len(result)} PUB and has a value:\\n {result}\\n\"\n", + " f\"The result of the submitted job had {len(result)} PUB and has a value:\\n \"\n", + " \"{result}\\n\"\n", ")\n", "print(\n", - " f\"The associated PubResult of this job has the following data bins:\\n {result[0].data}\\n\"\n", + " f\"The associated PubResult of this job has the following data bins:\\n \"\n", + " \"{result[0].data}\\n\"\n", ")\n", "print(f\"And this DataBin has attributes: {result[0].data.keys()}\")\n", "print(\n", - " \"Recall that this shape is due to our array of parameter binding sets having shape (100, 2) -- where 2 is the\\n\\\n", - " number of parameters in the circuit -- combined with our array of observables having shape (3, 1). \\n\"\n", + " \"Recall that this shape is due to our array of parameter binding sets\"\n", + " \"having shape (100, 2), where 2 is the number of parameters in the \"\n", + " \"circuit, combined with our array of observables having shape (3, 1). \\n\"\n", ")\n", "with np.printoptions(threshold=200):\n", " print(\n", - " f\"The expectation values measured from this PUB are: \\n{result[0].data.evs}\"\n", + " f\"The expectation values measured from this PUB are: \\n\"\n", + " \"{result[0].data.evs}\\n\"\n", " )" ] }, diff --git a/docs/guides/function-template-chemistry-workflow.ipynb b/docs/guides/function-template-chemistry-workflow.ipynb index 7a32734dd1b..26f6e8c0f00 100644 --- a/docs/guides/function-template-chemistry-workflow.ipynb +++ b/docs/guides/function-template-chemistry-workflow.ipynb @@ -1722,7 +1722,8 @@ }, "outputs": [], "source": [ - "# This cell is hidden from users. It verifies both source listings are identical then deletes the working folder we created\n", + "# This cell is hidden from users. It verifies both source listings are identical\n", + "# then deletes the working folder we created\n", "import shutil\n", "\n", "with open(\"./source_files/sqd_pcm_entrypoint.py\") as f1:\n", diff --git a/docs/guides/function-template-hamiltonian-simulation.ipynb b/docs/guides/function-template-hamiltonian-simulation.ipynb index ef3b33bab66..c16d7e89733 100644 --- a/docs/guides/function-template-hamiltonian-simulation.ipynb +++ b/docs/guides/function-template-hamiltonian-simulation.ipynb @@ -110,7 +110,8 @@ "\n", "# Extract parameters from arguments\n", "#\n", - "# Do this at the top of the program so it fails early if any required arguments are missing or invalid.\n", + "# Do this at the top of the program so it fails early if any required arguments are missing or\n", + "# invalid.\n", "\n", "arguments = get_arguments()\n", "\n", @@ -1146,7 +1147,8 @@ "\n", "# Extract parameters from arguments\n", "#\n", - "# Do this at the top of the program so it fails early if any required arguments are missing or invalid.\n", + "# Do this at the top of the program so it fails early if any required arguments\n", + "# are missing or invalid.\n", "\n", "arguments = get_arguments()\n", "\n", @@ -1406,7 +1408,8 @@ }, "outputs": [], "source": [ - "# This cell is hidden from users. It verifies both source listings are identical then deletes the working folder we created\n", + "# This cell is hidden from users. It verifies both source listings are identical then deletes the\n", + "# working folder we created\n", "import shutil\n", "\n", "with open(\"./source_files/template_hamiltonian_simulation.py\") as f1:\n", diff --git a/docs/guides/ibm-circuit-function.ipynb b/docs/guides/ibm-circuit-function.ipynb index e75921903c3..3cbd07fb814 100644 --- a/docs/guides/ibm-circuit-function.ipynb +++ b/docs/guides/ibm-circuit-function.ipynb @@ -98,7 +98,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "d56e1440", "metadata": {}, "outputs": [], @@ -106,9 +106,10 @@ "from qiskit.circuit.random import random_circuit\n", "from qiskit_ibm_runtime import QiskitRuntimeService\n", "\n", - "# You can skip this step if you have a target backend, e.g.\n", - "# backend_name = \"ibm_brisbane\"\n", - "# You'll need to specify the credentials when initializing QiskitRuntimeService, if they were not previously saved.\n", + "# You can skip this step if you have a target backend,\n", + "# e.g. backend_name = \"ibm_brisbane\"\n", + "# You'll need to specify the credentials when initializing QiskitRuntimeService,\n", + "# if they were not previously saved.\n", "service = QiskitRuntimeService()\n", "backend = service.least_busy(operational=True, simulator=False)\n", "\n", diff --git a/docs/guides/sampler-options.ipynb b/docs/guides/sampler-options.ipynb index 8551f196a02..5bf1f85454d 100644 --- a/docs/guides/sampler-options.ipynb +++ b/docs/guides/sampler-options.ipynb @@ -149,7 +149,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "e06376f4-fcba-4d03-8cbc-c53ca02d81af", "metadata": {}, "outputs": [ @@ -193,7 +193,8 @@ "# This uses auto-complete.\n", "sampler.options.default_shots = 2000\n", "\n", - "# This does bulk update. The value for default_shots is overridden if you specify shots with run() or in the PUB.\n", + "# This does bulk update. The value for default_shots is overridden\n", + "# if you specify shots with run() or in the PUB.\n", "sampler.options.update(\n", " default_shots=1024, dynamical_decoupling={\"sequence_type\": \"XpXm\"}\n", ")\n", diff --git a/docs/guides/sampler-rest-api.mdx b/docs/guides/sampler-rest-api.mdx index cbfa2531185..644883ffc56 100644 --- a/docs/guides/sampler-rest-api.mdx +++ b/docs/guides/sampler-rest-api.mdx @@ -1,347 +1,348 @@ ---- -title: ampler with the REST API -description: How to use the Sampler primitive with the Qiskit Runtime REST API. ---- -{/* cspell:ignore IIZII, XIZZZ, accum */} - - -# Sampler with the REST API - -The steps in this topic describe how to run and configure workloads with the REST API, and demonstrate how to invoke them in any program of your choice. - - - This documentation utilizes the Python `requests` module to demonstrate the Qiskit Runtime REST API. However, this workflow can be executed using any language or framework that supports working with REST APIs. Refer to the [API reference documentation](/docs/api/qiskit-ibm-runtime/tags/jobs) for details. - - -## 1. Initialize the account - -Because Qiskit Runtime Sampler is a managed service, you first need to initialize your account. You can then select the device you want to use to run your calculations on. - - -Find details on how to initialize your account, view available backends, and work with tokens in [Set up to use IBM Quantum Platform with REST API](/docs/guides/cloud-setup-rest-api). - - -## 2. Create a QASM circuit - -You need at least one circuit as the input to the Sampler primitive. - - -Define a QASM quantum circuit: - -```python -qasm_string=''' -OPENQASM 3; -include "stdgates.inc"; -qreg q[2]; -creg c[2]; -x q[0]; -cx q[0], q[1]; -c[0] = measure q[0]; -c[1] = measure q[1]; -''' -``` - - -The code snippets given below assume that the `qasm_string` has been transpiled to a new string `resulting_qasm`. - - -## 3. Run the quantum circuit using Sampler V2 API - - - - The jobs below use [Qiskit Runtime V2 primitives](/docs/guides/v2-primitives). `SamplerV2` takes one or more primitive unified blocs (PUBs) as the input. Each PUB is a tuple that contains one circuit and the data broadcasted to that circuit, which can be multiple parameters, and returns one result per PUB. - - - -```python -import requests - -url = 'https://quantum.cloud.ibm.com/api/v1/jobs' -auth_id = "Bearer " -crn = "" -backend = "" - -headers = { - 'Content-Type': 'application/json', - 'Authorization':auth_id, - 'Service-CRN': crn - } -job_input = { - 'program_id': 'sampler', - "backend": backend, - "params": { - "pubs": [[resulting_qasm],[resulting_qasm,None,500]] # primitive unified blocs (PUBs) containing one circuit each. -}} - -response = requests.post(url, headers=headers, json=job_input) - -if response.status_code == 200: - job_id = response.json().get('id') - print("Job created:",response.text) -else: - print(f"Error: {response.status_code}") -``` - - - -## 4. Check job status and get results - -Next, pass the `job_id` to the API: - -```python -response_status_singlejob= requests.get(url+'/'+job_id, headers=headers) -response_status_singlejob.json().get('state') -``` -Output - -```text ->>> Job ID: 58223448-5100-4dec-a47a-942fb30edced ->>> Job Status: JobStatus.RUNNING -``` - -Get job results: -```python -response_result= requests.get(url+'/'+job_id+'/results', headers=headers) - -res_dict=response_result.json() - -# Get results for the first PUB -counts=res_dict['results'][0]['data']['c']['samples'] - -print(counts[:20]) -``` -Output -```text -['0x3', '0x0', '0x2', '0x1', '0x0', '0x3', '0x0', '0x3', '0x1', '0x2', '0x2', '0x0', '0x2', '0x0', '0x3', '0x3', '0x2', '0x0', '0x1', '0x0'] -``` - -## 5. Work with Qiskit Runtime options - -Error mitigation techniques allow users to mitigate circuit errors by modeling the device noise at the time of execution. This typically results in quantum pre-processing overhead related to model training, and classical post-processing overhead to mitigate errors in the raw results by using the generated model. - -The error mitigation techniques built in to primitives are advanced resilience options. To specify these options, use the `resilience_level` option when submitting your job. -Sampler V2 does not support specifying resilience levels. However, you can turn on or off individual error mitigation / suppression methods. - -The following examples demonstrate the default options for dynamical decoupling and twirling. Find more options and further details in the [Error mitigation and suppression techniques](/docs/guides/error-mitigation-and-suppression-techniques) topic. - -### Dynamical decoupling -```python -import requests - -url = 'https://quantum.cloud.ibm.com/api/v1/jobs' -auth_id = "Bearer " -crn = "" -backend = "" - -headers = { - 'Content-Type': 'application/json', - 'Authorization':auth_id, - 'Service-CRN': crn - } -job_input = { - 'program_id': 'sampler', - "backend": backend, - "params": { - "pubs": [[resulting_qasm]], # primitive unified blocs (PUBs) containing one circuit each. - "options": { - "dynamical_decoupling": { - "enable": True, - "sequence_type": 'XpXm', - "extra_slack_distribution": 'middle', - "scheduling_method": 'alap', - }, - }, - } -} - -response = requests.post(url, headers=headers, json=job_input) - -if response.status_code == 200: - job_id = response.json().get('id') - print("Job created:",response.text) -else: - print(f"Error: {response.status_code}") -``` -### Twirling -```python -import requests - -url = 'https://quantum.cloud.ibm.com/api/v1/jobs' -auth_id = "Bearer " -crn = "" -backend = "" - -headers = { - 'Content-Type': 'application/json', - 'Authorization':auth_id, - 'Service-CRN': crn - } -job_input = { - 'program_id': 'sampler', - "backend": backend, - "params": { - "pubs": [[resulting_qasm]], # primitive unified blocs (PUBs) containing one circuit each. - "options": { - "twirling": { - "enable_gates": True, - "enable_measure": True, - "num_randomizations": "auto", - "shots_per_randomization": "auto", - "strategy": "active-accum", - }, - }, - } -} - -response = requests.post(url, headers=headers, json=job_input) - -if response.status_code == 200: - job_id = response.json().get('id') - print("Job created:",response.text) -else: - print(f"Error: {response.status_code}") -``` - - - -## Parameterized circuits - -### 1. Initialize the account - -Because Qiskit Runtime is a managed service, you first need to initialize your account. You can then select the device you want to use to run your calculations on. - - -Find details on how to initialize your account, view available backends, and invalidate tokens in this [topic](/docs/guides/cloud-setup-rest-api). - - -### 2. Define parameters - -```python -import requests -import qiskit_ibm_runtime -from qiskit_ibm_runtime import QiskitRuntimeService -from qiskit.transpiler import generate_preset_pass_manager -from qiskit.qasm3 import dumps -from qiskit import QuantumCircuit -from qiskit.circuit import Parameter -from qiskit import transpile - -service = QiskitRuntimeService(channel='ibm_quantum') -backend = service.backend("") - -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) - -theta = Parameter('theta') -phi = Parameter('phi') -parameter_values = {'theta': 1.57, 'phi': 3.14} # In case we want to pass a dictionary -``` - - -### 3. Create a quantum circuit and add parameterized gates - -```python -qc = QuantumCircuit(2) - -# Add parameterized gates -qc.rx(theta, 0) -qc.ry(phi, 1) -qc.cx(0, 1) -qc.measure_all() - -# Draw the original circuit -qc.draw('mpl') - -# Get an ISA circuit -isa_circuit = pm.run(qc) -``` - -### 4. Generate QASM 3 code - -```python -qasm_str = dumps(isa_circuit) -print("Generated QASM 3 code:") -print(qasm_str) -``` - -### 5. Run the quantum circuit using Sampler V2 API - -```python -import requests - -url = 'https://quantum.cloud.ibm.com/api/v1/jobs' -auth_id = "Bearer " -crn = "" -backend = "" - -headers = { - 'Content-Type': 'application/json', - 'Authorization':auth_id, - 'Service-CRN': crn - } - -job_input = { - 'program_id': 'sampler', - "backend": backend, - "params": { - # Choose one option: direct parameter transfer or through a dictionary - #"pubs": [[qasm_str,[1,2],500]], # primitive unified blocs (PUBs) containing one circuit each. - "pubs": [[qasm_str,parameter_values,500]], # primitive unified blocs (PUBs) containing one circuit each. -}} - -response = requests.post(url, headers=headers, json=job_input) - -if response.status_code == 200: - job_id = response.json().get('id') - print(f"Job created: {response.text}") -else: - print(f"Error: {response.status_code}") -``` - -```python -print(response.text) -``` - - -### 6. Check job status and get results - -Next, pass the `job_id` to the API: - -```python -response_status_singlejob = requests.get(f"{url}/{job_id}", headers=headers) -response_status_singlejob.json().get('state') -``` - -Output - -```text -{'status': 'Completed'} -``` - -Get job results: - -```python -response_result = requests.get(f"{url}/{job_id}/results", headers=headers) - -res_dict=response_result.json() - -# Get results for the first PUB -counts=res_dict['results'][0]['data']['c']['samples'] - -print(counts[:20]) -``` - -Output - -```text -['0x1', '0x2', '0x1', '0x2', '0x1', '0x2', '0x0', '0x2', '0x1', '0x1', '0x2', '0x2', '0x1', '0x1', '0x1', '0x1', '0x1', '0x1', '0x1', '0x1'] -``` - -## Next steps - - - - - There are several ways to run workloads, depending on your needs: job mode, session mode, and batch mode. Learn how to work with session mode and batch mode in the [execution modes topic](/docs/guides/execution-modes-rest-api). Note that Open Plan users cannot submit session jobs. - - Learn how to [initialize your account](/docs/guides/cloud-setup-rest-api) with REST API. - - Practice with primitives by working through the [Cost function lesson](/learning/courses/variational-algorithm-design/cost-functions) in IBM Quantum Learning. - - Learn how to transpile locally in the [Transpile](/docs/guides/transpile) section. - +--- +title: ampler with the REST API +description: How to use the Sampler primitive with the Qiskit Runtime REST API. +--- +{/* cspell:ignore IIZII, XIZZZ, accum */} + + +# Sampler with the REST API + +The steps in this topic describe how to run and configure workloads with the REST API, and demonstrate how to invoke them in any program of your choice. + + + This documentation utilizes the Python `requests` module to demonstrate the Qiskit Runtime REST API. However, this workflow can be executed using any language or framework that supports working with REST APIs. Refer to the [API reference documentation](/docs/api/qiskit-ibm-runtime/tags/jobs) for details. + + +## 1. Initialize the account + +Because Qiskit Runtime Sampler is a managed service, you first need to initialize your account. You can then select the device you want to use to run your calculations on. + + +Find details on how to initialize your account, view available backends, and work with tokens in [Set up to use IBM Quantum Platform with REST API](/docs/guides/cloud-setup-rest-api). + + +## 2. Create a QASM circuit + +You need at least one circuit as the input to the Sampler primitive. + + +Define a QASM quantum circuit: + +```python +qasm_string=''' +OPENQASM 3; +include "stdgates.inc"; +qreg q[2]; +creg c[2]; +x q[0]; +cx q[0], q[1]; +c[0] = measure q[0]; +c[1] = measure q[1]; +''' +``` + + +The code snippets given below assume that the `qasm_string` has been transpiled to a new string `resulting_qasm`. + + +## 3. Run the quantum circuit using Sampler V2 API + + + + The jobs below use [Qiskit Runtime V2 primitives](/docs/guides/v2-primitives). `SamplerV2` takes one or more primitive unified blocs (PUBs) as the input. Each PUB is a tuple that contains one circuit and the data broadcasted to that circuit, which can be multiple parameters, and returns one result per PUB. + + + +```python +import requests + +url = 'https://quantum.cloud.ibm.com/api/v1/jobs' +auth_id = "Bearer " +crn = "" +backend = "" + +headers = { + 'Content-Type': 'application/json', + 'Authorization':auth_id, + 'Service-CRN': crn + } +job_input = { + 'program_id': 'sampler', + "backend": backend, + "params": { + "pubs": [[resulting_qasm],[resulting_qasm,None,500]] # primitive unified blocs (PUBs) containing one circuit each. +}} + +response = requests.post(url, headers=headers, json=job_input) + +if response.status_code == 200: + job_id = response.json().get('id') + print("Job created:",response.text) +else: + print(f"Error: {response.status_code}") +``` + + + +## 4. Check job status and get results + +Next, pass the `job_id` to the API: + +```python +response_status_singlejob= requests.get(url+'/'+job_id, headers=headers) +response_status_singlejob.json().get('state') +``` +Output + +```text +>>> Job ID: 58223448-5100-4dec-a47a-942fb30edced +>>> Job Status: JobStatus.RUNNING +``` + +Get job results: +```python +response_result= requests.get(url+'/'+job_id+'/results', headers=headers) + +res_dict=response_result.json() + +# Get results for the first PUB +counts=res_dict['results'][0]['data']['c']['samples'] + +print(counts[:20]) +``` +Output +```text +['0x3', '0x0', '0x2', '0x1', '0x0', '0x3', '0x0', '0x3', '0x1', '0x2', '0x2', '0x0', '0x2', '0x0', '0x3', '0x3', '0x2', '0x0', '0x1', '0x0'] +``` + +## 5. Work with Qiskit Runtime options + +Error mitigation techniques allow users to mitigate circuit errors by modeling the device noise at the time of execution. This typically results in quantum pre-processing overhead related to model training, and classical post-processing overhead to mitigate errors in the raw results by using the generated model. + +The error mitigation techniques built in to primitives are advanced resilience options. To specify these options, use the `resilience_level` option when submitting your job. +Sampler V2 does not support specifying resilience levels. However, you can turn on or off individual error mitigation / suppression methods. + +The following examples demonstrate the default options for dynamical decoupling and twirling. Find more options and further details in the [Error mitigation and suppression techniques](/docs/guides/error-mitigation-and-suppression-techniques) topic. + +### Dynamical decoupling +```python +import requests + +url = 'https://quantum.cloud.ibm.com/api/v1/jobs' +auth_id = "Bearer " +crn = "" +backend = "" + +headers = { + 'Content-Type': 'application/json', + 'Authorization':auth_id, + 'Service-CRN': crn + } +job_input = { + 'program_id': 'sampler', + "backend": backend, + "params": { + "pubs": [[resulting_qasm]], # primitive unified blocs (PUBs) containing one circuit each. + "options": { + "dynamical_decoupling": { + "enable": True, + "sequence_type": 'XpXm', + "extra_slack_distribution": 'middle', + "scheduling_method": 'alap', + }, + }, + } +} + +response = requests.post(url, headers=headers, json=job_input) + +if response.status_code == 200: + job_id = response.json().get('id') + print("Job created:",response.text) +else: + print(f"Error: {response.status_code}") +``` +### Twirling +```python +import requests + +url = 'https://quantum.cloud.ibm.com/api/v1/jobs' +auth_id = "Bearer " +crn = "" +backend = "" + +headers = { + 'Content-Type': 'application/json', + 'Authorization':auth_id, + 'Service-CRN': crn + } +job_input = { + 'program_id': 'sampler', + "backend": backend, + "params": { + "pubs": [[resulting_qasm]], # primitive unified blocs (PUBs) containing one circuit each. + "options": { + "twirling": { + "enable_gates": True, + "enable_measure": True, + "num_randomizations": "auto", + "shots_per_randomization": "auto", + "strategy": "active-accum", + }, + }, + } +} + +response = requests.post(url, headers=headers, json=job_input) + +if response.status_code == 200: + job_id = response.json().get('id') + print("Job created:",response.text) +else: + print(f"Error: {response.status_code}") +``` + + + +## Parameterized circuits + +### 1. Initialize the account + +Because Qiskit Runtime is a managed service, you first need to initialize your account. You can then select the device you want to use to run your calculations on. + + +Find details on how to initialize your account, view available backends, and invalidate tokens in this [topic](/docs/guides/cloud-setup-rest-api). + + +### 2. Define parameters + +```python +import requests +import qiskit_ibm_runtime +from qiskit_ibm_runtime import QiskitRuntimeService +from qiskit.transpiler import generate_preset_pass_manager +from qiskit.qasm3 import dumps +from qiskit import QuantumCircuit +from qiskit.circuit import Parameter +from qiskit import transpile + +service = QiskitRuntimeService(channel='ibm_quantum') +backend = service.backend("") + +pm = generate_preset_pass_manager(backend=backend, optimization_level=1) + +theta = Parameter('theta') +phi = Parameter('phi') +parameter_values = {'theta': 1.57, 'phi': 3.14} # In case we want to pass a dictionary +``` + + +### 3. Create a quantum circuit and add parameterized gates + +```python +qc = QuantumCircuit(2) + +# Add parameterized gates +qc.rx(theta, 0) +qc.ry(phi, 1) +qc.cx(0, 1) +qc.measure_all() + +# Draw the original circuit +qc.draw('mpl') + +# Get an ISA circuit +isa_circuit = pm.run(qc) +``` + +### 4. Generate QASM 3 code + +```python +qasm_str = dumps(isa_circuit) +print("Generated QASM 3 code:") +print(qasm_str) +``` + +### 5. Run the quantum circuit using Sampler V2 API + +```python +import requests + +url = 'https://quantum.cloud.ibm.com/api/v1/jobs' +auth_id = "Bearer " +crn = "" +backend = "" + +headers = { + 'Content-Type': 'application/json', + 'Authorization':auth_id, + 'Service-CRN': crn + } + +job_input = { + 'program_id': 'sampler', + "backend": backend, + "params": { + # Choose one option: direct parameter transfer or through a dictionary + # "pubs": [[qasm_str,[1,2],500]], + # # primitive unified blocs (PUBs) containing one circuit each. + "pubs": [[qasm_str,parameter_values,500]], # primitive unified blocs (PUBs) containing one circuit each. +}} + +response = requests.post(url, headers=headers, json=job_input) + +if response.status_code == 200: + job_id = response.json().get('id') + print(f"Job created: {response.text}") +else: + print(f"Error: {response.status_code}") +``` + +```python +print(response.text) +``` + + +### 6. Check job status and get results + +Next, pass the `job_id` to the API: + +```python +response_status_singlejob = requests.get(f"{url}/{job_id}", headers=headers) +response_status_singlejob.json().get('state') +``` + +Output + +```text +{'status': 'Completed'} +``` + +Get job results: + +```python +response_result = requests.get(f"{url}/{job_id}/results", headers=headers) + +res_dict=response_result.json() + +# Get results for the first PUB +counts=res_dict['results'][0]['data']['c']['samples'] + +print(counts[:20]) +``` + +Output + +```text +['0x1', '0x2', '0x1', '0x2', '0x1', '0x2', '0x0', '0x2', '0x1', '0x1', '0x2', '0x2', '0x1', '0x1', '0x1', '0x1', '0x1', '0x1', '0x1', '0x1'] +``` + +## Next steps + + + + - There are several ways to run workloads, depending on your needs: job mode, session mode, and batch mode. Learn how to work with session mode and batch mode in the [execution modes topic](/docs/guides/execution-modes-rest-api). Note that Open Plan users cannot submit session jobs. + - Learn how to [initialize your account](/docs/guides/cloud-setup-rest-api) with REST API. + - Practice with primitives by working through the [Cost function lesson](/learning/courses/variational-algorithm-design/cost-functions) in IBM Quantum Learning. + - Learn how to transpile locally in the [Transpile](/docs/guides/transpile) section. + diff --git a/docs/guides/serverless-first-program.ipynb b/docs/guides/serverless-first-program.ipynb index 2a532614db7..ec1e9ed5196 100644 --- a/docs/guides/serverless-first-program.ipynb +++ b/docs/guides/serverless-first-program.ipynb @@ -138,7 +138,8 @@ "outputs": [], "source": [ "%%writefile ./source_files/transpile_remote.py\n", - "# If you include the preceding `%%writefile` command (visible only when you read this locally in a notebook), running this cell saves to disk rather than executing the code.\n", + "# If you include the preceding `%%writefile` command (visible only when you read this\n", + "# locally in a notebook), running this cell saves to disk rather than executing the code.\n", "\n", "from qiskit.transpiler import generate_preset_pass_manager\n", "\n", @@ -172,7 +173,8 @@ "outputs": [], "source": [ "%%writefile --append ./source_files/transpile_remote.py\n", - "# If you include the preceding `%%writefile` command (visible only when you read this locally in a notebook), running this cell saves to disk rather than executing the code.\n", + "# If you include the preceding `%%writefile` command (visible only when you read this\n", + "# locally in a notebook), running this cell saves to disk rather than executing the code.\n", "\n", "from qiskit_serverless import get_arguments, save_result, distribute_task, get\n", "\n", @@ -203,7 +205,8 @@ "outputs": [], "source": [ "%%writefile --append ./source_files/transpile_remote.py\n", - "# If you include the preceding `%%writefile` command (visible only when you read this locally in a notebook), running this cell saves to disk rather than executing the code.\n", + "# If you include the preceding `%%writefile` command (visible only when you read this\n", + "# locally in a notebook), running this cell saves to disk rather than executing the code.\n", "\n", "from qiskit_ibm_runtime import QiskitRuntimeService\n", "\n", @@ -229,7 +232,8 @@ "outputs": [], "source": [ "%%writefile --append ./source_files/transpile_remote.py\n", - "# If you include the preceding `%%writefile` command (visible only when you read this locally in a notebook), running this cell saves to disk rather than executing the code.\n", + "# If you include the preceding `%%writefile` command (visible only when you read this\n", + "# locally in a notebook), running this cell saves to disk rather than executing the code.\n", "\n", "# Each circuit is being transpiled and will populate the array\n", "results = [\n", diff --git a/docs/guides/serverless-manage-resources.ipynb b/docs/guides/serverless-manage-resources.ipynb index a5969825e04..db80273fc20 100644 --- a/docs/guides/serverless-manage-resources.ipynb +++ b/docs/guides/serverless-manage-resources.ipynb @@ -106,20 +106,25 @@ "source": [ "%%writefile ./source_files/status_example.py\n", "\n", - "# If you include the preceding `%%writefile` command (visible only when you read this locally in a notebook), running this cell saves to disk rather than executing the code.\n", + "# If you include the preceding `%%writefile` command (visible only when you read this locally in a\n", + "# notebook), running this cell saves to disk rather than executing the code.\n", "\n", "from qiskit_serverless import update_status, Job\n", "\n", - "## If your function has a mapping stage, particularly application functions, you can set the status to \"RUNNING: MAPPING\" as follows:\n", + "# # If your function has a mapping stage, particularly application functions, you can set the status\n", + "# to \"RUNNING: MAPPING\" as follows:\n", "update_status(Job.MAPPING)\n", "\n", - "## While handling transpilation, error suppression, and so forth, you can set the status to \"RUNNING: OPTIMIZING_FOR_HARDWARE\":\n", + "# # While handling transpilation, error suppression, and so forth, you can set the status to\n", + "# \"RUNNING: OPTIMIZING_FOR_HARDWARE\":\n", "update_status(Job.OPTIMIZING_HARDWARE)\n", "\n", - "## After you submit jobs to Qiskit Runtime, the underlying quantum job will be queued. You can set status to \"RUNNING: WAITING_FOR_QPU\":\n", + "# # After you submit jobs to Qiskit Runtime, the underlying quantum job will be queued. You can set\n", + "# status to \"RUNNING: WAITING_FOR_QPU\":\n", "update_status(Job.WAITING_QPU)\n", "\n", - "## When the Qiskit Runtime job starts running on the QPU, set the following status \"RUNNING: EXECUTING_QPU\":\n", + "# # When the Qiskit Runtime job starts running on the QPU, set the following status\n", + "# \"RUNNING: EXECUTING_QPU\":\n", "update_status(Job.EXECUTING_QPU)\n", "\n", "## Once QPU is completed and post-processing has begun, set the status \"RUNNING: POST_PROCESSING\":\n", @@ -163,7 +168,8 @@ "source": [ "%%writefile ./source_files/transpile_remote.py\n", "\n", - "# If you include the preceding `%%writefile` command (visible only when you read this locally in a notebook), running this cell saves to disk rather than executing the code.\n", + "# If you include the preceding `%%writefile` command (visible only when you read this locally in a\n", + "# notebook), running this cell saves to disk rather than executing the code.\n", "\n", "from qiskit.transpiler import generate_preset_pass_manager\n", "from qiskit_ibm_runtime import QiskitRuntimeService\n", @@ -207,7 +213,8 @@ "source": [ "%%writefile --append ./source_files/transpile_remote.py\n", "\n", - "# If you include the preceding `%%writefile` command (visible only when you read this locally in a notebook), running this cell saves to disk rather than executing the code.\n", + "# If you include the preceding `%%writefile` command (visible only when you read this locally in a\n", + "# notebook), running this cell saves to disk rather than executing the code.\n", "\n", "from time import time\n", "from qiskit_serverless import get, get_arguments, save_result, update_status, Job\n", @@ -235,7 +242,8 @@ ], "source": [ "%%writefile --append ./source_files/transpile_remote.py\n", - "# If you include the preceding `%%writefile` command (visible only when you read this locally in a notebook), running this cell saves to disk rather than executing the code.\n", + "# If you include the preceding `%%writefile` command (visible only when you read this locally in a\n", + "# notebook), running this cell saves to disk rather than executing the code.\n", "\n", "# Start distributed transpilation\n", "update_status(Job.OPTIMIZING_HARDWARE)\n", @@ -266,7 +274,8 @@ ], "source": [ "%%writefile --append ./source_files/transpile_remote.py\n", - "# If you include the preceding `%%writefile` command (visible only when you read this locally in a notebook), running this cell saves to disk rather than executing the code.\n", + "# If you include the preceding `%%writefile` command (visible only when you read this locally in a\n", + "# notebook), running this cell saves to disk rather than executing the code.\n", "\n", "# Save result, with metadata\n", "result = {\n", @@ -426,7 +435,8 @@ ], "source": [ "%%writefile --append ./source_files/transpile_remote.py\n", - "# If you include the preceding `%%writefile` command (visible only when you read this locally in a notebook), running this cell saves to disk rather than executing the code.\n", + "# If you include the preceding `%%writefile` command (visible only when you read this locally in a\n", + "# notebook), running this cell saves to disk rather than executing the code.\n", "\n", "@distribute_task(target={\n", " \"cpu\": 16,\n", @@ -631,7 +641,8 @@ "outputs": [], "source": [ "%%writefile ./source_files/extract_tarfile.py\n", - "# If you include the preceding `%%writefile` command (visible only when you read this locally in a notebook), running this cell saves to disk rather than executing the code.\n", + "# If you include the preceding `%%writefile` command (visible only when you read this locally in a\n", + "# notebook), running this cell saves to disk rather than executing the code.\n", "\n", "import tarfile\n", "from qiskit_serverless import IBMServerlessClient\n", diff --git a/docs/guides/slurm-hpc-ux.mdx b/docs/guides/slurm-hpc-ux.mdx index 9b2e555f50b..acbe2b7e20d 100644 --- a/docs/guides/slurm-hpc-ux.mdx +++ b/docs/guides/slurm-hpc-ux.mdx @@ -1,173 +1,174 @@ ---- -title: SPANK plugin user guide -description: User guide for the quantum resource management SPANK plugin ---- -{/* cspell:ignore QRMI, SBATCH, srun, Pasqal, slurmd, Doxyfile, Gres */} - -# SPANK plugin user guide -The [SPANK plugin](https://github.com/qiskit-community/spank-plugins/) for the [Quantum Resource Management Interface (QRMI)](/docs/guides/qrmi) is used to configure access to quantum resources from user jobs in a compute environment administrated by the [Slurm](https://slurm.schedmd.com/quickstart.html) workload manager. This is a guide for plugin users to configure QPU resource allocation when creating Slurm jobs. - -The Slurm QPU resource definitions determine what physical resources can be used by Slurm jobs in high-performance compute (HPC) environments. User source code should be agnostic to specific backend instances, and even backend types whenever possible. This keeps source code portable while the QPU selection criteria are part of the resource definition (which is considered configuration rather than source code). - -## Configure QPU resources in job creation - - -Note that this plugin is under active development and the exact syntax is subject to change. - - -### Administrator scope - -HPC administrators configure the SPANK plugin to specify what physical resources can be provided to Slurm jobs. -This configuration contains all the information needed to have Slurm jobs access the physical resources, such as endpoints and access credentials. - -Read the [`qrmi_config.json.example`](https://github.com/qiskit-community/spank-plugins/blob/main/plugins/spank_qrmi/qrmi_config.json.example) for a comprehensive example configuration. - -In `slurm.conf`, QPU resources can be assigned to some or all nodes for usage: -``` -... -GresTypes=qpu,name -NodeName=node[1-5000] Gres=qpu,name:ibm_fez -... -``` - -### User scope - -HPC users submit jobs using QPU resources that are tied to Slurm QPU resources. The name attribute references what the HPC administrator has defined. During a Slurm job's runtime, backend selection can be based on criteria other than a predefined name referring to a specific backend (for example, by capacity and error rate qualifiers, to help down-select among the defined set of backends). - -There might be additional environment variables required, depending on the backend type. - -`SBATCH` parameters will point to one or more QPU resources assigned to the application as generic resources. -Environment variables provided through the plugin will provide the necessary information to the application (see the [HPC application scope](#hpc-application-scope) section for details). - -```shell -#SBATCH --time=100 -#SBATCH --output= -#SBATCH --gres=qpu:1 -#SBATCH --qpu=ibm_fez -#SBATCH --... # other options - -srun ... -``` - -To use more QPU resources, add more QPUs to the `--qpu` parameter: - -```shell -#SBATCH --time=100 -#SBATCH --output= -#SBATCH --gres=qpu:3 -#SBATCH --qpu=my_local_qpu,ibm_fez,ibm_marrakesh -#SBATCH --... # other options - -srun ... -``` - -### HPC application scope - -HPC applications use the Slurm QPU resources assigned to the Slurm job. - -Environment variables provide more details for use by the application; for example, `SLURM_JOB_QPU_RESOURCES` lists the quantum resource names (comma-separated if several are provided). -These variables will be used by QRMI. (See the README files in the various QRMI directories ([IBM](https://github.com/qiskit-community/qrmi/blob/main/examples/qiskit_primitives/ibm/README.md), [pasqal](https://github.com/qiskit-community/qrmi/blob/main/examples/qiskit_primitives/pasqal/README.md)) for more details.) - -```python -from qiskit import QuantumCircuit -# Using an IBM QRMI flavor: -from qrmi.primitives import QRMIService -from qrmi.primitives.ibm import SamplerV2, get_backend - -# define circuit - -circuit = QuantumCircuit(2) -circuit.h(0) -circuit.cx(0, 1) -circuit.measure_all() - -# instantiate QRMI service and get quantum resource (we'll take the first one should there be several of them) -# inject credentials needed for accessing the service at this point -load_dotenv() -service = QRMIService() - -resources = service.resources() -qrmi = resources[0] - -# Generate transpiler target from backend configuration & properties and transpile -backend = get_backend(qrmi) -pm = generate_preset_pass_manager( - optimization_level=1, - backend=backend, -) - -isa_circuit = pm.run(circuit) - -# Run the circuit -options = {} -sampler = SamplerV2(qrmi, options=options) - -job = sampler.run([(isa_circuit, isa_observable, param_values)]) -print(f">>> Job ID: {job.job_id()}") - -result = job.result() - -if job.done(): - pub_result = result[0] - print(f"Counts for the 'meas' output register: {pub_result.data.meas.get_counts()}") -elif job.cancelled(): - print("Cancelled") -elif job.errored(): - print(qrmi.task_logs(job.job_id())) -``` - -See the [examples directory](https://github.com/qiskit-community/qrmi/tree/main/examples/qiskit_primitives/) for example files. - -### Backend specifics -#### IBM Quantum System API -##### Administrator scope -Configuration of Quantum System API backends (HPC admin scope) includes endpoints and credentials to the Quantum System endpoint and authentication services, as well as to the S3 endpoint. -Specifically, this includes: - -* IBM Cloud® API key for creating bearer tokens -* Endpoint of the Quantum System API -* S3 bucket and access details - -Access credentials should not be visible to HPC users or other non-privileged users on the system. -Therefore, sensitive data can be put in separate files, which can be access-protected accordingly. - -Note that Slurm has full access to the backend. -This has several implications: - -* The Slurm plugin is responsible for multi-tenancy (ensuring that users don't see results of other users' jobs) -* The HPC cluster side is responsible for vetting users (who is allowed to access the QPU) and ensuring according access -* The capacity and priority of the QPU usage is solely managed through Slurm; there is no other scheduling of users involved outside of Slurm - -##### User scope -Execution lanes are not exposed to the HPC administrator or user directly. -Instead, during runtime, there can be two different modes that HPC users can specify: - -* `exclusive=true` specifies that no other jobs can use the resource at the same time. An exclusive mode job gets all execution lanes and cannot run at the same time as a non-exclusive job -* `exclusive=false` allows other jobs to run in parallel. In this case, there can be as many jobs as there are execution lanes, all running at the same time, and the job is assigned one lane - -#### Qiskit Runtime Service -##### User scope - -It is expected that users specify additional access details in environment variables. -Specifically, this includes the following: - -* Qiskit Runtime service instance (CRN, Cloud Resource Name) -* Endpoint for Qiskit Runtime (unless auto-detected from the CRN) -* API key, which has access to the CRN -* S3 instance, bucket, and access token/credentials for data transfers - -These details determine under which user and service instance the Qiskit Runtime service is used. -Accordingly, IBM Quantum® Platform scheduling considers the user's and service instance's capabilities for scheduling. - -At this time, users must provide the above details (no shared cluster-wide quantum access). - -#### Pasqal Cloud Services -##### HPC admin scope -There is no specific setup required from HPC admins for PCS usage. - -##### HPC user scope -It is expected that users specify additional access details in environment variables. -Currently, this includes the following: - -* PCS resource to target (FRESNEL, EMU_FRESNEL, EMU_MPS) -* Authorization token +--- +title: SPANK plugin user guide +description: User guide for the quantum resource management SPANK plugin +--- +{/* cspell:ignore QRMI, SBATCH, srun, Pasqal, slurmd, Doxyfile, Gres */} + +# SPANK plugin user guide +The [SPANK plugin](https://github.com/qiskit-community/spank-plugins/) for the [Quantum Resource Management Interface (QRMI)](/docs/guides/qrmi) is used to configure access to quantum resources from user jobs in a compute environment administrated by the [Slurm](https://slurm.schedmd.com/quickstart.html) workload manager. This is a guide for plugin users to configure QPU resource allocation when creating Slurm jobs. + +The Slurm QPU resource definitions determine what physical resources can be used by Slurm jobs in high-performance compute (HPC) environments. User source code should be agnostic to specific backend instances, and even backend types whenever possible. This keeps source code portable while the QPU selection criteria are part of the resource definition (which is considered configuration rather than source code). + +## Configure QPU resources in job creation + + +Note that this plugin is under active development and the exact syntax is subject to change. + + +### Administrator scope + +HPC administrators configure the SPANK plugin to specify what physical resources can be provided to Slurm jobs. +This configuration contains all the information needed to have Slurm jobs access the physical resources, such as endpoints and access credentials. + +Read the [`qrmi_config.json.example`](https://github.com/qiskit-community/spank-plugins/blob/main/plugins/spank_qrmi/qrmi_config.json.example) for a comprehensive example configuration. + +In `slurm.conf`, QPU resources can be assigned to some or all nodes for usage: +``` +... +GresTypes=qpu,name +NodeName=node[1-5000] Gres=qpu,name:ibm_fez +... +``` + +### User scope + +HPC users submit jobs using QPU resources that are tied to Slurm QPU resources. The name attribute references what the HPC administrator has defined. During a Slurm job's runtime, backend selection can be based on criteria other than a predefined name referring to a specific backend (for example, by capacity and error rate qualifiers, to help down-select among the defined set of backends). + +There might be additional environment variables required, depending on the backend type. + +`SBATCH` parameters will point to one or more QPU resources assigned to the application as generic resources. +Environment variables provided through the plugin will provide the necessary information to the application (see the [HPC application scope](#hpc-application-scope) section for details). + +```shell +#SBATCH --time=100 +#SBATCH --output= +#SBATCH --gres=qpu:1 +#SBATCH --qpu=ibm_fez +#SBATCH --... # other options + +srun ... +``` + +To use more QPU resources, add more QPUs to the `--qpu` parameter: + +```shell +#SBATCH --time=100 +#SBATCH --output= +#SBATCH --gres=qpu:3 +#SBATCH --qpu=my_local_qpu,ibm_fez,ibm_marrakesh +#SBATCH --... # other options + +srun ... +``` + +### HPC application scope + +HPC applications use the Slurm QPU resources assigned to the Slurm job. + +Environment variables provide more details for use by the application; for example, `SLURM_JOB_QPU_RESOURCES` lists the quantum resource names (comma-separated if several are provided). +These variables will be used by QRMI. (See the README files in the various QRMI directories ([IBM](https://github.com/qiskit-community/qrmi/blob/main/examples/qiskit_primitives/ibm/README.md), [pasqal](https://github.com/qiskit-community/qrmi/blob/main/examples/qiskit_primitives/pasqal/README.md)) for more details.) + +```python +from qiskit import QuantumCircuit +# Using an IBM QRMI flavor: +from qrmi.primitives import QRMIService +from qrmi.primitives.ibm import SamplerV2, get_backend + +# define circuit + +circuit = QuantumCircuit(2) +circuit.h(0) +circuit.cx(0, 1) +circuit.measure_all() + +# instantiate QRMI service and +# get quantum resource (we'll take the first one should there be several of them) +# inject credentials needed for accessing the service at this point +load_dotenv() +service = QRMIService() + +resources = service.resources() +qrmi = resources[0] + +# Generate transpiler target from backend configuration & properties and transpile +backend = get_backend(qrmi) +pm = generate_preset_pass_manager( + optimization_level=1, + backend=backend, +) + +isa_circuit = pm.run(circuit) + +# Run the circuit +options = {} +sampler = SamplerV2(qrmi, options=options) + +job = sampler.run([(isa_circuit, isa_observable, param_values)]) +print(f">>> Job ID: {job.job_id()}") + +result = job.result() + +if job.done(): + pub_result = result[0] + print(f"Counts for the 'meas' output register: {pub_result.data.meas.get_counts()}") +elif job.cancelled(): + print("Cancelled") +elif job.errored(): + print(qrmi.task_logs(job.job_id())) +``` + +See the [examples directory](https://github.com/qiskit-community/qrmi/tree/main/examples/qiskit_primitives/) for example files. + +### Backend specifics +#### IBM Quantum System API +##### Administrator scope +Configuration of Quantum System API backends (HPC admin scope) includes endpoints and credentials to the Quantum System endpoint and authentication services, as well as to the S3 endpoint. +Specifically, this includes: + +* IBM Cloud® API key for creating bearer tokens +* Endpoint of the Quantum System API +* S3 bucket and access details + +Access credentials should not be visible to HPC users or other non-privileged users on the system. +Therefore, sensitive data can be put in separate files, which can be access-protected accordingly. + +Note that Slurm has full access to the backend. +This has several implications: + +* The Slurm plugin is responsible for multi-tenancy (ensuring that users don't see results of other users' jobs) +* The HPC cluster side is responsible for vetting users (who is allowed to access the QPU) and ensuring according access +* The capacity and priority of the QPU usage is solely managed through Slurm; there is no other scheduling of users involved outside of Slurm + +##### User scope +Execution lanes are not exposed to the HPC administrator or user directly. +Instead, during runtime, there can be two different modes that HPC users can specify: + +* `exclusive=true` specifies that no other jobs can use the resource at the same time. An exclusive mode job gets all execution lanes and cannot run at the same time as a non-exclusive job +* `exclusive=false` allows other jobs to run in parallel. In this case, there can be as many jobs as there are execution lanes, all running at the same time, and the job is assigned one lane + +#### Qiskit Runtime Service +##### User scope + +It is expected that users specify additional access details in environment variables. +Specifically, this includes the following: + +* Qiskit Runtime service instance (CRN, Cloud Resource Name) +* Endpoint for Qiskit Runtime (unless auto-detected from the CRN) +* API key, which has access to the CRN +* S3 instance, bucket, and access token/credentials for data transfers + +These details determine under which user and service instance the Qiskit Runtime service is used. +Accordingly, IBM Quantum® Platform scheduling considers the user's and service instance's capabilities for scheduling. + +At this time, users must provide the above details (no shared cluster-wide quantum access). + +#### Pasqal Cloud Services +##### HPC admin scope +There is no specific setup required from HPC admins for PCS usage. + +##### HPC user scope +It is expected that users specify additional access details in environment variables. +Currently, this includes the following: + +* PCS resource to target (FRESNEL, EMU_FRESNEL, EMU_MPS) +* Authorization token diff --git a/docs/guides/v2-primitives.mdx b/docs/guides/v2-primitives.mdx index 239049a6366..e596c041337 100644 --- a/docs/guides/v2-primitives.mdx +++ b/docs/guides/v2-primitives.mdx @@ -1,1212 +1,1213 @@ ---- -title: Migrate to the Qiskit Runtime V2 primitives -description: Migrate from using the original primitives (Estimator and Sampler) to using the updated V2 primitives (Estimator V2 and Sampler V2) - ---- - -{/* cspell:ignore expvals */} - - -# Migrate to the Qiskit Runtime V2 primitives - - -The original primitives (referred to as the V1 primitives), [V1 Sampler](/docs/api/qiskit-ibm-runtime/0.26/sampler-v1) and [V1 Estimator](/docs/api/qiskit-ibm-runtime/0.26/estimator-v1), have been deprecated in `qiskit-ibm-runtime` 0.23. -Their support was removed on 15 August 2024. - - -With the deprecation of the V1 primitives, all code should be migrated to use the V2 interfaces. This guide describes what changed in the Qiskit Runtime V2 primitives (available with qiskit-ibm-runtime 0.21.0) and why, describes each new primitive in detail, and gives examples to help you migrate code from using the legacy primitives to the V2 primitives. The examples in the guide all use the Qiskit Runtime primitives, but in general, the same changes apply to the other primitive implementations. The functions unique to Qiskit Runtime such as error mitigation remain unique to Qiskit Runtime. - -For information about the changes to the Qiskit reference primitives (now called *statevector* primitives), see [the qiskit.primitives section](qiskit-1.0-features#qiskit.primitives) in the Qiskit 1.0 feature changes page. See [StatevectorSampler](/docs/api/qiskit/qiskit.primitives.StatevectorSampler) and [StatevectorEstimator](/docs/api/qiskit/qiskit.primitives.StatevectorEstimator) for V2 primitive reference implementations. - -## Overview - -Version 2 of the primitives is introduced with a new base class for both Sampler and Estimator ([BaseSamplerV2](/docs/api/qiskit/qiskit.primitives.BaseSamplerV2) and [BaseEstimatorV2](/docs/api/qiskit/qiskit.primitives.BaseEstimatorV2)), along with new types for their inputs and outputs. - -The new interface lets you specify a single circuit and multiple observables (if using Estimator) and parameter value sets for that circuit, so that sweeps over parameter value sets and observables can be efficiently specified. Previously, you had to specify the same circuit multiple times to match the size of the data to be combined. Also, while you can still use `resilience_level` (if using Estimator) as the simple knob, V2 primitives give you the flexibility to turn on or off individual error mitigation / suppression methods to customize them for your needs. - -To reduce the total job execution time, V2 primitives only accept circuits and observables that use instructions supported by the target QPU (quantum processing unit). Such circuits and observables are referred to as instruction set architecture (ISA) circuits and observables. V2 primitives do not perform layout, routing, and translation operations. See the [transpilation documentation](/docs/guides/transpile) for instructions to transform circuits. - -Sampler V2 is simplified to focus on its core task of sampling the output register from execution of quantum circuits. It returns the samples, whose type is defined by the program, without weights. The output data is also separated by the output register names defined by the program. This change enables future support for circuits with classical control flow. - -See the [EstimatorV2 API reference](/docs/api/qiskit-ibm-runtime/estimator-v2) and [SamplerV2 API reference](/docs/api/qiskit-ibm-runtime/sampler-v2) for full details. - -## Major changes - -### Import - -For backward compatibility, you must explicity import the V2 primitives. Specifying `import V2 as ` is not required, but makes it easier to transition code to V2. - - -After the V1 primitives are no longer supported, `import ` will import the V2 version of the specified primitive. - - - - - ```python -from qiskit_ibm_runtime import EstimatorV2 as Estimator -``` - - - - ```python -from qiskit_ibm_runtime import Estimator -``` - - - - - - ```python -from qiskit_ibm_runtime import SamplerV2 as Sampler -``` - - - - ```python -from qiskit_ibm_runtime import Sampler -``` - - - - -### Input and output - -#### Input - -Both `SamplerV2` and `EstimatorV2` take one or more *primitive unified blocs* (PUBs) as the input. Each PUB is a tuple that contains **one** circuit and the data broadcasted to that circuit, which can be multiple observables and parameters. Each PUB returns a result. - -* Sampler V2 PUB format: (``, ``, ``), where ``and `` are optional. -* Estimator V2 PUB format: (``, ``, ``, ``), where ``and `` are optional. - Numpy [broadcasting rules](https://numpy.org/doc/stable/user/basics.broadcasting.html) are used when combining observables and parameter values. - -Additionally, the following changes have been made: - -* Estimator V2 has gained a `precision` argument in the `run()` method that specifies the targeted precision of the expectation value estimates. -* Sampler V2 has the `shots` argument in its `run()` method. - -##### Examples - -Estimator V2 example that uses precision in `run()`: - -```python -# Estimate expectation values for two PUBs, both with 0.05 precision. -estimator.run([(circuit1, obs_array1), (circuit2, obs_array_2)], precision=0.05) -``` - -Sampler V2 example that uses shots in `run()`: - -```python -# Sample two circuits at 128 shots each. -sampler.run([circuit1, circuit2], shots=128) - -# Sample two circuits at different amounts of shots. -# The "None"s are necessary as placeholders -# for the lack of parameter values in this example. -sampler.run([ - (circuit1, None, 123), - (circuit2, None, 456), -]) -``` - -#### Output - -The output is now in the [`PubResult`](/docs/api/qiskit/qiskit.primitives.PubResult) format. A `PubResult` is the data and metadata resulting from a single PUB’s execution. - -* Estimator V2 continues to return expectation values. -* The `data` portion of a Estimator V2 PubResult contains both expectation values and standard errors (`stds`). V1 returned variance in metadata. -* Sampler V2 returns per-shot measurements in the form of **bitstrings**, - instead of the quasi-probability distributions from the V1 - interface. The bitstrings show the measurement outcomes, preserving the shot - order in which they were measured. -* Sampler V2 has convenience methods like `get_counts()` to help with migration. -* The Sampler V2 result objects organize data in terms of their **input circuits' classical register names**, for - compatibility with dynamic circuits. By default, the classical register name is `meas`, as shown in the following example. When defining your circuit, if you create one or more classical registers with a non-default name, use that name to get the results. You can find the classical register name by running `.cregs`. For example, `qc.cregs`. - - ```python - # Define a quantum circuit with 2 qubits - circuit = QuantumCircuit(2) - circuit.h(0) - circuit.cx(0, 1) - circuit.measure_all() - circuit.draw() - ``` - ```text - ┌───┐ ░ ┌─┐ - q_0: ┤ H ├──■───░─┤M├─── - └───┘┌─┴─┐ ░ └╥┘┌─┐ - q_1: ─────┤ X ├─░──╫─┤M├ - └───┘ ░ ║ └╥┘ - meas: 2/══════════════╩══╩═ - 0 1 - ``` - -#### Estimator examples (input and output) - -{/*Verified by Elena.*/} - - - - ```python - # Estimator V1: Execute 1 circuit with 4 observables - job = estimator_v1.run([circuit] * 4, [obs1, obs2, obs3, obs4]) - evs = job.result().values - - # Estimator V2: Execute 1 circuit with 4 observables - job = estimator_v2.run([(circuit, [obs1, obs2, obs3, obs4])]) - evs = job.result()[0].data.evs -``` - - - - ```python - # Estimator V1: Execute 1 circuit with 4 observables and 2 parameter sets - job = estimator_v1.run([circuit] * 8, [obs1, obs2, obs3, obs4] * 2, [vals1, vals2] * 4) - evs = job.result().values - - # Estimator V2: Execute 1 circuit with 4 observables and 2 parameter sets - - job = estimator_v2.run([(circuit, [[obs1], [obs2], [obs3], [obs4]], [[vals1], [vals2]])]) - evs = job.result()[0].data.evs -``` - - - - ```python - # Estimator V1: Cannot execute 2 circuits with different observables - - # Estimator V2: Execute 2 circuits with 2 different observables. There are - # two PUBs because each PUB can have only one circuit. - job = estimator_v2.run([(circuit1, obs1), (circuit2, obs2)]) - evs1 = job.result()[0].data.evs # result for pub 1 (circuit 1) - evs2 = job.result()[1].data.evs # result for pub 2 (circuit 2) -``` - - - - -#### Sampler examples (input and output) - -{/*Verified by Elena.*/} - - - - ```python - # Sampler V1: Execute 1 circuit with 3 parameter sets - job = sampler_v1.run([circuit] * 3, [vals1, vals2, vals3]) - dists = job.result().quasi_dists - - # Sampler V2: Executing 1 circuit with 3 parameter sets - job = sampler_v2.run([(circuit, [vals1, vals2, vals3])]) - counts = job.result()[0].data.meas.get_counts() -``` - - - - ```python - # Sampler V1: Execute 2 circuits with 1 parameter set - job = sampler_v1.run([circuit1, circuit2], [vals1] * 2) - dists = job.result().quasi_dists - - # Sampler V2: Execute 2 circuits with 1 parameter set - job = sampler_v2.run([(circuit1, vals1), (circuit2, vals1)]) - counts1 = job.result()[0].data.meas.get_counts() # result for pub 1 (circuit 1) - counts2 = job.result()[1].data.meas.get_counts() # result for pub 2 (circuit 2) -``` - - - - The V1 output format was a dictionary of bitstrings (as an int) as the key and quasi-probabilities as the value for each circuit. The V2 format has the same key (but as a string) and counts as the value. To convert the V2 format to V1, divide the counts by the number of shots, where the number of shots selected is described in the [Sampler options](/docs/guides/sampler-options#shots) guide. - - ```python - v2_result = sampler_v2_job.result() - v1_format = [] - for pub_result in v2_result: - counts = pub_result.data.meas.get_counts() - v1_format.append( {int(key, 2): val/shots for key, val in counts.items()} ) -``` - - - -Example that uses different output registers - -```python -from qiskit import ClassicalRegister, QuantumRegister, QuantumCircuit -from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler - -alpha = ClassicalRegister(5, "alpha") -beta = ClassicalRegister(7, "beta") -qreg = QuantumRegister(12) - -circuit = QuantumCircuit(qreg, alpha, beta) -circuit.h(0) -circuit.measure(qreg[:5], alpha) -circuit.measure(qreg[5:], beta) - -service = QiskitRuntimeService() -backend = service.least_busy(operational=True, simulator=False, min_num_qubits=12) -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) -isa_circuit = pm.run(circuit) - -sampler = Sampler(backend) -job = sampler.run([isa_circuit]) -result = job.result() -# Get results for the first (and only) PUB -pub_result = result[0] -print(f" >> Counts for the alpha output register: {pub_result.data.alpha.get_counts()}") -print(f" >> Counts for the beta output register: {pub_result.data.beta.get_counts()}") -``` - - -### Options - -Options are specified differently in the V2 primitives in these ways: - -- `SamplerV2` and `EstimatorV2` now have separate options classes. You can see the available options and update option values during or after primitive initialization. -- Instead of the `set_options()` method, V2 primitive options have the `update()` method that applies changes to the `options` attribute. -- If you do not specify a value for an option, it is given a special value of `Unset` and the server defaults are used. -- For V2 primitives, the `options` attribute is the `dataclass` Python type. You can use the built-in `asdict` method to convert it to a dictionary. - -See the [API reference](/docs/api/qiskit-ibm-runtime/options) for the list of available options. - -{/*Verified EstimatorV2. 2/23 */} - - - - ```python -from dataclasses import asdict -from qiskit_ibm_runtime import QiskitRuntimeService -from qiskit_ibm_runtime import EstimatorV2 as Estimator - -service = QiskitRuntimeService() -backend = service.least_busy(operational=True, simulator=False) - -# Setting options during primitive initialization -estimator = Estimator(backend, options={"resilience_level": 2}) - -# Setting options after primitive initialization -# This uses auto complete. -estimator.options.default_shots = 4000 -# This does bulk update. -estimator.options.update(default_shots=4000, resilience_level=2) - -# Print the dictionary format. -# Server defaults are used for unset options. -print(asdict(estimator.options)) -``` - - - - ```python -from qiskit_ibm_runtime import QiskitRuntimeService, Sampler, Options - -service = QiskitRuntimeService() -backend = service.least_busy(operational=True, simulator=False) - -# Setting options during primitive initialization -options = Options() -# This uses auto complete. -options.resilience_level = 2 -estimator = Estimator(backend=backend, options=options) - -# Setting options after primitive initialization. -# This does bulk update. -estimator.set_options(shots=4000) -``` - - - - - - ```python -from dataclasses import asdict -from qiskit_ibm_runtime import QiskitRuntimeService -from qiskit_ibm_runtime import SamplerV2 as Sampler - -service = QiskitRuntimeService() -backend = service.least_busy(operational=True, simulator=False) - -# Setting options during primitive initialization -sampler = Sampler(backend, options={"default_shots": 4096}) - -# Setting options after primitive initialization -# This uses auto complete. -sampler.options.dynamical_decoupling.enable = True -# Turn on gate twirling. Requires qiskit_ibm_runtime 0.23.0 or later. -sampler.options.twirling.enable_gates = True - -# This does bulk update. The value for default_shots is overridden if you specify shots with run() or in the PUB. -sampler.options.update(default_shots=1024, dynamical_decoupling={"sequence_type": "XpXm"}) - -# Print the dictionary format. -# Server defaults are used for unset options. -print(asdict(sampler.options)) -``` - - - - ```python -from qiskit_ibm_runtime import QiskitRuntimeService, Sampler, Options - -service = QiskitRuntimeService() -backend = service.least_busy(operational=True, simulator=False) - -# Setting options during primitive initialization -options = Options() -# This uses auto complete. -options.resilience_level = 2 -sampler = Sampler(backend=backend, options=options) - -# Setting options after primitive initialization. -# This does bulk update. -sampler.set_options(shots=2000) - ``` - - - - -### Error mitigation and suppression - -* Because Sampler V2 returns samples without postprocessing, it does not support resilience levels. -* Sampler V2 does not support `optimization_level`. -* Estimator V2 will drop support for `optimization_level` on or around 30 September 2024. -* Estimator V2 does not support resilience level 3. This is because resilience level 3 in V1 Estimator uses Probabilistic Error Cancellation (PEC), which is proven to give unbiased results at the cost of exponential processing time. Level 3 was removed to draw attention to that tradeoff. You can, however, still use PEC as the error mitigation method by specifying the `pec_mitigation` option. -* Estimator V2 supports `resilience_level` 0-2, as described in the following table. These options are more advanced than their V1 counterparts. You can also explicitly turn on / off individual error mitigation / suppression methods. - - | Level 1 | Level 2 | - |---------------------------|---------------------------| - | Measurement twirling | Measurement twirling | - | Readout error mitigation | Readout error mitigation | - | | ZNE | - -{/*Verified EstimatorV2. 2/23 */} - - - - ```python -from dataclasses import asdict -from qiskit_ibm_runtime import QiskitRuntimeService -from qiskit_ibm_runtime import EstimatorV2 as Estimator - -service = QiskitRuntimeService() -backend = service.least_busy(operational=True, simulator=False) - -# Setting options during primitive initialization -estimator = Estimator(backend) - -# Set resilience_level to 0 -estimator.options.resilience_level = 0 - -# Turn on measurement error mitigation -estimator.options.resilience.measure_mitigation = True -``` - - - - ```python -from qiskit_ibm_runtime import Estimator, Options - -estimator = Estimator(backend, options=options) - -options = Options() - -options.resilience_level = 2 -``` - - - - - - - - ```python -from qiskit_ibm_runtime import SamplerV2 as Sampler - -sampler = Sampler(backend) -# Turn on dynamical decoupling with sequence XpXm. -sampler.options.dynamical_decoupling.enable = True -sampler.options.dynamical_decoupling.sequence_type = "XpXm" - -print(f">> dynamical decoupling sequence to use: {sampler.options.dynamical_decoupling.sequence_type}") -``` - - - - - ```python -from qiskit_ibm_runtime import Sampler, Options - -sampler = Sampler(backend, options=options) - -options = Options() - -options.resilience_level = 2 -``` - - - - -### Transpilation - -V2 primitives support only circuits that adhere to the Instruction Set Architecture (ISA) of a particular backend. Because the primitives do not perform layout, routing, and translation operations, the corresponding transpilation options from V1 are not supported. - - -### Job status - -The V2 primitives have a new `RuntimeJobV2` class, which inherits from `BasePrimitiveJob`. The `status()` method of this new class returns a string instead of a JobStatus enum from Qiskit. See the [RuntimeJobV2 API reference](/docs/api/qiskit-ibm-runtime/runtime-job-v2) for details. - - - -```python -job = estimator.run(...) - -# check if a job is still running -print(f"Job {job.job_id()} is still running: {job.status() == "RUNNING"}") -``` - - - -```python -from qiskit.providers.jobstatus import JobStatus - -job = estimator.run(...) - -#check if a job is still running -print(f"Job {job.job_id()} is still running: {job.status() is JobStatus.RUNNING}") -``` - - - -## Steps to migrate to Estimator V2 - -1. Replace `from qiskit_ibm_runtime import Estimator` with `from qiskit_ibm_runtime import EstimatorV2 as Estimator`. -2. Remove any `from qiskit_ibm_runtime import Options` statements, since the `Options` class is not used by V2 primitives. You can instead pass options as a dictionary when initializing the `EstimatorV2` class (for example `estimator = Estimator(backend, options={“dynamical_decoupling”: {“enable”: True}})`), or set them after initialization: - ```python - estimator = Estimator(backend) - estimator.options.dynamical_decoupling.enable = True - ``` - -3. Review all the [supported options](/docs/api/qiskit-ibm-runtime/options) and make updates accordingly. -4. Group each circuit you want to run with the observables and parameter values you want to apply to the circuit in a tuple (a PUB). For example, use `(circuit1, observable1, parameter_set1)` if you want to run `circuit1` with `observable1` and `parameter_set1`. - - 1. You might need to reshape your arrays of observables or parameter sets if you want to apply their outer product. For example, an array of observables of shape (4, 1) and an array of parameter sets of shape (1, 6) will give you a result of (4, 6) expectation values. See the [Numpy broadcasting rules](https://numpy.org/doc/stable/user/basics.broadcasting.html) for more details. - 2. You can optionally specify the precision you want for that specific PUB. -5. Update Estimator's `run()` method to pass in the list of PUBs. For example, `run([(circuit1, observable1, parameter_set1)])`. - You can optionally specify a `precision` here, which would apply to all PUBs. -6. Estimator V2 job results are grouped by PUBs. You can see the expectation value and standard error for each PUB by indexing to it. For example: - ```python - pub_result = job.result()[0] - print(f">>> Expectation values: {pub_result.data.evs}") - print(f">>> Standard errors: {pub_result.data.stds}") - ``` - -## Estimator full examples - -### Run a single experiment - -Use Estimator to determine the expectation value of a single circuit-observable pair. - -{/*Verified EstimatorV2. 2/23 */} - - - - ```python -import numpy as np -from qiskit.circuit.library import IQP -from qiskit.quantum_info import SparsePauliOp, random_hermitian -from qiskit_ibm_runtime import EstimatorV2 as Estimator, QiskitRuntimeService - -service = QiskitRuntimeService() -backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) -estimator = Estimator(backend) - -n_qubits = 127 - -mat = np.real(random_hermitian(n_qubits, seed=1234)) -circuit = IQP(mat) -observable = SparsePauliOp("Z" * n_qubits) - -pm = generate_preset_pass_manager(optimization_level=1, backend=backend) -isa_circuit = pm.run(circuit) -isa_observable = observable.apply_layout(isa_circuit.layout) - -job = estimator.run([(isa_circuit, isa_observable)]) -result = job.result() - -print(f" > Expectation value: {result[0].data.evs}") -print(f" > Metadata: {result[0].metadata}") -``` - - - - ```python -import numpy as np -from qiskit.circuit.library import IQP -from qiskit.quantum_info import SparsePauliOp, random_hermitian -from qiskit_ibm_runtime import QiskitRuntimeService, Estimator - -service = QiskitRuntimeService() - -backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) - -n_qubits = 127 - -mat = np.real(random_hermitian(n_qubits, seed=1234)) -circuit = IQP(mat) -observable = SparsePauliOp("Z" * n_qubits) - -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) -isa_circuit = pm.run(circuit) -isa_observable = observable.apply_layout(isa_circuit.layout) - -estimator = Estimator(backend) -job = estimator.run(isa_circuit, isa_observable) -result = job.result() - -print(f" > Observable: {observable.paulis}") -print(f" > Expectation value: {result.values}") -print(f" > Metadata: {result.metadata}") -``` - - - -### Run multiple experiments in a single job - -Use Estimator to determine the expectation values of multiple circuit-observable pairs. - -{/*Verified EstimatorV2. 2/23 */} - - - -```python -service = QiskitRuntimeService() -backend = service.least_busy(operational=True, simulator=False) - -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) - -n_qubits = 3 -rng = np.random.default_rng() -mats = [np.real(random_hermitian(n_qubits, seed=rng)) for _ in range(3)] -circuits = [IQP(mat) for mat in mats] -observables = [ - SparsePauliOp("X" * n_qubits), - SparsePauliOp("Y" * n_qubits), - SparsePauliOp("Z" * n_qubits), -] - -isa_circuits = pm.run(circuits) -isa_observables = [ob.apply_layout(isa_circuits[0].layout) for ob in observables] - - -estimator = Estimator(backend) -job = estimator.run([(isa_circuits[0], isa_observables[0]),(isa_circuits[1], isa_observables[1]),(isa_circuits[2], isa_observables[2])]) -job_result = job.result() -for idx in range(len(job_result)): - pub_result = job_result[idx] - print(f">>> Expectation values for PUB {idx}: {pub_result.data.evs}") - print(f">>> Standard errors for PUB {idx}: {pub_result.data.stds}") -``` - - - -```python -import numpy as np -from qiskit.circuit.library import IQP -from qiskit.quantum_info import SparsePauliOp, random_hermitian -from qiskit_ibm_runtime import QiskitRuntimeService, Estimator - -service = QiskitRuntimeService() - -backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) - -n_qubits = 127 - -rng = np.random.default_rng() -mats = [np.real(random_hermitian(n_qubits, seed=rng)) for _ in range(3)] -circuits = [IQP(mat) for mat in mats] -observables = [ - SparsePauliOp("X" * n_qubits), - SparsePauliOp("Y" * n_qubits), - SparsePauliOp("Z" * n_qubits), -] - -# Get ISA circuits - -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) -isa_circuits = pm.run(circuits) -isa_observables = [ob.apply_layout(isa_circuits[0].layout) for ob in observables] - -estimator = Estimator(backend) -job = estimator.run(isa_circuits, isa_observables) -result = job.result() - -print(f" > Expectation values: {result.values}") -``` - - - -### Run parameterized circuits - -Use Estimator to run multiple experiments in a single job, leveraging parameter values to increase circuit reusability. In the following example, notice that steps 1 and 2 are the same for V1 and V2. - - - - ```python -import numpy as np - -from qiskit.circuit import QuantumCircuit, Parameter -from qiskit.quantum_info import SparsePauliOp -from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit_ibm_runtime import QiskitRuntimeService - -# Step 1: Map classical inputs to a quantum problem - -theta = Parameter("θ") - -chsh_circuit = QuantumCircuit(2) -chsh_circuit.h(0) -chsh_circuit.cx(0, 1) -chsh_circuit.ry(theta, 0) - -number_of_phases = 21 -phases = np.linspace(0, 2 * np.pi, number_of_phases) -individual_phases = [[ph] for ph in phases] - -ZZ = SparsePauliOp.from_list([("ZZ", 1)]) -ZX = SparsePauliOp.from_list([("ZX", 1)]) -XZ = SparsePauliOp.from_list([("XZ", 1)]) -XX = SparsePauliOp.from_list([("XX", 1)]) -ops = [ZZ, ZX, XZ, XX] - -# Step 2: Optimize problem for quantum execution. - -service = QiskitRuntimeService() -backend = service.least_busy(operational=True, simulator=False) - -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) -chsh_isa_circuit = pm.run(chsh_circuit) -isa_observables = [operator.apply_layout(chsh_isa_circuit.layout) for operator in ops] - -from qiskit_ibm_runtime import EstimatorV2 as Estimator - -# Step 3: Execute using Qiskit primitives. - -# Reshape observable array for broadcasting -reshaped_ops = np.fromiter(isa_observables, dtype=object) -reshaped_ops = reshaped_ops.reshape((4, 1)) - -estimator = Estimator(backend, options={"default_shots": int(1e4)}) -job = estimator.run([(chsh_isa_circuit, reshaped_ops, individual_phases)]) -# Get results for the first (and only) PUB -pub_result = job.result()[0] -print(f">>> Expectation values: {pub_result.data.evs}") -print(f">>> Standard errors: {pub_result.data.stds}") -print(f">>> Metadata: {pub_result.metadata}") -``` - - - - ```python -import numpy as np - -from qiskit.circuit import QuantumCircuit, Parameter -from qiskit.quantum_info import SparsePauliOp -from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit_ibm_runtime import QiskitRuntimeService - -# Step 1: Map classical inputs to a quantum problem - -theta = Parameter("θ") - -chsh_circuit = QuantumCircuit(2) -chsh_circuit.h(0) -chsh_circuit.cx(0, 1) -chsh_circuit.ry(theta, 0) - -number_of_phases = 21 -phases = np.linspace(0, 2 * np.pi, number_of_phases) -individual_phases = [[ph] for ph in phases] - -ZZ = SparsePauliOp.from_list([("ZZ", 1)]) -ZX = SparsePauliOp.from_list([("ZX", 1)]) -XZ = SparsePauliOp.from_list([("XZ", 1)]) -XX = SparsePauliOp.from_list([("XX", 1)]) -ops = [ZZ, ZX, XZ, XX] - -# Step 2: Optimize problem for quantum execution. - -service = QiskitRuntimeService() -backend = service.least_busy(operational=True, simulator=False) - -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) -chsh_isa_circuit = pm.run(chsh_circuit) -isa_observables = [operator.apply_layout(chsh_isa_circuit.layout) for operator in ops] - -from qiskit_ibm_runtime import Estimator - -# Step 3: Execute using Qiskit Primitives. -num_ops = len(isa_observables) - -batch_circuits = [chsh_isa_circuit] * number_of_phases * num_ops -batch_ops = [op for op in isa_observables for _ in individual_phases] -batch_phases = individual_phases * num_ops - -estimator = Estimator(backend, options={"shots": int(1e4)}) -job = estimator.run(batch_circuits, batch_ops, batch_phases) -expvals = job.result().values -``` - - - -### Use sessions and advanced options - -Explore sessions and advanced options to optimize circuit performance on QPUs. - - - -The following code block will return an error for users on the Open Plan because it uses sessions. Workloads on the Open Plan can run only in [job mode](/docs/guides/execution-modes#job-mode) or [batch mode](/docs/guides/execution-modes#batch-mode). - - - -{/*Verified EstimatorV2. 2/23 */} - - - -```python -import numpy as np -from qiskit.circuit.library import IQP -from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit.quantum_info import SparsePauliOp, random_hermitian -from qiskit_ibm_runtime import QiskitRuntimeService, Session, EstimatorV2 as Estimator - -n_qubits = 127 - -rng = np.random.default_rng(1234) -mat = np.real(random_hermitian(n_qubits, seed=rng)) -circuit = IQP(mat) -mat = np.real(random_hermitian(n_qubits, seed=rng)) -another_circuit = IQP(mat) -observable = SparsePauliOp("X" * n_qubits) -another_observable = SparsePauliOp("Y" * n_qubits) - -pm = generate_preset_pass_manager(optimization_level=1, backend=backend) -isa_circuit = pm.run(circuit) -another_isa_circuit = pm.run(another_circuit) -isa_observable = observable.apply_layout(isa_circuit.layout) -another_isa_observable = another_observable.apply_layout(another_isa_circuit.layout) - -service = QiskitRuntimeService() - -backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) - -with Session(backend=backend) as session: - estimator = Estimator() - - estimator.options.resilience_level = 1 - - job = estimator.run([(isa_circuit, isa_observable)]) - another_job = estimator.run([(another_isa_circuit, another_isa_observable)]) - result = job.result() - another_result = another_job.result() - - # first job - print(f" > Expectation value: {result[0].data.evs}") - print(f" > Metadata: {result[0].metadata}") - - # second job - print(f" > Another Expectation value: {another_result[0].data.evs}") - print(f" > More Metadata: {another_result[0].metadata}") -``` - - - -```python -import numpy as np -from qiskit.circuit.library import IQP -from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit.quantum_info import SparsePauliOp, random_hermitian -from qiskit_ibm_runtime import QiskitRuntimeService, Session, Estimator, Options - -n_qubits = 127 - -rng = np.random.default_rng(1234) -mat = np.real(random_hermitian(n_qubits, seed=rng)) -circuit = IQP(mat) -mat = np.real(random_hermitian(n_qubits, seed=rng)) -another_circuit = IQP(mat) -observable = SparsePauliOp("X" * n_qubits) -another_observable = SparsePauliOp("Y" * n_qubits) - -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) -isa_circuit = pm.run(circuit) -another_isa_circuit = pm.run(another_circuit) -isa_observable = observable.apply_layout(isa_circuit.layout) -another_isa_observable = another_observable.apply_layout(another_isa_circuit.layout) - -options = Options() -options.optimization_level = 2 -options.resilience_level = 2 - -service = QiskitRuntimeService() - -backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) - -with Session(backend=backend) as session: - estimator = Estimator(options=options) - job = estimator.run(isa_circuit, isa_observable) - another_job = estimator.run(another_isa_circuit, another_isa_observable) - result = job.result() - another_result = another_job.result() - -# first job -print(f" > Expectation values job 1: {result.values}") - -# second job -print(f" > Expectation values job 2: {another_result.values}") -``` - - - -## Steps to migrate to Sampler V2 - -1. Replace `from qiskit_ibm_runtime import Sampler` with `from qiskit_ibm_runtime import SamplerV2 as Sampler`. -2. Remove any `from qiskit_ibm_runtime import Options` statements, since the `Options` class is not used by V2 primitives. You can instead pass options as a dictionary when initializing the `SamplerV2` class (for example `sampler = Sampler(backend, options={“default_shots”: 1024})`), or set them after initialization: - ```python - sampler = Sampler(backend) - sampler.options.default_shots = 1024 - ``` -3. Review all the [supported options](/docs/api/qiskit-ibm-runtime/options) and make updates accordingly. -4. Group each circuit you want to run with the observables and parameter values you want to apply to the circuit in a tuple (a PUB). For example, use `(circuit1, parameter_set1)` if you want to run `circuit1` with `parameter_set1`. - You can optionally specify the shots you want for that specific PUB. -5. Update Sampler's `run()` method to pass in the list of PUBs. For example, `run([(circuit1, parameter_set1)])`. - You can optionally specify `shots` here, which would apply to all PUBs. -6. Sampler V2 job results are grouped by PUBs. You can see the output data for each PUB by indexing to it. While Sampler V2 returns unweighted samples, the result class has a convenience method to get counts instead. For example: - ```python - pub_result = job.result()[0] - print(f">>> Counts: {pub_result.data.meas.get_counts()}") - print(f">>> Per-shot measurement: {pub_result.data.meas.get_counts()}") - ``` - - You need the classical register name to get the results. By default, it is named `meas` when you use `measure_all()`. When defining your circuit, if you create one or more classical registers with a non-default name, use that name to get the results. You can find the classical register name by running `.cregs`. For example, `qc.cregs`. - - -## Sampler full examples - -### Run a single experiment - -Use Sampler to determine the counts or quasi-probability distribution of a single circuit. - -{/*Verified 2/28 */} - - -```python -import numpy as np -from qiskit.circuit.library import IQP -from qiskit.quantum_info import random_hermitian -from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler -from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager - -service = QiskitRuntimeService() - -backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) - -n_qubits = 127 - -mat = np.real(random_hermitian(n_qubits, seed=1234)) -circuit = IQP(mat) -circuit.measure_all() - -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) -isa_circuit = pm.run(circuit) - -sampler = Sampler(backend) -job = sampler.run([isa_circuit]) -result = job.result() -``` - - - - ```python -import numpy as np -from qiskit.circuit.library import IQP -from qiskit.quantum_info import random_hermitian -from qiskit_ibm_runtime import QiskitRuntimeService, Sampler - -service = QiskitRuntimeService() - -backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) - -n_qubits = 127 - -mat = np.real(random_hermitian(n_qubits, seed=1234)) -circuit = IQP(mat) -circuit.measure_all() - -sampler = Sampler(backend) -job = sampler.run(circuit) -result = job.result() - -print(f" > Quasi-probability distribution: {result.quasi_dists}") -print(f" > Metadata: {result.metadata}") - ``` - - - -### Run multiple experiments in a single job - -Use Sampler to determine the counts or quasi-probability distributions of multiple circuits in one job. - -{/*Verified 2/28 */} - - - -```python -import numpy as np -from qiskit.circuit.library import IQP -from qiskit.quantum_info import random_hermitian -from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler - -service = QiskitRuntimeService() - -backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) - -n_qubits = 127 - -rng = np.random.default_rng() -mats = [np.real(random_hermitian(n_qubits, seed=rng)) for _ in range(3)] -circuits = [IQP(mat) for mat in mats] -for circuit in circuits: - circuit.measure_all() - -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) -isa_circuits = pm.run(circuits) - -sampler = Sampler(backend) -job = sampler.run(isa_circuits) -result = job.result() - -for idx, pub_result in enumerate(result): - print(f" > Counts for pub {idx}: {pub_result.data.meas.get_counts()}") -``` - - - -```python -import numpy as np -from qiskit.circuit.library import IQP -from qiskit.quantum_info import random_hermitian -from qiskit_ibm_runtime import QiskitRuntimeService, Sampler - -service = QiskitRuntimeService() - -backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) - -n_qubits = 127 - -rng = np.random.default_rng() -mats = [np.real(random_hermitian(n_qubits, seed=rng)) for _ in range(3)] -circuits = [IQP(mat) for mat in mats] -for circuit in circuits: - circuit.measure_all() - -sampler = Sampler(backend) -job = sampler.run(circuits) -result = job.result() - -print(f" > Quasi-probability distribution: {result.quasi_dists}") -``` - - - -### Run parameterized circuits - -Run several experiments in a single job, leveraging parameter values to increase circuit reusability. - -{/*Verified 2/28 */} - - - -```python -import numpy as np -from qiskit.circuit.library import RealAmplitudes -from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit_ibm_runtime import QiskitRuntimeService - -# Step 1: Map classical inputs to a quantum problem -num_qubits = 127 -circuit = RealAmplitudes(num_qubits=num_qubits, reps=2) -circuit.measure_all() - -# Define three sets of parameters for the circuit -rng = np.random.default_rng(1234) -parameter_values = [ - rng.uniform(-np.pi, np.pi, size=circuit.num_parameters) for _ in range(3) -] - -# Step 2: Optimize problem for quantum execution. - -service = QiskitRuntimeService() -backend = service.least_busy(operational=True, simulator=False, min_num_qubits=num_qubits) - -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) -isa_circuit = pm.run(circuit) - -# Step 3: Execute using Qiskit primitives. - -from qiskit_ibm_runtime import SamplerV2 as Sampler - -sampler = Sampler(backend) -job = sampler.run([(isa_circuit, parameter_values)]) -result = job.result() -# Get results for the first (and only) PUB -pub_result = result[0] -# Get counts from the classical register "meas". -print(f" >> Counts for the meas output register: {pub_result.data.meas.get_counts()}") -``` - - - -```python -import numpy as np -from qiskit.circuit.library import RealAmplitudes -from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit_ibm_runtime import QiskitRuntimeService - -# Step 1: Map classical inputs to a quantum problem -num_qubits = 5 -circuit = RealAmplitudes(num_qubits=num_qubits, reps=2) -circuit.measure_all() - -# Define three sets of parameters for the circuit -rng = np.random.default_rng(1234) -parameter_values = [ - rng.uniform(-np.pi, np.pi, size=circuit.num_parameters) for _ in range(3) -] - -# Step 2: Optimize problem for quantum execution. - -service = QiskitRuntimeService() -backend = service.least_busy(operational=True, simulator=False, min_num_qubits=num_qubits) - -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) -isa_circuit = pm.run(circuit) - -# Step 3: Execute using Qiskit primitives. - -from qiskit_ibm_runtime import Sampler - -sampler = Sampler(backend) -job = sampler.run([isa_circuit] * 3, parameter_values) -result = job.result() - -print(f" > Quasi-probability distribution: {result.quasi_dists}") -print(f" > Metadata: {result.metadata}") -``` - - - -### Use sessions and advanced options - -Explore sessions and advanced options to optimize circuit performance on QPUs. - - - -The following code block will return an error for users on the Open Plan because it uses sessions. Workloads on the Open Plan can run only in [job mode](/docs/guides/execution-modes#job-mode) or [batch mode](/docs/guides/execution-modes#batch-mode). - - - -{/*Verified 2/28 */} - - - -```python -import numpy as np -from qiskit.circuit.library import IQP -from qiskit.quantum_info import random_hermitian -from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler, Session - -n_qubits = 127 - -rng = np.random.default_rng(1234) -mat = np.real(random_hermitian(n_qubits, seed=rng)) -circuit = IQP(mat) -circuit.measure_all() -mat = np.real(random_hermitian(n_qubits, seed=rng)) -another_circuit = IQP(mat) -another_circuit.measure_all() - -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) -isa_circuit = pm.run(circuit) -another_isa_circuit = pm.run(another_circuit) - -service = QiskitRuntimeService() - -# Turn on dynamical decoupling with sequence XpXm. -sampler.options.dynamical_decoupling.enable = True -sampler.options.dynamical_decoupling.sequence_type = "XpXm" - -backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) - -with Session(backend=backend) as session: - sampler = Sampler() - job = sampler.run([isa_circuit]) - another_job = sampler.run([another_isa_circuit]) - result = job.result() - another_result = another_job.result() - -# first job -print(f" > Counts for job 1: {result[0].data.meas.get_counts()}") - -# second job -print(f" > Counts for job 2: {another_result[0].data.meas.get_counts()}") -``` - - - -```python -import numpy as np -from qiskit.circuit.library import IQP -from qiskit.quantum_info import random_hermitian -from qiskit_ibm_runtime import QiskitRuntimeService, Sampler, Session, Options - -n_qubits = 127 - -rng = np.random.default_rng(1234) -mat = np.real(random_hermitian(n_qubits, seed=rng)) -circuit = IQP(mat) -circuit.measure_all() -mat = np.real(random_hermitian(n_qubits, seed=rng)) -another_circuit = IQP(mat) -another_circuit.measure_all() - -options = Options() -options.optimization_level = 2 -options.resilience_level = 0 - -service = QiskitRuntimeService() - -backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) - -with Session(backend=backend) as session: - sampler = Sampler(options=options) - job = sampler.run(circuit) - another_job = sampler.run(another_circuit) - result = job.result() - another_result = another_job.result() - -# first job -print(f" > Quasi-probability distribution job 1: {result.quasi_dists}") - -# second job -print(f" > Quasi-probability distribution job 2: {another_result.quasi_dists}") -``` - - - -## Next steps - - - - Learn more about setting options in the [Sampler options](/docs/guides/sampler-options), [Estimator options](/docs/guides/estimator-options), and [Executor options](/docs/guides/executor-options) guides. - - Learn more details about [Primitive inputs and outputs](/docs/guides/primitive-input-output#pubs). - - Experiment with the [CHSH inequality](/docs/tutorials/chsh-inequality) tutorial. - +--- +title: Migrate to the Qiskit Runtime V2 primitives +description: Migrate from using the original primitives (Estimator and Sampler) to using the updated V2 primitives (Estimator V2 and Sampler V2) + +--- + +{/* cspell:ignore expvals */} + + +# Migrate to the Qiskit Runtime V2 primitives + + +The original primitives (referred to as the V1 primitives), [V1 Sampler](/docs/api/qiskit-ibm-runtime/0.26/sampler-v1) and [V1 Estimator](/docs/api/qiskit-ibm-runtime/0.26/estimator-v1), have been deprecated in `qiskit-ibm-runtime` 0.23. +Their support was removed on 15 August 2024. + + +With the deprecation of the V1 primitives, all code should be migrated to use the V2 interfaces. This guide describes what changed in the Qiskit Runtime V2 primitives (available with qiskit-ibm-runtime 0.21.0) and why, describes each new primitive in detail, and gives examples to help you migrate code from using the legacy primitives to the V2 primitives. The examples in the guide all use the Qiskit Runtime primitives, but in general, the same changes apply to the other primitive implementations. The functions unique to Qiskit Runtime such as error mitigation remain unique to Qiskit Runtime. + +For information about the changes to the Qiskit reference primitives (now called *statevector* primitives), see [the qiskit.primitives section](qiskit-1.0-features#qiskit.primitives) in the Qiskit 1.0 feature changes page. See [StatevectorSampler](/docs/api/qiskit/qiskit.primitives.StatevectorSampler) and [StatevectorEstimator](/docs/api/qiskit/qiskit.primitives.StatevectorEstimator) for V2 primitive reference implementations. + +## Overview + +Version 2 of the primitives is introduced with a new base class for both Sampler and Estimator ([BaseSamplerV2](/docs/api/qiskit/qiskit.primitives.BaseSamplerV2) and [BaseEstimatorV2](/docs/api/qiskit/qiskit.primitives.BaseEstimatorV2)), along with new types for their inputs and outputs. + +The new interface lets you specify a single circuit and multiple observables (if using Estimator) and parameter value sets for that circuit, so that sweeps over parameter value sets and observables can be efficiently specified. Previously, you had to specify the same circuit multiple times to match the size of the data to be combined. Also, while you can still use `resilience_level` (if using Estimator) as the simple knob, V2 primitives give you the flexibility to turn on or off individual error mitigation / suppression methods to customize them for your needs. + +To reduce the total job execution time, V2 primitives only accept circuits and observables that use instructions supported by the target QPU (quantum processing unit). Such circuits and observables are referred to as instruction set architecture (ISA) circuits and observables. V2 primitives do not perform layout, routing, and translation operations. See the [transpilation documentation](/docs/guides/transpile) for instructions to transform circuits. + +Sampler V2 is simplified to focus on its core task of sampling the output register from execution of quantum circuits. It returns the samples, whose type is defined by the program, without weights. The output data is also separated by the output register names defined by the program. This change enables future support for circuits with classical control flow. + +See the [EstimatorV2 API reference](/docs/api/qiskit-ibm-runtime/estimator-v2) and [SamplerV2 API reference](/docs/api/qiskit-ibm-runtime/sampler-v2) for full details. + +## Major changes + +### Import + +For backward compatibility, you must explicity import the V2 primitives. Specifying `import V2 as ` is not required, but makes it easier to transition code to V2. + + +After the V1 primitives are no longer supported, `import ` will import the V2 version of the specified primitive. + + + + + ```python +from qiskit_ibm_runtime import EstimatorV2 as Estimator +``` + + + + ```python +from qiskit_ibm_runtime import Estimator +``` + + + + + + ```python +from qiskit_ibm_runtime import SamplerV2 as Sampler +``` + + + + ```python +from qiskit_ibm_runtime import Sampler +``` + + + + +### Input and output + +#### Input + +Both `SamplerV2` and `EstimatorV2` take one or more *primitive unified blocs* (PUBs) as the input. Each PUB is a tuple that contains **one** circuit and the data broadcasted to that circuit, which can be multiple observables and parameters. Each PUB returns a result. + +* Sampler V2 PUB format: (``, ``, ``), where ``and `` are optional. +* Estimator V2 PUB format: (``, ``, ``, ``), where ``and `` are optional. + Numpy [broadcasting rules](https://numpy.org/doc/stable/user/basics.broadcasting.html) are used when combining observables and parameter values. + +Additionally, the following changes have been made: + +* Estimator V2 has gained a `precision` argument in the `run()` method that specifies the targeted precision of the expectation value estimates. +* Sampler V2 has the `shots` argument in its `run()` method. + +##### Examples + +Estimator V2 example that uses precision in `run()`: + +```python +# Estimate expectation values for two PUBs, both with 0.05 precision. +estimator.run([(circuit1, obs_array1), (circuit2, obs_array_2)], precision=0.05) +``` + +Sampler V2 example that uses shots in `run()`: + +```python +# Sample two circuits at 128 shots each. +sampler.run([circuit1, circuit2], shots=128) + +# Sample two circuits at different amounts of shots. +# The "None"s are necessary as placeholders +# for the lack of parameter values in this example. +sampler.run([ + (circuit1, None, 123), + (circuit2, None, 456), +]) +``` + +#### Output + +The output is now in the [`PubResult`](/docs/api/qiskit/qiskit.primitives.PubResult) format. A `PubResult` is the data and metadata resulting from a single PUB’s execution. + +* Estimator V2 continues to return expectation values. +* The `data` portion of a Estimator V2 PubResult contains both expectation values and standard errors (`stds`). V1 returned variance in metadata. +* Sampler V2 returns per-shot measurements in the form of **bitstrings**, + instead of the quasi-probability distributions from the V1 + interface. The bitstrings show the measurement outcomes, preserving the shot + order in which they were measured. +* Sampler V2 has convenience methods like `get_counts()` to help with migration. +* The Sampler V2 result objects organize data in terms of their **input circuits' classical register names**, for + compatibility with dynamic circuits. By default, the classical register name is `meas`, as shown in the following example. When defining your circuit, if you create one or more classical registers with a non-default name, use that name to get the results. You can find the classical register name by running `.cregs`. For example, `qc.cregs`. + + ```python + # Define a quantum circuit with 2 qubits + circuit = QuantumCircuit(2) + circuit.h(0) + circuit.cx(0, 1) + circuit.measure_all() + circuit.draw() + ``` + ```text + ┌───┐ ░ ┌─┐ + q_0: ┤ H ├──■───░─┤M├─── + └───┘┌─┴─┐ ░ └╥┘┌─┐ + q_1: ─────┤ X ├─░──╫─┤M├ + └───┘ ░ ║ └╥┘ + meas: 2/══════════════╩══╩═ + 0 1 + ``` + +#### Estimator examples (input and output) + +{/*Verified by Elena.*/} + + + + ```python + # Estimator V1: Execute 1 circuit with 4 observables + job = estimator_v1.run([circuit] * 4, [obs1, obs2, obs3, obs4]) + evs = job.result().values + + # Estimator V2: Execute 1 circuit with 4 observables + job = estimator_v2.run([(circuit, [obs1, obs2, obs3, obs4])]) + evs = job.result()[0].data.evs +``` + + + + ```python + # Estimator V1: Execute 1 circuit with 4 observables and 2 parameter sets + job = estimator_v1.run([circuit] * 8, [obs1, obs2, obs3, obs4] * 2, [vals1, vals2] * 4) + evs = job.result().values + + # Estimator V2: Execute 1 circuit with 4 observables and 2 parameter sets + + job = estimator_v2.run([(circuit, [[obs1], [obs2], [obs3], [obs4]], [[vals1], [vals2]])]) + evs = job.result()[0].data.evs +``` + + + + ```python + # Estimator V1: Cannot execute 2 circuits with different observables + + # Estimator V2: Execute 2 circuits with 2 different observables. There are + # two PUBs because each PUB can have only one circuit. + job = estimator_v2.run([(circuit1, obs1), (circuit2, obs2)]) + evs1 = job.result()[0].data.evs # result for pub 1 (circuit 1) + evs2 = job.result()[1].data.evs # result for pub 2 (circuit 2) +``` + + + + +#### Sampler examples (input and output) + +{/*Verified by Elena.*/} + + + + ```python + # Sampler V1: Execute 1 circuit with 3 parameter sets + job = sampler_v1.run([circuit] * 3, [vals1, vals2, vals3]) + dists = job.result().quasi_dists + + # Sampler V2: Executing 1 circuit with 3 parameter sets + job = sampler_v2.run([(circuit, [vals1, vals2, vals3])]) + counts = job.result()[0].data.meas.get_counts() +``` + + + + ```python + # Sampler V1: Execute 2 circuits with 1 parameter set + job = sampler_v1.run([circuit1, circuit2], [vals1] * 2) + dists = job.result().quasi_dists + + # Sampler V2: Execute 2 circuits with 1 parameter set + job = sampler_v2.run([(circuit1, vals1), (circuit2, vals1)]) + counts1 = job.result()[0].data.meas.get_counts() # result for pub 1 (circuit 1) + counts2 = job.result()[1].data.meas.get_counts() # result for pub 2 (circuit 2) +``` + + + + The V1 output format was a dictionary of bitstrings (as an int) as the key and quasi-probabilities as the value for each circuit. The V2 format has the same key (but as a string) and counts as the value. To convert the V2 format to V1, divide the counts by the number of shots, where the number of shots selected is described in the [Sampler options](/docs/guides/sampler-options#shots) guide. + + ```python + v2_result = sampler_v2_job.result() + v1_format = [] + for pub_result in v2_result: + counts = pub_result.data.meas.get_counts() + v1_format.append( {int(key, 2): val/shots for key, val in counts.items()} ) +``` + + + +Example that uses different output registers + +```python +from qiskit import ClassicalRegister, QuantumRegister, QuantumCircuit +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager +from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler + +alpha = ClassicalRegister(5, "alpha") +beta = ClassicalRegister(7, "beta") +qreg = QuantumRegister(12) + +circuit = QuantumCircuit(qreg, alpha, beta) +circuit.h(0) +circuit.measure(qreg[:5], alpha) +circuit.measure(qreg[5:], beta) + +service = QiskitRuntimeService() +backend = service.least_busy(operational=True, simulator=False, min_num_qubits=12) +pm = generate_preset_pass_manager(backend=backend, optimization_level=1) +isa_circuit = pm.run(circuit) + +sampler = Sampler(backend) +job = sampler.run([isa_circuit]) +result = job.result() +# Get results for the first (and only) PUB +pub_result = result[0] +print(f" >> Counts for the alpha output register: {pub_result.data.alpha.get_counts()}") +print(f" >> Counts for the beta output register: {pub_result.data.beta.get_counts()}") +``` + + +### Options + +Options are specified differently in the V2 primitives in these ways: + +- `SamplerV2` and `EstimatorV2` now have separate options classes. You can see the available options and update option values during or after primitive initialization. +- Instead of the `set_options()` method, V2 primitive options have the `update()` method that applies changes to the `options` attribute. +- If you do not specify a value for an option, it is given a special value of `Unset` and the server defaults are used. +- For V2 primitives, the `options` attribute is the `dataclass` Python type. You can use the built-in `asdict` method to convert it to a dictionary. + +See the [API reference](/docs/api/qiskit-ibm-runtime/options) for the list of available options. + +{/*Verified EstimatorV2. 2/23 */} + + + + ```python +from dataclasses import asdict +from qiskit_ibm_runtime import QiskitRuntimeService +from qiskit_ibm_runtime import EstimatorV2 as Estimator + +service = QiskitRuntimeService() +backend = service.least_busy(operational=True, simulator=False) + +# Setting options during primitive initialization +estimator = Estimator(backend, options={"resilience_level": 2}) + +# Setting options after primitive initialization +# This uses auto complete. +estimator.options.default_shots = 4000 +# This does bulk update. +estimator.options.update(default_shots=4000, resilience_level=2) + +# Print the dictionary format. +# Server defaults are used for unset options. +print(asdict(estimator.options)) +``` + + + + ```python +from qiskit_ibm_runtime import QiskitRuntimeService, Sampler, Options + +service = QiskitRuntimeService() +backend = service.least_busy(operational=True, simulator=False) + +# Setting options during primitive initialization +options = Options() +# This uses auto complete. +options.resilience_level = 2 +estimator = Estimator(backend=backend, options=options) + +# Setting options after primitive initialization. +# This does bulk update. +estimator.set_options(shots=4000) +``` + + + + + + ```python +from dataclasses import asdict +from qiskit_ibm_runtime import QiskitRuntimeService +from qiskit_ibm_runtime import SamplerV2 as Sampler + +service = QiskitRuntimeService() +backend = service.least_busy(operational=True, simulator=False) + +# Setting options during primitive initialization +sampler = Sampler(backend, options={"default_shots": 4096}) + +# Setting options after primitive initialization +# This uses auto complete. +sampler.options.dynamical_decoupling.enable = True +# Turn on gate twirling. Requires qiskit_ibm_runtime 0.23.0 or later. +sampler.options.twirling.enable_gates = True + +# This does bulk update. +# The value for default_shots is overridden if you specify shots with run() or in the PUB. +sampler.options.update(default_shots=1024, dynamical_decoupling={"sequence_type": "XpXm"}) + +# Print the dictionary format. +# Server defaults are used for unset options. +print(asdict(sampler.options)) +``` + + + + ```python +from qiskit_ibm_runtime import QiskitRuntimeService, Sampler, Options + +service = QiskitRuntimeService() +backend = service.least_busy(operational=True, simulator=False) + +# Setting options during primitive initialization +options = Options() +# This uses auto complete. +options.resilience_level = 2 +sampler = Sampler(backend=backend, options=options) + +# Setting options after primitive initialization. +# This does bulk update. +sampler.set_options(shots=2000) + ``` + + + + +### Error mitigation and suppression + +* Because Sampler V2 returns samples without postprocessing, it does not support resilience levels. +* Sampler V2 does not support `optimization_level`. +* Estimator V2 will drop support for `optimization_level` on or around 30 September 2024. +* Estimator V2 does not support resilience level 3. This is because resilience level 3 in V1 Estimator uses Probabilistic Error Cancellation (PEC), which is proven to give unbiased results at the cost of exponential processing time. Level 3 was removed to draw attention to that tradeoff. You can, however, still use PEC as the error mitigation method by specifying the `pec_mitigation` option. +* Estimator V2 supports `resilience_level` 0-2, as described in the following table. These options are more advanced than their V1 counterparts. You can also explicitly turn on / off individual error mitigation / suppression methods. + + | Level 1 | Level 2 | + |---------------------------|---------------------------| + | Measurement twirling | Measurement twirling | + | Readout error mitigation | Readout error mitigation | + | | ZNE | + +{/*Verified EstimatorV2. 2/23 */} + + + + ```python +from dataclasses import asdict +from qiskit_ibm_runtime import QiskitRuntimeService +from qiskit_ibm_runtime import EstimatorV2 as Estimator + +service = QiskitRuntimeService() +backend = service.least_busy(operational=True, simulator=False) + +# Setting options during primitive initialization +estimator = Estimator(backend) + +# Set resilience_level to 0 +estimator.options.resilience_level = 0 + +# Turn on measurement error mitigation +estimator.options.resilience.measure_mitigation = True +``` + + + + ```python +from qiskit_ibm_runtime import Estimator, Options + +estimator = Estimator(backend, options=options) + +options = Options() + +options.resilience_level = 2 +``` + + + + + + + + ```python +from qiskit_ibm_runtime import SamplerV2 as Sampler + +sampler = Sampler(backend) +# Turn on dynamical decoupling with sequence XpXm. +sampler.options.dynamical_decoupling.enable = True +sampler.options.dynamical_decoupling.sequence_type = "XpXm" + +print(f">> dynamical decoupling sequence to use: {sampler.options.dynamical_decoupling.sequence_type}") +``` + + + + + ```python +from qiskit_ibm_runtime import Sampler, Options + +sampler = Sampler(backend, options=options) + +options = Options() + +options.resilience_level = 2 +``` + + + + +### Transpilation + +V2 primitives support only circuits that adhere to the Instruction Set Architecture (ISA) of a particular backend. Because the primitives do not perform layout, routing, and translation operations, the corresponding transpilation options from V1 are not supported. + + +### Job status + +The V2 primitives have a new `RuntimeJobV2` class, which inherits from `BasePrimitiveJob`. The `status()` method of this new class returns a string instead of a JobStatus enum from Qiskit. See the [RuntimeJobV2 API reference](/docs/api/qiskit-ibm-runtime/runtime-job-v2) for details. + + + +```python +job = estimator.run(...) + +# check if a job is still running +print(f"Job {job.job_id()} is still running: {job.status() == "RUNNING"}") +``` + + + +```python +from qiskit.providers.jobstatus import JobStatus + +job = estimator.run(...) + +#check if a job is still running +print(f"Job {job.job_id()} is still running: {job.status() is JobStatus.RUNNING}") +``` + + + +## Steps to migrate to Estimator V2 + +1. Replace `from qiskit_ibm_runtime import Estimator` with `from qiskit_ibm_runtime import EstimatorV2 as Estimator`. +2. Remove any `from qiskit_ibm_runtime import Options` statements, since the `Options` class is not used by V2 primitives. You can instead pass options as a dictionary when initializing the `EstimatorV2` class (for example `estimator = Estimator(backend, options={“dynamical_decoupling”: {“enable”: True}})`), or set them after initialization: + ```python + estimator = Estimator(backend) + estimator.options.dynamical_decoupling.enable = True + ``` + +3. Review all the [supported options](/docs/api/qiskit-ibm-runtime/options) and make updates accordingly. +4. Group each circuit you want to run with the observables and parameter values you want to apply to the circuit in a tuple (a PUB). For example, use `(circuit1, observable1, parameter_set1)` if you want to run `circuit1` with `observable1` and `parameter_set1`. + + 1. You might need to reshape your arrays of observables or parameter sets if you want to apply their outer product. For example, an array of observables of shape (4, 1) and an array of parameter sets of shape (1, 6) will give you a result of (4, 6) expectation values. See the [Numpy broadcasting rules](https://numpy.org/doc/stable/user/basics.broadcasting.html) for more details. + 2. You can optionally specify the precision you want for that specific PUB. +5. Update Estimator's `run()` method to pass in the list of PUBs. For example, `run([(circuit1, observable1, parameter_set1)])`. + You can optionally specify a `precision` here, which would apply to all PUBs. +6. Estimator V2 job results are grouped by PUBs. You can see the expectation value and standard error for each PUB by indexing to it. For example: + ```python + pub_result = job.result()[0] + print(f">>> Expectation values: {pub_result.data.evs}") + print(f">>> Standard errors: {pub_result.data.stds}") + ``` + +## Estimator full examples + +### Run a single experiment + +Use Estimator to determine the expectation value of a single circuit-observable pair. + +{/*Verified EstimatorV2. 2/23 */} + + + + ```python +import numpy as np +from qiskit.circuit.library import IQP +from qiskit.quantum_info import SparsePauliOp, random_hermitian +from qiskit_ibm_runtime import EstimatorV2 as Estimator, QiskitRuntimeService + +service = QiskitRuntimeService() +backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) +estimator = Estimator(backend) + +n_qubits = 127 + +mat = np.real(random_hermitian(n_qubits, seed=1234)) +circuit = IQP(mat) +observable = SparsePauliOp("Z" * n_qubits) + +pm = generate_preset_pass_manager(optimization_level=1, backend=backend) +isa_circuit = pm.run(circuit) +isa_observable = observable.apply_layout(isa_circuit.layout) + +job = estimator.run([(isa_circuit, isa_observable)]) +result = job.result() + +print(f" > Expectation value: {result[0].data.evs}") +print(f" > Metadata: {result[0].metadata}") +``` + + + + ```python +import numpy as np +from qiskit.circuit.library import IQP +from qiskit.quantum_info import SparsePauliOp, random_hermitian +from qiskit_ibm_runtime import QiskitRuntimeService, Estimator + +service = QiskitRuntimeService() + +backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) + +n_qubits = 127 + +mat = np.real(random_hermitian(n_qubits, seed=1234)) +circuit = IQP(mat) +observable = SparsePauliOp("Z" * n_qubits) + +pm = generate_preset_pass_manager(backend=backend, optimization_level=1) +isa_circuit = pm.run(circuit) +isa_observable = observable.apply_layout(isa_circuit.layout) + +estimator = Estimator(backend) +job = estimator.run(isa_circuit, isa_observable) +result = job.result() + +print(f" > Observable: {observable.paulis}") +print(f" > Expectation value: {result.values}") +print(f" > Metadata: {result.metadata}") +``` + + + +### Run multiple experiments in a single job + +Use Estimator to determine the expectation values of multiple circuit-observable pairs. + +{/*Verified EstimatorV2. 2/23 */} + + + +```python +service = QiskitRuntimeService() +backend = service.least_busy(operational=True, simulator=False) + +pm = generate_preset_pass_manager(backend=backend, optimization_level=1) + +n_qubits = 3 +rng = np.random.default_rng() +mats = [np.real(random_hermitian(n_qubits, seed=rng)) for _ in range(3)] +circuits = [IQP(mat) for mat in mats] +observables = [ + SparsePauliOp("X" * n_qubits), + SparsePauliOp("Y" * n_qubits), + SparsePauliOp("Z" * n_qubits), +] + +isa_circuits = pm.run(circuits) +isa_observables = [ob.apply_layout(isa_circuits[0].layout) for ob in observables] + + +estimator = Estimator(backend) +job = estimator.run([(isa_circuits[0], isa_observables[0]),(isa_circuits[1], isa_observables[1]),(isa_circuits[2], isa_observables[2])]) +job_result = job.result() +for idx in range(len(job_result)): + pub_result = job_result[idx] + print(f">>> Expectation values for PUB {idx}: {pub_result.data.evs}") + print(f">>> Standard errors for PUB {idx}: {pub_result.data.stds}") +``` + + + +```python +import numpy as np +from qiskit.circuit.library import IQP +from qiskit.quantum_info import SparsePauliOp, random_hermitian +from qiskit_ibm_runtime import QiskitRuntimeService, Estimator + +service = QiskitRuntimeService() + +backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) + +n_qubits = 127 + +rng = np.random.default_rng() +mats = [np.real(random_hermitian(n_qubits, seed=rng)) for _ in range(3)] +circuits = [IQP(mat) for mat in mats] +observables = [ + SparsePauliOp("X" * n_qubits), + SparsePauliOp("Y" * n_qubits), + SparsePauliOp("Z" * n_qubits), +] + +# Get ISA circuits + +pm = generate_preset_pass_manager(backend=backend, optimization_level=1) +isa_circuits = pm.run(circuits) +isa_observables = [ob.apply_layout(isa_circuits[0].layout) for ob in observables] + +estimator = Estimator(backend) +job = estimator.run(isa_circuits, isa_observables) +result = job.result() + +print(f" > Expectation values: {result.values}") +``` + + + +### Run parameterized circuits + +Use Estimator to run multiple experiments in a single job, leveraging parameter values to increase circuit reusability. In the following example, notice that steps 1 and 2 are the same for V1 and V2. + + + + ```python +import numpy as np + +from qiskit.circuit import QuantumCircuit, Parameter +from qiskit.quantum_info import SparsePauliOp +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager +from qiskit_ibm_runtime import QiskitRuntimeService + +# Step 1: Map classical inputs to a quantum problem + +theta = Parameter("θ") + +chsh_circuit = QuantumCircuit(2) +chsh_circuit.h(0) +chsh_circuit.cx(0, 1) +chsh_circuit.ry(theta, 0) + +number_of_phases = 21 +phases = np.linspace(0, 2 * np.pi, number_of_phases) +individual_phases = [[ph] for ph in phases] + +ZZ = SparsePauliOp.from_list([("ZZ", 1)]) +ZX = SparsePauliOp.from_list([("ZX", 1)]) +XZ = SparsePauliOp.from_list([("XZ", 1)]) +XX = SparsePauliOp.from_list([("XX", 1)]) +ops = [ZZ, ZX, XZ, XX] + +# Step 2: Optimize problem for quantum execution. + +service = QiskitRuntimeService() +backend = service.least_busy(operational=True, simulator=False) + +pm = generate_preset_pass_manager(backend=backend, optimization_level=1) +chsh_isa_circuit = pm.run(chsh_circuit) +isa_observables = [operator.apply_layout(chsh_isa_circuit.layout) for operator in ops] + +from qiskit_ibm_runtime import EstimatorV2 as Estimator + +# Step 3: Execute using Qiskit primitives. + +# Reshape observable array for broadcasting +reshaped_ops = np.fromiter(isa_observables, dtype=object) +reshaped_ops = reshaped_ops.reshape((4, 1)) + +estimator = Estimator(backend, options={"default_shots": int(1e4)}) +job = estimator.run([(chsh_isa_circuit, reshaped_ops, individual_phases)]) +# Get results for the first (and only) PUB +pub_result = job.result()[0] +print(f">>> Expectation values: {pub_result.data.evs}") +print(f">>> Standard errors: {pub_result.data.stds}") +print(f">>> Metadata: {pub_result.metadata}") +``` + + + + ```python +import numpy as np + +from qiskit.circuit import QuantumCircuit, Parameter +from qiskit.quantum_info import SparsePauliOp +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager +from qiskit_ibm_runtime import QiskitRuntimeService + +# Step 1: Map classical inputs to a quantum problem + +theta = Parameter("θ") + +chsh_circuit = QuantumCircuit(2) +chsh_circuit.h(0) +chsh_circuit.cx(0, 1) +chsh_circuit.ry(theta, 0) + +number_of_phases = 21 +phases = np.linspace(0, 2 * np.pi, number_of_phases) +individual_phases = [[ph] for ph in phases] + +ZZ = SparsePauliOp.from_list([("ZZ", 1)]) +ZX = SparsePauliOp.from_list([("ZX", 1)]) +XZ = SparsePauliOp.from_list([("XZ", 1)]) +XX = SparsePauliOp.from_list([("XX", 1)]) +ops = [ZZ, ZX, XZ, XX] + +# Step 2: Optimize problem for quantum execution. + +service = QiskitRuntimeService() +backend = service.least_busy(operational=True, simulator=False) + +pm = generate_preset_pass_manager(backend=backend, optimization_level=1) +chsh_isa_circuit = pm.run(chsh_circuit) +isa_observables = [operator.apply_layout(chsh_isa_circuit.layout) for operator in ops] + +from qiskit_ibm_runtime import Estimator + +# Step 3: Execute using Qiskit Primitives. +num_ops = len(isa_observables) + +batch_circuits = [chsh_isa_circuit] * number_of_phases * num_ops +batch_ops = [op for op in isa_observables for _ in individual_phases] +batch_phases = individual_phases * num_ops + +estimator = Estimator(backend, options={"shots": int(1e4)}) +job = estimator.run(batch_circuits, batch_ops, batch_phases) +expvals = job.result().values +``` + + + +### Use sessions and advanced options + +Explore sessions and advanced options to optimize circuit performance on QPUs. + + + +The following code block will return an error for users on the Open Plan because it uses sessions. Workloads on the Open Plan can run only in [job mode](/docs/guides/execution-modes#job-mode) or [batch mode](/docs/guides/execution-modes#batch-mode). + + + +{/*Verified EstimatorV2. 2/23 */} + + + +```python +import numpy as np +from qiskit.circuit.library import IQP +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager +from qiskit.quantum_info import SparsePauliOp, random_hermitian +from qiskit_ibm_runtime import QiskitRuntimeService, Session, EstimatorV2 as Estimator + +n_qubits = 127 + +rng = np.random.default_rng(1234) +mat = np.real(random_hermitian(n_qubits, seed=rng)) +circuit = IQP(mat) +mat = np.real(random_hermitian(n_qubits, seed=rng)) +another_circuit = IQP(mat) +observable = SparsePauliOp("X" * n_qubits) +another_observable = SparsePauliOp("Y" * n_qubits) + +pm = generate_preset_pass_manager(optimization_level=1, backend=backend) +isa_circuit = pm.run(circuit) +another_isa_circuit = pm.run(another_circuit) +isa_observable = observable.apply_layout(isa_circuit.layout) +another_isa_observable = another_observable.apply_layout(another_isa_circuit.layout) + +service = QiskitRuntimeService() + +backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) + +with Session(backend=backend) as session: + estimator = Estimator() + + estimator.options.resilience_level = 1 + + job = estimator.run([(isa_circuit, isa_observable)]) + another_job = estimator.run([(another_isa_circuit, another_isa_observable)]) + result = job.result() + another_result = another_job.result() + + # first job + print(f" > Expectation value: {result[0].data.evs}") + print(f" > Metadata: {result[0].metadata}") + + # second job + print(f" > Another Expectation value: {another_result[0].data.evs}") + print(f" > More Metadata: {another_result[0].metadata}") +``` + + + +```python +import numpy as np +from qiskit.circuit.library import IQP +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager +from qiskit.quantum_info import SparsePauliOp, random_hermitian +from qiskit_ibm_runtime import QiskitRuntimeService, Session, Estimator, Options + +n_qubits = 127 + +rng = np.random.default_rng(1234) +mat = np.real(random_hermitian(n_qubits, seed=rng)) +circuit = IQP(mat) +mat = np.real(random_hermitian(n_qubits, seed=rng)) +another_circuit = IQP(mat) +observable = SparsePauliOp("X" * n_qubits) +another_observable = SparsePauliOp("Y" * n_qubits) + +pm = generate_preset_pass_manager(backend=backend, optimization_level=1) +isa_circuit = pm.run(circuit) +another_isa_circuit = pm.run(another_circuit) +isa_observable = observable.apply_layout(isa_circuit.layout) +another_isa_observable = another_observable.apply_layout(another_isa_circuit.layout) + +options = Options() +options.optimization_level = 2 +options.resilience_level = 2 + +service = QiskitRuntimeService() + +backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) + +with Session(backend=backend) as session: + estimator = Estimator(options=options) + job = estimator.run(isa_circuit, isa_observable) + another_job = estimator.run(another_isa_circuit, another_isa_observable) + result = job.result() + another_result = another_job.result() + +# first job +print(f" > Expectation values job 1: {result.values}") + +# second job +print(f" > Expectation values job 2: {another_result.values}") +``` + + + +## Steps to migrate to Sampler V2 + +1. Replace `from qiskit_ibm_runtime import Sampler` with `from qiskit_ibm_runtime import SamplerV2 as Sampler`. +2. Remove any `from qiskit_ibm_runtime import Options` statements, since the `Options` class is not used by V2 primitives. You can instead pass options as a dictionary when initializing the `SamplerV2` class (for example `sampler = Sampler(backend, options={“default_shots”: 1024})`), or set them after initialization: + ```python + sampler = Sampler(backend) + sampler.options.default_shots = 1024 + ``` +3. Review all the [supported options](/docs/api/qiskit-ibm-runtime/options) and make updates accordingly. +4. Group each circuit you want to run with the observables and parameter values you want to apply to the circuit in a tuple (a PUB). For example, use `(circuit1, parameter_set1)` if you want to run `circuit1` with `parameter_set1`. + You can optionally specify the shots you want for that specific PUB. +5. Update Sampler's `run()` method to pass in the list of PUBs. For example, `run([(circuit1, parameter_set1)])`. + You can optionally specify `shots` here, which would apply to all PUBs. +6. Sampler V2 job results are grouped by PUBs. You can see the output data for each PUB by indexing to it. While Sampler V2 returns unweighted samples, the result class has a convenience method to get counts instead. For example: + ```python + pub_result = job.result()[0] + print(f">>> Counts: {pub_result.data.meas.get_counts()}") + print(f">>> Per-shot measurement: {pub_result.data.meas.get_counts()}") + ``` + + You need the classical register name to get the results. By default, it is named `meas` when you use `measure_all()`. When defining your circuit, if you create one or more classical registers with a non-default name, use that name to get the results. You can find the classical register name by running `.cregs`. For example, `qc.cregs`. + + +## Sampler full examples + +### Run a single experiment + +Use Sampler to determine the counts or quasi-probability distribution of a single circuit. + +{/*Verified 2/28 */} + + +```python +import numpy as np +from qiskit.circuit.library import IQP +from qiskit.quantum_info import random_hermitian +from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager + +service = QiskitRuntimeService() + +backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) + +n_qubits = 127 + +mat = np.real(random_hermitian(n_qubits, seed=1234)) +circuit = IQP(mat) +circuit.measure_all() + +pm = generate_preset_pass_manager(backend=backend, optimization_level=1) +isa_circuit = pm.run(circuit) + +sampler = Sampler(backend) +job = sampler.run([isa_circuit]) +result = job.result() +``` + + + + ```python +import numpy as np +from qiskit.circuit.library import IQP +from qiskit.quantum_info import random_hermitian +from qiskit_ibm_runtime import QiskitRuntimeService, Sampler + +service = QiskitRuntimeService() + +backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) + +n_qubits = 127 + +mat = np.real(random_hermitian(n_qubits, seed=1234)) +circuit = IQP(mat) +circuit.measure_all() + +sampler = Sampler(backend) +job = sampler.run(circuit) +result = job.result() + +print(f" > Quasi-probability distribution: {result.quasi_dists}") +print(f" > Metadata: {result.metadata}") + ``` + + + +### Run multiple experiments in a single job + +Use Sampler to determine the counts or quasi-probability distributions of multiple circuits in one job. + +{/*Verified 2/28 */} + + + +```python +import numpy as np +from qiskit.circuit.library import IQP +from qiskit.quantum_info import random_hermitian +from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler + +service = QiskitRuntimeService() + +backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) + +n_qubits = 127 + +rng = np.random.default_rng() +mats = [np.real(random_hermitian(n_qubits, seed=rng)) for _ in range(3)] +circuits = [IQP(mat) for mat in mats] +for circuit in circuits: + circuit.measure_all() + +pm = generate_preset_pass_manager(backend=backend, optimization_level=1) +isa_circuits = pm.run(circuits) + +sampler = Sampler(backend) +job = sampler.run(isa_circuits) +result = job.result() + +for idx, pub_result in enumerate(result): + print(f" > Counts for pub {idx}: {pub_result.data.meas.get_counts()}") +``` + + + +```python +import numpy as np +from qiskit.circuit.library import IQP +from qiskit.quantum_info import random_hermitian +from qiskit_ibm_runtime import QiskitRuntimeService, Sampler + +service = QiskitRuntimeService() + +backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) + +n_qubits = 127 + +rng = np.random.default_rng() +mats = [np.real(random_hermitian(n_qubits, seed=rng)) for _ in range(3)] +circuits = [IQP(mat) for mat in mats] +for circuit in circuits: + circuit.measure_all() + +sampler = Sampler(backend) +job = sampler.run(circuits) +result = job.result() + +print(f" > Quasi-probability distribution: {result.quasi_dists}") +``` + + + +### Run parameterized circuits + +Run several experiments in a single job, leveraging parameter values to increase circuit reusability. + +{/*Verified 2/28 */} + + + +```python +import numpy as np +from qiskit.circuit.library import RealAmplitudes +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager +from qiskit_ibm_runtime import QiskitRuntimeService + +# Step 1: Map classical inputs to a quantum problem +num_qubits = 127 +circuit = RealAmplitudes(num_qubits=num_qubits, reps=2) +circuit.measure_all() + +# Define three sets of parameters for the circuit +rng = np.random.default_rng(1234) +parameter_values = [ + rng.uniform(-np.pi, np.pi, size=circuit.num_parameters) for _ in range(3) +] + +# Step 2: Optimize problem for quantum execution. + +service = QiskitRuntimeService() +backend = service.least_busy(operational=True, simulator=False, min_num_qubits=num_qubits) + +pm = generate_preset_pass_manager(backend=backend, optimization_level=1) +isa_circuit = pm.run(circuit) + +# Step 3: Execute using Qiskit primitives. + +from qiskit_ibm_runtime import SamplerV2 as Sampler + +sampler = Sampler(backend) +job = sampler.run([(isa_circuit, parameter_values)]) +result = job.result() +# Get results for the first (and only) PUB +pub_result = result[0] +# Get counts from the classical register "meas". +print(f" >> Counts for the meas output register: {pub_result.data.meas.get_counts()}") +``` + + + +```python +import numpy as np +from qiskit.circuit.library import RealAmplitudes +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager +from qiskit_ibm_runtime import QiskitRuntimeService + +# Step 1: Map classical inputs to a quantum problem +num_qubits = 5 +circuit = RealAmplitudes(num_qubits=num_qubits, reps=2) +circuit.measure_all() + +# Define three sets of parameters for the circuit +rng = np.random.default_rng(1234) +parameter_values = [ + rng.uniform(-np.pi, np.pi, size=circuit.num_parameters) for _ in range(3) +] + +# Step 2: Optimize problem for quantum execution. + +service = QiskitRuntimeService() +backend = service.least_busy(operational=True, simulator=False, min_num_qubits=num_qubits) + +pm = generate_preset_pass_manager(backend=backend, optimization_level=1) +isa_circuit = pm.run(circuit) + +# Step 3: Execute using Qiskit primitives. + +from qiskit_ibm_runtime import Sampler + +sampler = Sampler(backend) +job = sampler.run([isa_circuit] * 3, parameter_values) +result = job.result() + +print(f" > Quasi-probability distribution: {result.quasi_dists}") +print(f" > Metadata: {result.metadata}") +``` + + + +### Use sessions and advanced options + +Explore sessions and advanced options to optimize circuit performance on QPUs. + + + +The following code block will return an error for users on the Open Plan because it uses sessions. Workloads on the Open Plan can run only in [job mode](/docs/guides/execution-modes#job-mode) or [batch mode](/docs/guides/execution-modes#batch-mode). + + + +{/*Verified 2/28 */} + + + +```python +import numpy as np +from qiskit.circuit.library import IQP +from qiskit.quantum_info import random_hermitian +from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler, Session + +n_qubits = 127 + +rng = np.random.default_rng(1234) +mat = np.real(random_hermitian(n_qubits, seed=rng)) +circuit = IQP(mat) +circuit.measure_all() +mat = np.real(random_hermitian(n_qubits, seed=rng)) +another_circuit = IQP(mat) +another_circuit.measure_all() + +pm = generate_preset_pass_manager(backend=backend, optimization_level=1) +isa_circuit = pm.run(circuit) +another_isa_circuit = pm.run(another_circuit) + +service = QiskitRuntimeService() + +# Turn on dynamical decoupling with sequence XpXm. +sampler.options.dynamical_decoupling.enable = True +sampler.options.dynamical_decoupling.sequence_type = "XpXm" + +backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) + +with Session(backend=backend) as session: + sampler = Sampler() + job = sampler.run([isa_circuit]) + another_job = sampler.run([another_isa_circuit]) + result = job.result() + another_result = another_job.result() + +# first job +print(f" > Counts for job 1: {result[0].data.meas.get_counts()}") + +# second job +print(f" > Counts for job 2: {another_result[0].data.meas.get_counts()}") +``` + + + +```python +import numpy as np +from qiskit.circuit.library import IQP +from qiskit.quantum_info import random_hermitian +from qiskit_ibm_runtime import QiskitRuntimeService, Sampler, Session, Options + +n_qubits = 127 + +rng = np.random.default_rng(1234) +mat = np.real(random_hermitian(n_qubits, seed=rng)) +circuit = IQP(mat) +circuit.measure_all() +mat = np.real(random_hermitian(n_qubits, seed=rng)) +another_circuit = IQP(mat) +another_circuit.measure_all() + +options = Options() +options.optimization_level = 2 +options.resilience_level = 0 + +service = QiskitRuntimeService() + +backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) + +with Session(backend=backend) as session: + sampler = Sampler(options=options) + job = sampler.run(circuit) + another_job = sampler.run(another_circuit) + result = job.result() + another_result = another_job.result() + +# first job +print(f" > Quasi-probability distribution job 1: {result.quasi_dists}") + +# second job +print(f" > Quasi-probability distribution job 2: {another_result.quasi_dists}") +``` + + + +## Next steps + + + - Learn more about setting options in the [Sampler options](/docs/guides/sampler-options), [Estimator options](/docs/guides/estimator-options), and [Executor options](/docs/guides/executor-options) guides. + - Learn more details about [Primitive inputs and outputs](/docs/guides/primitive-input-output#pubs). + - Experiment with the [CHSH inequality](/docs/tutorials/chsh-inequality) tutorial. + diff --git a/docs/tutorials/advanced-techniques-for-qaoa.ipynb b/docs/tutorials/advanced-techniques-for-qaoa.ipynb index 60deeadbfb3..ac3401d227a 100644 --- a/docs/tutorials/advanced-techniques-for-qaoa.ipynb +++ b/docs/tutorials/advanced-techniques-for-qaoa.ipynb @@ -839,13 +839,15 @@ }, { "cell_type": "code", - "execution_count": 104, + "execution_count": null, "id": "7793ef92-ce59-4fd7-b43f-48d4e3427e3a", "metadata": {}, "outputs": [], "source": [ - "# We can define the edge_coloring map so that RZZ gates are positioned right before SWAP gates to exploit CX cancellations\n", - "# We use greedy edge coloring from rustworkx to color the edges of the graph. This coloring is used to order the RZZ gates in the circuit.\n", + "# We can define the edge_coloring map so that RZZ gates are positioned\n", + "# right before SWAP gates to exploit CX cancellations\n", + "# We use greedy edge coloring from rustworkx to color the edges of the graph.\n", + "# This coloring is used to order the RZZ gates in the circuit.\n", "\n", "edge_coloring_idx = rx.graph_greedy_edge_color(graph_100)\n", "edge_coloring = {\n", diff --git a/docs/tutorials/ai-transpiler-introduction.ipynb b/docs/tutorials/ai-transpiler-introduction.ipynb index 738b6c4fcf4..730022eb87c 100644 --- a/docs/tutorials/ai-transpiler-introduction.ipynb +++ b/docs/tutorials/ai-transpiler-introduction.ipynb @@ -1,1171 +1,1172 @@ -{ - "cells": [ - { - "attachments": {}, - "cell_type": "markdown", - "id": "f59032f0-f29a-4e52-9cab-855ed6f86b00", - "metadata": {}, - "source": [ - "---\n", - "title: Qiskit AI-powered transpiler service introduction\n", - "description: In this notebook, we will explore the key benefits of Qiskit AI-powered transpiler service and how it compares to traditional methods.\n", - "---\n", - "\n", - "\n", - "{/* cspell:ignore fontsize idxmin */}\n", - "\n", - "# Qiskit AI-powered transpiler service introduction\n", - "*Estimated QPU usage: None (NOTE: This tutorial does not execute jobs because it is focused on transpilation)*\n", - "\n", - "## Background\n", - "\n", - "The **Qiskit AI-powered transpiler service (QTS)** introduces machine learning-based optimizations in both routing and synthesis passes. These AI modes have been designed to tackle the limitations of traditional transpilation, particularly for large-scale circuits and complex hardware topologies.\n", - "\n", - "As of **July 2025**, the **Transpiler Service** has been migrated to the new IBM Quantum® Platform and is no longer available. For the latest updates about the status of the Transpiler Service, please refer to the [transpiler service documentation](/docs/guides/qiskit-transpiler-service). You can still use the AI transpiler locally, similar to standard Qiskit transpilation. Simply replace `generate_preset_pass_manager()` with `generate_ai_pass_manager()`. This function constructs a pass manager that integrates the AI-powered routing and synthesis passes directly into your local transpilation workflow.\n", - "\n", - "### Key features of AI passes\n", - "\n", - "- Routing passes: AI-powered routing can dynamically adjust qubit paths based on the specific circuit and backend, reducing the need for excessive SWAP gates.\n", - " - `AIRouting`: Layout selection and circuit routing\n", - "\n", - "- Synthesis passes: AI techniques optimize the decomposition of multi-qubit gates, minimizing the number of two-qubit gates, which are typically more error-prone.\n", - " - `AICliffordSynthesis`: Clifford gate synthesis\n", - " - `AILinearFunctionSynthesis`: Linear function circuit synthesis\n", - " - `AIPermutationSynthesis`: Permutation circuit synthesis\n", - " - `AIPauliNetworkSynthesis`: Pauli Network circuit synthesis (only available in the Qiskit Transpiler Service, not in local environment)\n", - "\n", - "- Comparison with traditional transpilation: The standard Qiskit transpiler is a robust tool that can handle a broad spectrum of quantum circuits effectively. However, when circuits grow larger in scale or hardware configurations become more complex, AI passes can deliver additional optimization gains. By using learned models for routing and synthesis, QTS further refines circuit layouts and reduces overhead for challenging or large-scale quantum tasks.\n", - "\n", - "\n", - "This tutorial evaluates the AI modes using both routing and synthesis passes, comparing the results to traditional transpilation to highlight where AI offers performance gains.\n", - "\n", - "For more details on the available AI passes, see the [AI passes documentation](/docs/guides/ai-transpiler-passes).\n", - "\n", - "\n", - "### Why use AI for quantum circuit transpilation?\n", - "\n", - "As quantum circuits grow in size and complexity, traditional transpilation methods struggle to optimize layouts and reduce gate counts efficiently. Larger circuits, particularly those involving hundreds of qubits, impose significant challenges on routing and synthesis due to device constraints, limited connectivity, and qubit error rates.\n", - "\n", - "This is where AI-powered transpilation offers a potential solution. By leveraging machine learning techniques, the AI-powered transpiler in Qiskit can make smarter decisions about qubit routing and gate synthesis, leading to better optimization of large-scale quantum circuits.\n", - "\n", - "### Brief benchmarking results\n", - "![Graph showing AI transpiler performance against Qiskit](/docs/images/tutorials/ai-transpiler-introduction/ai-transpiler-benchmarks.avif)\n", - "\n", - "\n", - "In benchmarking tests, the AI transpiler consistently produced shallower, higher-quality circuits compared to the standard Qiskit transpiler. For these tests, we used Qiskit’s default pass manager strategy, configured with [`generate_preset_passmanager`]. While this default strategy is often effective, it can struggle with larger or more complex circuits. By contrast, AI-powered passes achieved an average 24% reduction in two-qubit gate counts and a 36% reduction in circuit depth for large circuits (100+ qubits) when transpiling to the heavy-hex topology of IBM Quantum hardware. For more information on these benchmarks, refer to this [blog](https://www.ibm.com/quantum/blog/qiskit-performance).\n", - "\n", - "This tutorial explores the key benefits of AI passes and how it compares to traditional methods." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "2aa75e36-471f-49aa-8478-134f13e3630b", - "metadata": { - "tags": [ - "remove-cell" - ] - }, - "outputs": [], - "source": [ - "# This cell is hidden from users;\n", - "# it just disables a linting rule.\n", - "# ruff: noqa: F811" - ] - }, - { - "cell_type": "markdown", - "id": "4a781d15-0953-4af6-b581-ea6cb3a74228", - "metadata": {}, - "source": [ - "## Requirements\n", - "\n", - "Before starting this tutorial, ensure that you have the following installed:\n", - "\n", - "* Qiskit SDK v1.0 or later, with [visualization](/docs/api/qiskit/visualization) support\n", - "* Qiskit Runtime (`pip install qiskit-ibm-runtime`) v0.22 or later\n", - "* Qiskit IBM® Transpiler with AI local mode(`pip install 'qiskit-ibm-transpiler[ai-local-mode]'`)" - ] - }, - { - "cell_type": "markdown", - "id": "c7c26e24-329b-4283-9cc0-67a241807049", - "metadata": {}, - "source": [ - "## Setup" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "2c462d48-ae45-4528-9b09-cebc869a6812", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit import QuantumCircuit\n", - "from qiskit.circuit.library import efficient_su2, PermutationGate\n", - "from qiskit.synthesis.qft import synth_qft_full\n", - "from qiskit.circuit.random import random_circuit, random_clifford_circuit\n", - "from qiskit.transpiler import generate_preset_pass_manager, CouplingMap\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "from qiskit_ibm_transpiler import generate_ai_pass_manager\n", - "from qiskit.synthesis.permutation import (\n", - " synth_permutation_depth_lnn_kms,\n", - " synth_permutation_basic,\n", - ")\n", - "import matplotlib.pyplot as plt\n", - "import pandas as pd\n", - "import numpy as np\n", - "import time\n", - "import logging\n", - "\n", - "seed = 42\n", - "\n", - "\n", - "# Used for generating permutation circuits in part two for comparison\n", - "def generate_permutation_circuit(width, pattern):\n", - " circuit = QuantumCircuit(width)\n", - " circuit.append(\n", - " PermutationGate(pattern=pattern),\n", - " qargs=range(width),\n", - " )\n", - " return circuit\n", - "\n", - "\n", - "# Creates a Bernstein-Vazirani circuit given the number of qubits\n", - "def create_bv_circuit(num_qubits):\n", - " qc = QuantumCircuit(num_qubits, num_qubits - 1)\n", - " qc.x(num_qubits - 1)\n", - " qc.h(qc.qubits)\n", - " for i in range(num_qubits - 1):\n", - " qc.cx(i, num_qubits - 1)\n", - " qc.h(qc.qubits[:-1])\n", - " return qc\n", - "\n", - "\n", - "# Transpile a circuit with a given pass manager and return metrics\n", - "def transpile_with_metrics(pass_manager, circuit):\n", - " start = time.time()\n", - " qc_out = pass_manager.run(circuit)\n", - " elapsed = time.time() - start\n", - "\n", - " depth_2q = qc_out.depth(lambda x: x.operation.num_qubits == 2)\n", - " gate_count = qc_out.size()\n", - "\n", - " return qc_out, {\n", - " \"depth_2q\": depth_2q,\n", - " \"gate_count\": gate_count,\n", - " \"time_s\": elapsed,\n", - " }\n", - "\n", - "\n", - "# Used for collecting metrics for part 3 of synthesis methods\n", - "def synth_transpile_with_metrics(qc, pm, pattern_id, method):\n", - " start = time.time()\n", - " qc = pm.run(qc)\n", - " elapsed = time.time() - start\n", - "\n", - " return {\n", - " \"Pattern\": pattern_id,\n", - " \"Method\": method,\n", - " \"Depth (2Q)\": qc.depth(lambda x: x.operation.num_qubits == 2),\n", - " \"Gates\": qc.size(),\n", - " \"Time (s)\": elapsed,\n", - " }\n", - "\n", - "\n", - "# Ignore logs like \"INFO:qiskit_ibm_transpiler.wrappers.ai_local_synthesis:Running Linear Functions AI synthesis on local mode\"\n", - "\n", - "logging.getLogger(\n", - " \"qiskit_ibm_transpiler.wrappers.ai_local_synthesis\"\n", - ").setLevel(logging.WARNING)" - ] - }, - { - "cell_type": "markdown", - "id": "ba7568f8-50c9-47b4-acc0-33ea34f5fca0", - "metadata": {}, - "source": [ - "# Part I. Qiskit patterns\n", - "\n", - "Let's now see how to use the AI transpiler service with a simple quantum circuit, using Qiskit patterns. The key is creating a `PassManager` with `generate_ai_pass_manager()` instead of the standard `generate_preset_pass_manager()`." - ] - }, - { - "cell_type": "markdown", - "id": "5ba1bb22-272f-4f8f-ae78-7c3d1cdaacc6", - "metadata": {}, - "source": [ - "## Step 1: Map classical inputs to a quantum problem\n", - "\n", - "In this section, we will test the AI transpiler on the `efficient_su2` circuit, a widely used hardware-efficient ansatz. This circuit is particularly relevant for variational quantum algorithms (for example, VQE) and quantum machine-learning tasks, making it an ideal test case for assessing transpilation performance.\n", - "\n", - "The `efficient_su2` circuit consists of alternating layers of single-qubit rotations and entangling gates like CNOTs. These layers enable flexible exploration of the quantum state space while keeping the gate depth manageable. By optimizing this circuit, we aim to reduce gate count, improve fidelity, and minimize noise. This makes it a strong candidate for testing the AI transpiler’s efficiency." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "c6e9c2c0-e02c-4276-bae8-d5692e60b6b8", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# For our transpilation, we will use a large circuit of 101 qubits\n", - "qc = efficient_su2(90, entanglement=\"circular\", reps=1).decompose()\n", - "\n", - "# Draw a smaller version of the circuit to get a visual representation\n", - "qc_small = efficient_su2(5, entanglement=\"circular\", reps=1).decompose()\n", - "qc_small.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "6c7c76f7-c376-47e9-bc9c-dbe32b2c89b7", - "metadata": {}, - "source": [ - "## Step 2: Optimize problem for quantum hardware execution\n", - "\n", - "### Choose a backend\n", - "\n", - "For this example, we select the least busy operational IBM Quantum backend that is not a simulator and has at least 100 qubits:\n", - "\n", - "**Note:** Since the least-busy backend can change over time, different devices might be selected for different runs. Device-specific properties, such as coupling maps, can lead to differences in the transpiled circuits." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c6b6e55e-9b70-4c94-8bbf-5ea47d0510ff", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Using backend: ibm_torino\n" - ] - } - ], - "source": [ - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(\n", - " operational=True, simulator=False, min_num_qubits=100\n", - ")\n", - "cm = backend.coupling_map\n", - "print(f\"Using backend: {backend.name}\")" - ] - }, - { - "cell_type": "markdown", - "id": "7b02350f-998e-40cf-a79e-2e6182b5a875", - "metadata": {}, - "source": [ - "### Create AI and traditional pass managers\n", - "To evaluate the effectiveness of the AI transpiler, we will perform two transpilation runs. First, we will transpile the circuit using the AI transpiler. Then, we will run a comparison by transpiling the same circuit without the AI transpiler, using traditional methods. Both transpilation processes will use the same coupling map from the chosen backend and the optimization level set to 3 for a fair comparison.\n", - "\n", - "Both of these methods reflect the standard approach to create `PassManager` instances to transpile circuits in Qiskit." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "a1aa25dd-41a9-4416-a959-44f28af613c8", - "metadata": {}, - "outputs": [], - "source": [ - "pm_ai = generate_ai_pass_manager(\n", - " optimization_level=3,\n", - " ai_optimization_level=3,\n", - " coupling_map=cm,\n", - " include_ai_synthesis=True, # used for part 3 when comparing synthesis methods\n", - ")\n", - "\n", - "pm_no_ai = generate_preset_pass_manager(\n", - " optimization_level=3,\n", - " coupling_map=cm,\n", - " seed_transpiler=seed, # note that the AI pass manager does not currently support seeding\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "a06d6144-3445-4446-a3e1-18ca78a1173c", - "metadata": {}, - "source": [ - "Transpile the circuits and record the times." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "fb5167bd-35f0-432f-af6d-023c70783d20", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Standard transpilation: Depth (2q) 95, Gate count 458, Time 0.04650712013244629\n", - "AI transpilation : Depth (2q) 90, Gate count 456, Time 0.9342479705810547\n" - ] - } - ], - "source": [ - "# Transpile using standard (non-AI) pass manager\n", - "_, metrics_no_ai = transpile_with_metrics(pm_no_ai, qc)\n", - "print(\n", - " f\"Standard transpilation: Depth (2q) {metrics_no_ai['depth_2q']}, \"\n", - " f\"Gate count {metrics_no_ai['gate_count']}, Time {metrics_no_ai['time_s']}\"\n", - ")\n", - "\n", - "# Transpile using AI pass manager\n", - "_, metrics_ai = transpile_with_metrics(pm_ai, qc)\n", - "print(\n", - " f\"AI transpilation : Depth (2q) {metrics_ai['depth_2q']}, \"\n", - " f\"Gate count {metrics_ai['gate_count']}, Time {metrics_ai['time_s']}\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "d934ebd2-e594-4076-8b21-822087df01ea", - "metadata": {}, - "source": [ - "In this test, we compare the performance of the AI transpiler and the standard transpilation method on the efficient_su2 circuit. The AI transpiler achieves a noticeably shallower circuit depth while maintaining a similar gate count.\n", - "\n", - "- **Circuit depth:** The AI transpiler produces a circuit with lower two-qubit depth. This is expected, as the AI passes are trained to optimize depth by learning qubit interaction patterns and exploiting hardware connectivity more effectively than rule-based heuristics.\n", - "\n", - "- **Gate count:** The total gate count remains similar between the two methods. This aligns with expectations since the standard SABRE-based transpilation explicitly minimizes swap count, which dominates gate overhead. The AI transpiler instead prioritizes overall depth and may occasionally trade off a few additional gates for a shorter execution path.\n", - "\n", - "- **Transpilation time:** The AI transpiler takes longer to run than the standard method. This is due to the added computational cost of invoking learned models during routing and synthesis. In contrast, the SABRE-based transpiler is now significantly faster after being rewritten and optimized in Rust, providing highly efficient heuristic routing at scale.\n", - "\n", - "It is important to note that these results are based on just one circuit. To obtain a comprehensive understanding of how the AI transpiler compares to traditional methods, it is necessary to test a variety of circuits. The performance of QTS can vary greatly depending on the type of circuit being optimized. For a broader comparison, refer to the benchmarks above or visit the [blog.](https://www.ibm.com/quantum/blog/qiskit-performance)" - ] - }, - { - "cell_type": "markdown", - "id": "c8a55587-abf6-4096-85fd-2702a077ae75", - "metadata": {}, - "source": [ - "## Step 3: Execute using Qiskit primitives\n", - "As this tutorial focuses on transpilation, no experiments will be executed on the quantum device. The goal is to leverage the optimizations from Step 2 to obtain a transpiled circuit with reduced depth or gate count." - ] - }, - { - "cell_type": "markdown", - "id": "8d0cfca9-be4e-40ab-ab98-d7899bb8b3fa", - "metadata": {}, - "source": [ - "## Step 4: Post-process and return result in desired classical format\n", - "Since there is no execution for this notebook, there are no results to post-process." - ] - }, - { - "cell_type": "markdown", - "id": "c82277b2-22e9-44fe-886e-e8ceb2178278", - "metadata": {}, - "source": [ - "# Part II. Analyze and benchmark the transpiled circuits\n", - "\n", - "In this section, we will demonstrate how to analyze the transpiled circuit and benchmark it against the original version in more detail. We will focus on metrics such as circuit depth, gate count, and transpilation time to assess the effectiveness of the optimization. Additionally, we will discuss how the results may differ across various circuit types, offering insights into the broader performance of the transpiler across different scenarios." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "ee24725b-64c9-4d6a-aa97-5a3502b0982a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Completed transpilation for Random\n", - "Completed transpilation for Clifford\n", - "Completed transpilation for QFT\n", - "Completed transpilation for BV\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
CircuitDepth 2Q (No AI)Gate Count (No AI)Time (No AI)Depth 2Q (AI)Gate Count (AI)Time (AI)
0Random372210.039347241810.773718
1Clifford362320.036633432671.097431
2QFT1659240.0774581309133.660771
3BV651550.024993701550.345522
\n", - "
" - ], - "text/plain": [ - " Circuit Depth 2Q (No AI) Gate Count (No AI) Time (No AI) \\\n", - "0 Random 37 221 0.039347 \n", - "1 Clifford 36 232 0.036633 \n", - "2 QFT 165 924 0.077458 \n", - "3 BV 65 155 0.024993 \n", - "\n", - " Depth 2Q (AI) Gate Count (AI) Time (AI) \n", - "0 24 181 0.773718 \n", - "1 43 267 1.097431 \n", - "2 130 913 3.660771 \n", - "3 70 155 0.345522 " - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Circuits to benchmark\n", - "seed = 42\n", - "circuits = [\n", - " {\n", - " \"name\": \"Random\",\n", - " \"qc\": random_circuit(num_qubits=30, depth=10, seed=seed),\n", - " },\n", - " {\n", - " \"name\": \"Clifford\",\n", - " \"qc\": random_clifford_circuit(\n", - " num_qubits=40, num_gates=200, seed=seed\n", - " ),\n", - " },\n", - " {\n", - " \"name\": \"QFT\",\n", - " \"qc\": synth_qft_full(num_qubits=20, do_swaps=False).decompose(),\n", - " },\n", - " {\n", - " \"name\": \"BV\",\n", - " \"qc\": create_bv_circuit(40),\n", - " },\n", - "]\n", - "\n", - "results = []\n", - "\n", - "# Run the transpilation for each circuit and store the results\n", - "for circuit in circuits:\n", - " qc_no_ai, metrics_no_ai = transpile_with_metrics(pm_no_ai, circuit[\"qc\"])\n", - " qc_ai, metrics_ai = transpile_with_metrics(pm_ai, circuit[\"qc\"])\n", - "\n", - " print(\"Completed transpilation for\", circuit[\"name\"])\n", - "\n", - " results.append(\n", - " {\n", - " \"Circuit\": circuit[\"name\"],\n", - " \"Depth 2Q (No AI)\": metrics_no_ai[\"depth_2q\"],\n", - " \"Gate Count (No AI)\": metrics_no_ai[\"gate_count\"],\n", - " \"Time (No AI)\": metrics_no_ai[\"time_s\"],\n", - " \"Depth 2Q (AI)\": metrics_ai[\"depth_2q\"],\n", - " \"Gate Count (AI)\": metrics_ai[\"gate_count\"],\n", - " \"Time (AI)\": metrics_ai[\"time_s\"],\n", - " }\n", - " )\n", - "\n", - "df = pd.DataFrame(results)\n", - "df" - ] - }, - { - "cell_type": "markdown", - "id": "061d85cf-3841-4ed3-bd0d-cd950564efb7", - "metadata": {}, - "source": [ - "Average percentage reduction for each metric. Positive are improvements, negative are degradations." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "70cf9c05-62a3-4049-9712-319902107ba6", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Average reduction in depth: 11.88%\n", - "Average reduction in gate count: 1.04%\n", - "Average reduction in transpilation time: -3193.95%\n" - ] - } - ], - "source": [ - "# Average reduction from non-AI to AI transpilation as a percentage\n", - "avg_reduction_depth = (\n", - " (df[\"Depth 2Q (No AI)\"] - df[\"Depth 2Q (AI)\"]).mean()\n", - " / df[\"Depth 2Q (No AI)\"].mean()\n", - " * 100\n", - ")\n", - "avg_reduction_gates = (\n", - " (df[\"Gate Count (No AI)\"] - df[\"Gate Count (AI)\"]).mean()\n", - " / df[\"Gate Count (No AI)\"].mean()\n", - " * 100\n", - ")\n", - "avg_reduction_time = (\n", - " (df[\"Time (No AI)\"] - df[\"Time (AI)\"]).mean()\n", - " / df[\"Time (No AI)\"].mean()\n", - " * 100\n", - ")\n", - "\n", - "print(f\"Average reduction in depth: {avg_reduction_depth:.2f}%\")\n", - "print(f\"Average reduction in gate count: {avg_reduction_gates:.2f}%\")\n", - "print(f\"Average reduction in transpilation time: {avg_reduction_time:.2f}%\")" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "79b8d5d9-0f9d-42ca-9583-8bec17430014", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "fig, axs = plt.subplots(1, 3, figsize=(21, 6))\n", - "df.plot(\n", - " x=\"Circuit\",\n", - " y=[\"Depth 2Q (No AI)\", \"Depth 2Q (AI)\"],\n", - " kind=\"bar\",\n", - " ax=axs[0],\n", - ")\n", - "axs[0].set_title(\"Circuit Depth Comparison\")\n", - "axs[0].set_ylabel(\"Depth\")\n", - "axs[0].set_xlabel(\"Circuit\")\n", - "axs[0].tick_params(axis=\"x\", rotation=45)\n", - "df.plot(\n", - " x=\"Circuit\",\n", - " y=[\"Gate Count (No AI)\", \"Gate Count (AI)\"],\n", - " kind=\"bar\",\n", - " ax=axs[1],\n", - ")\n", - "axs[1].set_title(\"Gate Count Comparison\")\n", - "axs[1].set_ylabel(\"Gate Count\")\n", - "axs[1].set_xlabel(\"Circuit\")\n", - "axs[1].tick_params(axis=\"x\", rotation=45)\n", - "df.plot(x=\"Circuit\", y=[\"Time (No AI)\", \"Time (AI)\"], kind=\"bar\", ax=axs[2])\n", - "axs[2].set_title(\"Time Comparison\")\n", - "axs[2].set_ylabel(\"Time (seconds)\")\n", - "axs[2].set_xlabel(\"Circuit\")\n", - "axs[2].tick_params(axis=\"x\", rotation=45)\n", - "fig.suptitle(\n", - " \"Benchmarking AI transpilation vs Non-AI transpilation for various circuits\"\n", - ")\n", - "\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "345022d3-e302-47e6-9453-9261136923a7", - "metadata": {}, - "source": [ - "The AI transpiler's performance varies significantly based on the type of circuit being optimized. In some cases, it achieves notable reductions in circuit depth and gate count compared to the standard transpiler. However, these improvements often come with a substantial increase in runtime.\n", - "\n", - "For certain types of circuits, the AI transpiler may yield slightly better results in terms of circuit depth but may also lead to an increase in gate count and a significant runtime penalty. These observations suggest that the AI transpiler's benefits are not uniform across all circuit types. Instead, its effectiveness depends on the specific characteristics of the circuit, making it more suitable for some use cases than others." - ] - }, - { - "cell_type": "markdown", - "id": "9e496e7a-64a8-46fd-b240-c494e7825bd2", - "metadata": {}, - "source": [ - "## When should users choose AI-powered transpilation?\n", - "\n", - "The AI-powered transpiler in Qiskit excels in scenarios where traditional transpilation methods struggle, particularly with large-scale and complex quantum circuits. For circuits involving hundreds of qubits or those targeting hardware with intricate coupling maps, the AI transpiler offers superior optimization in terms of circuit depth, gate count, and runtime efficiency. In benchmarking tests, it has consistently outperformed traditional methods, delivering significantly shallower circuits and reducing gate counts, which are critical for enhancing performance and mitigating noise on real quantum hardware.\n", - "\n", - "Users should consider AI-powered transpilation when working with:\n", - "- Large circuits where traditional methods fail to efficiently handle the scale.\n", - "- Complex hardware topologies where device connectivity and routing challenges arise.\n", - "- Performance-sensitive applications where reducing circuit depth and improving fidelity are paramount." - ] - }, - { - "cell_type": "markdown", - "id": "c345cb54-a838-427f-898f-51fb607da493", - "metadata": {}, - "source": [ - "# Part III. Explore AI-powered permutation network synthesis\n", - "\n", - "Permutation networks are foundational in quantum computing, particularly for systems constrained by restricted topologies. These networks facilitate long-range interactions by dynamically swapping qubits to mimic all-to-all connectivity on hardware with limited connectivity. Such transformations are essential for implementing complex quantum algorithms on near-term devices, where interactions often span beyond nearest neighbors.\n", - "\n", - "In this section, we highlight the synthesis of permutation networks as a compelling use case for the AI-powered transpiler in Qiskit. Specifically, the `AIPermutationSynthesis` pass leverages AI-driven optimization to generate efficient circuits for qubit permutation tasks. By contrast, generic synthesis approaches often struggle to balance gate count and circuit depth, especially in scenarios with dense qubit interactions or when attempting to achieve full connectivity.\n", - "\n", - "We will walk through a Qiskit patterns example showcasing the synthesis of a permutation network to achieve all-to-all connectivity for a set of qubits. We will compare the performance of `AIPermutationSynthesis` against the standard synthesis methods in Qiskit. This example will demonstrate how the AI transpiler optimizes for lower circuit depth and gate count, highlighting its advantages in practical quantum workflows. To activate the AI synthesis pass, we will use the `generate_ai_pass_manager()` function with the `include_ai_synthesis` parameter set to `True`." - ] - }, - { - "cell_type": "markdown", - "id": "76de0959-1eca-43d9-b8fe-f9aea9a122d8", - "metadata": {}, - "source": [ - "## Step 1: Map classical inputs to a quantum problem\n", - "\n", - "To represent a classical permutation problem on a quantum computer, we start by defining the structure of the quantum circuits. For this example:\n", - "\n", - "1. Quantum circuit initialization:\n", - " We allocate 27 qubits to match the backend we will use, which has 27 qubits.\n", - "\n", - "2. Apply permutations:\n", - " We generate ten random permutation patterns (`pattern_1` through `pattern_10`) using a fixed seed for reproducibility. Each permutation pattern is applied to a separate quantum circuit (`qc_1` through `qc_10`).\n", - "\n", - "3. Circuit decomposition:\n", - " Each permutation operation is decomposed into native gate sets compatible with the target quantum hardware. We analyze the depth and the number of two-qubit gates (nonlocal gates) for each decomposed circuit.\n", - "\n", - "The results provide insight into the complexity of representing classical permutation problems on a quantum device, demonstrating the resource requirements for different permutation patterns." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "76a3e847-0808-4413-bd0c-c760cd2df3f4", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Parameters\n", - "width = 27\n", - "num_circuits = 10\n", - "\n", - "# Set random seed\n", - "np.random.seed(seed)\n", - "\n", - "\n", - "# Generate random patterns and circuits\n", - "patterns = [\n", - " np.random.permutation(width).tolist() for _ in range(num_circuits)\n", - "]\n", - "circuits = {\n", - " f\"qc_{i}\": generate_permutation_circuit(width, pattern)\n", - " for i, pattern in enumerate(patterns, start=1)\n", - "}\n", - "\n", - "# Display one of the circuits\n", - "circuits[\"qc_1\"].decompose(reps=3).draw(output=\"mpl\", fold=-1)" - ] - }, - { - "cell_type": "markdown", - "id": "a8b79798-fa80-44d8-8a52-2d2a50e0c280", - "metadata": {}, - "source": [ - "## Step 2: Optimize problem for quantum hardware execution\n", - "In this step, we proceed with optimization using the AI synthesis passes.\n", - "\n", - "For the AI synthesis passes, the `PassManager` requires only the coupling map of the backend. However, it is important to note that not all coupling maps are compatible; only those that the `AIPermutationSynthesis` pass has been trained on will work. Currently, the `AIPermutationSynthesis` pass supports blocks of sizes 65, 33, and 27 qubits. For this example we use a 27-qubit QPU.\n", - "\n", - "For comparison, we will evaluate the performance of AI synthesis against generic permutation synthesis methods in Qiskit, including:\n", - "\n", - "- `synth_permutation_depth_lnn_kms`: This method synthesizes a permutation circuit for a linear nearest-neighbor (LNN) architecture using the Kutin, Moulton, and Smithline (KMS) algorithm. It guarantees a circuit with a depth of at most $ n $ and a size of at most $ n(n-1)/2 $, where both depth and size are measured in terms of SWAP gates.\n", - "\n", - "- `synth_permutation_basic`: This is a straightforward implementation that synthesizes permutation circuits without imposing constraints on connectivity or optimization for specific architectures. It serves as a baseline for comparing performance with more advanced methods.\n", - "\n", - "Each of these methods represents a distinct approach to synthesizing permutation networks, providing a comprehensive benchmark against the AI-powered methods.\n", - "\n", - "For more details about synthesis methods in Qiskit, refer to the [Qiskit API documentation](/docs/api/qiskit/synthesis)." - ] - }, - { - "cell_type": "markdown", - "id": "b1733a10-c285-444e-af47-4a32329c5f7a", - "metadata": {}, - "source": [ - "Define the coupling map representing the 27-qubit QPU." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "84dff2c2-a496-4828-bb8e-08d373816a36", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "coupling_map = [\n", - " [1, 0],\n", - " [2, 1],\n", - " [3, 2],\n", - " [3, 5],\n", - " [4, 1],\n", - " [6, 7],\n", - " [7, 4],\n", - " [7, 10],\n", - " [8, 5],\n", - " [8, 9],\n", - " [8, 11],\n", - " [11, 14],\n", - " [12, 10],\n", - " [12, 13],\n", - " [12, 15],\n", - " [13, 14],\n", - " [16, 14],\n", - " [17, 18],\n", - " [18, 15],\n", - " [18, 21],\n", - " [19, 16],\n", - " [19, 22],\n", - " [20, 19],\n", - " [21, 23],\n", - " [23, 24],\n", - " [25, 22],\n", - " [25, 24],\n", - " [26, 25],\n", - "]\n", - "CouplingMap(coupling_map).draw()" - ] - }, - { - "cell_type": "markdown", - "id": "47bdb1f5-1fc6-46c4-8fc9-98d16a4d2529", - "metadata": {}, - "source": [ - "Transpile each of the permutation circuits using the AI synthesis passes and generic synthesis methods." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "128cc285-094a-4b07-a37d-8424a4003b2c", - "metadata": {}, - "outputs": [], - "source": [ - "results = []\n", - "pm_no_ai_synth = generate_preset_pass_manager(\n", - " coupling_map=cm,\n", - " optimization_level=1, # set to 1 since we are using the synthesis methods\n", - ")\n", - "\n", - "# Transpile and analyze all circuits\n", - "for i, (qc_name, qc) in enumerate(circuits.items(), start=1):\n", - " pattern = patterns[i - 1] # Get the corresponding pattern\n", - "\n", - " qc_depth_lnn_kms = synth_permutation_depth_lnn_kms(pattern)\n", - " qc_basic = synth_permutation_basic(pattern)\n", - "\n", - " # AI synthesis\n", - " results.append(\n", - " synth_transpile_with_metrics(\n", - " qc.decompose(reps=3),\n", - " pm_ai,\n", - " qc_name,\n", - " \"AI\",\n", - " )\n", - " )\n", - "\n", - " # Depth-LNN-KMS Method\n", - " results.append(\n", - " synth_transpile_with_metrics(\n", - " qc_depth_lnn_kms.decompose(reps=3),\n", - " pm_no_ai_synth,\n", - " qc_name,\n", - " \"Depth-LNN-KMS\",\n", - " )\n", - " )\n", - "\n", - " # Basic Method\n", - " results.append(\n", - " synth_transpile_with_metrics(\n", - " qc_basic.decompose(reps=3),\n", - " pm_no_ai_synth,\n", - " qc_name,\n", - " \"Basic\",\n", - " )\n", - " )\n", - "\n", - "\n", - "results_df = pd.DataFrame(results)" - ] - }, - { - "cell_type": "markdown", - "id": "42f80e32-60fd-46a8-a6b5-4bcadb15810a", - "metadata": {}, - "source": [ - "Record the metrics (depth, gate count, time) for each circuit after transpilation." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "72ee8474-eea6-421a-9d7d-070587eaff71", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "=== Average Metrics ===\n", - " Depth (2Q) Gates Time (s)\n", - "Method \n", - "AI 23.9 82.8 0.248\n", - "Basic 29.8 91.0 0.012\n", - "Depth-LNN-KMS 70.8 531.6 0.017\n", - "\n", - "Best Non-AI Method (based on least average depth): Basic\n", - "\n", - "=== Comparison of AI vs Best Non-AI Method ===\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
MetricAIBasicImprovement (AI vs Best Non-AI)
0Depth (2Q)23.90029.800-5.900
1Gates82.80091.000-8.200
2Time (s)0.2480.0120.236
\n", - "
" - ], - "text/plain": [ - " Metric AI Basic Improvement (AI vs Best Non-AI)\n", - "0 Depth (2Q) 23.900 29.800 -5.900\n", - "1 Gates 82.800 91.000 -8.200\n", - "2 Time (s) 0.248 0.012 0.236" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Calculate averages for each metric\n", - "average_metrics = results_df.groupby(\"Method\")[\n", - " [\"Depth (2Q)\", \"Gates\", \"Time (s)\"]\n", - "].mean()\n", - "average_metrics = average_metrics.round(3) # Round to two decimal places\n", - "print(\"\\n=== Average Metrics ===\")\n", - "print(average_metrics)\n", - "\n", - "# Identify the best non-AI method based on least average depth\n", - "non_ai_methods = [\n", - " method for method in results_df[\"Method\"].unique() if method != \"AI\"\n", - "]\n", - "best_non_ai_method = average_metrics.loc[non_ai_methods][\n", - " \"Depth (2Q)\"\n", - "].idxmin()\n", - "print(\n", - " f\"\\nBest Non-AI Method (based on least average depth): {best_non_ai_method}\"\n", - ")\n", - "\n", - "# Compare AI to the best non-AI method\n", - "ai_metrics = average_metrics.loc[\"AI\"]\n", - "best_non_ai_metrics = average_metrics.loc[best_non_ai_method]\n", - "\n", - "comparison = {\n", - " \"Metric\": [\"Depth (2Q)\", \"Gates\", \"Time (s)\"],\n", - " \"AI\": [\n", - " ai_metrics[\"Depth (2Q)\"],\n", - " ai_metrics[\"Gates\"],\n", - " ai_metrics[\"Time (s)\"],\n", - " ],\n", - " best_non_ai_method: [\n", - " best_non_ai_metrics[\"Depth (2Q)\"],\n", - " best_non_ai_metrics[\"Gates\"],\n", - " best_non_ai_metrics[\"Time (s)\"],\n", - " ],\n", - " \"Improvement (AI vs Best Non-AI)\": [\n", - " ai_metrics[\"Depth (2Q)\"] - best_non_ai_metrics[\"Depth (2Q)\"],\n", - " ai_metrics[\"Gates\"] - best_non_ai_metrics[\"Gates\"],\n", - " ai_metrics[\"Time (s)\"] - best_non_ai_metrics[\"Time (s)\"],\n", - " ],\n", - "}\n", - "\n", - "comparison_df = pd.DataFrame(comparison)\n", - "print(\"\\n=== Comparison of AI vs Best Non-AI Method ===\")\n", - "comparison_df" - ] - }, - { - "cell_type": "markdown", - "id": "e1ba3767-5ce1-4663-803b-73ccfc22f03b", - "metadata": {}, - "source": [ - "The results demonstrate that the AI transpiler outperforms all other Qiskit synthesis methods for this set of random permutation circuits. Key findings include:\n", - "\n", - "1. Depth: The AI transpiler achieves the lowest average depth, indicating superior optimization of circuit layouts.\n", - "2. Gate count: It significantly reduces the number of gates compared to other methods, improving execution fidelity and efficiency.\n", - "3. Transpilation time: All methods run very quickly at this scale, making them practical for use. However, the AI transpiler does has a notable runtime increase compared to traditional methods due to the complexity of the AI models used.\n", - "\n", - "These results establish the AI transpiler as the most effective approach for this benchmark, particularly for depth and gate count optimization." - ] - }, - { - "cell_type": "markdown", - "id": "dbaab943-5fd7-4720-98bf-8602b2ab4473", - "metadata": {}, - "source": [ - "Plot the results to compare the performance of the AI synthesis passes against the generic synthesis methods." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "a326f268-0115-442c-8563-968676b66670", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "methods = results_df[\"Method\"].unique()\n", - "\n", - "fig, axs = plt.subplots(1, 3, figsize=(18, 5))\n", - "\n", - "# Pivot the DataFrame and reorder columns to ensure AI is first\n", - "pivot_depth = results_df.pivot(\n", - " index=\"Pattern\", columns=\"Method\", values=\"Depth (2Q)\"\n", - ")[[\"AI\", \"Depth-LNN-KMS\", \"Basic\"]]\n", - "pivot_gates = results_df.pivot(\n", - " index=\"Pattern\", columns=\"Method\", values=\"Gates\"\n", - ")[[\"AI\", \"Depth-LNN-KMS\", \"Basic\"]]\n", - "pivot_time = results_df.pivot(\n", - " index=\"Pattern\", columns=\"Method\", values=\"Time (s)\"\n", - ")[[\"AI\", \"Depth-LNN-KMS\", \"Basic\"]]\n", - "\n", - "pivot_depth.plot(kind=\"bar\", ax=axs[0], legend=False)\n", - "axs[0].set_title(\"Circuit Depth Comparison\")\n", - "axs[0].set_ylabel(\"Depth\")\n", - "axs[0].set_xlabel(\"Pattern\")\n", - "axs[0].tick_params(axis=\"x\", rotation=45)\n", - "pivot_gates.plot(kind=\"bar\", ax=axs[1], legend=False)\n", - "axs[1].set_title(\"2Q Gate Count Comparison\")\n", - "axs[1].set_ylabel(\"Number of 2Q Gates\")\n", - "axs[1].set_xlabel(\"Pattern\")\n", - "axs[1].tick_params(axis=\"x\", rotation=45)\n", - "pivot_time.plot(\n", - " kind=\"bar\", ax=axs[2], legend=True, title=\"Legend\"\n", - ") # Show legend on the last plot\n", - "axs[2].set_title(\"Time Comparison\")\n", - "axs[2].set_ylabel(\"Time (seconds)\")\n", - "axs[2].set_xlabel(\"Pattern\")\n", - "axs[2].tick_params(axis=\"x\", rotation=45)\n", - "fig.suptitle(\n", - " \"Benchmarking AI Synthesis Methods vs Non-AI Synthesis Methods For Random Permutations Circuits\",\n", - " fontsize=16,\n", - " y=1,\n", - ")\n", - "\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "03a9af42-42a7-4344-b834-0d2b506d4d78", - "metadata": {}, - "source": [ - "This graph highlights the individual results for each circuit (`qc_1` to `qc_10`) across different synthesis methods:\n", - "\n", - "While these results underscore the AI transpiler’s effectiveness for permutation circuits, it is important to note its limitations. The AI synthesis method is currently only available for certain coupling maps, which may restrict its broader applicability. This constraint should be considered when evaluating its usage in different scenarios.\n", - "\n", - "Overall, the AI transpiler demonstrates promising improvements in depth and gate count optimization for these specific circuits while maintaining comparable transpilation times." - ] - }, - { - "cell_type": "markdown", - "id": "41b1405d-fa90-48b6-9ce2-933f05358778", - "metadata": {}, - "source": [ - "## Step 3: Execute using Qiskit primitives\n", - "As this tutorial focuses on transpilation, no experiments will be executed on the quantum device. The goal is to leverage the optimizations from Step 2 to obtain a transpiled circuit with reduced depth or gate count." - ] - }, - { - "cell_type": "markdown", - "id": "3d942ee4-e4d7-4e87-8c8a-17c662d5379f", - "metadata": {}, - "source": [ - "## Step 4: Post-process and return result in desired classical format\n", - "Since there is no execution for this notebook, there are no results to post-process." - ] - }, - { - "cell_type": "markdown", - "id": "3b21bb06-7a2b-4181-af59-734c89435d45", - "metadata": {}, - "source": [ - "## Tutorial survey\n", - "\n", - "Please take this short survey to provide feedback on this tutorial. Your insights will help us improve our content offerings and user experience.\n", - "\n", - "[Link to survey](https://your.feedback.ibm.com/jfe/form/SV_0igXMtMCQfApgDI)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "id": "f59032f0-f29a-4e52-9cab-855ed6f86b00", + "metadata": {}, + "source": [ + "---\n", + "title: Qiskit AI-powered transpiler service introduction\n", + "description: In this notebook, we will explore the key benefits of Qiskit AI-powered transpiler service and how it compares to traditional methods.\n", + "---\n", + "\n", + "\n", + "{/* cspell:ignore fontsize idxmin */}\n", + "\n", + "# Qiskit AI-powered transpiler service introduction\n", + "*Estimated QPU usage: None (NOTE: This tutorial does not execute jobs because it is focused on transpilation)*\n", + "\n", + "## Background\n", + "\n", + "The **Qiskit AI-powered transpiler service (QTS)** introduces machine learning-based optimizations in both routing and synthesis passes. These AI modes have been designed to tackle the limitations of traditional transpilation, particularly for large-scale circuits and complex hardware topologies.\n", + "\n", + "As of **July 2025**, the **Transpiler Service** has been migrated to the new IBM Quantum® Platform and is no longer available. For the latest updates about the status of the Transpiler Service, please refer to the [transpiler service documentation](/docs/guides/qiskit-transpiler-service). You can still use the AI transpiler locally, similar to standard Qiskit transpilation. Simply replace `generate_preset_pass_manager()` with `generate_ai_pass_manager()`. This function constructs a pass manager that integrates the AI-powered routing and synthesis passes directly into your local transpilation workflow.\n", + "\n", + "### Key features of AI passes\n", + "\n", + "- Routing passes: AI-powered routing can dynamically adjust qubit paths based on the specific circuit and backend, reducing the need for excessive SWAP gates.\n", + " - `AIRouting`: Layout selection and circuit routing\n", + "\n", + "- Synthesis passes: AI techniques optimize the decomposition of multi-qubit gates, minimizing the number of two-qubit gates, which are typically more error-prone.\n", + " - `AICliffordSynthesis`: Clifford gate synthesis\n", + " - `AILinearFunctionSynthesis`: Linear function circuit synthesis\n", + " - `AIPermutationSynthesis`: Permutation circuit synthesis\n", + " - `AIPauliNetworkSynthesis`: Pauli Network circuit synthesis (only available in the Qiskit Transpiler Service, not in local environment)\n", + "\n", + "- Comparison with traditional transpilation: The standard Qiskit transpiler is a robust tool that can handle a broad spectrum of quantum circuits effectively. However, when circuits grow larger in scale or hardware configurations become more complex, AI passes can deliver additional optimization gains. By using learned models for routing and synthesis, QTS further refines circuit layouts and reduces overhead for challenging or large-scale quantum tasks.\n", + "\n", + "\n", + "This tutorial evaluates the AI modes using both routing and synthesis passes, comparing the results to traditional transpilation to highlight where AI offers performance gains.\n", + "\n", + "For more details on the available AI passes, see the [AI passes documentation](/docs/guides/ai-transpiler-passes).\n", + "\n", + "\n", + "### Why use AI for quantum circuit transpilation?\n", + "\n", + "As quantum circuits grow in size and complexity, traditional transpilation methods struggle to optimize layouts and reduce gate counts efficiently. Larger circuits, particularly those involving hundreds of qubits, impose significant challenges on routing and synthesis due to device constraints, limited connectivity, and qubit error rates.\n", + "\n", + "This is where AI-powered transpilation offers a potential solution. By leveraging machine learning techniques, the AI-powered transpiler in Qiskit can make smarter decisions about qubit routing and gate synthesis, leading to better optimization of large-scale quantum circuits.\n", + "\n", + "### Brief benchmarking results\n", + "![Graph showing AI transpiler performance against Qiskit](/docs/images/tutorials/ai-transpiler-introduction/ai-transpiler-benchmarks.avif)\n", + "\n", + "\n", + "In benchmarking tests, the AI transpiler consistently produced shallower, higher-quality circuits compared to the standard Qiskit transpiler. For these tests, we used Qiskit’s default pass manager strategy, configured with [`generate_preset_passmanager`]. While this default strategy is often effective, it can struggle with larger or more complex circuits. By contrast, AI-powered passes achieved an average 24% reduction in two-qubit gate counts and a 36% reduction in circuit depth for large circuits (100+ qubits) when transpiling to the heavy-hex topology of IBM Quantum hardware. For more information on these benchmarks, refer to this [blog](https://www.ibm.com/quantum/blog/qiskit-performance).\n", + "\n", + "This tutorial explores the key benefits of AI passes and how it compares to traditional methods." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "2aa75e36-471f-49aa-8478-134f13e3630b", + "metadata": { + "tags": [ + "remove-cell" + ] + }, + "outputs": [], + "source": [ + "# This cell is hidden from users;\n", + "# it just disables a linting rule.\n", + "# ruff: noqa: F811" + ] + }, + { + "cell_type": "markdown", + "id": "4a781d15-0953-4af6-b581-ea6cb3a74228", + "metadata": {}, + "source": [ + "## Requirements\n", + "\n", + "Before starting this tutorial, ensure that you have the following installed:\n", + "\n", + "* Qiskit SDK v1.0 or later, with [visualization](/docs/api/qiskit/visualization) support\n", + "* Qiskit Runtime (`pip install qiskit-ibm-runtime`) v0.22 or later\n", + "* Qiskit IBM® Transpiler with AI local mode(`pip install 'qiskit-ibm-transpiler[ai-local-mode]'`)" + ] + }, + { + "cell_type": "markdown", + "id": "c7c26e24-329b-4283-9cc0-67a241807049", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "2c462d48-ae45-4528-9b09-cebc869a6812", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import QuantumCircuit\n", + "from qiskit.circuit.library import efficient_su2, PermutationGate\n", + "from qiskit.synthesis.qft import synth_qft_full\n", + "from qiskit.circuit.random import random_circuit, random_clifford_circuit\n", + "from qiskit.transpiler import generate_preset_pass_manager, CouplingMap\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "from qiskit_ibm_transpiler import generate_ai_pass_manager\n", + "from qiskit.synthesis.permutation import (\n", + " synth_permutation_depth_lnn_kms,\n", + " synth_permutation_basic,\n", + ")\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "import numpy as np\n", + "import time\n", + "import logging\n", + "\n", + "seed = 42\n", + "\n", + "\n", + "# Used for generating permutation circuits in part two for comparison\n", + "def generate_permutation_circuit(width, pattern):\n", + " circuit = QuantumCircuit(width)\n", + " circuit.append(\n", + " PermutationGate(pattern=pattern),\n", + " qargs=range(width),\n", + " )\n", + " return circuit\n", + "\n", + "\n", + "# Creates a Bernstein-Vazirani circuit given the number of qubits\n", + "def create_bv_circuit(num_qubits):\n", + " qc = QuantumCircuit(num_qubits, num_qubits - 1)\n", + " qc.x(num_qubits - 1)\n", + " qc.h(qc.qubits)\n", + " for i in range(num_qubits - 1):\n", + " qc.cx(i, num_qubits - 1)\n", + " qc.h(qc.qubits[:-1])\n", + " return qc\n", + "\n", + "\n", + "# Transpile a circuit with a given pass manager and return metrics\n", + "def transpile_with_metrics(pass_manager, circuit):\n", + " start = time.time()\n", + " qc_out = pass_manager.run(circuit)\n", + " elapsed = time.time() - start\n", + "\n", + " depth_2q = qc_out.depth(lambda x: x.operation.num_qubits == 2)\n", + " gate_count = qc_out.size()\n", + "\n", + " return qc_out, {\n", + " \"depth_2q\": depth_2q,\n", + " \"gate_count\": gate_count,\n", + " \"time_s\": elapsed,\n", + " }\n", + "\n", + "\n", + "# Used for collecting metrics for part 3 of synthesis methods\n", + "def synth_transpile_with_metrics(qc, pm, pattern_id, method):\n", + " start = time.time()\n", + " qc = pm.run(qc)\n", + " elapsed = time.time() - start\n", + "\n", + " return {\n", + " \"Pattern\": pattern_id,\n", + " \"Method\": method,\n", + " \"Depth (2Q)\": qc.depth(lambda x: x.operation.num_qubits == 2),\n", + " \"Gates\": qc.size(),\n", + " \"Time (s)\": elapsed,\n", + " }\n", + "\n", + "\n", + "# Ignore logs like \"INFO:qiskit_ibm_transpiler.wrappers.ai_local_synthesis:Running Linear Functions\n", + "# AI synthesis on local mode\"\n", + "\n", + "logging.getLogger(\n", + " \"qiskit_ibm_transpiler.wrappers.ai_local_synthesis\"\n", + ").setLevel(logging.WARNING)" + ] + }, + { + "cell_type": "markdown", + "id": "ba7568f8-50c9-47b4-acc0-33ea34f5fca0", + "metadata": {}, + "source": [ + "# Part I. Qiskit patterns\n", + "\n", + "Let's now see how to use the AI transpiler service with a simple quantum circuit, using Qiskit patterns. The key is creating a `PassManager` with `generate_ai_pass_manager()` instead of the standard `generate_preset_pass_manager()`." + ] + }, + { + "cell_type": "markdown", + "id": "5ba1bb22-272f-4f8f-ae78-7c3d1cdaacc6", + "metadata": {}, + "source": [ + "## Step 1: Map classical inputs to a quantum problem\n", + "\n", + "In this section, we will test the AI transpiler on the `efficient_su2` circuit, a widely used hardware-efficient ansatz. This circuit is particularly relevant for variational quantum algorithms (for example, VQE) and quantum machine-learning tasks, making it an ideal test case for assessing transpilation performance.\n", + "\n", + "The `efficient_su2` circuit consists of alternating layers of single-qubit rotations and entangling gates like CNOTs. These layers enable flexible exploration of the quantum state space while keeping the gate depth manageable. By optimizing this circuit, we aim to reduce gate count, improve fidelity, and minimize noise. This makes it a strong candidate for testing the AI transpiler’s efficiency." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "c6e9c2c0-e02c-4276-bae8-d5692e60b6b8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# For our transpilation, we will use a large circuit of 101 qubits\n", + "qc = efficient_su2(90, entanglement=\"circular\", reps=1).decompose()\n", + "\n", + "# Draw a smaller version of the circuit to get a visual representation\n", + "qc_small = efficient_su2(5, entanglement=\"circular\", reps=1).decompose()\n", + "qc_small.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "6c7c76f7-c376-47e9-bc9c-dbe32b2c89b7", + "metadata": {}, + "source": [ + "## Step 2: Optimize problem for quantum hardware execution\n", + "\n", + "### Choose a backend\n", + "\n", + "For this example, we select the least busy operational IBM Quantum backend that is not a simulator and has at least 100 qubits:\n", + "\n", + "**Note:** Since the least-busy backend can change over time, different devices might be selected for different runs. Device-specific properties, such as coupling maps, can lead to differences in the transpiled circuits." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c6b6e55e-9b70-4c94-8bbf-5ea47d0510ff", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using backend: ibm_torino\n" + ] + } + ], + "source": [ + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(\n", + " operational=True, simulator=False, min_num_qubits=100\n", + ")\n", + "cm = backend.coupling_map\n", + "print(f\"Using backend: {backend.name}\")" + ] + }, + { + "cell_type": "markdown", + "id": "7b02350f-998e-40cf-a79e-2e6182b5a875", + "metadata": {}, + "source": [ + "### Create AI and traditional pass managers\n", + "To evaluate the effectiveness of the AI transpiler, we will perform two transpilation runs. First, we will transpile the circuit using the AI transpiler. Then, we will run a comparison by transpiling the same circuit without the AI transpiler, using traditional methods. Both transpilation processes will use the same coupling map from the chosen backend and the optimization level set to 3 for a fair comparison.\n", + "\n", + "Both of these methods reflect the standard approach to create `PassManager` instances to transpile circuits in Qiskit." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "a1aa25dd-41a9-4416-a959-44f28af613c8", + "metadata": {}, + "outputs": [], + "source": [ + "pm_ai = generate_ai_pass_manager(\n", + " optimization_level=3,\n", + " ai_optimization_level=3,\n", + " coupling_map=cm,\n", + " include_ai_synthesis=True, # used for part 3 when comparing synthesis methods\n", + ")\n", + "\n", + "pm_no_ai = generate_preset_pass_manager(\n", + " optimization_level=3,\n", + " coupling_map=cm,\n", + " seed_transpiler=seed, # note that the AI pass manager does not currently support seeding\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "a06d6144-3445-4446-a3e1-18ca78a1173c", + "metadata": {}, + "source": [ + "Transpile the circuits and record the times." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "fb5167bd-35f0-432f-af6d-023c70783d20", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Standard transpilation: Depth (2q) 95, Gate count 458, Time 0.04650712013244629\n", + "AI transpilation : Depth (2q) 90, Gate count 456, Time 0.9342479705810547\n" + ] + } + ], + "source": [ + "# Transpile using standard (non-AI) pass manager\n", + "_, metrics_no_ai = transpile_with_metrics(pm_no_ai, qc)\n", + "print(\n", + " f\"Standard transpilation: Depth (2q) {metrics_no_ai['depth_2q']}, \"\n", + " f\"Gate count {metrics_no_ai['gate_count']}, Time {metrics_no_ai['time_s']}\"\n", + ")\n", + "\n", + "# Transpile using AI pass manager\n", + "_, metrics_ai = transpile_with_metrics(pm_ai, qc)\n", + "print(\n", + " f\"AI transpilation : Depth (2q) {metrics_ai['depth_2q']}, \"\n", + " f\"Gate count {metrics_ai['gate_count']}, Time {metrics_ai['time_s']}\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "d934ebd2-e594-4076-8b21-822087df01ea", + "metadata": {}, + "source": [ + "In this test, we compare the performance of the AI transpiler and the standard transpilation method on the efficient_su2 circuit. The AI transpiler achieves a noticeably shallower circuit depth while maintaining a similar gate count.\n", + "\n", + "- **Circuit depth:** The AI transpiler produces a circuit with lower two-qubit depth. This is expected, as the AI passes are trained to optimize depth by learning qubit interaction patterns and exploiting hardware connectivity more effectively than rule-based heuristics.\n", + "\n", + "- **Gate count:** The total gate count remains similar between the two methods. This aligns with expectations since the standard SABRE-based transpilation explicitly minimizes swap count, which dominates gate overhead. The AI transpiler instead prioritizes overall depth and may occasionally trade off a few additional gates for a shorter execution path.\n", + "\n", + "- **Transpilation time:** The AI transpiler takes longer to run than the standard method. This is due to the added computational cost of invoking learned models during routing and synthesis. In contrast, the SABRE-based transpiler is now significantly faster after being rewritten and optimized in Rust, providing highly efficient heuristic routing at scale.\n", + "\n", + "It is important to note that these results are based on just one circuit. To obtain a comprehensive understanding of how the AI transpiler compares to traditional methods, it is necessary to test a variety of circuits. The performance of QTS can vary greatly depending on the type of circuit being optimized. For a broader comparison, refer to the benchmarks above or visit the [blog.](https://www.ibm.com/quantum/blog/qiskit-performance)" + ] + }, + { + "cell_type": "markdown", + "id": "c8a55587-abf6-4096-85fd-2702a077ae75", + "metadata": {}, + "source": [ + "## Step 3: Execute using Qiskit primitives\n", + "As this tutorial focuses on transpilation, no experiments will be executed on the quantum device. The goal is to leverage the optimizations from Step 2 to obtain a transpiled circuit with reduced depth or gate count." + ] + }, + { + "cell_type": "markdown", + "id": "8d0cfca9-be4e-40ab-ab98-d7899bb8b3fa", + "metadata": {}, + "source": [ + "## Step 4: Post-process and return result in desired classical format\n", + "Since there is no execution for this notebook, there are no results to post-process." + ] + }, + { + "cell_type": "markdown", + "id": "c82277b2-22e9-44fe-886e-e8ceb2178278", + "metadata": {}, + "source": [ + "# Part II. Analyze and benchmark the transpiled circuits\n", + "\n", + "In this section, we will demonstrate how to analyze the transpiled circuit and benchmark it against the original version in more detail. We will focus on metrics such as circuit depth, gate count, and transpilation time to assess the effectiveness of the optimization. Additionally, we will discuss how the results may differ across various circuit types, offering insights into the broader performance of the transpiler across different scenarios." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "ee24725b-64c9-4d6a-aa97-5a3502b0982a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Completed transpilation for Random\n", + "Completed transpilation for Clifford\n", + "Completed transpilation for QFT\n", + "Completed transpilation for BV\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
CircuitDepth 2Q (No AI)Gate Count (No AI)Time (No AI)Depth 2Q (AI)Gate Count (AI)Time (AI)
0Random372210.039347241810.773718
1Clifford362320.036633432671.097431
2QFT1659240.0774581309133.660771
3BV651550.024993701550.345522
\n", + "
" + ], + "text/plain": [ + " Circuit Depth 2Q (No AI) Gate Count (No AI) Time (No AI) \\\n", + "0 Random 37 221 0.039347 \n", + "1 Clifford 36 232 0.036633 \n", + "2 QFT 165 924 0.077458 \n", + "3 BV 65 155 0.024993 \n", + "\n", + " Depth 2Q (AI) Gate Count (AI) Time (AI) \n", + "0 24 181 0.773718 \n", + "1 43 267 1.097431 \n", + "2 130 913 3.660771 \n", + "3 70 155 0.345522 " + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Circuits to benchmark\n", + "seed = 42\n", + "circuits = [\n", + " {\n", + " \"name\": \"Random\",\n", + " \"qc\": random_circuit(num_qubits=30, depth=10, seed=seed),\n", + " },\n", + " {\n", + " \"name\": \"Clifford\",\n", + " \"qc\": random_clifford_circuit(\n", + " num_qubits=40, num_gates=200, seed=seed\n", + " ),\n", + " },\n", + " {\n", + " \"name\": \"QFT\",\n", + " \"qc\": synth_qft_full(num_qubits=20, do_swaps=False).decompose(),\n", + " },\n", + " {\n", + " \"name\": \"BV\",\n", + " \"qc\": create_bv_circuit(40),\n", + " },\n", + "]\n", + "\n", + "results = []\n", + "\n", + "# Run the transpilation for each circuit and store the results\n", + "for circuit in circuits:\n", + " qc_no_ai, metrics_no_ai = transpile_with_metrics(pm_no_ai, circuit[\"qc\"])\n", + " qc_ai, metrics_ai = transpile_with_metrics(pm_ai, circuit[\"qc\"])\n", + "\n", + " print(\"Completed transpilation for\", circuit[\"name\"])\n", + "\n", + " results.append(\n", + " {\n", + " \"Circuit\": circuit[\"name\"],\n", + " \"Depth 2Q (No AI)\": metrics_no_ai[\"depth_2q\"],\n", + " \"Gate Count (No AI)\": metrics_no_ai[\"gate_count\"],\n", + " \"Time (No AI)\": metrics_no_ai[\"time_s\"],\n", + " \"Depth 2Q (AI)\": metrics_ai[\"depth_2q\"],\n", + " \"Gate Count (AI)\": metrics_ai[\"gate_count\"],\n", + " \"Time (AI)\": metrics_ai[\"time_s\"],\n", + " }\n", + " )\n", + "\n", + "df = pd.DataFrame(results)\n", + "df" + ] + }, + { + "cell_type": "markdown", + "id": "061d85cf-3841-4ed3-bd0d-cd950564efb7", + "metadata": {}, + "source": [ + "Average percentage reduction for each metric. Positive are improvements, negative are degradations." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "70cf9c05-62a3-4049-9712-319902107ba6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Average reduction in depth: 11.88%\n", + "Average reduction in gate count: 1.04%\n", + "Average reduction in transpilation time: -3193.95%\n" + ] + } + ], + "source": [ + "# Average reduction from non-AI to AI transpilation as a percentage\n", + "avg_reduction_depth = (\n", + " (df[\"Depth 2Q (No AI)\"] - df[\"Depth 2Q (AI)\"]).mean()\n", + " / df[\"Depth 2Q (No AI)\"].mean()\n", + " * 100\n", + ")\n", + "avg_reduction_gates = (\n", + " (df[\"Gate Count (No AI)\"] - df[\"Gate Count (AI)\"]).mean()\n", + " / df[\"Gate Count (No AI)\"].mean()\n", + " * 100\n", + ")\n", + "avg_reduction_time = (\n", + " (df[\"Time (No AI)\"] - df[\"Time (AI)\"]).mean()\n", + " / df[\"Time (No AI)\"].mean()\n", + " * 100\n", + ")\n", + "\n", + "print(f\"Average reduction in depth: {avg_reduction_depth:.2f}%\")\n", + "print(f\"Average reduction in gate count: {avg_reduction_gates:.2f}%\")\n", + "print(f\"Average reduction in transpilation time: {avg_reduction_time:.2f}%\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "79b8d5d9-0f9d-42ca-9583-8bec17430014", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, axs = plt.subplots(1, 3, figsize=(21, 6))\n", + "df.plot(\n", + " x=\"Circuit\",\n", + " y=[\"Depth 2Q (No AI)\", \"Depth 2Q (AI)\"],\n", + " kind=\"bar\",\n", + " ax=axs[0],\n", + ")\n", + "axs[0].set_title(\"Circuit Depth Comparison\")\n", + "axs[0].set_ylabel(\"Depth\")\n", + "axs[0].set_xlabel(\"Circuit\")\n", + "axs[0].tick_params(axis=\"x\", rotation=45)\n", + "df.plot(\n", + " x=\"Circuit\",\n", + " y=[\"Gate Count (No AI)\", \"Gate Count (AI)\"],\n", + " kind=\"bar\",\n", + " ax=axs[1],\n", + ")\n", + "axs[1].set_title(\"Gate Count Comparison\")\n", + "axs[1].set_ylabel(\"Gate Count\")\n", + "axs[1].set_xlabel(\"Circuit\")\n", + "axs[1].tick_params(axis=\"x\", rotation=45)\n", + "df.plot(x=\"Circuit\", y=[\"Time (No AI)\", \"Time (AI)\"], kind=\"bar\", ax=axs[2])\n", + "axs[2].set_title(\"Time Comparison\")\n", + "axs[2].set_ylabel(\"Time (seconds)\")\n", + "axs[2].set_xlabel(\"Circuit\")\n", + "axs[2].tick_params(axis=\"x\", rotation=45)\n", + "fig.suptitle(\n", + " \"Benchmarking AI transpilation vs Non-AI transpilation for various circuits\"\n", + ")\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "345022d3-e302-47e6-9453-9261136923a7", + "metadata": {}, + "source": [ + "The AI transpiler's performance varies significantly based on the type of circuit being optimized. In some cases, it achieves notable reductions in circuit depth and gate count compared to the standard transpiler. However, these improvements often come with a substantial increase in runtime.\n", + "\n", + "For certain types of circuits, the AI transpiler may yield slightly better results in terms of circuit depth but may also lead to an increase in gate count and a significant runtime penalty. These observations suggest that the AI transpiler's benefits are not uniform across all circuit types. Instead, its effectiveness depends on the specific characteristics of the circuit, making it more suitable for some use cases than others." + ] + }, + { + "cell_type": "markdown", + "id": "9e496e7a-64a8-46fd-b240-c494e7825bd2", + "metadata": {}, + "source": [ + "## When should users choose AI-powered transpilation?\n", + "\n", + "The AI-powered transpiler in Qiskit excels in scenarios where traditional transpilation methods struggle, particularly with large-scale and complex quantum circuits. For circuits involving hundreds of qubits or those targeting hardware with intricate coupling maps, the AI transpiler offers superior optimization in terms of circuit depth, gate count, and runtime efficiency. In benchmarking tests, it has consistently outperformed traditional methods, delivering significantly shallower circuits and reducing gate counts, which are critical for enhancing performance and mitigating noise on real quantum hardware.\n", + "\n", + "Users should consider AI-powered transpilation when working with:\n", + "- Large circuits where traditional methods fail to efficiently handle the scale.\n", + "- Complex hardware topologies where device connectivity and routing challenges arise.\n", + "- Performance-sensitive applications where reducing circuit depth and improving fidelity are paramount." + ] + }, + { + "cell_type": "markdown", + "id": "c345cb54-a838-427f-898f-51fb607da493", + "metadata": {}, + "source": [ + "# Part III. Explore AI-powered permutation network synthesis\n", + "\n", + "Permutation networks are foundational in quantum computing, particularly for systems constrained by restricted topologies. These networks facilitate long-range interactions by dynamically swapping qubits to mimic all-to-all connectivity on hardware with limited connectivity. Such transformations are essential for implementing complex quantum algorithms on near-term devices, where interactions often span beyond nearest neighbors.\n", + "\n", + "In this section, we highlight the synthesis of permutation networks as a compelling use case for the AI-powered transpiler in Qiskit. Specifically, the `AIPermutationSynthesis` pass leverages AI-driven optimization to generate efficient circuits for qubit permutation tasks. By contrast, generic synthesis approaches often struggle to balance gate count and circuit depth, especially in scenarios with dense qubit interactions or when attempting to achieve full connectivity.\n", + "\n", + "We will walk through a Qiskit patterns example showcasing the synthesis of a permutation network to achieve all-to-all connectivity for a set of qubits. We will compare the performance of `AIPermutationSynthesis` against the standard synthesis methods in Qiskit. This example will demonstrate how the AI transpiler optimizes for lower circuit depth and gate count, highlighting its advantages in practical quantum workflows. To activate the AI synthesis pass, we will use the `generate_ai_pass_manager()` function with the `include_ai_synthesis` parameter set to `True`." + ] + }, + { + "cell_type": "markdown", + "id": "76de0959-1eca-43d9-b8fe-f9aea9a122d8", + "metadata": {}, + "source": [ + "## Step 1: Map classical inputs to a quantum problem\n", + "\n", + "To represent a classical permutation problem on a quantum computer, we start by defining the structure of the quantum circuits. For this example:\n", + "\n", + "1. Quantum circuit initialization:\n", + " We allocate 27 qubits to match the backend we will use, which has 27 qubits.\n", + "\n", + "2. Apply permutations:\n", + " We generate ten random permutation patterns (`pattern_1` through `pattern_10`) using a fixed seed for reproducibility. Each permutation pattern is applied to a separate quantum circuit (`qc_1` through `qc_10`).\n", + "\n", + "3. Circuit decomposition:\n", + " Each permutation operation is decomposed into native gate sets compatible with the target quantum hardware. We analyze the depth and the number of two-qubit gates (nonlocal gates) for each decomposed circuit.\n", + "\n", + "The results provide insight into the complexity of representing classical permutation problems on a quantum device, demonstrating the resource requirements for different permutation patterns." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "76a3e847-0808-4413-bd0c-c760cd2df3f4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Parameters\n", + "width = 27\n", + "num_circuits = 10\n", + "\n", + "# Set random seed\n", + "np.random.seed(seed)\n", + "\n", + "\n", + "# Generate random patterns and circuits\n", + "patterns = [\n", + " np.random.permutation(width).tolist() for _ in range(num_circuits)\n", + "]\n", + "circuits = {\n", + " f\"qc_{i}\": generate_permutation_circuit(width, pattern)\n", + " for i, pattern in enumerate(patterns, start=1)\n", + "}\n", + "\n", + "# Display one of the circuits\n", + "circuits[\"qc_1\"].decompose(reps=3).draw(output=\"mpl\", fold=-1)" + ] + }, + { + "cell_type": "markdown", + "id": "a8b79798-fa80-44d8-8a52-2d2a50e0c280", + "metadata": {}, + "source": [ + "## Step 2: Optimize problem for quantum hardware execution\n", + "In this step, we proceed with optimization using the AI synthesis passes.\n", + "\n", + "For the AI synthesis passes, the `PassManager` requires only the coupling map of the backend. However, it is important to note that not all coupling maps are compatible; only those that the `AIPermutationSynthesis` pass has been trained on will work. Currently, the `AIPermutationSynthesis` pass supports blocks of sizes 65, 33, and 27 qubits. For this example we use a 27-qubit QPU.\n", + "\n", + "For comparison, we will evaluate the performance of AI synthesis against generic permutation synthesis methods in Qiskit, including:\n", + "\n", + "- `synth_permutation_depth_lnn_kms`: This method synthesizes a permutation circuit for a linear nearest-neighbor (LNN) architecture using the Kutin, Moulton, and Smithline (KMS) algorithm. It guarantees a circuit with a depth of at most $ n $ and a size of at most $ n(n-1)/2 $, where both depth and size are measured in terms of SWAP gates.\n", + "\n", + "- `synth_permutation_basic`: This is a straightforward implementation that synthesizes permutation circuits without imposing constraints on connectivity or optimization for specific architectures. It serves as a baseline for comparing performance with more advanced methods.\n", + "\n", + "Each of these methods represents a distinct approach to synthesizing permutation networks, providing a comprehensive benchmark against the AI-powered methods.\n", + "\n", + "For more details about synthesis methods in Qiskit, refer to the [Qiskit API documentation](/docs/api/qiskit/synthesis)." + ] + }, + { + "cell_type": "markdown", + "id": "b1733a10-c285-444e-af47-4a32329c5f7a", + "metadata": {}, + "source": [ + "Define the coupling map representing the 27-qubit QPU." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "84dff2c2-a496-4828-bb8e-08d373816a36", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "coupling_map = [\n", + " [1, 0],\n", + " [2, 1],\n", + " [3, 2],\n", + " [3, 5],\n", + " [4, 1],\n", + " [6, 7],\n", + " [7, 4],\n", + " [7, 10],\n", + " [8, 5],\n", + " [8, 9],\n", + " [8, 11],\n", + " [11, 14],\n", + " [12, 10],\n", + " [12, 13],\n", + " [12, 15],\n", + " [13, 14],\n", + " [16, 14],\n", + " [17, 18],\n", + " [18, 15],\n", + " [18, 21],\n", + " [19, 16],\n", + " [19, 22],\n", + " [20, 19],\n", + " [21, 23],\n", + " [23, 24],\n", + " [25, 22],\n", + " [25, 24],\n", + " [26, 25],\n", + "]\n", + "CouplingMap(coupling_map).draw()" + ] + }, + { + "cell_type": "markdown", + "id": "47bdb1f5-1fc6-46c4-8fc9-98d16a4d2529", + "metadata": {}, + "source": [ + "Transpile each of the permutation circuits using the AI synthesis passes and generic synthesis methods." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "128cc285-094a-4b07-a37d-8424a4003b2c", + "metadata": {}, + "outputs": [], + "source": [ + "results = []\n", + "pm_no_ai_synth = generate_preset_pass_manager(\n", + " coupling_map=cm,\n", + " optimization_level=1, # set to 1 since we are using the synthesis methods\n", + ")\n", + "\n", + "# Transpile and analyze all circuits\n", + "for i, (qc_name, qc) in enumerate(circuits.items(), start=1):\n", + " pattern = patterns[i - 1] # Get the corresponding pattern\n", + "\n", + " qc_depth_lnn_kms = synth_permutation_depth_lnn_kms(pattern)\n", + " qc_basic = synth_permutation_basic(pattern)\n", + "\n", + " # AI synthesis\n", + " results.append(\n", + " synth_transpile_with_metrics(\n", + " qc.decompose(reps=3),\n", + " pm_ai,\n", + " qc_name,\n", + " \"AI\",\n", + " )\n", + " )\n", + "\n", + " # Depth-LNN-KMS Method\n", + " results.append(\n", + " synth_transpile_with_metrics(\n", + " qc_depth_lnn_kms.decompose(reps=3),\n", + " pm_no_ai_synth,\n", + " qc_name,\n", + " \"Depth-LNN-KMS\",\n", + " )\n", + " )\n", + "\n", + " # Basic Method\n", + " results.append(\n", + " synth_transpile_with_metrics(\n", + " qc_basic.decompose(reps=3),\n", + " pm_no_ai_synth,\n", + " qc_name,\n", + " \"Basic\",\n", + " )\n", + " )\n", + "\n", + "\n", + "results_df = pd.DataFrame(results)" + ] + }, + { + "cell_type": "markdown", + "id": "42f80e32-60fd-46a8-a6b5-4bcadb15810a", + "metadata": {}, + "source": [ + "Record the metrics (depth, gate count, time) for each circuit after transpilation." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "72ee8474-eea6-421a-9d7d-070587eaff71", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "=== Average Metrics ===\n", + " Depth (2Q) Gates Time (s)\n", + "Method \n", + "AI 23.9 82.8 0.248\n", + "Basic 29.8 91.0 0.012\n", + "Depth-LNN-KMS 70.8 531.6 0.017\n", + "\n", + "Best Non-AI Method (based on least average depth): Basic\n", + "\n", + "=== Comparison of AI vs Best Non-AI Method ===\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
MetricAIBasicImprovement (AI vs Best Non-AI)
0Depth (2Q)23.90029.800-5.900
1Gates82.80091.000-8.200
2Time (s)0.2480.0120.236
\n", + "
" + ], + "text/plain": [ + " Metric AI Basic Improvement (AI vs Best Non-AI)\n", + "0 Depth (2Q) 23.900 29.800 -5.900\n", + "1 Gates 82.800 91.000 -8.200\n", + "2 Time (s) 0.248 0.012 0.236" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Calculate averages for each metric\n", + "average_metrics = results_df.groupby(\"Method\")[\n", + " [\"Depth (2Q)\", \"Gates\", \"Time (s)\"]\n", + "].mean()\n", + "average_metrics = average_metrics.round(3) # Round to two decimal places\n", + "print(\"\\n=== Average Metrics ===\")\n", + "print(average_metrics)\n", + "\n", + "# Identify the best non-AI method based on least average depth\n", + "non_ai_methods = [\n", + " method for method in results_df[\"Method\"].unique() if method != \"AI\"\n", + "]\n", + "best_non_ai_method = average_metrics.loc[non_ai_methods][\n", + " \"Depth (2Q)\"\n", + "].idxmin()\n", + "print(\n", + " f\"\\nBest Non-AI Method (based on least average depth): {best_non_ai_method}\"\n", + ")\n", + "\n", + "# Compare AI to the best non-AI method\n", + "ai_metrics = average_metrics.loc[\"AI\"]\n", + "best_non_ai_metrics = average_metrics.loc[best_non_ai_method]\n", + "\n", + "comparison = {\n", + " \"Metric\": [\"Depth (2Q)\", \"Gates\", \"Time (s)\"],\n", + " \"AI\": [\n", + " ai_metrics[\"Depth (2Q)\"],\n", + " ai_metrics[\"Gates\"],\n", + " ai_metrics[\"Time (s)\"],\n", + " ],\n", + " best_non_ai_method: [\n", + " best_non_ai_metrics[\"Depth (2Q)\"],\n", + " best_non_ai_metrics[\"Gates\"],\n", + " best_non_ai_metrics[\"Time (s)\"],\n", + " ],\n", + " \"Improvement (AI vs Best Non-AI)\": [\n", + " ai_metrics[\"Depth (2Q)\"] - best_non_ai_metrics[\"Depth (2Q)\"],\n", + " ai_metrics[\"Gates\"] - best_non_ai_metrics[\"Gates\"],\n", + " ai_metrics[\"Time (s)\"] - best_non_ai_metrics[\"Time (s)\"],\n", + " ],\n", + "}\n", + "\n", + "comparison_df = pd.DataFrame(comparison)\n", + "print(\"\\n=== Comparison of AI vs Best Non-AI Method ===\")\n", + "comparison_df" + ] + }, + { + "cell_type": "markdown", + "id": "e1ba3767-5ce1-4663-803b-73ccfc22f03b", + "metadata": {}, + "source": [ + "The results demonstrate that the AI transpiler outperforms all other Qiskit synthesis methods for this set of random permutation circuits. Key findings include:\n", + "\n", + "1. Depth: The AI transpiler achieves the lowest average depth, indicating superior optimization of circuit layouts.\n", + "2. Gate count: It significantly reduces the number of gates compared to other methods, improving execution fidelity and efficiency.\n", + "3. Transpilation time: All methods run very quickly at this scale, making them practical for use. However, the AI transpiler does has a notable runtime increase compared to traditional methods due to the complexity of the AI models used.\n", + "\n", + "These results establish the AI transpiler as the most effective approach for this benchmark, particularly for depth and gate count optimization." + ] + }, + { + "cell_type": "markdown", + "id": "dbaab943-5fd7-4720-98bf-8602b2ab4473", + "metadata": {}, + "source": [ + "Plot the results to compare the performance of the AI synthesis passes against the generic synthesis methods." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "a326f268-0115-442c-8563-968676b66670", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "methods = results_df[\"Method\"].unique()\n", + "\n", + "fig, axs = plt.subplots(1, 3, figsize=(18, 5))\n", + "\n", + "# Pivot the DataFrame and reorder columns to ensure AI is first\n", + "pivot_depth = results_df.pivot(\n", + " index=\"Pattern\", columns=\"Method\", values=\"Depth (2Q)\"\n", + ")[[\"AI\", \"Depth-LNN-KMS\", \"Basic\"]]\n", + "pivot_gates = results_df.pivot(\n", + " index=\"Pattern\", columns=\"Method\", values=\"Gates\"\n", + ")[[\"AI\", \"Depth-LNN-KMS\", \"Basic\"]]\n", + "pivot_time = results_df.pivot(\n", + " index=\"Pattern\", columns=\"Method\", values=\"Time (s)\"\n", + ")[[\"AI\", \"Depth-LNN-KMS\", \"Basic\"]]\n", + "\n", + "pivot_depth.plot(kind=\"bar\", ax=axs[0], legend=False)\n", + "axs[0].set_title(\"Circuit Depth Comparison\")\n", + "axs[0].set_ylabel(\"Depth\")\n", + "axs[0].set_xlabel(\"Pattern\")\n", + "axs[0].tick_params(axis=\"x\", rotation=45)\n", + "pivot_gates.plot(kind=\"bar\", ax=axs[1], legend=False)\n", + "axs[1].set_title(\"2Q Gate Count Comparison\")\n", + "axs[1].set_ylabel(\"Number of 2Q Gates\")\n", + "axs[1].set_xlabel(\"Pattern\")\n", + "axs[1].tick_params(axis=\"x\", rotation=45)\n", + "pivot_time.plot(\n", + " kind=\"bar\", ax=axs[2], legend=True, title=\"Legend\"\n", + ") # Show legend on the last plot\n", + "axs[2].set_title(\"Time Comparison\")\n", + "axs[2].set_ylabel(\"Time (seconds)\")\n", + "axs[2].set_xlabel(\"Pattern\")\n", + "axs[2].tick_params(axis=\"x\", rotation=45)\n", + "fig.suptitle(\n", + " \"Benchmarking AI Synthesis Methods vs Non-AI Synthesis Methods For Random Permutations Circuits\",\n", + " fontsize=16,\n", + " y=1,\n", + ")\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "03a9af42-42a7-4344-b834-0d2b506d4d78", + "metadata": {}, + "source": [ + "This graph highlights the individual results for each circuit (`qc_1` to `qc_10`) across different synthesis methods:\n", + "\n", + "While these results underscore the AI transpiler’s effectiveness for permutation circuits, it is important to note its limitations. The AI synthesis method is currently only available for certain coupling maps, which may restrict its broader applicability. This constraint should be considered when evaluating its usage in different scenarios.\n", + "\n", + "Overall, the AI transpiler demonstrates promising improvements in depth and gate count optimization for these specific circuits while maintaining comparable transpilation times." + ] + }, + { + "cell_type": "markdown", + "id": "41b1405d-fa90-48b6-9ce2-933f05358778", + "metadata": {}, + "source": [ + "## Step 3: Execute using Qiskit primitives\n", + "As this tutorial focuses on transpilation, no experiments will be executed on the quantum device. The goal is to leverage the optimizations from Step 2 to obtain a transpiled circuit with reduced depth or gate count." + ] + }, + { + "cell_type": "markdown", + "id": "3d942ee4-e4d7-4e87-8c8a-17c662d5379f", + "metadata": {}, + "source": [ + "## Step 4: Post-process and return result in desired classical format\n", + "Since there is no execution for this notebook, there are no results to post-process." + ] + }, + { + "cell_type": "markdown", + "id": "3b21bb06-7a2b-4181-af59-734c89435d45", + "metadata": {}, + "source": [ + "## Tutorial survey\n", + "\n", + "Please take this short survey to provide feedback on this tutorial. Your insights will help us improve our content offerings and user experience.\n", + "\n", + "[Link to survey](https://your.feedback.ibm.com/jfe/form/SV_0igXMtMCQfApgDI)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/tutorials/approximate-quantum-compilation-for-time-evolution.ipynb b/docs/tutorials/approximate-quantum-compilation-for-time-evolution.ipynb index 9ea79755449..42cc94bcbec 100644 --- a/docs/tutorials/approximate-quantum-compilation-for-time-evolution.ipynb +++ b/docs/tutorials/approximate-quantum-compilation-for-time-evolution.ipynb @@ -477,7 +477,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "1f050059-a281-41f1-a277-d7450e8b3ee3", "metadata": {}, "outputs": [ @@ -496,7 +496,9 @@ ")\n", "print(\"Target MPS maximum bond dimension:\", aqc_target_mps.psi.max_bond())\n", "\n", - "# Obtains the reference MPS, where we can obtain the exact expectation value by examining the `local_expectation``\n", + "# Obtains the reference MPS, where we can obtain the exact expectation value\n", + "# by examining the `local_expectation`\n", + "\n", "reference_mps = tensornetwork_from_circuit(\n", " reference_circuit, simulator_settings\n", ")\n", @@ -1308,7 +1310,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": null, "id": "6030706e-1451-47d9-9e47-bcccf4cb5d9c", "metadata": {}, "outputs": [ @@ -1332,7 +1334,9 @@ ")\n", "print(\"Target MPS maximum bond dimension:\", aqc_target_mps.psi.max_bond())\n", "\n", - "# Obtains the reference MPS, where we can obtain the exact expectation value by examining the `local_expectation``\n", + "# Obtains the reference MPS, where we can obtain the exact expectation value\n", + "# by examining the `local_expectation`\n", + "\n", "reference_mps = tensornetwork_from_circuit(\n", " reference_circuit, simulator_settings\n", ")\n", diff --git a/docs/tutorials/compilation-methods-for-hamiltonian-simulation-circuits.ipynb b/docs/tutorials/compilation-methods-for-hamiltonian-simulation-circuits.ipynb index 5805af0929e..031a498b47d 100644 --- a/docs/tutorials/compilation-methods-for-hamiltonian-simulation-circuits.ipynb +++ b/docs/tutorials/compilation-methods-for-hamiltonian-simulation-circuits.ipynb @@ -1,1638 +1,1639 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "f056ca4a-fbe4-4051-bbcd-79c2d7848cd0", - "metadata": {}, - "source": [ - "---\n", - "title: Compilation methods for Hamiltonian simulation circuits\n", - "description: This tutorial provides a comparative overview of three compilation methods in Qiskit for Hamiltonian simulation workloads.\n", - "---\n", - "\n", - "\n", - "# Compilation methods for Hamiltonian simulation circuits\n", - "Estimated QPU usage: no execution was done in this tutorial because it is focused on the transpilation process.\n", - "\n", - "{/* cspell:ignore Rustiq, nshuffles, edgecolors, edgecolor, Hamlib, Benchpress, Brugière, Goubault, Martiel, Dubal, Lishman, Ivrii, fontweight, fontsize, textprops, wedgeprops, startangle, autopct */}" - ] - }, - { - "cell_type": "markdown", - "id": "d960a90b-1487-4310-a222-b95b8a77a080", - "metadata": {}, - "source": [ - "## Background\n", - "\n", - "Quantum circuit compilation is a crucial step in the quantum computing workflow. It involves transforming a high-level quantum algorithm into a physical quantum circuit that adheres to the constraints of the target quantum hardware. Effective compilation can significantly impact the performance of quantum algorithms by reducing circuit depth, gate count, and execution time. This tutorial explores three distinct approaches to quantum circuit compilation in Qiskit, showcasing their strengths and applications through practical examples.\n", - "\n", - "The goal of this tutorial is to teach users how to apply and evaluate three compilation methods in Qiskit: the SABRE transpiler, the AI-powered transpiler, and the Rustiq plugin. Users will learn how to use each method effectively and how to benchmark their performance across different quantum circuits. By the end of this tutorial, users will be able to choose and tailor compilation strategies based on specific optimization goals such as reducing circuit depth, minimizing gate count, or improving runtime.\n", - "\n", - "### What you will learn\n", - "- **How to use the Qiskit transpiler with SABRE for layout and routing optimization.**\n", - "- **How to leverage the AI transpiler for advanced, automated circuit optimization.**\n", - "- **How to employ the Rustiq plugin for circuits requiring precise synthesis of operations, particularly in Hamiltonian simulation tasks.**\n", - "\n", - "This tutorial uses three example circuits following the [Qiskit patterns](/docs/guides/intro-to-patterns) workflow to illustrate the performance of each compilation method. By the end of this tutorial, users will be equipped to choose the appropriate compilation strategy based on their specific requirements and constraints.\n", - "\n", - "### Compilation methods overview\n", - "\n", - "#### 1. **Qiskit transpiler with SABRE**\n", - "The Qiskit transpiler uses the SABRE (SWAP-based BidiREctional heuristic search) algorithm to optimize circuit layout and routing. SABRE focuses on minimizing SWAP gates and their impact on circuit depth while adhering to hardware connectivity constraints. This method is highly versatile and suitable for general-purpose circuit optimization, providing a balance between performance and computation time. To take advantage of the latest improvements in SABRE, detailed in [\\[1\\]](https://arxiv.org/abs/2409.08368), you can increase the number of trials (for example, `layout_trials=400, swap_trials=400`). For the purposes of this tutorial, we will use the default values for the number of trials in order to compare to Qiskit's default transpiler. The advantages and parameter exploration of SABRE are covered in a separate [deep-dive tutorial](/docs/tutorials/transpilation-optimizations-with-sabre).\n", - "\n", - "#### 2. **AI transpiler**\n", - "\n", - "The AI-powered transpiler in Qiskit uses machine learning to predict optimal transpilation strategies by analyzing patterns in circuit structure and hardware constraints to select the best sequence of optimizations for a given input. This method is particularly effective for large-scale quantum circuits, offering a high degree of automation and adaptability to diverse problem types. In addition to general circuit optimization, the AI transpiler can be used with the `AIPauliNetworkSynthesis` pass, which targets Pauli network circuits — blocks composed of H, S, SX, CX, RX, RY, and RZ gates — and applies a reinforcement learning-based synthesis approach. For more information on the AI transpiler and its synthesis strategies, see [\\[2\\]](https://arxiv.org/abs/2405.13196) and [\\[3\\]](https://arxiv.org/abs/2503.14448).\n", - "\n", - "\n", - "\n", - "#### 3. **Rustiq plugin**\n", - "The Rustiq plugin introduces advanced synthesis techniques specifically for `PauliEvolutionGate` operations, which represent Pauli rotations commonly used in Trotterized dynamics. This plugin is valuable for circuits implementing Hamiltonian simulation, such as those used in quantum chemistry and physics problems, where accurate Pauli rotations are essential for simulating problem Hamiltonians effectively. Rustiq offers precise, low-depth circuit synthesis for these specialized operations. For more details about the implementation and performance of Rustiq, please refer to [\\[4\\]](https://arxiv.org/abs/2404.03280).\n", - "\n", - "By exploring these compilation methods in depth, this tutorial provides users with the tools to enhance the performance of their quantum circuits, paving the way for more efficient and practical quantum computations." - ] - }, - { - "cell_type": "markdown", - "id": "53c589f4-c63c-47f3-8642-1f189e445307", - "metadata": {}, - "source": [ - "## Requirements\n", - "\n", - "Before starting this tutorial, be sure you have the following installed:\n", - "- Qiskit SDK v1.3 or later, with [visualization](/docs/api/qiskit/visualization) support\n", - "- Qiskit Runtime v0.28 or later (`pip install qiskit-ibm-runtime`)\n", - "- Qiskit IBM Transpiler (`pip install qiskit-ibm-transpiler`)\n", - "- Qiskit AI Transpiler local mode (`pip install qiskit_ibm_ai_local_transpiler`)\n", - "- Networkx graph library (`pip install networkx`)" - ] - }, - { - "cell_type": "markdown", - "id": "4f79e4a8-48bc-4af8-a172-f0057fa851eb", - "metadata": {}, - "source": [ - "## Setup" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "1ecd9900-e511-486e-97be-aac5f75b1917", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.circuit import QuantumCircuit\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "from qiskit.circuit.library import (\n", - " efficient_su2,\n", - " PauliEvolutionGate,\n", - ")\n", - "from qiskit_ibm_transpiler import generate_ai_pass_manager\n", - "from qiskit.quantum_info import SparsePauliOp\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "from qiskit.transpiler.passes.synthesis.high_level_synthesis import HLSConfig\n", - "from collections import Counter\n", - "from IPython.display import display\n", - "import time\n", - "import pandas as pd\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "import json\n", - "import requests\n", - "import logging\n", - "\n", - "# Suppress noisy loggers\n", - "logging.getLogger(\n", - " \"qiskit_ibm_transpiler.wrappers.ai_local_synthesis\"\n", - ").setLevel(logging.ERROR)\n", - "\n", - "seed = 42 # Seed for reproducibility" - ] - }, - { - "cell_type": "markdown", - "id": "c3a31b4d-d679-4657-b372-ba19bcaf8eca", - "metadata": {}, - "source": [ - "## Part 1: Efficient SU2 Circuit\n", - "\n", - "### Step 1: Map classical inputs to a quantum problem\n", - "\n", - "In this section, we explore the `efficient_su2` circuit, a hardware-efficient ansatz commonly used in variational quantum algorithms (such as VQE) and quantum machine-learning tasks. The circuit consists of alternating layers of single-qubit rotations and entangling gates arranged in a circular pattern, designed to explore the quantum state space effectively while maintaining manageable depth.\n", - "\n", - "We will begin by constructing one `efficient_su2` circuit to demonstrate how to compare different compilation methods. After Part 1, we will expand our analysis to a larger set of circuits, enabling a comprehensive benchmark for evaluating the performance of various compilation techniques." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "f362cdac-94d8-4cc5-85f4-015c3d9eba3a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qubit_size = list(range(10, 101, 10))\n", - "qc_su2_list = [\n", - " efficient_su2(n, entanglement=\"circular\", reps=1)\n", - " .decompose()\n", - " .copy(name=f\"SU2_{n}\")\n", - " for n in qubit_size\n", - "]\n", - "\n", - "# Draw the first circuit\n", - "qc_su2_list[0].draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "d6671456-9b17-42bb-b94a-d42a29e6fad9", - "metadata": {}, - "source": [ - "### Step 2: Optimize problem for quantum hardware execution\n", - "\n", - "This step is the main focus of the tutorial. Here, we aim to optimize quantum circuits for efficient execution on real quantum hardware. Our primary objective is to reduce circuit depth and gate count, which are key factors in improving execution fidelity and mitigating hardware noise.\n", - "\n", - "- **SABRE transpiler**: Uses Qiskit’s default transpiler with the SABRE layout and routing algorithm.\n", - "- **AI transpiler (local mode)**: The standard AI-powered transpiler using local inference and the default synthesis strategy.\n", - "- **Rustiq plugin**: A transpiler plugin designed for low-depth compilation tailored to Hamiltonian simulation tasks.\n", - "\n", - "The goal of this step is to compare the results of these methods in terms of the transpiled circuit’s depth and gate count. Another important metric we consider is the transpilation runtime. By analyzing these metrics, we can evaluate the relative strengths of each method and determine which produces the most efficient circuit for execution on the selected hardware.\n", - "\n", - "Note: For the initial SU2 circuit example, we will only compare the SABRE transpiler to the default AI transpiler. However, in the subsequent benchmark using Hamlib circuits, we will compare all three transpilation methods." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c1ce1ad9-d529-49a1-91df-b540603ceb88", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "qiskit_runtime_service._get_crn_from_instance_name:WARNING:2025-07-30 21:46:30,843: Multiple instances found. Using all matching instances.\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Using backend: \n" - ] - } - ], - "source": [ - "# QiskitRuntimeService.save_account(channel=\"ibm_quantum_platform\", token=\"\", overwrite=True, set_as_default=True)\n", - "service = QiskitRuntimeService(channel=\"ibm_quantum_platform\")\n", - "backend = service.backend(\"ibm_torino\")\n", - "print(f\"Using backend: {backend}\")" - ] - }, - { - "cell_type": "markdown", - "id": "381cfaca-103b-41bc-b3b3-f76808f44638", - "metadata": {}, - "source": [ - "Qiskit transpiler with SABRE:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "92a8f12d-3f97-400d-b6b6-a8c717f9ff0f", - "metadata": {}, - "outputs": [], - "source": [ - "pm_sabre = generate_preset_pass_manager(\n", - " optimization_level=3, backend=backend, seed_transpiler=seed\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "d1ae6e70-f276-41f3-8e40-68a9523eae06", - "metadata": {}, - "source": [ - "AI transpiler:" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "6c27960f-e1cd-4cb5-b807-4dace42ea970", - "metadata": {}, - "outputs": [], - "source": [ - "# Standard AI transpiler pass manager, using the local mode\n", - "pm_ai = generate_ai_pass_manager(\n", - " backend=backend, optimization_level=3, ai_optimization_level=3\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "b20dfb96-bf70-473f-9c91-d99fe3ea3e88", - "metadata": {}, - "source": [ - "Rustiq plugin:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "7c0ae7c4-f4cd-4575-bbc9-b92e9c1d588c", - "metadata": {}, - "outputs": [], - "source": [ - "hls_config = HLSConfig(\n", - " PauliEvolution=[\n", - " (\n", - " \"rustiq\",\n", - " {\n", - " \"nshuffles\": 400,\n", - " \"upto_phase\": True,\n", - " \"fix_clifford\": True,\n", - " \"preserve_order\": False,\n", - " \"metric\": \"depth\",\n", - " },\n", - " )\n", - " ]\n", - ")\n", - "pm_rustiq = generate_preset_pass_manager(\n", - " optimization_level=3,\n", - " backend=backend,\n", - " hls_config=hls_config,\n", - " seed_transpiler=seed,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "a16c8082-d0f1-462f-ad10-ebd7d87a0d82", - "metadata": {}, - "source": [ - "#### Transpile and capture metrics\n", - "\n", - "To compare the performance of the compilation methods, we define a function that transpiles the input circuit and captures relevant metrics in a consistent manner. This includes the total circuit depth, overall gate count, and transpilation time.\n", - "\n", - "In addition to these standard metrics, we also record the 2-qubit gate depth, which is a particularly important metric for evaluating execution on quantum hardware. Unlike total depth, which includes all gates, the 2-qubit depth more accurately reflects the circuit's*actual execution duration on hardware. This is because 2-qubit gates typically dominate the time and error budget in most quantum devices. As such, minimizing 2-qubit depth is critical for improving fidelity and reducing decoherence effects during execution.\n", - "\n", - "We will use this function to analyze the performance of the different compilation methods across multiple circuits." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "eb236a9d-238e-4236-a195-e7b8df43b7a2", - "metadata": {}, - "outputs": [], - "source": [ - "def capture_transpilation_metrics(\n", - " results, pass_manager, circuits, method_name\n", - "):\n", - " \"\"\"\n", - " Capture transpilation metrics for a list of circuits and stores the results in a DataFrame.\n", - "\n", - " Args:\n", - " results (pd.DataFrame): DataFrame to store the results.\n", - " pass_manager: Pass manager used for transpilation.\n", - " circuits (list): List of quantum circuits to transpile.\n", - " method_name (str): Name of the transpilation method.\n", - "\n", - " Returns:\n", - " list: List of transpiled circuits.\n", - " \"\"\"\n", - " transpiled_circuits = []\n", - "\n", - " for i, qc in enumerate(circuits):\n", - " # Transpile the circuit\n", - " start_time = time.time()\n", - " transpiled_qc = pass_manager.run(qc)\n", - " end_time = time.time()\n", - "\n", - " # Needed for AI transpiler to be consistent with other methods\n", - " transpiled_qc = transpiled_qc.decompose(gates_to_decompose=[\"swap\"])\n", - "\n", - " # Collect metrics\n", - " transpilation_time = end_time - start_time\n", - " circuit_depth = transpiled_qc.depth(\n", - " lambda x: x.operation.num_qubits == 2\n", - " )\n", - " circuit_size = transpiled_qc.size()\n", - "\n", - " # Append results to DataFrame\n", - " results.loc[len(results)] = {\n", - " \"method\": method_name,\n", - " \"qc_name\": qc.name,\n", - " \"qc_index\": i,\n", - " \"num_qubits\": qc.num_qubits,\n", - " \"ops\": transpiled_qc.count_ops(),\n", - " \"depth\": circuit_depth,\n", - " \"size\": circuit_size,\n", - " \"runtime\": transpilation_time,\n", - " }\n", - " transpiled_circuits.append(transpiled_qc)\n", - " print(\n", - " f\"Transpiled circuit index {i} ({qc.name}) in {transpilation_time:.2f} seconds with method {method_name}, \"\n", - " f\"depth {circuit_depth}, and size {circuit_size}.\"\n", - " )\n", - "\n", - " return transpiled_circuits" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "d3107fc8-3151-488f-8eb8-8d8dc5f4d085", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Transpiled circuit index 0 (SU2_10) in 0.06 seconds with method sabre, depth 13, and size 167.\n", - "Transpiled circuit index 1 (SU2_20) in 0.24 seconds with method sabre, depth 20, and size 299.\n", - "Transpiled circuit index 2 (SU2_30) in 10.72 seconds with method sabre, depth 72, and size 627.\n", - "Transpiled circuit index 3 (SU2_40) in 16.16 seconds with method sabre, depth 40, and size 599.\n", - "Transpiled circuit index 4 (SU2_50) in 76.89 seconds with method sabre, depth 77, and size 855.\n", - "Transpiled circuit index 5 (SU2_60) in 86.12 seconds with method sabre, depth 60, and size 899.\n", - "Transpiled circuit index 6 (SU2_70) in 94.46 seconds with method sabre, depth 79, and size 1085.\n", - "Transpiled circuit index 7 (SU2_80) in 69.05 seconds with method sabre, depth 80, and size 1199.\n", - "Transpiled circuit index 8 (SU2_90) in 88.25 seconds with method sabre, depth 105, and size 1420.\n", - "Transpiled circuit index 9 (SU2_100) in 83.80 seconds with method sabre, depth 100, and size 1499.\n", - "Transpiled circuit index 0 (SU2_10) in 0.17 seconds with method ai, depth 10, and size 168.\n", - "Transpiled circuit index 1 (SU2_20) in 0.29 seconds with method ai, depth 20, and size 299.\n", - "Transpiled circuit index 2 (SU2_30) in 13.56 seconds with method ai, depth 36, and size 548.\n", - "Transpiled circuit index 3 (SU2_40) in 15.95 seconds with method ai, depth 40, and size 599.\n", - "Transpiled circuit index 4 (SU2_50) in 80.70 seconds with method ai, depth 54, and size 823.\n", - "Transpiled circuit index 5 (SU2_60) in 75.99 seconds with method ai, depth 60, and size 899.\n", - "Transpiled circuit index 6 (SU2_70) in 64.96 seconds with method ai, depth 74, and size 1087.\n", - "Transpiled circuit index 7 (SU2_80) in 68.25 seconds with method ai, depth 80, and size 1199.\n", - "Transpiled circuit index 8 (SU2_90) in 75.07 seconds with method ai, depth 90, and size 1404.\n", - "Transpiled circuit index 9 (SU2_100) in 63.97 seconds with method ai, depth 100, and size 1499.\n" - ] - } - ], - "source": [ - "results_su2 = pd.DataFrame(\n", - " columns=[\n", - " \"method\",\n", - " \"qc_name\",\n", - " \"qc_index\",\n", - " \"num_qubits\",\n", - " \"ops\",\n", - " \"depth\",\n", - " \"size\",\n", - " \"runtime\",\n", - " ]\n", - ")\n", - "\n", - "tqc_sabre = capture_transpilation_metrics(\n", - " results_su2, pm_sabre, qc_su2_list, \"sabre\"\n", - ")\n", - "tqc_ai = capture_transpilation_metrics(results_su2, pm_ai, qc_su2_list, \"ai\")" - ] - }, - { - "cell_type": "markdown", - "id": "a2d68a18-6656-4858-b439-7f9acbb516bb", - "metadata": {}, - "source": [ - "Display transpiled results of one of the circuit." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "37924fc2-8fb6-451a-b8f9-cd79573f2384", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Sabre transpilation\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "AI transpilation\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "print(\"Sabre transpilation\")\n", - "display(tqc_sabre[0].draw(\"mpl\", fold=-1, idle_wires=False))\n", - "print(\"AI transpilation\")\n", - "display(tqc_ai[0].draw(\"mpl\", fold=-1, idle_wires=False))" - ] - }, - { - "cell_type": "markdown", - "id": "1633a4f4-7b01-4eb5-9764-0959f209a077", - "metadata": {}, - "source": [ - "Results table:" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "a5911224-3d1d-490d-b730-9cb90f954498", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " depth size runtime\n", - "method \n", - "ai 56.4 852.5 45.89\n", - "sabre 64.6 864.9 52.57\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
methodqc_nameqc_indexnum_qubitsopsdepthsizeruntime
0sabreSU2_10010{'rz': 81, 'sx': 70, 'cz': 16}131670.058845
1sabreSU2_20120{'rz': 160, 'sx': 119, 'cz': 20}202990.238217
2sabreSU2_30230{'sx': 295, 'rz': 242, 'cz': 90}7262710.723922
3sabreSU2_40340{'rz': 320, 'sx': 239, 'cz': 40}4059916.159262
4sabreSU2_50450{'rz': 402, 'sx': 367, 'cz': 86}7785576.886604
5sabreSU2_60560{'rz': 480, 'sx': 359, 'cz': 60}6089986.118255
6sabreSU2_70670{'rz': 562, 'sx': 441, 'cz': 82}79108594.458287
7sabreSU2_80780{'rz': 640, 'sx': 479, 'cz': 80}80119969.048184
8sabreSU2_90890{'rz': 721, 'sx': 585, 'cz': 114}105142088.254809
9sabreSU2_1009100{'rz': 800, 'sx': 599, 'cz': 100}100149983.795482
10aiSU2_10010{'rz': 81, 'sx': 71, 'cz': 16}101680.171532
11aiSU2_20120{'rz': 160, 'sx': 119, 'cz': 20}202990.291691
12aiSU2_30230{'sx': 243, 'rz': 242, 'cz': 63}3654813.555931
13aiSU2_40340{'rz': 320, 'sx': 239, 'cz': 40}4059915.952733
14aiSU2_50450{'rz': 403, 'sx': 346, 'cz': 74}5482380.702141
15aiSU2_60560{'rz': 480, 'sx': 359, 'cz': 60}6089975.993404
16aiSU2_70670{'rz': 563, 'sx': 442, 'cz': 82}74108764.960162
17aiSU2_80780{'rz': 640, 'sx': 479, 'cz': 80}80119968.253280
18aiSU2_90890{'rz': 721, 'sx': 575, 'cz': 108}90140475.072412
19aiSU2_1009100{'rz': 800, 'sx': 599, 'cz': 100}100149963.967446
\n", - "
" - ], - "text/plain": [ - " method qc_name qc_index num_qubits ops \\\n", - "0 sabre SU2_10 0 10 {'rz': 81, 'sx': 70, 'cz': 16} \n", - "1 sabre SU2_20 1 20 {'rz': 160, 'sx': 119, 'cz': 20} \n", - "2 sabre SU2_30 2 30 {'sx': 295, 'rz': 242, 'cz': 90} \n", - "3 sabre SU2_40 3 40 {'rz': 320, 'sx': 239, 'cz': 40} \n", - "4 sabre SU2_50 4 50 {'rz': 402, 'sx': 367, 'cz': 86} \n", - "5 sabre SU2_60 5 60 {'rz': 480, 'sx': 359, 'cz': 60} \n", - "6 sabre SU2_70 6 70 {'rz': 562, 'sx': 441, 'cz': 82} \n", - "7 sabre SU2_80 7 80 {'rz': 640, 'sx': 479, 'cz': 80} \n", - "8 sabre SU2_90 8 90 {'rz': 721, 'sx': 585, 'cz': 114} \n", - "9 sabre SU2_100 9 100 {'rz': 800, 'sx': 599, 'cz': 100} \n", - "10 ai SU2_10 0 10 {'rz': 81, 'sx': 71, 'cz': 16} \n", - "11 ai SU2_20 1 20 {'rz': 160, 'sx': 119, 'cz': 20} \n", - "12 ai SU2_30 2 30 {'sx': 243, 'rz': 242, 'cz': 63} \n", - "13 ai SU2_40 3 40 {'rz': 320, 'sx': 239, 'cz': 40} \n", - "14 ai SU2_50 4 50 {'rz': 403, 'sx': 346, 'cz': 74} \n", - "15 ai SU2_60 5 60 {'rz': 480, 'sx': 359, 'cz': 60} \n", - "16 ai SU2_70 6 70 {'rz': 563, 'sx': 442, 'cz': 82} \n", - "17 ai SU2_80 7 80 {'rz': 640, 'sx': 479, 'cz': 80} \n", - "18 ai SU2_90 8 90 {'rz': 721, 'sx': 575, 'cz': 108} \n", - "19 ai SU2_100 9 100 {'rz': 800, 'sx': 599, 'cz': 100} \n", - "\n", - " depth size runtime \n", - "0 13 167 0.058845 \n", - "1 20 299 0.238217 \n", - "2 72 627 10.723922 \n", - "3 40 599 16.159262 \n", - "4 77 855 76.886604 \n", - "5 60 899 86.118255 \n", - "6 79 1085 94.458287 \n", - "7 80 1199 69.048184 \n", - "8 105 1420 88.254809 \n", - "9 100 1499 83.795482 \n", - "10 10 168 0.171532 \n", - "11 20 299 0.291691 \n", - "12 36 548 13.555931 \n", - "13 40 599 15.952733 \n", - "14 54 823 80.702141 \n", - "15 60 899 75.993404 \n", - "16 74 1087 64.960162 \n", - "17 80 1199 68.253280 \n", - "18 90 1404 75.072412 \n", - "19 100 1499 63.967446 " - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "summary_su2 = (\n", - " results_su2.groupby(\"method\")[[\"depth\", \"size\", \"runtime\"]]\n", - " .mean()\n", - " .round(2)\n", - ")\n", - "print(summary_su2)\n", - "\n", - "results_su2" - ] - }, - { - "cell_type": "markdown", - "id": "167834f1-00a7-4190-99e4-d221d1952357", - "metadata": {}, - "source": [ - "#### Results graph\n", - "\n", - "As we define a function to consistently capture metrics, we will also define one to graph the metrics. Here, we will plot the two-qubit depth, gate count, and runtime for each compilation method across the circuits." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "d90fb4fa-e031-40f6-90e6-e1208a855bec", - "metadata": {}, - "outputs": [], - "source": [ - "def plot_transpilation_metrics(results, overall_title, x_axis=\"qc_index\"):\n", - " \"\"\"\n", - " Plots transpilation metrics (depth, size, runtime) for different transpilation methods.\n", - "\n", - " Parameters:\n", - " results (DataFrame): Data containing columns ['num_qubits', 'method', 'depth', 'size', 'runtime']\n", - " overall_title (str): The title of the overall figure.\n", - " x_axis (str): The x-axis label, either 'num_qubits' or 'qc_index'.\n", - " \"\"\"\n", - "\n", - " fig, axs = plt.subplots(1, 3, figsize=(24, 6))\n", - " metrics = [\"depth\", \"size\", \"runtime\"]\n", - " titles = [\"Circuit Depth\", \"Circuit Size\", \"Transpilation Runtime\"]\n", - " y_labels = [\"Depth\", \"Size (Gate Count)\", \"Runtime (s)\"]\n", - "\n", - " methods = results[\"method\"].unique()\n", - " colors = plt.colormaps[\"tab10\"]\n", - " markers = [\"o\", \"^\", \"s\", \"D\", \"P\", \"*\", \"X\", \"v\"]\n", - " color_list = [colors(i % colors.N) for i in range(len(methods))]\n", - " color_map = {method: color_list[i] for i, method in enumerate(methods)}\n", - " marker_map = {\n", - " method: markers[i % len(markers)] for i, method in enumerate(methods)\n", - " }\n", - " jitter_factor = 0.1 # Small x-axis jitter for visibility\n", - " handles, labels = [], [] # Unique handles for legend\n", - "\n", - " # Plot each metric\n", - " for i, metric in enumerate(metrics):\n", - " for method in methods:\n", - " method_data = results[results[\"method\"] == method]\n", - "\n", - " # Introduce slight jitter to avoid exact overlap\n", - " jitter = np.random.uniform(\n", - " -jitter_factor, jitter_factor, len(method_data)\n", - " )\n", - "\n", - " scatter = axs[i].scatter(\n", - " method_data[x_axis] + jitter,\n", - " method_data[metric],\n", - " color=color_map[method],\n", - " label=method,\n", - " marker=marker_map[method],\n", - " alpha=0.7,\n", - " edgecolors=\"black\",\n", - " s=80,\n", - " )\n", - "\n", - " if method not in labels:\n", - " handles.append(scatter)\n", - " labels.append(method)\n", - "\n", - " axs[i].set_title(titles[i])\n", - " axs[i].set_xlabel(x_axis)\n", - " axs[i].set_ylabel(y_labels[i])\n", - " axs[i].grid(axis=\"y\", linestyle=\"--\", alpha=0.7)\n", - " axs[i].tick_params(axis=\"x\", rotation=45)\n", - " axs[i].set_xticks(sorted(results[x_axis].unique()))\n", - "\n", - " fig.suptitle(overall_title, fontsize=16)\n", - " fig.legend(\n", - " handles=handles,\n", - " labels=labels,\n", - " loc=\"upper right\",\n", - " bbox_to_anchor=(1.05, 1),\n", - " )\n", - "\n", - " plt.tight_layout()\n", - " plt.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "7f7b502a-8ed6-45fa-a698-02977149e283", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plot_transpilation_metrics(\n", - " results_su2, \"Transpilation Metrics for SU2 Circuits\", x_axis=\"num_qubits\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "f75e8e3a-803f-4386-a60a-b088faa3c81a", - "metadata": {}, - "source": [ - "#### Analysis of SU2 circuit compilation results\n", - "\n", - "In this experiment, we compare two transpilation methods — Qiskit's SABRE transpiler and the AI-powered transpiler — on a set of `efficient_su2` circuits. Since these circuits do not include any `PauliEvolutionGate` operations, the Rustiq plugin is not included in this comparison.\n", - "\n", - "On average, the AI transpiler performs better in terms of circuit depth, with a greater than 10% improvement across the full range of SU2 circuits. For gate count (circuit size) and transpilation runtime, both methods yield similar results overall.\n", - "\n", - "However, inspecting the individual data points reveals a deeper insight:\n", - "- For most qubit sizes, both SABRE and AI produce nearly identical results, suggesting that in many cases, both methods converge to similarly efficient solutions.\n", - "- For certain circuit sizes, specifically at 30, 50, 70, and 90 qubits, the AI transpiler finds significantly shallower circuits than SABRE. This indicates that AI's learning-based approach is able to discover more optimal layouts or routing paths in cases where the SABRE heuristic does not.\n", - "\n", - "This behavior highlights an important takeaway:\n", - "> While SABRE and AI often produce comparable results, the AI transpiler can occasionally discover much better solutions, particularly in terms of depth, which can lead to significantly improved performance on hardware." - ] - }, - { - "cell_type": "markdown", - "id": "4c3b2aa8-8187-488a-8e5b-197cf26085bb", - "metadata": {}, - "source": [ - "## Part 2: Hamiltonian simulation circuit\n", - "\n", - "### Step 1: Investigate circuits with `PauliEvolutionGate`\n", - "\n", - "In this section, we investigate quantum circuits constructed using `PauliEvolutionGate`, which enables efficient simulation of Hamiltonians. We will analyze how different compilation methods optimize these circuits across various Hamiltonians.\n", - "\n", - "#### Hamiltonians used in the benchmark\n", - "\n", - "The Hamiltonians used in this benchmark describe pairwise interactions between qubits, including terms such as $ZZ$, $XX$, and $YY$. These Hamiltonians are commonly used in quantum chemistry, condensed matter physics, and materials science, where they model systems of interacting particles.\n", - "\n", - "For reference, users can explore a broader set of Hamiltonians in this paper: [Efficient Hamiltonian Simulation on Noisy Quantum Devices](https://arxiv.org/pdf/2306.13126).\n", - "\n", - "#### Benchmark source: Hamlib and Benchpress\n", - "\n", - "The circuits used in this benchmark are drawn from the [Hamlib benchmark repository](https://github.com/SRI-International/QC-App-Oriented-Benchmarks/tree/master/hamlib), which contains realistic Hamiltonian simulation workloads.\n", - "\n", - "These same circuits were previously benchmarked using [Benchpress](https://github.com/Qiskit/benchpress), an open-source framework for evaluating quantum transpilation performance. By using this standardized set of circuits, we can directly compare the effectiveness of different compilation strategies on representative simulation problems.\n", - "\n", - "Hamiltonian simulation is a foundational task in quantum computing, with applications in molecular simulations, optimization problems, and quantum many-body physics. Understanding how different compilation methods optimize these circuits can help users improve practical execution of such circuits on near-term quantum devices." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "66347c00-1607-4405-bb76-610690adf6b8", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Number of Hamiltonian circuits: 35\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Obtain the Hamiltonian JSON from the benchpress repository\n", - "url = \"https://raw.githubusercontent.com/Qiskit/benchpress/e7b29ef7be4cc0d70237b8fdc03edbd698908eff/benchpress/hamiltonian/hamlib/100_representative.json\"\n", - "response = requests.get(url)\n", - "response.raise_for_status() # Raise an error if download failed\n", - "ham_records = json.loads(response.text)\n", - "# Remove circuits that are too large for the backend\n", - "ham_records = [\n", - " h for h in ham_records if h[\"ham_qubits\"] <= backend.num_qubits\n", - "]\n", - "# Remove the circuits that are large to save transpilation time\n", - "ham_records = sorted(ham_records, key=lambda x: x[\"ham_terms\"])[:35]\n", - "\n", - "qc_ham_list = []\n", - "for h in ham_records:\n", - " terms = h[\"ham_hamlib_hamiltonian_terms\"]\n", - " coeff = h[\"ham_hamlib_hamiltonian_coefficients\"]\n", - " num_qubits = h[\"ham_qubits\"]\n", - " name = h[\"ham_problem\"]\n", - "\n", - " evo_gate = PauliEvolutionGate(SparsePauliOp(terms, coeff))\n", - "\n", - " qc_ham = QuantumCircuit(num_qubits)\n", - " qc_ham.name = name\n", - "\n", - " qc_ham.append(evo_gate, range(num_qubits))\n", - " qc_ham_list.append(qc_ham)\n", - "print(f\"Number of Hamiltonian circuits: {len(qc_ham_list)}\")\n", - "\n", - "# Draw the first Hamiltonian circuit\n", - "qc_ham_list[0].draw(\"mpl\", fold=-1)" - ] - }, - { - "cell_type": "markdown", - "id": "9690d94d-7d38-45d3-b37d-85eaf268dbd6", - "metadata": {}, - "source": [ - "### Step 2: Optimize problem for quantum hardware execution\n", - "\n", - "As in the previous example, we will use the same backend to ensure consistency in our comparisons. Since the pass managers (`pm_sabre`, `pm_ai`, and `pm_rustiq`) have already been initialized, we can directly proceed with transpiling the Hamiltonian circuits using each method.\n", - "\n", - "This step focuses solely on performing the transpilation and recording the resulting circuit metrics, including depth, gate count, and transpilation runtime. By analyzing these results, we aim to determine the efficiency of each transpilation method for this type of circuit." - ] - }, - { - "cell_type": "markdown", - "id": "22fb1fe7-333f-421f-a17d-bc32190a0f86", - "metadata": {}, - "source": [ - "Transpile and capture metrics:" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "4138c7d6-5ec8-4c8f-aef4-08ec1b13633b", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Transpiled circuit index 0 (all-vib-o3) in 0.02 seconds with method sabre, depth 6, and size 58.\n", - "Transpiled circuit index 1 (all-vib-c2h) in 1.10 seconds with method sabre, depth 2, and size 39.\n", - "Transpiled circuit index 2 (all-vib-bh) in 0.01 seconds with method sabre, depth 3, and size 30.\n", - "Transpiled circuit index 3 (all-vib-c2h) in 0.03 seconds with method sabre, depth 18, and size 115.\n", - "Transpiled circuit index 4 (graph-gnp_k-2) in 0.02 seconds with method sabre, depth 24, and size 129.\n", - "Transpiled circuit index 5 (all-vib-fccf) in 0.05 seconds with method sabre, depth 14, and size 134.\n", - "Transpiled circuit index 6 (all-vib-hno) in 8.39 seconds with method sabre, depth 6, and size 174.\n", - "Transpiled circuit index 7 (all-vib-bhf2) in 3.92 seconds with method sabre, depth 22, and size 220.\n", - "Transpiled circuit index 8 (LiH) in 0.03 seconds with method sabre, depth 67, and size 290.\n", - "Transpiled circuit index 9 (uf20-ham) in 0.04 seconds with method sabre, depth 50, and size 340.\n", - "Transpiled circuit index 10 (all-vib-fccf) in 0.62 seconds with method sabre, depth 30, and size 286.\n", - "Transpiled circuit index 11 (all-vib-fccf) in 0.04 seconds with method sabre, depth 67, and size 339.\n", - "Transpiled circuit index 12 (all-vib-ch2) in 0.04 seconds with method sabre, depth 87, and size 421.\n", - "Transpiled circuit index 13 (tfim) in 0.05 seconds with method sabre, depth 36, and size 222.\n", - "Transpiled circuit index 14 (all-vib-cyclo_propene) in 9.51 seconds with method sabre, depth 22, and size 345.\n", - "Transpiled circuit index 15 (graph-gnp_k-4) in 0.05 seconds with method sabre, depth 128, and size 704.\n", - "Transpiled circuit index 16 (all-vib-hc3h2cn) in 13.83 seconds with method sabre, depth 2, and size 242.\n", - "Transpiled circuit index 17 (TSP_Ncity-4) in 0.05 seconds with method sabre, depth 106, and size 609.\n", - "Transpiled circuit index 18 (tfim) in 0.29 seconds with method sabre, depth 73, and size 399.\n", - "Transpiled circuit index 19 (all-vib-h2co) in 21.97 seconds with method sabre, depth 30, and size 572.\n", - "Transpiled circuit index 20 (Be2) in 0.09 seconds with method sabre, depth 324, and size 1555.\n", - "Transpiled circuit index 21 (graph-complete_bipart) in 0.12 seconds with method sabre, depth 250, and size 1394.\n", - "Transpiled circuit index 22 (all-vib-f2) in 0.07 seconds with method sabre, depth 215, and size 1027.\n", - "Transpiled circuit index 23 (all-vib-cyclo_propene) in 41.22 seconds with method sabre, depth 30, and size 1144.\n", - "Transpiled circuit index 24 (TSP_Ncity-5) in 1.89 seconds with method sabre, depth 175, and size 1933.\n", - "Transpiled circuit index 25 (H2) in 0.32 seconds with method sabre, depth 1237, and size 5502.\n", - "Transpiled circuit index 26 (uuf100-ham) in 0.20 seconds with method sabre, depth 385, and size 4303.\n", - "Transpiled circuit index 27 (ham-graph-gnp_k-5) in 0.20 seconds with method sabre, depth 311, and size 3654.\n", - "Transpiled circuit index 28 (tfim) in 0.15 seconds with method sabre, depth 276, and size 3213.\n", - "Transpiled circuit index 29 (uuf100-ham) in 0.21 seconds with method sabre, depth 520, and size 5250.\n", - "Transpiled circuit index 30 (flat100-ham) in 0.15 seconds with method sabre, depth 131, and size 3157.\n", - "Transpiled circuit index 31 (uf100-ham) in 0.24 seconds with method sabre, depth 624, and size 7378.\n", - "Transpiled circuit index 32 (OH) in 0.88 seconds with method sabre, depth 2175, and size 9808.\n", - "Transpiled circuit index 33 (HF) in 0.66 seconds with method sabre, depth 2206, and size 9417.\n", - "Transpiled circuit index 34 (BH) in 0.89 seconds with method sabre, depth 2177, and size 9802.\n", - "Transpiled circuit index 0 (all-vib-o3) in 0.02 seconds with method ai, depth 6, and size 58.\n", - "Transpiled circuit index 1 (all-vib-c2h) in 1.11 seconds with method ai, depth 2, and size 39.\n", - "Transpiled circuit index 2 (all-vib-bh) in 0.01 seconds with method ai, depth 3, and size 30.\n", - "Transpiled circuit index 3 (all-vib-c2h) in 0.11 seconds with method ai, depth 18, and size 94.\n", - "Transpiled circuit index 4 (graph-gnp_k-2) in 0.11 seconds with method ai, depth 22, and size 129.\n", - "Transpiled circuit index 5 (all-vib-fccf) in 0.06 seconds with method ai, depth 22, and size 177.\n", - "Transpiled circuit index 6 (all-vib-hno) in 8.62 seconds with method ai, depth 10, and size 198.\n", - "Transpiled circuit index 7 (all-vib-bhf2) in 3.71 seconds with method ai, depth 18, and size 195.\n", - "Transpiled circuit index 8 (LiH) in 0.19 seconds with method ai, depth 62, and size 267.\n", - "Transpiled circuit index 9 (uf20-ham) in 0.22 seconds with method ai, depth 47, and size 321.\n", - "Transpiled circuit index 10 (all-vib-fccf) in 0.71 seconds with method ai, depth 38, and size 369.\n", - "Transpiled circuit index 11 (all-vib-fccf) in 0.24 seconds with method ai, depth 65, and size 315.\n", - "Transpiled circuit index 12 (all-vib-ch2) in 0.24 seconds with method ai, depth 91, and size 430.\n", - "Transpiled circuit index 13 (tfim) in 0.15 seconds with method ai, depth 12, and size 251.\n", - "Transpiled circuit index 14 (all-vib-cyclo_propene) in 8.50 seconds with method ai, depth 18, and size 311.\n", - "Transpiled circuit index 15 (graph-gnp_k-4) in 0.25 seconds with method ai, depth 117, and size 659.\n", - "Transpiled circuit index 16 (all-vib-hc3h2cn) in 16.11 seconds with method ai, depth 2, and size 242.\n", - "Transpiled circuit index 17 (TSP_Ncity-4) in 0.39 seconds with method ai, depth 98, and size 564.\n", - "Transpiled circuit index 18 (tfim) in 0.38 seconds with method ai, depth 23, and size 437.\n", - "Transpiled circuit index 19 (all-vib-h2co) in 24.97 seconds with method ai, depth 38, and size 707.\n", - "Transpiled circuit index 20 (Be2) in 1.07 seconds with method ai, depth 293, and size 1392.\n", - "Transpiled circuit index 21 (graph-complete_bipart) in 0.61 seconds with method ai, depth 229, and size 1437.\n", - "Transpiled circuit index 22 (all-vib-f2) in 0.57 seconds with method ai, depth 178, and size 964.\n", - "Transpiled circuit index 23 (all-vib-cyclo_propene) in 50.89 seconds with method ai, depth 34, and size 1425.\n", - "Transpiled circuit index 24 (TSP_Ncity-5) in 1.61 seconds with method ai, depth 171, and size 2020.\n", - "Transpiled circuit index 25 (H2) in 6.39 seconds with method ai, depth 1148, and size 5208.\n", - "Transpiled circuit index 26 (uuf100-ham) in 3.97 seconds with method ai, depth 376, and size 5048.\n", - "Transpiled circuit index 27 (ham-graph-gnp_k-5) in 3.54 seconds with method ai, depth 357, and size 4451.\n", - "Transpiled circuit index 28 (tfim) in 1.72 seconds with method ai, depth 216, and size 3026.\n", - "Transpiled circuit index 29 (uuf100-ham) in 4.45 seconds with method ai, depth 426, and size 5399.\n", - "Transpiled circuit index 30 (flat100-ham) in 7.02 seconds with method ai, depth 86, and size 3108.\n", - "Transpiled circuit index 31 (uf100-ham) in 12.85 seconds with method ai, depth 623, and size 8354.\n", - "Transpiled circuit index 32 (OH) in 15.19 seconds with method ai, depth 2084, and size 9543.\n", - "Transpiled circuit index 33 (HF) in 17.51 seconds with method ai, depth 2063, and size 9446.\n", - "Transpiled circuit index 34 (BH) in 15.33 seconds with method ai, depth 2094, and size 9730.\n", - "Transpiled circuit index 0 (all-vib-o3) in 0.02 seconds with method rustiq, depth 13, and size 83.\n", - "Transpiled circuit index 1 (all-vib-c2h) in 1.11 seconds with method rustiq, depth 2, and size 39.\n", - "Transpiled circuit index 2 (all-vib-bh) in 0.01 seconds with method rustiq, depth 3, and size 30.\n", - "Transpiled circuit index 3 (all-vib-c2h) in 0.01 seconds with method rustiq, depth 13, and size 79.\n", - "Transpiled circuit index 4 (graph-gnp_k-2) in 0.02 seconds with method rustiq, depth 31, and size 131.\n", - "Transpiled circuit index 5 (all-vib-fccf) in 0.04 seconds with method rustiq, depth 50, and size 306.\n", - "Transpiled circuit index 6 (all-vib-hno) in 14.03 seconds with method rustiq, depth 22, and size 276.\n", - "Transpiled circuit index 7 (all-vib-bhf2) in 3.15 seconds with method rustiq, depth 13, and size 155.\n", - "Transpiled circuit index 8 (LiH) in 0.03 seconds with method rustiq, depth 54, and size 270.\n", - "Transpiled circuit index 9 (uf20-ham) in 0.04 seconds with method rustiq, depth 65, and size 398.\n", - "Transpiled circuit index 10 (all-vib-fccf) in 0.16 seconds with method rustiq, depth 41, and size 516.\n", - "Transpiled circuit index 11 (all-vib-fccf) in 0.02 seconds with method rustiq, depth 34, and size 189.\n", - "Transpiled circuit index 12 (all-vib-ch2) in 0.03 seconds with method rustiq, depth 49, and size 240.\n", - "Transpiled circuit index 13 (tfim) in 0.05 seconds with method rustiq, depth 20, and size 366.\n", - "Transpiled circuit index 14 (all-vib-cyclo_propene) in 9.08 seconds with method rustiq, depth 16, and size 277.\n", - "Transpiled circuit index 15 (graph-gnp_k-4) in 0.04 seconds with method rustiq, depth 116, and size 612.\n", - "Transpiled circuit index 16 (all-vib-hc3h2cn) in 13.89 seconds with method rustiq, depth 2, and size 257.\n", - "Transpiled circuit index 17 (TSP_Ncity-4) in 0.05 seconds with method rustiq, depth 133, and size 737.\n", - "Transpiled circuit index 18 (tfim) in 0.11 seconds with method rustiq, depth 25, and size 680.\n", - "Transpiled circuit index 19 (all-vib-h2co) in 27.19 seconds with method rustiq, depth 66, and size 983.\n", - "Transpiled circuit index 20 (Be2) in 0.07 seconds with method rustiq, depth 215, and size 1030.\n", - "Transpiled circuit index 21 (graph-complete_bipart) in 0.14 seconds with method rustiq, depth 328, and size 1918.\n", - "Transpiled circuit index 22 (all-vib-f2) in 0.05 seconds with method rustiq, depth 114, and size 692.\n", - "Transpiled circuit index 23 (all-vib-cyclo_propene) in 62.25 seconds with method rustiq, depth 74, and size 2348.\n", - "Transpiled circuit index 24 (TSP_Ncity-5) in 0.20 seconds with method rustiq, depth 436, and size 3605.\n", - "Transpiled circuit index 25 (H2) in 0.21 seconds with method rustiq, depth 643, and size 3476.\n", - "Transpiled circuit index 26 (uuf100-ham) in 0.24 seconds with method rustiq, depth 678, and size 6120.\n", - "Transpiled circuit index 27 (ham-graph-gnp_k-5) in 0.22 seconds with method rustiq, depth 588, and size 5241.\n", - "Transpiled circuit index 28 (tfim) in 0.34 seconds with method rustiq, depth 340, and size 5901.\n", - "Transpiled circuit index 29 (uuf100-ham) in 0.33 seconds with method rustiq, depth 881, and size 7667.\n", - "Transpiled circuit index 30 (flat100-ham) in 0.31 seconds with method rustiq, depth 279, and size 4910.\n", - "Transpiled circuit index 31 (uf100-ham) in 0.38 seconds with method rustiq, depth 1138, and size 10607.\n", - "Transpiled circuit index 32 (OH) in 0.38 seconds with method rustiq, depth 1148, and size 6512.\n", - "Transpiled circuit index 33 (HF) in 0.37 seconds with method rustiq, depth 1090, and size 6256.\n", - "Transpiled circuit index 34 (BH) in 0.37 seconds with method rustiq, depth 1148, and size 6501.\n" - ] - } - ], - "source": [ - "results_ham = pd.DataFrame(\n", - " columns=[\n", - " \"method\",\n", - " \"qc_name\",\n", - " \"qc_index\",\n", - " \"num_qubits\",\n", - " \"ops\",\n", - " \"depth\",\n", - " \"size\",\n", - " \"runtime\",\n", - " ]\n", - ")\n", - "\n", - "tqc_sabre = capture_transpilation_metrics(\n", - " results_ham, pm_sabre, qc_ham_list, \"sabre\"\n", - ")\n", - "tqc_ai = capture_transpilation_metrics(results_ham, pm_ai, qc_ham_list, \"ai\")\n", - "tqc_rustiq = capture_transpilation_metrics(\n", - " results_ham, pm_rustiq, qc_ham_list, \"rustiq\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "ef88a127-79b8-4a53-90ef-6caa84c29e06", - "metadata": {}, - "source": [ - "Results table (skipping visualization as the output circuits are very large):" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "bc4a7910-5e8a-48ec-bd7e-2508ae8dfccc", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " depth size runtime\n", - "method \n", - "ai 316.86 2181.26 5.97\n", - "rustiq 281.94 2268.80 3.86\n", - "sabre 337.97 2120.14 3.07\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
methodqc_nameqc_indexnum_qubitsopsdepthsizeruntime
0sabreall-vib-o304{'rz': 28, 'sx': 24, 'cz': 6}6580.016597
1sabreall-vib-c2h14{'rz': 17, 'sx': 16, 'cz': 4, 'x': 2}2391.102089
2sabreall-vib-bh22{'sx': 14, 'rz': 13, 'cz': 3}3300.011042
3sabreall-vib-c2h33{'sx': 46, 'rz': 45, 'cz': 18, 'x': 6}181150.025816
4sabregraph-gnp_k-244{'sx': 49, 'rz': 47, 'cz': 24, 'x': 9}241290.023077
...........................
100rustiqflat100-ham3090{'sx': 2709, 'cz': 1379, 'rz': 817, 'x': 5}27949100.309448
101rustiquf100-ham3146{'sx': 6180, 'cz': 3120, 'rz': 1303, 'x': 4}1138106070.380977
102rustiqOH3210{'sx': 3330, 'cz': 1704, 'rz': 1455, 'x': 23}114865120.383564
103rustiqHF3310{'sx': 3213, 'cz': 1620, 'rz': 1406, 'x': 17}109062560.368578
104rustiqBH3410{'sx': 3331, 'cz': 1704, 'rz': 1447, 'x': 19}114865010.374822
\n", - "

105 rows × 8 columns

\n", - "
" - ], - "text/plain": [ - " method qc_name qc_index num_qubits \\\n", - "0 sabre all-vib-o3 0 4 \n", - "1 sabre all-vib-c2h 1 4 \n", - "2 sabre all-vib-bh 2 2 \n", - "3 sabre all-vib-c2h 3 3 \n", - "4 sabre graph-gnp_k-2 4 4 \n", - ".. ... ... ... ... \n", - "100 rustiq flat100-ham 30 90 \n", - "101 rustiq uf100-ham 31 46 \n", - "102 rustiq OH 32 10 \n", - "103 rustiq HF 33 10 \n", - "104 rustiq BH 34 10 \n", - "\n", - " ops depth size runtime \n", - "0 {'rz': 28, 'sx': 24, 'cz': 6} 6 58 0.016597 \n", - "1 {'rz': 17, 'sx': 16, 'cz': 4, 'x': 2} 2 39 1.102089 \n", - "2 {'sx': 14, 'rz': 13, 'cz': 3} 3 30 0.011042 \n", - "3 {'sx': 46, 'rz': 45, 'cz': 18, 'x': 6} 18 115 0.025816 \n", - "4 {'sx': 49, 'rz': 47, 'cz': 24, 'x': 9} 24 129 0.023077 \n", - ".. ... ... ... ... \n", - "100 {'sx': 2709, 'cz': 1379, 'rz': 817, 'x': 5} 279 4910 0.309448 \n", - "101 {'sx': 6180, 'cz': 3120, 'rz': 1303, 'x': 4} 1138 10607 0.380977 \n", - "102 {'sx': 3330, 'cz': 1704, 'rz': 1455, 'x': 23} 1148 6512 0.383564 \n", - "103 {'sx': 3213, 'cz': 1620, 'rz': 1406, 'x': 17} 1090 6256 0.368578 \n", - "104 {'sx': 3331, 'cz': 1704, 'rz': 1447, 'x': 19} 1148 6501 0.374822 \n", - "\n", - "[105 rows x 8 columns]" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "summary_ham = (\n", - " results_ham.groupby(\"method\")[[\"depth\", \"size\", \"runtime\"]]\n", - " .mean()\n", - " .round(2)\n", - ")\n", - "print(summary_ham)\n", - "\n", - "results_ham" - ] - }, - { - "cell_type": "markdown", - "id": "17e7b228-01b0-4fcc-b659-9f9958f5477e", - "metadata": {}, - "source": [ - "Visualize performance based on circuit index:" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "4c6b810d-a4cf-4de3-aae6-c6c1cdd83d8f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plot_transpilation_metrics(\n", - " results_ham, \"Transpilation Metrics for Hamiltonian Circuits\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "7ad6f4ee-c9d1-4e8b-ae25-cf39672acbec", - "metadata": {}, - "source": [ - "Visualize the percentage of circuits for which each method performed best." - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "01b4644e-ac91-483c-944b-924f8b41718d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Best-performing methods based on depth:\n", - " ai: 16 circuit(s)\n", - " rustiq: 16 circuit(s)\n", - " sabre: 10 circuit(s)\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Best-performing methods based on size:\n", - " sabre: 18 circuit(s)\n", - " rustiq: 14 circuit(s)\n", - " ai: 10 circuit(s)\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "def analyze_and_plot_best_methods(results, metric):\n", - " \"\"\"\n", - " Analyze the best-performing methods for a given metric and plot a pie chart.\n", - "\n", - " Parameters:\n", - " results (DataFrame): The input DataFrame containing method performance data.\n", - " metric (str): The metric to evaluate (\"depth\" or \"size\").\n", - " \"\"\"\n", - " method_counts = Counter()\n", - " for qc_idx, group in results.groupby(\"qc_index\"):\n", - " min_value = group[metric].min()\n", - "\n", - " # Find all methods that achieved this minimum value\n", - " best_methods = group[group[metric] == min_value][\"method\"]\n", - " # Update counts for all best methods (handling ties)\n", - " method_counts.update(best_methods)\n", - " best_method_counts = dict(\n", - " sorted(method_counts.items(), key=lambda x: x[1], reverse=True)\n", - " )\n", - "\n", - " # Print summary\n", - " print(f\"Best-performing methods based on {metric}:\")\n", - " for method, count in best_method_counts.items():\n", - " print(f\" {method}: {count} circuit(s)\")\n", - "\n", - " # Plot pie chart\n", - " num_methods = len(best_method_counts)\n", - " colors = plt.cm.viridis_r(range(0, 256, 256 // num_methods))\n", - " plt.figure(figsize=(5, 5))\n", - " plt.pie(\n", - " best_method_counts.values(),\n", - " labels=best_method_counts.keys(),\n", - " autopct=\"%1.1f%%\",\n", - " startangle=140,\n", - " wedgeprops={\"edgecolor\": \"black\"},\n", - " textprops={\"fontsize\": 10},\n", - " colors=colors,\n", - " )\n", - " plt.title(\n", - " f\"Percentage of Circuits Method Performed Best for {metric.capitalize()}\",\n", - " fontsize=12,\n", - " fontweight=\"bold\",\n", - " )\n", - " plt.show()\n", - "\n", - "\n", - "analyze_and_plot_best_methods(results_ham, \"depth\")\n", - "analyze_and_plot_best_methods(results_ham, \"size\")" - ] - }, - { - "cell_type": "markdown", - "id": "e4f6aa23-d528-4d66-92e0-31d29c522792", - "metadata": {}, - "source": [ - "#### Analysis of Hamiltonian circuit compilation results\n", - "\n", - "In this section, we evaluate the performance of three transpilation methods — SABRE, the AI-powered transpiler, and Rustiq — on quantum circuits constructed with `PauliEvolutionGate`, which are commonly used in Hamiltonian simulation tasks.\n", - "\n", - "Rustiq performed best on average in terms of circuit depth**, achieving approximately 20% lower depth than SABRE. This is expected, as Rustiq is specifically designed to synthesize `PauliEvolutionGate` operations with optimized, low-depth decomposition strategies. Furthermore, the depth plot shows that as the circuits scale in size and complexity, Rustiq scales most effectively, maintaining significantly lower depth than both AI and SABRE on larger circuits.\n", - "\n", - "AI transpiler showed strong and consistent performance for circuit depth, consistently outperforming SABRE across most circuits. However, it incurred the highest runtime, especially on larger circuits, which may limit its practicality in time-sensitive workloads. Its scalability in runtime remains a key limitation, even though it offers solid improvements in depth.\n", - "\n", - "SABRE, while producing the highest average depth, achieved the lowest average gate count, closely followed by the AI transpiler. This aligns with the design of SABRE’s heuristic, which prioritizes minimizing gate count directly. Rustiq, despite its strength in lowering depth, had the highest average gate count, which is a notable trade-off to consider in applications where circuit size matters more than circuit duration.\n", - "\n", - "### Summary\n", - "\n", - "While the AI transpiler generally delivers better results than SABRE, particularly in circuit depth, the takeaway should not simply be \"always use the AI transpiler.\" There are important nuances to consider:\n", - "\n", - "- **AI transpiler** is typically reliable and provides depth-optimized circuits, but it comes with trade-offs in runtime, and also has other limitations, including supported coupling maps and synthesis capabilities. These are detailed in the [Qiskit Transpiler Service documentation](/docs/guides/qiskit-transpiler-service).\n", - "\n", - "- In some cases, particularly with very large or hardware-specific circuits, the AI transpiler may not be as effective. In these cases, the default SABRE transpiler remains extremely reliable and can be further optimized by adjusting its parameters (see the [SABRE optimization tutorial](/docs/tutorials/transpilation-optimizations-with-sabre)).\n", - "\n", - "- It's also important to consider circuit structure when choosing a method. For example, `rustiq` is purpose-built for circuits involving `PauliEvolutionGate` and often yields the best performance for Hamiltonian simulation problems.\n", - "\n", - "**Recommendation:**\n", - "> There is no one-size-fits-all transpilation strategy. Users are encouraged to understand the structure of their circuit and test multiple transpilation methods — including AI, SABRE, and specialized tools like Rustiq — to find the most efficient solution for their specific problem and hardware constraints." - ] - }, - { - "cell_type": "markdown", - "id": "9a747477-1dc1-4706-b844-d29570fb5844", - "metadata": {}, - "source": [ - "### Step 3: Execute using Qiskit primitives" - ] - }, - { - "cell_type": "markdown", - "id": "d3d6d473-7508-4e28-82ad-eee09c9a1acf", - "metadata": {}, - "source": [ - "As this tutorial focuses on transpilation, no experiments are executed on a quantum device. The goal is to leverage the optimizations from Step 2 to obtain a transpiled circuit with reduced depth and gate count." - ] - }, - { - "cell_type": "markdown", - "id": "bb4c4c67-a7e8-4ca1-a5d6-908a2e1d27e5", - "metadata": {}, - "source": [ - "### Step 4: Post-process and return result in desired classical format" - ] - }, - { - "cell_type": "markdown", - "id": "4aa45929-f8d9-4be8-9008-0588615fa45e", - "metadata": {}, - "source": [ - "Since there is no execution for this notebook, there are no results to post-process." - ] - }, - { - "cell_type": "markdown", - "id": "552ef44b-e567-4870-9699-d152806b9505", - "metadata": {}, - "source": [ - "## References\n", - "\n", - "[1] \"LightSABRE: A Lightweight and Enhanced SABRE Algorithm\". H. Zou, M. Treinish, K. Hartman, A. Ivrii, J. Lishman et al. https://arxiv.org/abs/2409.08368\n", - "\n", - "[2] \"Practical and efficient quantum circuit synthesis and transpiling with Reinforcement Learning\". D. Kremer, V. Villar, H. Paik, I. Duran, I. Faro, J. Cruz-Benito et al. https://arxiv.org/abs/2405.13196\n", - "\n", - "[3] \"Pauli Network Circuit Synthesis with Reinforcement Learning\". A. Dubal, D. Kremer, S. Martiel, V. Villar, D. Wang, J. Cruz-Benito et al. https://arxiv.org/abs/2503.14448\n", - "\n", - "[4] \"Faster and shorter synthesis of Hamiltonian simulation circuits\". T. Goubault de Brugière, S. Martiel et al. https://arxiv.org/abs/2404.03280" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f056ca4a-fbe4-4051-bbcd-79c2d7848cd0", + "metadata": {}, + "source": [ + "---\n", + "title: Compilation methods for Hamiltonian simulation circuits\n", + "description: This tutorial provides a comparative overview of three compilation methods in Qiskit for Hamiltonian simulation workloads.\n", + "---\n", + "\n", + "\n", + "# Compilation methods for Hamiltonian simulation circuits\n", + "Estimated QPU usage: no execution was done in this tutorial because it is focused on the transpilation process.\n", + "\n", + "{/* cspell:ignore Rustiq, nshuffles, edgecolors, edgecolor, Hamlib, Benchpress, Brugière, Goubault, Martiel, Dubal, Lishman, Ivrii, fontweight, fontsize, textprops, wedgeprops, startangle, autopct */}" + ] + }, + { + "cell_type": "markdown", + "id": "d960a90b-1487-4310-a222-b95b8a77a080", + "metadata": {}, + "source": [ + "## Background\n", + "\n", + "Quantum circuit compilation is a crucial step in the quantum computing workflow. It involves transforming a high-level quantum algorithm into a physical quantum circuit that adheres to the constraints of the target quantum hardware. Effective compilation can significantly impact the performance of quantum algorithms by reducing circuit depth, gate count, and execution time. This tutorial explores three distinct approaches to quantum circuit compilation in Qiskit, showcasing their strengths and applications through practical examples.\n", + "\n", + "The goal of this tutorial is to teach users how to apply and evaluate three compilation methods in Qiskit: the SABRE transpiler, the AI-powered transpiler, and the Rustiq plugin. Users will learn how to use each method effectively and how to benchmark their performance across different quantum circuits. By the end of this tutorial, users will be able to choose and tailor compilation strategies based on specific optimization goals such as reducing circuit depth, minimizing gate count, or improving runtime.\n", + "\n", + "### What you will learn\n", + "- **How to use the Qiskit transpiler with SABRE for layout and routing optimization.**\n", + "- **How to leverage the AI transpiler for advanced, automated circuit optimization.**\n", + "- **How to employ the Rustiq plugin for circuits requiring precise synthesis of operations, particularly in Hamiltonian simulation tasks.**\n", + "\n", + "This tutorial uses three example circuits following the [Qiskit patterns](/docs/guides/intro-to-patterns) workflow to illustrate the performance of each compilation method. By the end of this tutorial, users will be equipped to choose the appropriate compilation strategy based on their specific requirements and constraints.\n", + "\n", + "### Compilation methods overview\n", + "\n", + "#### 1. **Qiskit transpiler with SABRE**\n", + "The Qiskit transpiler uses the SABRE (SWAP-based BidiREctional heuristic search) algorithm to optimize circuit layout and routing. SABRE focuses on minimizing SWAP gates and their impact on circuit depth while adhering to hardware connectivity constraints. This method is highly versatile and suitable for general-purpose circuit optimization, providing a balance between performance and computation time. To take advantage of the latest improvements in SABRE, detailed in [\\[1\\]](https://arxiv.org/abs/2409.08368), you can increase the number of trials (for example, `layout_trials=400, swap_trials=400`). For the purposes of this tutorial, we will use the default values for the number of trials in order to compare to Qiskit's default transpiler. The advantages and parameter exploration of SABRE are covered in a separate [deep-dive tutorial](/docs/tutorials/transpilation-optimizations-with-sabre).\n", + "\n", + "#### 2. **AI transpiler**\n", + "\n", + "The AI-powered transpiler in Qiskit uses machine learning to predict optimal transpilation strategies by analyzing patterns in circuit structure and hardware constraints to select the best sequence of optimizations for a given input. This method is particularly effective for large-scale quantum circuits, offering a high degree of automation and adaptability to diverse problem types. In addition to general circuit optimization, the AI transpiler can be used with the `AIPauliNetworkSynthesis` pass, which targets Pauli network circuits — blocks composed of H, S, SX, CX, RX, RY, and RZ gates — and applies a reinforcement learning-based synthesis approach. For more information on the AI transpiler and its synthesis strategies, see [\\[2\\]](https://arxiv.org/abs/2405.13196) and [\\[3\\]](https://arxiv.org/abs/2503.14448).\n", + "\n", + "\n", + "\n", + "#### 3. **Rustiq plugin**\n", + "The Rustiq plugin introduces advanced synthesis techniques specifically for `PauliEvolutionGate` operations, which represent Pauli rotations commonly used in Trotterized dynamics. This plugin is valuable for circuits implementing Hamiltonian simulation, such as those used in quantum chemistry and physics problems, where accurate Pauli rotations are essential for simulating problem Hamiltonians effectively. Rustiq offers precise, low-depth circuit synthesis for these specialized operations. For more details about the implementation and performance of Rustiq, please refer to [\\[4\\]](https://arxiv.org/abs/2404.03280).\n", + "\n", + "By exploring these compilation methods in depth, this tutorial provides users with the tools to enhance the performance of their quantum circuits, paving the way for more efficient and practical quantum computations." + ] + }, + { + "cell_type": "markdown", + "id": "53c589f4-c63c-47f3-8642-1f189e445307", + "metadata": {}, + "source": [ + "## Requirements\n", + "\n", + "Before starting this tutorial, be sure you have the following installed:\n", + "- Qiskit SDK v1.3 or later, with [visualization](/docs/api/qiskit/visualization) support\n", + "- Qiskit Runtime v0.28 or later (`pip install qiskit-ibm-runtime`)\n", + "- Qiskit IBM Transpiler (`pip install qiskit-ibm-transpiler`)\n", + "- Qiskit AI Transpiler local mode (`pip install qiskit_ibm_ai_local_transpiler`)\n", + "- Networkx graph library (`pip install networkx`)" + ] + }, + { + "cell_type": "markdown", + "id": "4f79e4a8-48bc-4af8-a172-f0057fa851eb", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "1ecd9900-e511-486e-97be-aac5f75b1917", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.circuit import QuantumCircuit\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "from qiskit.circuit.library import (\n", + " efficient_su2,\n", + " PauliEvolutionGate,\n", + ")\n", + "from qiskit_ibm_transpiler import generate_ai_pass_manager\n", + "from qiskit.quantum_info import SparsePauliOp\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "from qiskit.transpiler.passes.synthesis.high_level_synthesis import HLSConfig\n", + "from collections import Counter\n", + "from IPython.display import display\n", + "import time\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import json\n", + "import requests\n", + "import logging\n", + "\n", + "# Suppress noisy loggers\n", + "logging.getLogger(\n", + " \"qiskit_ibm_transpiler.wrappers.ai_local_synthesis\"\n", + ").setLevel(logging.ERROR)\n", + "\n", + "seed = 42 # Seed for reproducibility" + ] + }, + { + "cell_type": "markdown", + "id": "c3a31b4d-d679-4657-b372-ba19bcaf8eca", + "metadata": {}, + "source": [ + "## Part 1: Efficient SU2 Circuit\n", + "\n", + "### Step 1: Map classical inputs to a quantum problem\n", + "\n", + "In this section, we explore the `efficient_su2` circuit, a hardware-efficient ansatz commonly used in variational quantum algorithms (such as VQE) and quantum machine-learning tasks. The circuit consists of alternating layers of single-qubit rotations and entangling gates arranged in a circular pattern, designed to explore the quantum state space effectively while maintaining manageable depth.\n", + "\n", + "We will begin by constructing one `efficient_su2` circuit to demonstrate how to compare different compilation methods. After Part 1, we will expand our analysis to a larger set of circuits, enabling a comprehensive benchmark for evaluating the performance of various compilation techniques." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "f362cdac-94d8-4cc5-85f4-015c3d9eba3a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qubit_size = list(range(10, 101, 10))\n", + "qc_su2_list = [\n", + " efficient_su2(n, entanglement=\"circular\", reps=1)\n", + " .decompose()\n", + " .copy(name=f\"SU2_{n}\")\n", + " for n in qubit_size\n", + "]\n", + "\n", + "# Draw the first circuit\n", + "qc_su2_list[0].draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "d6671456-9b17-42bb-b94a-d42a29e6fad9", + "metadata": {}, + "source": [ + "### Step 2: Optimize problem for quantum hardware execution\n", + "\n", + "This step is the main focus of the tutorial. Here, we aim to optimize quantum circuits for efficient execution on real quantum hardware. Our primary objective is to reduce circuit depth and gate count, which are key factors in improving execution fidelity and mitigating hardware noise.\n", + "\n", + "- **SABRE transpiler**: Uses Qiskit’s default transpiler with the SABRE layout and routing algorithm.\n", + "- **AI transpiler (local mode)**: The standard AI-powered transpiler using local inference and the default synthesis strategy.\n", + "- **Rustiq plugin**: A transpiler plugin designed for low-depth compilation tailored to Hamiltonian simulation tasks.\n", + "\n", + "The goal of this step is to compare the results of these methods in terms of the transpiled circuit’s depth and gate count. Another important metric we consider is the transpilation runtime. By analyzing these metrics, we can evaluate the relative strengths of each method and determine which produces the most efficient circuit for execution on the selected hardware.\n", + "\n", + "Note: For the initial SU2 circuit example, we will only compare the SABRE transpiler to the default AI transpiler. However, in the subsequent benchmark using Hamlib circuits, we will compare all three transpilation methods." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c1ce1ad9-d529-49a1-91df-b540603ceb88", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "qiskit_runtime_service._get_crn_from_instance_name:WARNING:2025-07-30 21:46:30,843: Multiple instances found. Using all matching instances.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Using backend: \n" + ] + } + ], + "source": [ + "# QiskitRuntimeService.save_account(channel=\"ibm_quantum_platform\", token=\"\",\n", + "# overwrite=True, set_as_default=True)\n", + "service = QiskitRuntimeService(channel=\"ibm_quantum_platform\")\n", + "backend = service.backend(\"ibm_torino\")\n", + "print(f\"Using backend: {backend}\")" + ] + }, + { + "cell_type": "markdown", + "id": "381cfaca-103b-41bc-b3b3-f76808f44638", + "metadata": {}, + "source": [ + "Qiskit transpiler with SABRE:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "92a8f12d-3f97-400d-b6b6-a8c717f9ff0f", + "metadata": {}, + "outputs": [], + "source": [ + "pm_sabre = generate_preset_pass_manager(\n", + " optimization_level=3, backend=backend, seed_transpiler=seed\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "d1ae6e70-f276-41f3-8e40-68a9523eae06", + "metadata": {}, + "source": [ + "AI transpiler:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "6c27960f-e1cd-4cb5-b807-4dace42ea970", + "metadata": {}, + "outputs": [], + "source": [ + "# Standard AI transpiler pass manager, using the local mode\n", + "pm_ai = generate_ai_pass_manager(\n", + " backend=backend, optimization_level=3, ai_optimization_level=3\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "b20dfb96-bf70-473f-9c91-d99fe3ea3e88", + "metadata": {}, + "source": [ + "Rustiq plugin:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "7c0ae7c4-f4cd-4575-bbc9-b92e9c1d588c", + "metadata": {}, + "outputs": [], + "source": [ + "hls_config = HLSConfig(\n", + " PauliEvolution=[\n", + " (\n", + " \"rustiq\",\n", + " {\n", + " \"nshuffles\": 400,\n", + " \"upto_phase\": True,\n", + " \"fix_clifford\": True,\n", + " \"preserve_order\": False,\n", + " \"metric\": \"depth\",\n", + " },\n", + " )\n", + " ]\n", + ")\n", + "pm_rustiq = generate_preset_pass_manager(\n", + " optimization_level=3,\n", + " backend=backend,\n", + " hls_config=hls_config,\n", + " seed_transpiler=seed,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "a16c8082-d0f1-462f-ad10-ebd7d87a0d82", + "metadata": {}, + "source": [ + "#### Transpile and capture metrics\n", + "\n", + "To compare the performance of the compilation methods, we define a function that transpiles the input circuit and captures relevant metrics in a consistent manner. This includes the total circuit depth, overall gate count, and transpilation time.\n", + "\n", + "In addition to these standard metrics, we also record the 2-qubit gate depth, which is a particularly important metric for evaluating execution on quantum hardware. Unlike total depth, which includes all gates, the 2-qubit depth more accurately reflects the circuit's*actual execution duration on hardware. This is because 2-qubit gates typically dominate the time and error budget in most quantum devices. As such, minimizing 2-qubit depth is critical for improving fidelity and reducing decoherence effects during execution.\n", + "\n", + "We will use this function to analyze the performance of the different compilation methods across multiple circuits." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "eb236a9d-238e-4236-a195-e7b8df43b7a2", + "metadata": {}, + "outputs": [], + "source": [ + "def capture_transpilation_metrics(\n", + " results, pass_manager, circuits, method_name\n", + "):\n", + " \"\"\"\n", + " Capture transpilation metrics for a list of circuits and stores the results in a DataFrame.\n", + "\n", + " Args:\n", + " results (pd.DataFrame): DataFrame to store the results.\n", + " pass_manager: Pass manager used for transpilation.\n", + " circuits (list): List of quantum circuits to transpile.\n", + " method_name (str): Name of the transpilation method.\n", + "\n", + " Returns:\n", + " list: List of transpiled circuits.\n", + " \"\"\"\n", + " transpiled_circuits = []\n", + "\n", + " for i, qc in enumerate(circuits):\n", + " # Transpile the circuit\n", + " start_time = time.time()\n", + " transpiled_qc = pass_manager.run(qc)\n", + " end_time = time.time()\n", + "\n", + " # Needed for AI transpiler to be consistent with other methods\n", + " transpiled_qc = transpiled_qc.decompose(gates_to_decompose=[\"swap\"])\n", + "\n", + " # Collect metrics\n", + " transpilation_time = end_time - start_time\n", + " circuit_depth = transpiled_qc.depth(\n", + " lambda x: x.operation.num_qubits == 2\n", + " )\n", + " circuit_size = transpiled_qc.size()\n", + "\n", + " # Append results to DataFrame\n", + " results.loc[len(results)] = {\n", + " \"method\": method_name,\n", + " \"qc_name\": qc.name,\n", + " \"qc_index\": i,\n", + " \"num_qubits\": qc.num_qubits,\n", + " \"ops\": transpiled_qc.count_ops(),\n", + " \"depth\": circuit_depth,\n", + " \"size\": circuit_size,\n", + " \"runtime\": transpilation_time,\n", + " }\n", + " transpiled_circuits.append(transpiled_qc)\n", + " print(\n", + " f\"Transpiled circuit index {i} ({qc.name}) in {transpilation_time:.2f} seconds with method {method_name}, \"\n", + " f\"depth {circuit_depth}, and size {circuit_size}.\"\n", + " )\n", + "\n", + " return transpiled_circuits" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "d3107fc8-3151-488f-8eb8-8d8dc5f4d085", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Transpiled circuit index 0 (SU2_10) in 0.06 seconds with method sabre, depth 13, and size 167.\n", + "Transpiled circuit index 1 (SU2_20) in 0.24 seconds with method sabre, depth 20, and size 299.\n", + "Transpiled circuit index 2 (SU2_30) in 10.72 seconds with method sabre, depth 72, and size 627.\n", + "Transpiled circuit index 3 (SU2_40) in 16.16 seconds with method sabre, depth 40, and size 599.\n", + "Transpiled circuit index 4 (SU2_50) in 76.89 seconds with method sabre, depth 77, and size 855.\n", + "Transpiled circuit index 5 (SU2_60) in 86.12 seconds with method sabre, depth 60, and size 899.\n", + "Transpiled circuit index 6 (SU2_70) in 94.46 seconds with method sabre, depth 79, and size 1085.\n", + "Transpiled circuit index 7 (SU2_80) in 69.05 seconds with method sabre, depth 80, and size 1199.\n", + "Transpiled circuit index 8 (SU2_90) in 88.25 seconds with method sabre, depth 105, and size 1420.\n", + "Transpiled circuit index 9 (SU2_100) in 83.80 seconds with method sabre, depth 100, and size 1499.\n", + "Transpiled circuit index 0 (SU2_10) in 0.17 seconds with method ai, depth 10, and size 168.\n", + "Transpiled circuit index 1 (SU2_20) in 0.29 seconds with method ai, depth 20, and size 299.\n", + "Transpiled circuit index 2 (SU2_30) in 13.56 seconds with method ai, depth 36, and size 548.\n", + "Transpiled circuit index 3 (SU2_40) in 15.95 seconds with method ai, depth 40, and size 599.\n", + "Transpiled circuit index 4 (SU2_50) in 80.70 seconds with method ai, depth 54, and size 823.\n", + "Transpiled circuit index 5 (SU2_60) in 75.99 seconds with method ai, depth 60, and size 899.\n", + "Transpiled circuit index 6 (SU2_70) in 64.96 seconds with method ai, depth 74, and size 1087.\n", + "Transpiled circuit index 7 (SU2_80) in 68.25 seconds with method ai, depth 80, and size 1199.\n", + "Transpiled circuit index 8 (SU2_90) in 75.07 seconds with method ai, depth 90, and size 1404.\n", + "Transpiled circuit index 9 (SU2_100) in 63.97 seconds with method ai, depth 100, and size 1499.\n" + ] + } + ], + "source": [ + "results_su2 = pd.DataFrame(\n", + " columns=[\n", + " \"method\",\n", + " \"qc_name\",\n", + " \"qc_index\",\n", + " \"num_qubits\",\n", + " \"ops\",\n", + " \"depth\",\n", + " \"size\",\n", + " \"runtime\",\n", + " ]\n", + ")\n", + "\n", + "tqc_sabre = capture_transpilation_metrics(\n", + " results_su2, pm_sabre, qc_su2_list, \"sabre\"\n", + ")\n", + "tqc_ai = capture_transpilation_metrics(results_su2, pm_ai, qc_su2_list, \"ai\")" + ] + }, + { + "cell_type": "markdown", + "id": "a2d68a18-6656-4858-b439-7f9acbb516bb", + "metadata": {}, + "source": [ + "Display transpiled results of one of the circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "37924fc2-8fb6-451a-b8f9-cd79573f2384", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sabre transpilation\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AI transpilation\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "print(\"Sabre transpilation\")\n", + "display(tqc_sabre[0].draw(\"mpl\", fold=-1, idle_wires=False))\n", + "print(\"AI transpilation\")\n", + "display(tqc_ai[0].draw(\"mpl\", fold=-1, idle_wires=False))" + ] + }, + { + "cell_type": "markdown", + "id": "1633a4f4-7b01-4eb5-9764-0959f209a077", + "metadata": {}, + "source": [ + "Results table:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "a5911224-3d1d-490d-b730-9cb90f954498", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " depth size runtime\n", + "method \n", + "ai 56.4 852.5 45.89\n", + "sabre 64.6 864.9 52.57\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
methodqc_nameqc_indexnum_qubitsopsdepthsizeruntime
0sabreSU2_10010{'rz': 81, 'sx': 70, 'cz': 16}131670.058845
1sabreSU2_20120{'rz': 160, 'sx': 119, 'cz': 20}202990.238217
2sabreSU2_30230{'sx': 295, 'rz': 242, 'cz': 90}7262710.723922
3sabreSU2_40340{'rz': 320, 'sx': 239, 'cz': 40}4059916.159262
4sabreSU2_50450{'rz': 402, 'sx': 367, 'cz': 86}7785576.886604
5sabreSU2_60560{'rz': 480, 'sx': 359, 'cz': 60}6089986.118255
6sabreSU2_70670{'rz': 562, 'sx': 441, 'cz': 82}79108594.458287
7sabreSU2_80780{'rz': 640, 'sx': 479, 'cz': 80}80119969.048184
8sabreSU2_90890{'rz': 721, 'sx': 585, 'cz': 114}105142088.254809
9sabreSU2_1009100{'rz': 800, 'sx': 599, 'cz': 100}100149983.795482
10aiSU2_10010{'rz': 81, 'sx': 71, 'cz': 16}101680.171532
11aiSU2_20120{'rz': 160, 'sx': 119, 'cz': 20}202990.291691
12aiSU2_30230{'sx': 243, 'rz': 242, 'cz': 63}3654813.555931
13aiSU2_40340{'rz': 320, 'sx': 239, 'cz': 40}4059915.952733
14aiSU2_50450{'rz': 403, 'sx': 346, 'cz': 74}5482380.702141
15aiSU2_60560{'rz': 480, 'sx': 359, 'cz': 60}6089975.993404
16aiSU2_70670{'rz': 563, 'sx': 442, 'cz': 82}74108764.960162
17aiSU2_80780{'rz': 640, 'sx': 479, 'cz': 80}80119968.253280
18aiSU2_90890{'rz': 721, 'sx': 575, 'cz': 108}90140475.072412
19aiSU2_1009100{'rz': 800, 'sx': 599, 'cz': 100}100149963.967446
\n", + "
" + ], + "text/plain": [ + " method qc_name qc_index num_qubits ops \\\n", + "0 sabre SU2_10 0 10 {'rz': 81, 'sx': 70, 'cz': 16} \n", + "1 sabre SU2_20 1 20 {'rz': 160, 'sx': 119, 'cz': 20} \n", + "2 sabre SU2_30 2 30 {'sx': 295, 'rz': 242, 'cz': 90} \n", + "3 sabre SU2_40 3 40 {'rz': 320, 'sx': 239, 'cz': 40} \n", + "4 sabre SU2_50 4 50 {'rz': 402, 'sx': 367, 'cz': 86} \n", + "5 sabre SU2_60 5 60 {'rz': 480, 'sx': 359, 'cz': 60} \n", + "6 sabre SU2_70 6 70 {'rz': 562, 'sx': 441, 'cz': 82} \n", + "7 sabre SU2_80 7 80 {'rz': 640, 'sx': 479, 'cz': 80} \n", + "8 sabre SU2_90 8 90 {'rz': 721, 'sx': 585, 'cz': 114} \n", + "9 sabre SU2_100 9 100 {'rz': 800, 'sx': 599, 'cz': 100} \n", + "10 ai SU2_10 0 10 {'rz': 81, 'sx': 71, 'cz': 16} \n", + "11 ai SU2_20 1 20 {'rz': 160, 'sx': 119, 'cz': 20} \n", + "12 ai SU2_30 2 30 {'sx': 243, 'rz': 242, 'cz': 63} \n", + "13 ai SU2_40 3 40 {'rz': 320, 'sx': 239, 'cz': 40} \n", + "14 ai SU2_50 4 50 {'rz': 403, 'sx': 346, 'cz': 74} \n", + "15 ai SU2_60 5 60 {'rz': 480, 'sx': 359, 'cz': 60} \n", + "16 ai SU2_70 6 70 {'rz': 563, 'sx': 442, 'cz': 82} \n", + "17 ai SU2_80 7 80 {'rz': 640, 'sx': 479, 'cz': 80} \n", + "18 ai SU2_90 8 90 {'rz': 721, 'sx': 575, 'cz': 108} \n", + "19 ai SU2_100 9 100 {'rz': 800, 'sx': 599, 'cz': 100} \n", + "\n", + " depth size runtime \n", + "0 13 167 0.058845 \n", + "1 20 299 0.238217 \n", + "2 72 627 10.723922 \n", + "3 40 599 16.159262 \n", + "4 77 855 76.886604 \n", + "5 60 899 86.118255 \n", + "6 79 1085 94.458287 \n", + "7 80 1199 69.048184 \n", + "8 105 1420 88.254809 \n", + "9 100 1499 83.795482 \n", + "10 10 168 0.171532 \n", + "11 20 299 0.291691 \n", + "12 36 548 13.555931 \n", + "13 40 599 15.952733 \n", + "14 54 823 80.702141 \n", + "15 60 899 75.993404 \n", + "16 74 1087 64.960162 \n", + "17 80 1199 68.253280 \n", + "18 90 1404 75.072412 \n", + "19 100 1499 63.967446 " + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "summary_su2 = (\n", + " results_su2.groupby(\"method\")[[\"depth\", \"size\", \"runtime\"]]\n", + " .mean()\n", + " .round(2)\n", + ")\n", + "print(summary_su2)\n", + "\n", + "results_su2" + ] + }, + { + "cell_type": "markdown", + "id": "167834f1-00a7-4190-99e4-d221d1952357", + "metadata": {}, + "source": [ + "#### Results graph\n", + "\n", + "As we define a function to consistently capture metrics, we will also define one to graph the metrics. Here, we will plot the two-qubit depth, gate count, and runtime for each compilation method across the circuits." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "d90fb4fa-e031-40f6-90e6-e1208a855bec", + "metadata": {}, + "outputs": [], + "source": [ + "def plot_transpilation_metrics(results, overall_title, x_axis=\"qc_index\"):\n", + " \"\"\"\n", + " Plots transpilation metrics (depth, size, runtime) for different transpilation methods.\n", + "\n", + " Parameters:\n", + " results (DataFrame): Data containing columns ['num_qubits', 'method', 'depth', 'size', 'runtime']\n", + " overall_title (str): The title of the overall figure.\n", + " x_axis (str): The x-axis label, either 'num_qubits' or 'qc_index'.\n", + " \"\"\"\n", + "\n", + " fig, axs = plt.subplots(1, 3, figsize=(24, 6))\n", + " metrics = [\"depth\", \"size\", \"runtime\"]\n", + " titles = [\"Circuit Depth\", \"Circuit Size\", \"Transpilation Runtime\"]\n", + " y_labels = [\"Depth\", \"Size (Gate Count)\", \"Runtime (s)\"]\n", + "\n", + " methods = results[\"method\"].unique()\n", + " colors = plt.colormaps[\"tab10\"]\n", + " markers = [\"o\", \"^\", \"s\", \"D\", \"P\", \"*\", \"X\", \"v\"]\n", + " color_list = [colors(i % colors.N) for i in range(len(methods))]\n", + " color_map = {method: color_list[i] for i, method in enumerate(methods)}\n", + " marker_map = {\n", + " method: markers[i % len(markers)] for i, method in enumerate(methods)\n", + " }\n", + " jitter_factor = 0.1 # Small x-axis jitter for visibility\n", + " handles, labels = [], [] # Unique handles for legend\n", + "\n", + " # Plot each metric\n", + " for i, metric in enumerate(metrics):\n", + " for method in methods:\n", + " method_data = results[results[\"method\"] == method]\n", + "\n", + " # Introduce slight jitter to avoid exact overlap\n", + " jitter = np.random.uniform(\n", + " -jitter_factor, jitter_factor, len(method_data)\n", + " )\n", + "\n", + " scatter = axs[i].scatter(\n", + " method_data[x_axis] + jitter,\n", + " method_data[metric],\n", + " color=color_map[method],\n", + " label=method,\n", + " marker=marker_map[method],\n", + " alpha=0.7,\n", + " edgecolors=\"black\",\n", + " s=80,\n", + " )\n", + "\n", + " if method not in labels:\n", + " handles.append(scatter)\n", + " labels.append(method)\n", + "\n", + " axs[i].set_title(titles[i])\n", + " axs[i].set_xlabel(x_axis)\n", + " axs[i].set_ylabel(y_labels[i])\n", + " axs[i].grid(axis=\"y\", linestyle=\"--\", alpha=0.7)\n", + " axs[i].tick_params(axis=\"x\", rotation=45)\n", + " axs[i].set_xticks(sorted(results[x_axis].unique()))\n", + "\n", + " fig.suptitle(overall_title, fontsize=16)\n", + " fig.legend(\n", + " handles=handles,\n", + " labels=labels,\n", + " loc=\"upper right\",\n", + " bbox_to_anchor=(1.05, 1),\n", + " )\n", + "\n", + " plt.tight_layout()\n", + " plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "7f7b502a-8ed6-45fa-a698-02977149e283", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plot_transpilation_metrics(\n", + " results_su2, \"Transpilation Metrics for SU2 Circuits\", x_axis=\"num_qubits\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "f75e8e3a-803f-4386-a60a-b088faa3c81a", + "metadata": {}, + "source": [ + "#### Analysis of SU2 circuit compilation results\n", + "\n", + "In this experiment, we compare two transpilation methods — Qiskit's SABRE transpiler and the AI-powered transpiler — on a set of `efficient_su2` circuits. Since these circuits do not include any `PauliEvolutionGate` operations, the Rustiq plugin is not included in this comparison.\n", + "\n", + "On average, the AI transpiler performs better in terms of circuit depth, with a greater than 10% improvement across the full range of SU2 circuits. For gate count (circuit size) and transpilation runtime, both methods yield similar results overall.\n", + "\n", + "However, inspecting the individual data points reveals a deeper insight:\n", + "- For most qubit sizes, both SABRE and AI produce nearly identical results, suggesting that in many cases, both methods converge to similarly efficient solutions.\n", + "- For certain circuit sizes, specifically at 30, 50, 70, and 90 qubits, the AI transpiler finds significantly shallower circuits than SABRE. This indicates that AI's learning-based approach is able to discover more optimal layouts or routing paths in cases where the SABRE heuristic does not.\n", + "\n", + "This behavior highlights an important takeaway:\n", + "> While SABRE and AI often produce comparable results, the AI transpiler can occasionally discover much better solutions, particularly in terms of depth, which can lead to significantly improved performance on hardware." + ] + }, + { + "cell_type": "markdown", + "id": "4c3b2aa8-8187-488a-8e5b-197cf26085bb", + "metadata": {}, + "source": [ + "## Part 2: Hamiltonian simulation circuit\n", + "\n", + "### Step 1: Investigate circuits with `PauliEvolutionGate`\n", + "\n", + "In this section, we investigate quantum circuits constructed using `PauliEvolutionGate`, which enables efficient simulation of Hamiltonians. We will analyze how different compilation methods optimize these circuits across various Hamiltonians.\n", + "\n", + "#### Hamiltonians used in the benchmark\n", + "\n", + "The Hamiltonians used in this benchmark describe pairwise interactions between qubits, including terms such as $ZZ$, $XX$, and $YY$. These Hamiltonians are commonly used in quantum chemistry, condensed matter physics, and materials science, where they model systems of interacting particles.\n", + "\n", + "For reference, users can explore a broader set of Hamiltonians in this paper: [Efficient Hamiltonian Simulation on Noisy Quantum Devices](https://arxiv.org/pdf/2306.13126).\n", + "\n", + "#### Benchmark source: Hamlib and Benchpress\n", + "\n", + "The circuits used in this benchmark are drawn from the [Hamlib benchmark repository](https://github.com/SRI-International/QC-App-Oriented-Benchmarks/tree/master/hamlib), which contains realistic Hamiltonian simulation workloads.\n", + "\n", + "These same circuits were previously benchmarked using [Benchpress](https://github.com/Qiskit/benchpress), an open-source framework for evaluating quantum transpilation performance. By using this standardized set of circuits, we can directly compare the effectiveness of different compilation strategies on representative simulation problems.\n", + "\n", + "Hamiltonian simulation is a foundational task in quantum computing, with applications in molecular simulations, optimization problems, and quantum many-body physics. Understanding how different compilation methods optimize these circuits can help users improve practical execution of such circuits on near-term quantum devices." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "66347c00-1607-4405-bb76-610690adf6b8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of Hamiltonian circuits: 35\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Obtain the Hamiltonian JSON from the benchpress repository\n", + "url = \"https://raw.githubusercontent.com/Qiskit/benchpress/e7b29ef7be4cc0d70237b8fdc03edbd698908eff/benchpress/hamiltonian/hamlib/100_representative.json\"\n", + "response = requests.get(url)\n", + "response.raise_for_status() # Raise an error if download failed\n", + "ham_records = json.loads(response.text)\n", + "# Remove circuits that are too large for the backend\n", + "ham_records = [\n", + " h for h in ham_records if h[\"ham_qubits\"] <= backend.num_qubits\n", + "]\n", + "# Remove the circuits that are large to save transpilation time\n", + "ham_records = sorted(ham_records, key=lambda x: x[\"ham_terms\"])[:35]\n", + "\n", + "qc_ham_list = []\n", + "for h in ham_records:\n", + " terms = h[\"ham_hamlib_hamiltonian_terms\"]\n", + " coeff = h[\"ham_hamlib_hamiltonian_coefficients\"]\n", + " num_qubits = h[\"ham_qubits\"]\n", + " name = h[\"ham_problem\"]\n", + "\n", + " evo_gate = PauliEvolutionGate(SparsePauliOp(terms, coeff))\n", + "\n", + " qc_ham = QuantumCircuit(num_qubits)\n", + " qc_ham.name = name\n", + "\n", + " qc_ham.append(evo_gate, range(num_qubits))\n", + " qc_ham_list.append(qc_ham)\n", + "print(f\"Number of Hamiltonian circuits: {len(qc_ham_list)}\")\n", + "\n", + "# Draw the first Hamiltonian circuit\n", + "qc_ham_list[0].draw(\"mpl\", fold=-1)" + ] + }, + { + "cell_type": "markdown", + "id": "9690d94d-7d38-45d3-b37d-85eaf268dbd6", + "metadata": {}, + "source": [ + "### Step 2: Optimize problem for quantum hardware execution\n", + "\n", + "As in the previous example, we will use the same backend to ensure consistency in our comparisons. Since the pass managers (`pm_sabre`, `pm_ai`, and `pm_rustiq`) have already been initialized, we can directly proceed with transpiling the Hamiltonian circuits using each method.\n", + "\n", + "This step focuses solely on performing the transpilation and recording the resulting circuit metrics, including depth, gate count, and transpilation runtime. By analyzing these results, we aim to determine the efficiency of each transpilation method for this type of circuit." + ] + }, + { + "cell_type": "markdown", + "id": "22fb1fe7-333f-421f-a17d-bc32190a0f86", + "metadata": {}, + "source": [ + "Transpile and capture metrics:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "4138c7d6-5ec8-4c8f-aef4-08ec1b13633b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Transpiled circuit index 0 (all-vib-o3) in 0.02 seconds with method sabre, depth 6, and size 58.\n", + "Transpiled circuit index 1 (all-vib-c2h) in 1.10 seconds with method sabre, depth 2, and size 39.\n", + "Transpiled circuit index 2 (all-vib-bh) in 0.01 seconds with method sabre, depth 3, and size 30.\n", + "Transpiled circuit index 3 (all-vib-c2h) in 0.03 seconds with method sabre, depth 18, and size 115.\n", + "Transpiled circuit index 4 (graph-gnp_k-2) in 0.02 seconds with method sabre, depth 24, and size 129.\n", + "Transpiled circuit index 5 (all-vib-fccf) in 0.05 seconds with method sabre, depth 14, and size 134.\n", + "Transpiled circuit index 6 (all-vib-hno) in 8.39 seconds with method sabre, depth 6, and size 174.\n", + "Transpiled circuit index 7 (all-vib-bhf2) in 3.92 seconds with method sabre, depth 22, and size 220.\n", + "Transpiled circuit index 8 (LiH) in 0.03 seconds with method sabre, depth 67, and size 290.\n", + "Transpiled circuit index 9 (uf20-ham) in 0.04 seconds with method sabre, depth 50, and size 340.\n", + "Transpiled circuit index 10 (all-vib-fccf) in 0.62 seconds with method sabre, depth 30, and size 286.\n", + "Transpiled circuit index 11 (all-vib-fccf) in 0.04 seconds with method sabre, depth 67, and size 339.\n", + "Transpiled circuit index 12 (all-vib-ch2) in 0.04 seconds with method sabre, depth 87, and size 421.\n", + "Transpiled circuit index 13 (tfim) in 0.05 seconds with method sabre, depth 36, and size 222.\n", + "Transpiled circuit index 14 (all-vib-cyclo_propene) in 9.51 seconds with method sabre, depth 22, and size 345.\n", + "Transpiled circuit index 15 (graph-gnp_k-4) in 0.05 seconds with method sabre, depth 128, and size 704.\n", + "Transpiled circuit index 16 (all-vib-hc3h2cn) in 13.83 seconds with method sabre, depth 2, and size 242.\n", + "Transpiled circuit index 17 (TSP_Ncity-4) in 0.05 seconds with method sabre, depth 106, and size 609.\n", + "Transpiled circuit index 18 (tfim) in 0.29 seconds with method sabre, depth 73, and size 399.\n", + "Transpiled circuit index 19 (all-vib-h2co) in 21.97 seconds with method sabre, depth 30, and size 572.\n", + "Transpiled circuit index 20 (Be2) in 0.09 seconds with method sabre, depth 324, and size 1555.\n", + "Transpiled circuit index 21 (graph-complete_bipart) in 0.12 seconds with method sabre, depth 250, and size 1394.\n", + "Transpiled circuit index 22 (all-vib-f2) in 0.07 seconds with method sabre, depth 215, and size 1027.\n", + "Transpiled circuit index 23 (all-vib-cyclo_propene) in 41.22 seconds with method sabre, depth 30, and size 1144.\n", + "Transpiled circuit index 24 (TSP_Ncity-5) in 1.89 seconds with method sabre, depth 175, and size 1933.\n", + "Transpiled circuit index 25 (H2) in 0.32 seconds with method sabre, depth 1237, and size 5502.\n", + "Transpiled circuit index 26 (uuf100-ham) in 0.20 seconds with method sabre, depth 385, and size 4303.\n", + "Transpiled circuit index 27 (ham-graph-gnp_k-5) in 0.20 seconds with method sabre, depth 311, and size 3654.\n", + "Transpiled circuit index 28 (tfim) in 0.15 seconds with method sabre, depth 276, and size 3213.\n", + "Transpiled circuit index 29 (uuf100-ham) in 0.21 seconds with method sabre, depth 520, and size 5250.\n", + "Transpiled circuit index 30 (flat100-ham) in 0.15 seconds with method sabre, depth 131, and size 3157.\n", + "Transpiled circuit index 31 (uf100-ham) in 0.24 seconds with method sabre, depth 624, and size 7378.\n", + "Transpiled circuit index 32 (OH) in 0.88 seconds with method sabre, depth 2175, and size 9808.\n", + "Transpiled circuit index 33 (HF) in 0.66 seconds with method sabre, depth 2206, and size 9417.\n", + "Transpiled circuit index 34 (BH) in 0.89 seconds with method sabre, depth 2177, and size 9802.\n", + "Transpiled circuit index 0 (all-vib-o3) in 0.02 seconds with method ai, depth 6, and size 58.\n", + "Transpiled circuit index 1 (all-vib-c2h) in 1.11 seconds with method ai, depth 2, and size 39.\n", + "Transpiled circuit index 2 (all-vib-bh) in 0.01 seconds with method ai, depth 3, and size 30.\n", + "Transpiled circuit index 3 (all-vib-c2h) in 0.11 seconds with method ai, depth 18, and size 94.\n", + "Transpiled circuit index 4 (graph-gnp_k-2) in 0.11 seconds with method ai, depth 22, and size 129.\n", + "Transpiled circuit index 5 (all-vib-fccf) in 0.06 seconds with method ai, depth 22, and size 177.\n", + "Transpiled circuit index 6 (all-vib-hno) in 8.62 seconds with method ai, depth 10, and size 198.\n", + "Transpiled circuit index 7 (all-vib-bhf2) in 3.71 seconds with method ai, depth 18, and size 195.\n", + "Transpiled circuit index 8 (LiH) in 0.19 seconds with method ai, depth 62, and size 267.\n", + "Transpiled circuit index 9 (uf20-ham) in 0.22 seconds with method ai, depth 47, and size 321.\n", + "Transpiled circuit index 10 (all-vib-fccf) in 0.71 seconds with method ai, depth 38, and size 369.\n", + "Transpiled circuit index 11 (all-vib-fccf) in 0.24 seconds with method ai, depth 65, and size 315.\n", + "Transpiled circuit index 12 (all-vib-ch2) in 0.24 seconds with method ai, depth 91, and size 430.\n", + "Transpiled circuit index 13 (tfim) in 0.15 seconds with method ai, depth 12, and size 251.\n", + "Transpiled circuit index 14 (all-vib-cyclo_propene) in 8.50 seconds with method ai, depth 18, and size 311.\n", + "Transpiled circuit index 15 (graph-gnp_k-4) in 0.25 seconds with method ai, depth 117, and size 659.\n", + "Transpiled circuit index 16 (all-vib-hc3h2cn) in 16.11 seconds with method ai, depth 2, and size 242.\n", + "Transpiled circuit index 17 (TSP_Ncity-4) in 0.39 seconds with method ai, depth 98, and size 564.\n", + "Transpiled circuit index 18 (tfim) in 0.38 seconds with method ai, depth 23, and size 437.\n", + "Transpiled circuit index 19 (all-vib-h2co) in 24.97 seconds with method ai, depth 38, and size 707.\n", + "Transpiled circuit index 20 (Be2) in 1.07 seconds with method ai, depth 293, and size 1392.\n", + "Transpiled circuit index 21 (graph-complete_bipart) in 0.61 seconds with method ai, depth 229, and size 1437.\n", + "Transpiled circuit index 22 (all-vib-f2) in 0.57 seconds with method ai, depth 178, and size 964.\n", + "Transpiled circuit index 23 (all-vib-cyclo_propene) in 50.89 seconds with method ai, depth 34, and size 1425.\n", + "Transpiled circuit index 24 (TSP_Ncity-5) in 1.61 seconds with method ai, depth 171, and size 2020.\n", + "Transpiled circuit index 25 (H2) in 6.39 seconds with method ai, depth 1148, and size 5208.\n", + "Transpiled circuit index 26 (uuf100-ham) in 3.97 seconds with method ai, depth 376, and size 5048.\n", + "Transpiled circuit index 27 (ham-graph-gnp_k-5) in 3.54 seconds with method ai, depth 357, and size 4451.\n", + "Transpiled circuit index 28 (tfim) in 1.72 seconds with method ai, depth 216, and size 3026.\n", + "Transpiled circuit index 29 (uuf100-ham) in 4.45 seconds with method ai, depth 426, and size 5399.\n", + "Transpiled circuit index 30 (flat100-ham) in 7.02 seconds with method ai, depth 86, and size 3108.\n", + "Transpiled circuit index 31 (uf100-ham) in 12.85 seconds with method ai, depth 623, and size 8354.\n", + "Transpiled circuit index 32 (OH) in 15.19 seconds with method ai, depth 2084, and size 9543.\n", + "Transpiled circuit index 33 (HF) in 17.51 seconds with method ai, depth 2063, and size 9446.\n", + "Transpiled circuit index 34 (BH) in 15.33 seconds with method ai, depth 2094, and size 9730.\n", + "Transpiled circuit index 0 (all-vib-o3) in 0.02 seconds with method rustiq, depth 13, and size 83.\n", + "Transpiled circuit index 1 (all-vib-c2h) in 1.11 seconds with method rustiq, depth 2, and size 39.\n", + "Transpiled circuit index 2 (all-vib-bh) in 0.01 seconds with method rustiq, depth 3, and size 30.\n", + "Transpiled circuit index 3 (all-vib-c2h) in 0.01 seconds with method rustiq, depth 13, and size 79.\n", + "Transpiled circuit index 4 (graph-gnp_k-2) in 0.02 seconds with method rustiq, depth 31, and size 131.\n", + "Transpiled circuit index 5 (all-vib-fccf) in 0.04 seconds with method rustiq, depth 50, and size 306.\n", + "Transpiled circuit index 6 (all-vib-hno) in 14.03 seconds with method rustiq, depth 22, and size 276.\n", + "Transpiled circuit index 7 (all-vib-bhf2) in 3.15 seconds with method rustiq, depth 13, and size 155.\n", + "Transpiled circuit index 8 (LiH) in 0.03 seconds with method rustiq, depth 54, and size 270.\n", + "Transpiled circuit index 9 (uf20-ham) in 0.04 seconds with method rustiq, depth 65, and size 398.\n", + "Transpiled circuit index 10 (all-vib-fccf) in 0.16 seconds with method rustiq, depth 41, and size 516.\n", + "Transpiled circuit index 11 (all-vib-fccf) in 0.02 seconds with method rustiq, depth 34, and size 189.\n", + "Transpiled circuit index 12 (all-vib-ch2) in 0.03 seconds with method rustiq, depth 49, and size 240.\n", + "Transpiled circuit index 13 (tfim) in 0.05 seconds with method rustiq, depth 20, and size 366.\n", + "Transpiled circuit index 14 (all-vib-cyclo_propene) in 9.08 seconds with method rustiq, depth 16, and size 277.\n", + "Transpiled circuit index 15 (graph-gnp_k-4) in 0.04 seconds with method rustiq, depth 116, and size 612.\n", + "Transpiled circuit index 16 (all-vib-hc3h2cn) in 13.89 seconds with method rustiq, depth 2, and size 257.\n", + "Transpiled circuit index 17 (TSP_Ncity-4) in 0.05 seconds with method rustiq, depth 133, and size 737.\n", + "Transpiled circuit index 18 (tfim) in 0.11 seconds with method rustiq, depth 25, and size 680.\n", + "Transpiled circuit index 19 (all-vib-h2co) in 27.19 seconds with method rustiq, depth 66, and size 983.\n", + "Transpiled circuit index 20 (Be2) in 0.07 seconds with method rustiq, depth 215, and size 1030.\n", + "Transpiled circuit index 21 (graph-complete_bipart) in 0.14 seconds with method rustiq, depth 328, and size 1918.\n", + "Transpiled circuit index 22 (all-vib-f2) in 0.05 seconds with method rustiq, depth 114, and size 692.\n", + "Transpiled circuit index 23 (all-vib-cyclo_propene) in 62.25 seconds with method rustiq, depth 74, and size 2348.\n", + "Transpiled circuit index 24 (TSP_Ncity-5) in 0.20 seconds with method rustiq, depth 436, and size 3605.\n", + "Transpiled circuit index 25 (H2) in 0.21 seconds with method rustiq, depth 643, and size 3476.\n", + "Transpiled circuit index 26 (uuf100-ham) in 0.24 seconds with method rustiq, depth 678, and size 6120.\n", + "Transpiled circuit index 27 (ham-graph-gnp_k-5) in 0.22 seconds with method rustiq, depth 588, and size 5241.\n", + "Transpiled circuit index 28 (tfim) in 0.34 seconds with method rustiq, depth 340, and size 5901.\n", + "Transpiled circuit index 29 (uuf100-ham) in 0.33 seconds with method rustiq, depth 881, and size 7667.\n", + "Transpiled circuit index 30 (flat100-ham) in 0.31 seconds with method rustiq, depth 279, and size 4910.\n", + "Transpiled circuit index 31 (uf100-ham) in 0.38 seconds with method rustiq, depth 1138, and size 10607.\n", + "Transpiled circuit index 32 (OH) in 0.38 seconds with method rustiq, depth 1148, and size 6512.\n", + "Transpiled circuit index 33 (HF) in 0.37 seconds with method rustiq, depth 1090, and size 6256.\n", + "Transpiled circuit index 34 (BH) in 0.37 seconds with method rustiq, depth 1148, and size 6501.\n" + ] + } + ], + "source": [ + "results_ham = pd.DataFrame(\n", + " columns=[\n", + " \"method\",\n", + " \"qc_name\",\n", + " \"qc_index\",\n", + " \"num_qubits\",\n", + " \"ops\",\n", + " \"depth\",\n", + " \"size\",\n", + " \"runtime\",\n", + " ]\n", + ")\n", + "\n", + "tqc_sabre = capture_transpilation_metrics(\n", + " results_ham, pm_sabre, qc_ham_list, \"sabre\"\n", + ")\n", + "tqc_ai = capture_transpilation_metrics(results_ham, pm_ai, qc_ham_list, \"ai\")\n", + "tqc_rustiq = capture_transpilation_metrics(\n", + " results_ham, pm_rustiq, qc_ham_list, \"rustiq\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "ef88a127-79b8-4a53-90ef-6caa84c29e06", + "metadata": {}, + "source": [ + "Results table (skipping visualization as the output circuits are very large):" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "bc4a7910-5e8a-48ec-bd7e-2508ae8dfccc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " depth size runtime\n", + "method \n", + "ai 316.86 2181.26 5.97\n", + "rustiq 281.94 2268.80 3.86\n", + "sabre 337.97 2120.14 3.07\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
methodqc_nameqc_indexnum_qubitsopsdepthsizeruntime
0sabreall-vib-o304{'rz': 28, 'sx': 24, 'cz': 6}6580.016597
1sabreall-vib-c2h14{'rz': 17, 'sx': 16, 'cz': 4, 'x': 2}2391.102089
2sabreall-vib-bh22{'sx': 14, 'rz': 13, 'cz': 3}3300.011042
3sabreall-vib-c2h33{'sx': 46, 'rz': 45, 'cz': 18, 'x': 6}181150.025816
4sabregraph-gnp_k-244{'sx': 49, 'rz': 47, 'cz': 24, 'x': 9}241290.023077
...........................
100rustiqflat100-ham3090{'sx': 2709, 'cz': 1379, 'rz': 817, 'x': 5}27949100.309448
101rustiquf100-ham3146{'sx': 6180, 'cz': 3120, 'rz': 1303, 'x': 4}1138106070.380977
102rustiqOH3210{'sx': 3330, 'cz': 1704, 'rz': 1455, 'x': 23}114865120.383564
103rustiqHF3310{'sx': 3213, 'cz': 1620, 'rz': 1406, 'x': 17}109062560.368578
104rustiqBH3410{'sx': 3331, 'cz': 1704, 'rz': 1447, 'x': 19}114865010.374822
\n", + "

105 rows × 8 columns

\n", + "
" + ], + "text/plain": [ + " method qc_name qc_index num_qubits \\\n", + "0 sabre all-vib-o3 0 4 \n", + "1 sabre all-vib-c2h 1 4 \n", + "2 sabre all-vib-bh 2 2 \n", + "3 sabre all-vib-c2h 3 3 \n", + "4 sabre graph-gnp_k-2 4 4 \n", + ".. ... ... ... ... \n", + "100 rustiq flat100-ham 30 90 \n", + "101 rustiq uf100-ham 31 46 \n", + "102 rustiq OH 32 10 \n", + "103 rustiq HF 33 10 \n", + "104 rustiq BH 34 10 \n", + "\n", + " ops depth size runtime \n", + "0 {'rz': 28, 'sx': 24, 'cz': 6} 6 58 0.016597 \n", + "1 {'rz': 17, 'sx': 16, 'cz': 4, 'x': 2} 2 39 1.102089 \n", + "2 {'sx': 14, 'rz': 13, 'cz': 3} 3 30 0.011042 \n", + "3 {'sx': 46, 'rz': 45, 'cz': 18, 'x': 6} 18 115 0.025816 \n", + "4 {'sx': 49, 'rz': 47, 'cz': 24, 'x': 9} 24 129 0.023077 \n", + ".. ... ... ... ... \n", + "100 {'sx': 2709, 'cz': 1379, 'rz': 817, 'x': 5} 279 4910 0.309448 \n", + "101 {'sx': 6180, 'cz': 3120, 'rz': 1303, 'x': 4} 1138 10607 0.380977 \n", + "102 {'sx': 3330, 'cz': 1704, 'rz': 1455, 'x': 23} 1148 6512 0.383564 \n", + "103 {'sx': 3213, 'cz': 1620, 'rz': 1406, 'x': 17} 1090 6256 0.368578 \n", + "104 {'sx': 3331, 'cz': 1704, 'rz': 1447, 'x': 19} 1148 6501 0.374822 \n", + "\n", + "[105 rows x 8 columns]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "summary_ham = (\n", + " results_ham.groupby(\"method\")[[\"depth\", \"size\", \"runtime\"]]\n", + " .mean()\n", + " .round(2)\n", + ")\n", + "print(summary_ham)\n", + "\n", + "results_ham" + ] + }, + { + "cell_type": "markdown", + "id": "17e7b228-01b0-4fcc-b659-9f9958f5477e", + "metadata": {}, + "source": [ + "Visualize performance based on circuit index:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "4c6b810d-a4cf-4de3-aae6-c6c1cdd83d8f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plot_transpilation_metrics(\n", + " results_ham, \"Transpilation Metrics for Hamiltonian Circuits\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "7ad6f4ee-c9d1-4e8b-ae25-cf39672acbec", + "metadata": {}, + "source": [ + "Visualize the percentage of circuits for which each method performed best." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "01b4644e-ac91-483c-944b-924f8b41718d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Best-performing methods based on depth:\n", + " ai: 16 circuit(s)\n", + " rustiq: 16 circuit(s)\n", + " sabre: 10 circuit(s)\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Best-performing methods based on size:\n", + " sabre: 18 circuit(s)\n", + " rustiq: 14 circuit(s)\n", + " ai: 10 circuit(s)\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def analyze_and_plot_best_methods(results, metric):\n", + " \"\"\"\n", + " Analyze the best-performing methods for a given metric and plot a pie chart.\n", + "\n", + " Parameters:\n", + " results (DataFrame): The input DataFrame containing method performance data.\n", + " metric (str): The metric to evaluate (\"depth\" or \"size\").\n", + " \"\"\"\n", + " method_counts = Counter()\n", + " for qc_idx, group in results.groupby(\"qc_index\"):\n", + " min_value = group[metric].min()\n", + "\n", + " # Find all methods that achieved this minimum value\n", + " best_methods = group[group[metric] == min_value][\"method\"]\n", + " # Update counts for all best methods (handling ties)\n", + " method_counts.update(best_methods)\n", + " best_method_counts = dict(\n", + " sorted(method_counts.items(), key=lambda x: x[1], reverse=True)\n", + " )\n", + "\n", + " # Print summary\n", + " print(f\"Best-performing methods based on {metric}:\")\n", + " for method, count in best_method_counts.items():\n", + " print(f\" {method}: {count} circuit(s)\")\n", + "\n", + " # Plot pie chart\n", + " num_methods = len(best_method_counts)\n", + " colors = plt.cm.viridis_r(range(0, 256, 256 // num_methods))\n", + " plt.figure(figsize=(5, 5))\n", + " plt.pie(\n", + " best_method_counts.values(),\n", + " labels=best_method_counts.keys(),\n", + " autopct=\"%1.1f%%\",\n", + " startangle=140,\n", + " wedgeprops={\"edgecolor\": \"black\"},\n", + " textprops={\"fontsize\": 10},\n", + " colors=colors,\n", + " )\n", + " plt.title(\n", + " f\"Percentage of Circuits Method Performed Best for {metric.capitalize()}\",\n", + " fontsize=12,\n", + " fontweight=\"bold\",\n", + " )\n", + " plt.show()\n", + "\n", + "\n", + "analyze_and_plot_best_methods(results_ham, \"depth\")\n", + "analyze_and_plot_best_methods(results_ham, \"size\")" + ] + }, + { + "cell_type": "markdown", + "id": "e4f6aa23-d528-4d66-92e0-31d29c522792", + "metadata": {}, + "source": [ + "#### Analysis of Hamiltonian circuit compilation results\n", + "\n", + "In this section, we evaluate the performance of three transpilation methods — SABRE, the AI-powered transpiler, and Rustiq — on quantum circuits constructed with `PauliEvolutionGate`, which are commonly used in Hamiltonian simulation tasks.\n", + "\n", + "Rustiq performed best on average in terms of circuit depth**, achieving approximately 20% lower depth than SABRE. This is expected, as Rustiq is specifically designed to synthesize `PauliEvolutionGate` operations with optimized, low-depth decomposition strategies. Furthermore, the depth plot shows that as the circuits scale in size and complexity, Rustiq scales most effectively, maintaining significantly lower depth than both AI and SABRE on larger circuits.\n", + "\n", + "AI transpiler showed strong and consistent performance for circuit depth, consistently outperforming SABRE across most circuits. However, it incurred the highest runtime, especially on larger circuits, which may limit its practicality in time-sensitive workloads. Its scalability in runtime remains a key limitation, even though it offers solid improvements in depth.\n", + "\n", + "SABRE, while producing the highest average depth, achieved the lowest average gate count, closely followed by the AI transpiler. This aligns with the design of SABRE’s heuristic, which prioritizes minimizing gate count directly. Rustiq, despite its strength in lowering depth, had the highest average gate count, which is a notable trade-off to consider in applications where circuit size matters more than circuit duration.\n", + "\n", + "### Summary\n", + "\n", + "While the AI transpiler generally delivers better results than SABRE, particularly in circuit depth, the takeaway should not simply be \"always use the AI transpiler.\" There are important nuances to consider:\n", + "\n", + "- **AI transpiler** is typically reliable and provides depth-optimized circuits, but it comes with trade-offs in runtime, and also has other limitations, including supported coupling maps and synthesis capabilities. These are detailed in the [Qiskit Transpiler Service documentation](/docs/guides/qiskit-transpiler-service).\n", + "\n", + "- In some cases, particularly with very large or hardware-specific circuits, the AI transpiler may not be as effective. In these cases, the default SABRE transpiler remains extremely reliable and can be further optimized by adjusting its parameters (see the [SABRE optimization tutorial](/docs/tutorials/transpilation-optimizations-with-sabre)).\n", + "\n", + "- It's also important to consider circuit structure when choosing a method. For example, `rustiq` is purpose-built for circuits involving `PauliEvolutionGate` and often yields the best performance for Hamiltonian simulation problems.\n", + "\n", + "**Recommendation:**\n", + "> There is no one-size-fits-all transpilation strategy. Users are encouraged to understand the structure of their circuit and test multiple transpilation methods — including AI, SABRE, and specialized tools like Rustiq — to find the most efficient solution for their specific problem and hardware constraints." + ] + }, + { + "cell_type": "markdown", + "id": "9a747477-1dc1-4706-b844-d29570fb5844", + "metadata": {}, + "source": [ + "### Step 3: Execute using Qiskit primitives" + ] + }, + { + "cell_type": "markdown", + "id": "d3d6d473-7508-4e28-82ad-eee09c9a1acf", + "metadata": {}, + "source": [ + "As this tutorial focuses on transpilation, no experiments are executed on a quantum device. The goal is to leverage the optimizations from Step 2 to obtain a transpiled circuit with reduced depth and gate count." + ] + }, + { + "cell_type": "markdown", + "id": "bb4c4c67-a7e8-4ca1-a5d6-908a2e1d27e5", + "metadata": {}, + "source": [ + "### Step 4: Post-process and return result in desired classical format" + ] + }, + { + "cell_type": "markdown", + "id": "4aa45929-f8d9-4be8-9008-0588615fa45e", + "metadata": {}, + "source": [ + "Since there is no execution for this notebook, there are no results to post-process." + ] + }, + { + "cell_type": "markdown", + "id": "552ef44b-e567-4870-9699-d152806b9505", + "metadata": {}, + "source": [ + "## References\n", + "\n", + "[1] \"LightSABRE: A Lightweight and Enhanced SABRE Algorithm\". H. Zou, M. Treinish, K. Hartman, A. Ivrii, J. Lishman et al. https://arxiv.org/abs/2409.08368\n", + "\n", + "[2] \"Practical and efficient quantum circuit synthesis and transpiling with Reinforcement Learning\". D. Kremer, V. Villar, H. Paik, I. Duran, I. Faro, J. Cruz-Benito et al. https://arxiv.org/abs/2405.13196\n", + "\n", + "[3] \"Pauli Network Circuit Synthesis with Reinforcement Learning\". A. Dubal, D. Kremer, S. Martiel, V. Villar, D. Wang, J. Cruz-Benito et al. https://arxiv.org/abs/2503.14448\n", + "\n", + "[4] \"Faster and shorter synthesis of Hamiltonian simulation circuits\". T. Goubault de Brugière, S. Martiel et al. https://arxiv.org/abs/2404.03280" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/docs/tutorials/long-range-entanglement.ipynb b/docs/tutorials/long-range-entanglement.ipynb index 55d5c305a0b..230def2f851 100644 --- a/docs/tutorials/long-range-entanglement.ipynb +++ b/docs/tutorials/long-range-entanglement.ipynb @@ -194,7 +194,8 @@ "\n", " qc = QuantumCircuit(qr, *allcr, name=\"CNOT\")\n", "\n", - " # Apply a Hadamard gate to the control qubit such that the long-range CNOT gate will prepare a Bell state (|00> + |11>)/sqrt(2)\n", + " # Apply a Hadamard gate to the control qubit such that the long-range CNOT gate will prepare a\n", + " # Bell state (|00> + |11>)/sqrt(2)\n", " qc.h(control)\n", "\n", " return qc\n", @@ -301,7 +302,8 @@ " # Determine where to start the Bell pair chain and add an extra CNOT when n is odd\n", " x0 = 1 if n % 2 == 0 else 2\n", "\n", - " # Entangling layer that implements the Bell measurement (and additionally adds the CNOT to be teleported, if n is even)\n", + " # Entangling layer that implements the Bell measurement (and additionally adds the CNOT to be\n", + " # teleported, if n is even)\n", " for i in range(k + 1):\n", " qc.cx(x0 - 1 + 2 * i, x0 + 2 * i)\n", "\n", @@ -1435,7 +1437,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": null, "id": "3dcff343", "metadata": {}, "outputs": [ @@ -1450,7 +1452,8 @@ } ], "source": [ - "# Compute metrics for each distance, skipping the basis circuits since they are identical for each distance\n", + "# Compute metrics for each distance, skipping the basis circuits since\n", + "# they are identical for each distance\n", "depths_2q_dyn = [\n", " c.depth(lambda x: x.operation.num_qubits == 2)\n", " for c in isa_circuits_dyn[::3]\n", diff --git a/docs/tutorials/nishimori-phase-transition.ipynb b/docs/tutorials/nishimori-phase-transition.ipynb index a5b3ac16fcf..a30a3836c83 100644 --- a/docs/tutorials/nishimori-phase-transition.ipynb +++ b/docs/tutorials/nishimori-phase-transition.ipynb @@ -1,717 +1,718 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "a10c2a41-ab48-4da5-bbe1-a16fd47906fe", - "metadata": {}, - "source": [ - "---\n", - "title: Nishimori phase transition\n", - "description: This tutorial demonstrates how to realize a Nishimori phase transition on an IBM quantum processor.\n", - "---\n", - "\n", - "\n", - "# Nishimori phase transition\n", - "*Usage estimate: 3 minutes on a Heron r2 processor (NOTE: This is an estimate only. Your runtime might vary.)*" - ] - }, - { - "cell_type": "markdown", - "id": "838ac5c7-0819-4482-8875-c57db88ba82a", - "metadata": {}, - "source": [ - "## Background\n", - "This tutorial demonstrates how to realize a Nishimori phase transition on an IBM® quantum processor. This experiment was originally described in [*Realizing the Nishimori transition across the error threshold for constant-depth quantum circuits*](https://arxiv.org/abs/2309.02863).\n", - "\n", - "The Nishimori phase transition refers to the transition between short- and long-range ordered phases in the random-bond Ising model. On a quantum computer, the long-range ordered phase manifests as a state in which qubits are entangled across the entire device. This highly entangled state is prepared using the *generation of entanglement by measurement* (GEM) protocol. By utilizing mid-circuit measurements, the GEM protocol is able to entangle qubits across the entire device using circuits of only constant depth. This tutorial uses the implementation of the GEM protocol from the [GEM Suite](https://github.com/qiskit-community/gem-suite) software package." - ] - }, - { - "cell_type": "markdown", - "id": "4091005d-ecd5-40c9-b90f-263e49a4cc16", - "metadata": {}, - "source": [ - "## Requirements\n", - "Before starting this tutorial, be sure you have the following installed:\n", - "\n", - "- Qiskit SDK v1.0 or later, with [visualization](/docs/api/qiskit/visualization) support\n", - "- Qiskit Runtime v0.22 or later ( `pip install qiskit-ibm-runtime` )\n", - "- GEM Suite ( `pip install gem-suite` )" - ] - }, - { - "cell_type": "markdown", - "id": "210b8730-ca3e-46d9-b8b0-64c36f13df8e", - "metadata": {}, - "source": [ - "## Setup" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "4ad70734-54f8-400a-8399-188ee8a0f3ac", - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "from collections import defaultdict\n", - "\n", - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "\n", - "from qiskit.transpiler import generate_preset_pass_manager\n", - "\n", - "from gem_suite import PlaquetteLattice\n", - "from gem_suite.experiments import GemExperiment" - ] - }, - { - "cell_type": "markdown", - "id": "39f59f3a-eb2a-4160-9da2-2bb83c58771c", - "metadata": {}, - "source": [ - "## Step 1: Map classical inputs to a quantum problem\n", - "\n", - "The GEM protocol works on a quantum processor with qubit connectivity described by a lattice. Today's IBM quantum processors use the [heavy hex lattice](https://www.ibm.com/quantum/blog/heavy-hex-lattice). The qubits of the processor are grouped into *plaquettes* based on which unit cell of the lattice they occupy. Because a qubit might occur in more than one unit cell, the plaquettes are not disjoint. On the heavy hex lattice, a plaquette contains 12 qubits. The plaquettes themselves also form lattice, where two plaquettes are connected if they share any qubits. On the heavy hex lattice, neighboring plaquettes share 3 qubits.\n", - "\n", - "In the GEM Suite software package, the fundamental class for implementing the GEM protocol is `PlaquetteLattice`, which represents the lattice of plaquettes (which is distinct from the heavy hex lattice). A `PlaquetteLattice` can be initialized from a qubit coupling map. Currently, only heavy hex coupling maps are supported.\n", - "\n", - "The following code cell initializes a plaquette lattice from the coupling map of a IBM quantum processor. The plaquette lattice does not always encompass the entire hardware. For example, `ibm_torino` has 133 total qubits, but the largest plaquette lattice that fits on the device uses only 125 of them, and comprises a total of 18 plaquettes. Similar can be observed for IBM Quantum® devices with different qubit counts as well." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "553dbafe-1778-4971-83c3-0408605b701d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Number of qubits in backend: 133\n", - "Number of qubits in plaquette lattice: 125\n", - "Number of plaquettes: 18\n" - ] - } - ], - "source": [ - "# QiskitRuntimeService.save_account(channel=\"ibm_quantum\", token=\"\", overwrite=True, set_as_default=True)\n", - "service = QiskitRuntimeService()\n", - "backend = service.least_busy(\n", - " operational=True, simulator=False, min_num_qubits=127\n", - ")\n", - "plaquette_lattice = PlaquetteLattice.from_coupling_map(backend.coupling_map)\n", - "\n", - "print(f\"Number of qubits in backend: {backend.num_qubits}\")\n", - "print(\n", - " f\"Number of qubits in plaquette lattice: {len(list(plaquette_lattice.qubits()))}\"\n", - ")\n", - "print(f\"Number of plaquettes: {len(list(plaquette_lattice.plaquettes()))}\")" - ] - }, - { - "cell_type": "markdown", - "id": "0bf43539-d782-44a9-ba4e-a1b6c7510803", - "metadata": {}, - "source": [ - "You can visualize the plaquette lattice by generating a diagram of its graph representation. In the diagram, the plaquettes are represented by labeled hexagons, and two plaquettes are connected by an edge if they share qubits." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "625882a4-faeb-4d96-b441-c989f43c4dea", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plaquette_lattice.draw_plaquettes()" - ] - }, - { - "cell_type": "markdown", - "id": "2f0a6b73-d1d2-4117-80cf-6fe3adbb82c5", - "metadata": {}, - "source": [ - "You can retrieve information about individual plaquettes, such as the qubits they contain, using the `plaquettes` method." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "e412a612-c7d5-4689-840a-2383dd538f06", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "PyPlaquette(index=0, qubits=[0, 1, 2, 3, 4, 15, 16, 19, 20, 21, 22, 23], neighbors=[3, 1])" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Get a list of the plaquettes\n", - "plaquettes = list(plaquette_lattice.plaquettes())\n", - "# Display information about plaquette 0\n", - "plaquettes[0]" - ] - }, - { - "cell_type": "markdown", - "id": "75edf1b4-f95f-4ff7-9e3c-2e810711636f", - "metadata": {}, - "source": [ - "You can also produce a diagram of the underlying qubits that form the plaquette lattice." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "a19d63ce-3572-4081-a008-c1332fbbe303", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plaquette_lattice.draw_qubits()" - ] - }, - { - "cell_type": "markdown", - "id": "e7b0f54b-5bc2-42bb-b4d4-1346f0a902e3", - "metadata": {}, - "source": [ - "In addition to the qubit labels and the edges indicating which qubits are connected, the diagram contains three additional pieces of information that are relevant to the GEM protocol:\n", - "- Each qubit is either shaded (gray) or unshaded. The shaded qubits are \"site\" qubits that represent the sites of the Ising model, and the unshaded qubits are \"bond\" qubits used to mediate interactions between the site qubits.\n", - "- Each site qubit is labeled either (A) or (B), indicating one of two roles a site qubit can play in the GEM protocol (the roles are explained later).\n", - "- Each edge is colored using one of six colors, thus partitioning the edges into six groups. This partitioning determines how two-qubit gates can be parallelized, as well as different scheduling patterns that are likely to incur different amounts of error on a noisy quantum processor. Because edges in a group are disjoint, a layer of two-qubit gates can be applied on those edges simultaneously. In fact, it is possible to partition the six colors into three groups of two colors such that the union of each group of two colors is still disjoint. Therefore, only three layers of two-qubit gates are needed to activate every edge. There are 12 ways to so partition the six colors, and each such partition yields a different 3-layer gate schedule.\n", - "\n", - "Now that you have created a plaquette lattice, the next step is to initialize a `GemExperiment` object, passing both the plaquette lattice and the backend that you intend to run the experiment on. The `GemExperiment` class manages the actual implementation of the GEM protocol, including generating circuits, submitting jobs, and analyzing the data. The following code cell initializes the experiment class while restricting the plaquette lattice to only two of the plaquettes (21 qubits), reducing the size of the experiment to ensure that the noise in the hardware doesn't overwhelm the signal." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "02357c6e-5c83-4ac0-811d-22602d9f33d5", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "gem_exp = GemExperiment(plaquette_lattice.filter([9, 12]), backend=backend)\n", - "\n", - "# visualize the plaquette lattice after filtering\n", - "plaquette_lattice.filter([9, 12]).draw_qubits()" - ] - }, - { - "cell_type": "markdown", - "id": "3234e018-a1a8-47b7-b10c-57d6f7f77ff1", - "metadata": {}, - "source": [ - "A GEM protocol circuit is built using the following steps:\n", - "1. Prepare the all-$|+\\rangle$ state by applying a Hadamard gate to every qubit.\n", - "2. Apply an $R_{ZZ}$ gate between every pair of connected qubits. This can be achieved using 3 layers of gates. Each $R_{ZZ}$ gate acts on a site qubit and a bond qubit. If the site qubit is labeled (B), then the angle is fixed to $\\frac{\\pi}{2}$. If the site qubit is labeled (A), then the angle is allowed to vary, producing different circuits. By default, the range of angles is set to 21 equally spaced points between $0$ and $\\frac{\\pi}{2}$, inclusive.\n", - "3. Measure each bond qubit in the Pauli $X$ basis. Since qubits are measured in the Pauli $Z$ basis, this can be accomplished by applying a Hadamard gate before measuring the qubit.\n", - "\n", - "Note that the paper cited in the introduction to this tutorial uses a different convention for the $R_{ZZ}$ angle, which differs from the convention used in this tutorial by a factor of 2.\n", - "\n", - "In step 3, only the bond qubits are measured. To understand what state the site qubits remain in, it is instructive to consider the case that the $R_{ZZ}$ angle applied to site qubits (A) in step 2 is equal to $\\frac{\\pi}{2}$. In this case, the site qubits are left in a highly entangled state similar to the GHZ state,\n", - "\n", - "$$\n", - "\\lvert \\text{GHZ} \\rangle = \\lvert 00 \\cdots 00 \\rangle + \\lvert 11 \\cdots 11 \\rangle.\n", - "$$\n", - "\n", - "Due to the randomness in the measurement outcomes, the actual state of the site qubits might be a different state with long-range order, for example, $\\lvert 00110 \\rangle + \\lvert 11001 \\rangle$. However, the GHZ state can be recovered by applying a decoding operation based on the measurement outcomes. When the $R_{ZZ}$ angle is tuned down from $\\frac{\\pi}{2}$, the long-range order can still be recovered up until a critical angle, which in the absence of noise, is approximately $0.3 \\pi$. Below this angle, the resulting state no longer exhibits long-range entanglement. This transition between the presence and absence of long-range order is the Nishimori phase transition.\n", - "\n", - "In the description above, the site qubits were left unmeasured, and the decoding operation can be performed by applying quantum gates. In the experiment as implemented in the GEM suite, which this tutorial follows, the site qubits are in fact measured, and the decoding operation is applied in a classical post-processing step.\n", - "\n", - "In the description above, the decoding operation can be performed by applying quantum gates to the site qubits to recover the quantum state. However, if the goal is to immediately measure the state, for example, for characterization purposes, then the site qubits are measured together with the bond qubits, and the decoding operation can be applied in a classical post-processing step. This is how the experiment is implemented in the GEM suite, which this tutorial follows.\n", - "\n", - "In addition to depending on the $R_{ZZ}$ angle in step 2, which by default sweeps across 21 values, the GEM protocol circuit also depends on the scheduling pattern used to implement the 3 layers of $R_{ZZ}$ gates. As discussed previously, there are 12 such scheduling patterns. Therefore, the total number of circuits in the experiment is $21 \\times 12 = 252$.\n", - "\n", - "The circuits of the experiment can be generated using the `circuits` method of the `GemExperiment` class." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "8e2ade62-9a57-42c3-9a85-3fe2dec3c426", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Total number of circuits: 252\n" - ] - } - ], - "source": [ - "circuits = gem_exp.circuits()\n", - "print(f\"Total number of circuits: {len(circuits)}\")" - ] - }, - { - "cell_type": "markdown", - "id": "56c3c656-dcfa-4ca0-9df9-2d97c187c685", - "metadata": {}, - "source": [ - "For the purposes of this tutorial, it is enough to consider just a single scheduling pattern. The following code cell restricts the experiment to the first scheduling pattern. As a result, the experiment only has 21 circuits, one for each $R_{ZZ}$ angle swept over." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "4f8a2c73-752d-47b9-95d5-83439933fc08", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Total number of circuits: 21\n", - "RZZ angles:\n", - "[0. 0.07853982 0.15707963 0.23561945 0.31415927 0.39269908\n", - " 0.4712389 0.54977871 0.62831853 0.70685835 0.78539816 0.86393798\n", - " 0.9424778 1.02101761 1.09955743 1.17809725 1.25663706 1.33517688\n", - " 1.41371669 1.49225651 1.57079633]\n" - ] - } - ], - "source": [ - "# Restrict experiment to the first scheduling pattern\n", - "gem_exp.set_experiment_options(schedule_idx=0)\n", - "\n", - "# There are less circuits now\n", - "circuits = gem_exp.circuits()\n", - "print(f\"Total number of circuits: {len(circuits)}\")\n", - "\n", - "# Print the RZZ angles swept over\n", - "print(f\"RZZ angles:\\n{gem_exp.parameters()}\")" - ] - }, - { - "cell_type": "markdown", - "id": "7011dc79-8561-42ef-909b-00fd8be9ef34", - "metadata": {}, - "source": [ - "The following code cell draws a diagram of the circuit at index 5. To reduce the size of the diagram, the measurement gates at the end of the circuit are removed." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "fd57d483-c70b-4ad5-b309-15750ad38bac", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Get the circuit at index 5\n", - "circuit = circuits[5]\n", - "# Remove the final measurements to ease visualization\n", - "circuit.remove_final_measurements()\n", - "# Draw the circuit\n", - "circuit.draw(\"mpl\", fold=-1, scale=0.5)" - ] - }, - { - "cell_type": "markdown", - "id": "a3aa063b-44cf-49f3-9e12-23b2f6a1c85b", - "metadata": {}, - "source": [ - "## Step 2: Optimize problem for quantum hardware execution\n", - "\n", - "Transpiling quantum circuits for execution on hardware typically involves a [number of stages](/docs/guides/transpiler-stages). Typically, the stages that incur the most computational overhead are choosing the qubit layout, routing the two-qubit gates to conform to the qubit connectivity of the hardware, and optimizing the circuit to minimize its gate count and depth. In the GEM protocol, the layout and routing stages are unnecessary because the hardware connectivity is already incorporated into the design of the protocol. The circuits already have a qubit layout, and the two-qubit gates are already mapped onto native connections. Furthermore, in order to preserve the structure of the circuit as the $R_{ZZ}$ angle is varied, only very basic circuit optimization should be performed.\n", - "\n", - "The `GemExperiment` class transparently transpiles circuits when executing the experiment. The layout and routing stages are already overridden by default to do nothing, and circuit optimization is performed at a level that only optimizes single-qubit gates. However, you can override or pass additional options using the `set_transpile_options` method. For the sake of visualization, the following code cell manually transpiles the circuit displayed previously, and draws the transpiled circuit." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "e9b99d48-8d33-46b5-bff5-480ab1c1c1f2", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Demonstrate setting transpile options\n", - "gem_exp.set_transpile_options(\n", - " optimization_level=1 # This is the default optimization level\n", - ")\n", - "pass_manager = generate_preset_pass_manager(\n", - " backend=backend,\n", - " initial_layout=list(gem_exp.physical_qubits),\n", - " **dict(gem_exp.transpile_options),\n", - ")\n", - "transpiled = pass_manager.run(circuit)\n", - "transpiled.draw(\"mpl\", idle_wires=False, fold=-1, scale=0.5)" - ] - }, - { - "cell_type": "markdown", - "id": "8d0dcd59-54ef-4af8-9213-0784ef94b838", - "metadata": {}, - "source": [ - "## Step 3: Execute using Qiskit primitives\n", - "\n", - "To execute the GEM protocol circuits on the hardware, call the `run` method of the `GemExperiment` object. You can specify the number of shots you want to sample from each circuit. The `run` method returns an [ExperimentData](https://qiskit-community.github.io/qiskit-experiments/stubs/qiskit_experiments.framework.ExperimentData.html) object which you should save to a variable. Note that the `run` method only submits jobs without waiting for them to finish, so it is a non-blocking call." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "6171a383-dd58-4e3f-88aa-bbec7b5870df", - "metadata": {}, - "outputs": [], - "source": [ - "exp_data = gem_exp.run(shots=10_000)" - ] - }, - { - "cell_type": "markdown", - "id": "71e81552-0d33-4950-8d45-e6c0a8a056c9", - "metadata": {}, - "source": [ - "To wait for the results, call the `block_for_results` method of the `ExperimentData` object. This call will cause the interpreter to hang until the jobs are finished." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "ed14a067-35ba-4ffc-8534-4ae5ec6bc4c9", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "ExperimentData(GemExperiment, d0d5880a-34c1-4aab-a7b6-c4f58516bc03, job_ids=['cwg12ptmptp00082khhg'], metadata=<5 items>, figure_names=['two_point_correlation.svg', 'normalized_variance.svg', 'plaquette_ops.svg', 'bond_ops.svg'])" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "exp_data.block_for_results()" - ] - }, - { - "cell_type": "markdown", - "id": "36e0570b-f091-45f2-bb83-143edbc3b433", - "metadata": {}, - "source": [ - "## Step 4: Post-process and return result in desired classical format\n", - "\n", - "At an $R_{ZZ}$ angle of $\\frac{\\pi}{2}$, the decoded state would be the GHZ state in the absence of noise. The long-range order of the GHZ state can be visualized by plotting the magnetization of the measured bitstrings. The magnetization $M$ is defined as the sum of the single-qubit Pauli $Z$ operators,\n", - "$$\n", - "M = \\sum_{j=1}^N Z_j,\n", - "$$\n", - "where $N$ is the number of site qubits. Its value for a bitstring is equal to the difference between the number of zeros and the number of ones. Measuring the GHZ state yields the all zeros state or the all ones state with equal probability, so the magnetization would be $+N$ half of the time and $-N$ the other half of the time. In the presence of errors due to noise, other values would also appear, but if the noise is not too great, the distribution would still be peaked near $+N$ and $-N$.\n", - "\n", - "For the raw bitstrings before decoding, the distribution of the magnetization would be equivalent to that of uniformly random bitstrings, in the absence of noise.\n", - "\n", - "The following code cell plots the magnetization of the raw bitstrings and the decoded bitstrings at the $R_{ZZ}$ angle of $\\frac{\\pi}{2}$." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8ead3582-16df-4616-836c-bdce867ad6b8", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Text(0.5, 1.0, 'Magnetization distribution with and without decoding')" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "def magnetization_distribution(\n", - " counts_dict: dict[str, int],\n", - ") -> dict[str, float]:\n", - " \"\"\"Compute magnetization distribution from counts dictionary.\"\"\"\n", - " # Construct dictionary from magnetization to count\n", - " mag_dist = defaultdict(float)\n", - " for bitstring, count in counts_dict.items():\n", - " mag = bitstring.count(\"0\") - bitstring.count(\"1\")\n", - " mag_dist[mag] += count\n", - " # Normalize\n", - " shots = sum(counts_dict.values())\n", - " for mag in mag_dist:\n", - " mag_dist[mag] /= shots\n", - " return mag_dist\n", - "\n", - "\n", - "# Get counts dictionaries with and without decoding\n", - "data = exp_data.data()\n", - "# Get the last data point, which is at the angle for the GHZ state\n", - "raw_counts = data[-1][\"counts\"]\n", - "# Without decoding\n", - "site_indices = [\n", - " i for i, q in enumerate(gem_exp.plaquettes.qubits()) if q.role == \"Site\"\n", - "]\n", - "site_raw_counts = defaultdict(int)\n", - "for key, val in raw_counts.items():\n", - " site_str = \"\".join(key[-1 - i] for i in site_indices)\n", - " site_raw_counts[site_str] += val\n", - "# With decoding\n", - "_, site_decoded_counts = gem_exp.plaquettes.decode_outcomes(\n", - " raw_counts, return_counts=True\n", - ")\n", - "\n", - "# Compute magnetization distribution\n", - "raw_magnetization = magnetization_distribution(site_raw_counts)\n", - "decoded_magnetization = magnetization_distribution(site_decoded_counts)\n", - "\n", - "# Plot\n", - "plt.bar(*zip(*raw_magnetization.items()), label=\"raw\")\n", - "plt.bar(*zip(*decoded_magnetization.items()), label=\"decoded\", width=0.3)\n", - "plt.legend()\n", - "plt.xlabel(\"Magnetization\")\n", - "plt.ylabel(\"Frequency\")\n", - "plt.title(\"Magnetization distribution with and without decoding\")" - ] - }, - { - "cell_type": "markdown", - "id": "90a7ae7a-5175-421f-bda9-bc6b986bdf5f", - "metadata": {}, - "source": [ - "To more rigorously characterize the long-range order, you can examine the average two-point correlation $f$, defined as\n", - "$$\n", - "f = \\frac{1}{N^2} \\left(\\langle M^2 \\rangle - \\langle M \\rangle ^2\\right).\n", - "$$\n", - "A higher value indicates a greater degree of entanglement. The `GemExperiment` class automatically computes this value for the decoded bitstrings as part of processing the experimental data. It stores a figure that is accessible via the `figure` method of the experiment data class. In this case, the name of the figure is `two_point_correlation`." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "4ecb25c8-e572-49af-a879-9943039db131", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "exp_data.figure(\"two_point_correlation\")" - ] - }, - { - "cell_type": "markdown", - "id": "f2f3e7e2-2a8b-4790-8ba7-b190c4ed1049", - "metadata": {}, - "source": [ - "To determine the critical point of the Nishimori phase transition, you can look at the normalized variance of $M^2 / N$, defined as\n", - "$$\n", - "g = \\frac{1}{N^3} \\left(\\langle M^4 \\rangle - \\langle M^2 \\rangle^2\\right),\n", - "$$\n", - "which quantifies the amount of fluctuation in the squared magnetization. This value is maximized at the critical point of the Nishimori phase transition. In the absence of noise, the critical point occurs at approximately $0.3 \\pi$. In the presence of noise, the critical point is shifted higher, but the phase transition is still observed as long as the critical point occurs below $0.5 \\pi$." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "2b351d68-3924-445a-94ef-047b16214e8a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "exp_data.figure(\"normalized_variance\")" - ] - }, - { - "cell_type": "markdown", - "id": "a94e0cbe-8429-487c-b203-50a8b2eacee3", - "metadata": {}, - "source": [ - "## Scale up the experiment\n", - "\n", - "The following code cells run the experiment for six plaquettes (49 qubits) and the full 12 plaquettes (125 qubits) and plot the normalized variance. As the experiment is scaled to larger sizes, the greater amount of noise shifts the critical point rightwards." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "08581c09-a6a5-4a56-9fc4-abf22b063c6a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "gem_exp = GemExperiment(\n", - " plaquette_lattice.filter(range(3, 9)), backend=backend\n", - ")\n", - "gem_exp.set_experiment_options(schedule_idx=0)\n", - "exp_data = gem_exp.run(shots=10_000)\n", - "exp_data.block_for_results()\n", - "exp_data.figure(\"normalized_variance\")" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "37e9a4cd-6efb-4ade-ad09-8139db9d58e9", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "gem_exp = GemExperiment(plaquette_lattice, backend=backend)\n", - "gem_exp.set_experiment_options(schedule_idx=0)\n", - "exp_data = gem_exp.run(shots=10_000)\n", - "exp_data.block_for_results()\n", - "exp_data.figure(\"normalized_variance\")" - ] - }, - { - "cell_type": "markdown", - "id": "6abd9701-58e4-43a8-a1d7-279506570de4", - "metadata": {}, - "source": [ - "## Conclusion\n", - "\n", - "In this tutorial, you realized a Nishimori phase transition on a quantum processor using the GEM protocol. The metrics that you examined during post-processing, in particular the two-point correlation and the normalized variance, serve as benchmarks of the device's ability to generate long-range entangled states. These benchmarks extend the utility of the GEM protocol beyond probing interesting physics. As part of the protocol, you entangled qubits across the entire device using circuits of only constant depth. This feat is only possible due to the protocol's use of mid-circuit measurements. In this experiment, the entangled state was immediately measured, but an interesting avenue to explore would be to continue using the state in additional quantum processing!" - ] - }, - { - "cell_type": "markdown", - "id": "b9562a76", - "metadata": {}, - "source": [ - "## Tutorial survey\n", - "\n", - "Please take this short survey to provide feedback on this tutorial. Your insights will help us improve our content offerings and user experience.\n", - "\n", - "[Link to survey](https://your.feedback.ibm.com/jfe/form/SV_bsCKQkgzuQUQ7ky)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a10c2a41-ab48-4da5-bbe1-a16fd47906fe", + "metadata": {}, + "source": [ + "---\n", + "title: Nishimori phase transition\n", + "description: This tutorial demonstrates how to realize a Nishimori phase transition on an IBM quantum processor.\n", + "---\n", + "\n", + "\n", + "# Nishimori phase transition\n", + "*Usage estimate: 3 minutes on a Heron r2 processor (NOTE: This is an estimate only. Your runtime might vary.)*" + ] + }, + { + "cell_type": "markdown", + "id": "838ac5c7-0819-4482-8875-c57db88ba82a", + "metadata": {}, + "source": [ + "## Background\n", + "This tutorial demonstrates how to realize a Nishimori phase transition on an IBM® quantum processor. This experiment was originally described in [*Realizing the Nishimori transition across the error threshold for constant-depth quantum circuits*](https://arxiv.org/abs/2309.02863).\n", + "\n", + "The Nishimori phase transition refers to the transition between short- and long-range ordered phases in the random-bond Ising model. On a quantum computer, the long-range ordered phase manifests as a state in which qubits are entangled across the entire device. This highly entangled state is prepared using the *generation of entanglement by measurement* (GEM) protocol. By utilizing mid-circuit measurements, the GEM protocol is able to entangle qubits across the entire device using circuits of only constant depth. This tutorial uses the implementation of the GEM protocol from the [GEM Suite](https://github.com/qiskit-community/gem-suite) software package." + ] + }, + { + "cell_type": "markdown", + "id": "4091005d-ecd5-40c9-b90f-263e49a4cc16", + "metadata": {}, + "source": [ + "## Requirements\n", + "Before starting this tutorial, be sure you have the following installed:\n", + "\n", + "- Qiskit SDK v1.0 or later, with [visualization](/docs/api/qiskit/visualization) support\n", + "- Qiskit Runtime v0.22 or later ( `pip install qiskit-ibm-runtime` )\n", + "- GEM Suite ( `pip install gem-suite` )" + ] + }, + { + "cell_type": "markdown", + "id": "210b8730-ca3e-46d9-b8b0-64c36f13df8e", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "4ad70734-54f8-400a-8399-188ee8a0f3ac", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "from collections import defaultdict\n", + "\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "\n", + "from qiskit.transpiler import generate_preset_pass_manager\n", + "\n", + "from gem_suite import PlaquetteLattice\n", + "from gem_suite.experiments import GemExperiment" + ] + }, + { + "cell_type": "markdown", + "id": "39f59f3a-eb2a-4160-9da2-2bb83c58771c", + "metadata": {}, + "source": [ + "## Step 1: Map classical inputs to a quantum problem\n", + "\n", + "The GEM protocol works on a quantum processor with qubit connectivity described by a lattice. Today's IBM quantum processors use the [heavy hex lattice](https://www.ibm.com/quantum/blog/heavy-hex-lattice). The qubits of the processor are grouped into *plaquettes* based on which unit cell of the lattice they occupy. Because a qubit might occur in more than one unit cell, the plaquettes are not disjoint. On the heavy hex lattice, a plaquette contains 12 qubits. The plaquettes themselves also form lattice, where two plaquettes are connected if they share any qubits. On the heavy hex lattice, neighboring plaquettes share 3 qubits.\n", + "\n", + "In the GEM Suite software package, the fundamental class for implementing the GEM protocol is `PlaquetteLattice`, which represents the lattice of plaquettes (which is distinct from the heavy hex lattice). A `PlaquetteLattice` can be initialized from a qubit coupling map. Currently, only heavy hex coupling maps are supported.\n", + "\n", + "The following code cell initializes a plaquette lattice from the coupling map of a IBM quantum processor. The plaquette lattice does not always encompass the entire hardware. For example, `ibm_torino` has 133 total qubits, but the largest plaquette lattice that fits on the device uses only 125 of them, and comprises a total of 18 plaquettes. Similar can be observed for IBM Quantum® devices with different qubit counts as well." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "553dbafe-1778-4971-83c3-0408605b701d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of qubits in backend: 133\n", + "Number of qubits in plaquette lattice: 125\n", + "Number of plaquettes: 18\n" + ] + } + ], + "source": [ + "# QiskitRuntimeService.save_account(channel=\"ibm_quantum\", token=\"\", overwrite=True,\n", + "# set_as_default=True)\n", + "service = QiskitRuntimeService()\n", + "backend = service.least_busy(\n", + " operational=True, simulator=False, min_num_qubits=127\n", + ")\n", + "plaquette_lattice = PlaquetteLattice.from_coupling_map(backend.coupling_map)\n", + "\n", + "print(f\"Number of qubits in backend: {backend.num_qubits}\")\n", + "print(\n", + " f\"Number of qubits in plaquette lattice: {len(list(plaquette_lattice.qubits()))}\"\n", + ")\n", + "print(f\"Number of plaquettes: {len(list(plaquette_lattice.plaquettes()))}\")" + ] + }, + { + "cell_type": "markdown", + "id": "0bf43539-d782-44a9-ba4e-a1b6c7510803", + "metadata": {}, + "source": [ + "You can visualize the plaquette lattice by generating a diagram of its graph representation. In the diagram, the plaquettes are represented by labeled hexagons, and two plaquettes are connected by an edge if they share qubits." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "625882a4-faeb-4d96-b441-c989f43c4dea", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plaquette_lattice.draw_plaquettes()" + ] + }, + { + "cell_type": "markdown", + "id": "2f0a6b73-d1d2-4117-80cf-6fe3adbb82c5", + "metadata": {}, + "source": [ + "You can retrieve information about individual plaquettes, such as the qubits they contain, using the `plaquettes` method." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "e412a612-c7d5-4689-840a-2383dd538f06", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "PyPlaquette(index=0, qubits=[0, 1, 2, 3, 4, 15, 16, 19, 20, 21, 22, 23], neighbors=[3, 1])" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Get a list of the plaquettes\n", + "plaquettes = list(plaquette_lattice.plaquettes())\n", + "# Display information about plaquette 0\n", + "plaquettes[0]" + ] + }, + { + "cell_type": "markdown", + "id": "75edf1b4-f95f-4ff7-9e3c-2e810711636f", + "metadata": {}, + "source": [ + "You can also produce a diagram of the underlying qubits that form the plaquette lattice." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "a19d63ce-3572-4081-a008-c1332fbbe303", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plaquette_lattice.draw_qubits()" + ] + }, + { + "cell_type": "markdown", + "id": "e7b0f54b-5bc2-42bb-b4d4-1346f0a902e3", + "metadata": {}, + "source": [ + "In addition to the qubit labels and the edges indicating which qubits are connected, the diagram contains three additional pieces of information that are relevant to the GEM protocol:\n", + "- Each qubit is either shaded (gray) or unshaded. The shaded qubits are \"site\" qubits that represent the sites of the Ising model, and the unshaded qubits are \"bond\" qubits used to mediate interactions between the site qubits.\n", + "- Each site qubit is labeled either (A) or (B), indicating one of two roles a site qubit can play in the GEM protocol (the roles are explained later).\n", + "- Each edge is colored using one of six colors, thus partitioning the edges into six groups. This partitioning determines how two-qubit gates can be parallelized, as well as different scheduling patterns that are likely to incur different amounts of error on a noisy quantum processor. Because edges in a group are disjoint, a layer of two-qubit gates can be applied on those edges simultaneously. In fact, it is possible to partition the six colors into three groups of two colors such that the union of each group of two colors is still disjoint. Therefore, only three layers of two-qubit gates are needed to activate every edge. There are 12 ways to so partition the six colors, and each such partition yields a different 3-layer gate schedule.\n", + "\n", + "Now that you have created a plaquette lattice, the next step is to initialize a `GemExperiment` object, passing both the plaquette lattice and the backend that you intend to run the experiment on. The `GemExperiment` class manages the actual implementation of the GEM protocol, including generating circuits, submitting jobs, and analyzing the data. The following code cell initializes the experiment class while restricting the plaquette lattice to only two of the plaquettes (21 qubits), reducing the size of the experiment to ensure that the noise in the hardware doesn't overwhelm the signal." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "02357c6e-5c83-4ac0-811d-22602d9f33d5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gem_exp = GemExperiment(plaquette_lattice.filter([9, 12]), backend=backend)\n", + "\n", + "# visualize the plaquette lattice after filtering\n", + "plaquette_lattice.filter([9, 12]).draw_qubits()" + ] + }, + { + "cell_type": "markdown", + "id": "3234e018-a1a8-47b7-b10c-57d6f7f77ff1", + "metadata": {}, + "source": [ + "A GEM protocol circuit is built using the following steps:\n", + "1. Prepare the all-$|+\\rangle$ state by applying a Hadamard gate to every qubit.\n", + "2. Apply an $R_{ZZ}$ gate between every pair of connected qubits. This can be achieved using 3 layers of gates. Each $R_{ZZ}$ gate acts on a site qubit and a bond qubit. If the site qubit is labeled (B), then the angle is fixed to $\\frac{\\pi}{2}$. If the site qubit is labeled (A), then the angle is allowed to vary, producing different circuits. By default, the range of angles is set to 21 equally spaced points between $0$ and $\\frac{\\pi}{2}$, inclusive.\n", + "3. Measure each bond qubit in the Pauli $X$ basis. Since qubits are measured in the Pauli $Z$ basis, this can be accomplished by applying a Hadamard gate before measuring the qubit.\n", + "\n", + "Note that the paper cited in the introduction to this tutorial uses a different convention for the $R_{ZZ}$ angle, which differs from the convention used in this tutorial by a factor of 2.\n", + "\n", + "In step 3, only the bond qubits are measured. To understand what state the site qubits remain in, it is instructive to consider the case that the $R_{ZZ}$ angle applied to site qubits (A) in step 2 is equal to $\\frac{\\pi}{2}$. In this case, the site qubits are left in a highly entangled state similar to the GHZ state,\n", + "\n", + "$$\n", + "\\lvert \\text{GHZ} \\rangle = \\lvert 00 \\cdots 00 \\rangle + \\lvert 11 \\cdots 11 \\rangle.\n", + "$$\n", + "\n", + "Due to the randomness in the measurement outcomes, the actual state of the site qubits might be a different state with long-range order, for example, $\\lvert 00110 \\rangle + \\lvert 11001 \\rangle$. However, the GHZ state can be recovered by applying a decoding operation based on the measurement outcomes. When the $R_{ZZ}$ angle is tuned down from $\\frac{\\pi}{2}$, the long-range order can still be recovered up until a critical angle, which in the absence of noise, is approximately $0.3 \\pi$. Below this angle, the resulting state no longer exhibits long-range entanglement. This transition between the presence and absence of long-range order is the Nishimori phase transition.\n", + "\n", + "In the description above, the site qubits were left unmeasured, and the decoding operation can be performed by applying quantum gates. In the experiment as implemented in the GEM suite, which this tutorial follows, the site qubits are in fact measured, and the decoding operation is applied in a classical post-processing step.\n", + "\n", + "In the description above, the decoding operation can be performed by applying quantum gates to the site qubits to recover the quantum state. However, if the goal is to immediately measure the state, for example, for characterization purposes, then the site qubits are measured together with the bond qubits, and the decoding operation can be applied in a classical post-processing step. This is how the experiment is implemented in the GEM suite, which this tutorial follows.\n", + "\n", + "In addition to depending on the $R_{ZZ}$ angle in step 2, which by default sweeps across 21 values, the GEM protocol circuit also depends on the scheduling pattern used to implement the 3 layers of $R_{ZZ}$ gates. As discussed previously, there are 12 such scheduling patterns. Therefore, the total number of circuits in the experiment is $21 \\times 12 = 252$.\n", + "\n", + "The circuits of the experiment can be generated using the `circuits` method of the `GemExperiment` class." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "8e2ade62-9a57-42c3-9a85-3fe2dec3c426", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total number of circuits: 252\n" + ] + } + ], + "source": [ + "circuits = gem_exp.circuits()\n", + "print(f\"Total number of circuits: {len(circuits)}\")" + ] + }, + { + "cell_type": "markdown", + "id": "56c3c656-dcfa-4ca0-9df9-2d97c187c685", + "metadata": {}, + "source": [ + "For the purposes of this tutorial, it is enough to consider just a single scheduling pattern. The following code cell restricts the experiment to the first scheduling pattern. As a result, the experiment only has 21 circuits, one for each $R_{ZZ}$ angle swept over." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "4f8a2c73-752d-47b9-95d5-83439933fc08", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total number of circuits: 21\n", + "RZZ angles:\n", + "[0. 0.07853982 0.15707963 0.23561945 0.31415927 0.39269908\n", + " 0.4712389 0.54977871 0.62831853 0.70685835 0.78539816 0.86393798\n", + " 0.9424778 1.02101761 1.09955743 1.17809725 1.25663706 1.33517688\n", + " 1.41371669 1.49225651 1.57079633]\n" + ] + } + ], + "source": [ + "# Restrict experiment to the first scheduling pattern\n", + "gem_exp.set_experiment_options(schedule_idx=0)\n", + "\n", + "# There are less circuits now\n", + "circuits = gem_exp.circuits()\n", + "print(f\"Total number of circuits: {len(circuits)}\")\n", + "\n", + "# Print the RZZ angles swept over\n", + "print(f\"RZZ angles:\\n{gem_exp.parameters()}\")" + ] + }, + { + "cell_type": "markdown", + "id": "7011dc79-8561-42ef-909b-00fd8be9ef34", + "metadata": {}, + "source": [ + "The following code cell draws a diagram of the circuit at index 5. To reduce the size of the diagram, the measurement gates at the end of the circuit are removed." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "fd57d483-c70b-4ad5-b309-15750ad38bac", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Get the circuit at index 5\n", + "circuit = circuits[5]\n", + "# Remove the final measurements to ease visualization\n", + "circuit.remove_final_measurements()\n", + "# Draw the circuit\n", + "circuit.draw(\"mpl\", fold=-1, scale=0.5)" + ] + }, + { + "cell_type": "markdown", + "id": "a3aa063b-44cf-49f3-9e12-23b2f6a1c85b", + "metadata": {}, + "source": [ + "## Step 2: Optimize problem for quantum hardware execution\n", + "\n", + "Transpiling quantum circuits for execution on hardware typically involves a [number of stages](/docs/guides/transpiler-stages). Typically, the stages that incur the most computational overhead are choosing the qubit layout, routing the two-qubit gates to conform to the qubit connectivity of the hardware, and optimizing the circuit to minimize its gate count and depth. In the GEM protocol, the layout and routing stages are unnecessary because the hardware connectivity is already incorporated into the design of the protocol. The circuits already have a qubit layout, and the two-qubit gates are already mapped onto native connections. Furthermore, in order to preserve the structure of the circuit as the $R_{ZZ}$ angle is varied, only very basic circuit optimization should be performed.\n", + "\n", + "The `GemExperiment` class transparently transpiles circuits when executing the experiment. The layout and routing stages are already overridden by default to do nothing, and circuit optimization is performed at a level that only optimizes single-qubit gates. However, you can override or pass additional options using the `set_transpile_options` method. For the sake of visualization, the following code cell manually transpiles the circuit displayed previously, and draws the transpiled circuit." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "e9b99d48-8d33-46b5-bff5-480ab1c1c1f2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Demonstrate setting transpile options\n", + "gem_exp.set_transpile_options(\n", + " optimization_level=1 # This is the default optimization level\n", + ")\n", + "pass_manager = generate_preset_pass_manager(\n", + " backend=backend,\n", + " initial_layout=list(gem_exp.physical_qubits),\n", + " **dict(gem_exp.transpile_options),\n", + ")\n", + "transpiled = pass_manager.run(circuit)\n", + "transpiled.draw(\"mpl\", idle_wires=False, fold=-1, scale=0.5)" + ] + }, + { + "cell_type": "markdown", + "id": "8d0dcd59-54ef-4af8-9213-0784ef94b838", + "metadata": {}, + "source": [ + "## Step 3: Execute using Qiskit primitives\n", + "\n", + "To execute the GEM protocol circuits on the hardware, call the `run` method of the `GemExperiment` object. You can specify the number of shots you want to sample from each circuit. The `run` method returns an [ExperimentData](https://qiskit-community.github.io/qiskit-experiments/stubs/qiskit_experiments.framework.ExperimentData.html) object which you should save to a variable. Note that the `run` method only submits jobs without waiting for them to finish, so it is a non-blocking call." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "6171a383-dd58-4e3f-88aa-bbec7b5870df", + "metadata": {}, + "outputs": [], + "source": [ + "exp_data = gem_exp.run(shots=10_000)" + ] + }, + { + "cell_type": "markdown", + "id": "71e81552-0d33-4950-8d45-e6c0a8a056c9", + "metadata": {}, + "source": [ + "To wait for the results, call the `block_for_results` method of the `ExperimentData` object. This call will cause the interpreter to hang until the jobs are finished." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "ed14a067-35ba-4ffc-8534-4ae5ec6bc4c9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "ExperimentData(GemExperiment, d0d5880a-34c1-4aab-a7b6-c4f58516bc03, job_ids=['cwg12ptmptp00082khhg'], metadata=<5 items>, figure_names=['two_point_correlation.svg', 'normalized_variance.svg', 'plaquette_ops.svg', 'bond_ops.svg'])" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "exp_data.block_for_results()" + ] + }, + { + "cell_type": "markdown", + "id": "36e0570b-f091-45f2-bb83-143edbc3b433", + "metadata": {}, + "source": [ + "## Step 4: Post-process and return result in desired classical format\n", + "\n", + "At an $R_{ZZ}$ angle of $\\frac{\\pi}{2}$, the decoded state would be the GHZ state in the absence of noise. The long-range order of the GHZ state can be visualized by plotting the magnetization of the measured bitstrings. The magnetization $M$ is defined as the sum of the single-qubit Pauli $Z$ operators,\n", + "$$\n", + "M = \\sum_{j=1}^N Z_j,\n", + "$$\n", + "where $N$ is the number of site qubits. Its value for a bitstring is equal to the difference between the number of zeros and the number of ones. Measuring the GHZ state yields the all zeros state or the all ones state with equal probability, so the magnetization would be $+N$ half of the time and $-N$ the other half of the time. In the presence of errors due to noise, other values would also appear, but if the noise is not too great, the distribution would still be peaked near $+N$ and $-N$.\n", + "\n", + "For the raw bitstrings before decoding, the distribution of the magnetization would be equivalent to that of uniformly random bitstrings, in the absence of noise.\n", + "\n", + "The following code cell plots the magnetization of the raw bitstrings and the decoded bitstrings at the $R_{ZZ}$ angle of $\\frac{\\pi}{2}$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8ead3582-16df-4616-836c-bdce867ad6b8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Text(0.5, 1.0, 'Magnetization distribution with and without decoding')" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "def magnetization_distribution(\n", + " counts_dict: dict[str, int],\n", + ") -> dict[str, float]:\n", + " \"\"\"Compute magnetization distribution from counts dictionary.\"\"\"\n", + " # Construct dictionary from magnetization to count\n", + " mag_dist = defaultdict(float)\n", + " for bitstring, count in counts_dict.items():\n", + " mag = bitstring.count(\"0\") - bitstring.count(\"1\")\n", + " mag_dist[mag] += count\n", + " # Normalize\n", + " shots = sum(counts_dict.values())\n", + " for mag in mag_dist:\n", + " mag_dist[mag] /= shots\n", + " return mag_dist\n", + "\n", + "\n", + "# Get counts dictionaries with and without decoding\n", + "data = exp_data.data()\n", + "# Get the last data point, which is at the angle for the GHZ state\n", + "raw_counts = data[-1][\"counts\"]\n", + "# Without decoding\n", + "site_indices = [\n", + " i for i, q in enumerate(gem_exp.plaquettes.qubits()) if q.role == \"Site\"\n", + "]\n", + "site_raw_counts = defaultdict(int)\n", + "for key, val in raw_counts.items():\n", + " site_str = \"\".join(key[-1 - i] for i in site_indices)\n", + " site_raw_counts[site_str] += val\n", + "# With decoding\n", + "_, site_decoded_counts = gem_exp.plaquettes.decode_outcomes(\n", + " raw_counts, return_counts=True\n", + ")\n", + "\n", + "# Compute magnetization distribution\n", + "raw_magnetization = magnetization_distribution(site_raw_counts)\n", + "decoded_magnetization = magnetization_distribution(site_decoded_counts)\n", + "\n", + "# Plot\n", + "plt.bar(*zip(*raw_magnetization.items()), label=\"raw\")\n", + "plt.bar(*zip(*decoded_magnetization.items()), label=\"decoded\", width=0.3)\n", + "plt.legend()\n", + "plt.xlabel(\"Magnetization\")\n", + "plt.ylabel(\"Frequency\")\n", + "plt.title(\"Magnetization distribution with and without decoding\")" + ] + }, + { + "cell_type": "markdown", + "id": "90a7ae7a-5175-421f-bda9-bc6b986bdf5f", + "metadata": {}, + "source": [ + "To more rigorously characterize the long-range order, you can examine the average two-point correlation $f$, defined as\n", + "$$\n", + "f = \\frac{1}{N^2} \\left(\\langle M^2 \\rangle - \\langle M \\rangle ^2\\right).\n", + "$$\n", + "A higher value indicates a greater degree of entanglement. The `GemExperiment` class automatically computes this value for the decoded bitstrings as part of processing the experimental data. It stores a figure that is accessible via the `figure` method of the experiment data class. In this case, the name of the figure is `two_point_correlation`." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "4ecb25c8-e572-49af-a879-9943039db131", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "exp_data.figure(\"two_point_correlation\")" + ] + }, + { + "cell_type": "markdown", + "id": "f2f3e7e2-2a8b-4790-8ba7-b190c4ed1049", + "metadata": {}, + "source": [ + "To determine the critical point of the Nishimori phase transition, you can look at the normalized variance of $M^2 / N$, defined as\n", + "$$\n", + "g = \\frac{1}{N^3} \\left(\\langle M^4 \\rangle - \\langle M^2 \\rangle^2\\right),\n", + "$$\n", + "which quantifies the amount of fluctuation in the squared magnetization. This value is maximized at the critical point of the Nishimori phase transition. In the absence of noise, the critical point occurs at approximately $0.3 \\pi$. In the presence of noise, the critical point is shifted higher, but the phase transition is still observed as long as the critical point occurs below $0.5 \\pi$." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "2b351d68-3924-445a-94ef-047b16214e8a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "exp_data.figure(\"normalized_variance\")" + ] + }, + { + "cell_type": "markdown", + "id": "a94e0cbe-8429-487c-b203-50a8b2eacee3", + "metadata": {}, + "source": [ + "## Scale up the experiment\n", + "\n", + "The following code cells run the experiment for six plaquettes (49 qubits) and the full 12 plaquettes (125 qubits) and plot the normalized variance. As the experiment is scaled to larger sizes, the greater amount of noise shifts the critical point rightwards." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "08581c09-a6a5-4a56-9fc4-abf22b063c6a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gem_exp = GemExperiment(\n", + " plaquette_lattice.filter(range(3, 9)), backend=backend\n", + ")\n", + "gem_exp.set_experiment_options(schedule_idx=0)\n", + "exp_data = gem_exp.run(shots=10_000)\n", + "exp_data.block_for_results()\n", + "exp_data.figure(\"normalized_variance\")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "37e9a4cd-6efb-4ade-ad09-8139db9d58e9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gem_exp = GemExperiment(plaquette_lattice, backend=backend)\n", + "gem_exp.set_experiment_options(schedule_idx=0)\n", + "exp_data = gem_exp.run(shots=10_000)\n", + "exp_data.block_for_results()\n", + "exp_data.figure(\"normalized_variance\")" + ] + }, + { + "cell_type": "markdown", + "id": "6abd9701-58e4-43a8-a1d7-279506570de4", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "In this tutorial, you realized a Nishimori phase transition on a quantum processor using the GEM protocol. The metrics that you examined during post-processing, in particular the two-point correlation and the normalized variance, serve as benchmarks of the device's ability to generate long-range entangled states. These benchmarks extend the utility of the GEM protocol beyond probing interesting physics. As part of the protocol, you entangled qubits across the entire device using circuits of only constant depth. This feat is only possible due to the protocol's use of mid-circuit measurements. In this experiment, the entangled state was immediately measured, but an interesting avenue to explore would be to continue using the state in additional quantum processing!" + ] + }, + { + "cell_type": "markdown", + "id": "b9562a76", + "metadata": {}, + "source": [ + "## Tutorial survey\n", + "\n", + "Please take this short survey to provide feedback on this tutorial. Your insights will help us improve our content offerings and user experience.\n", + "\n", + "[Link to survey](https://your.feedback.ibm.com/jfe/form/SV_bsCKQkgzuQUQ7ky)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/tutorials/operator-back-propagation.ipynb b/docs/tutorials/operator-back-propagation.ipynb index 43f9e4913e0..f7224c28323 100644 --- a/docs/tutorials/operator-back-propagation.ipynb +++ b/docs/tutorials/operator-back-propagation.ipynb @@ -570,7 +570,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "id": "28ac4dbf", "metadata": {}, "outputs": [ @@ -612,7 +612,8 @@ "\n", "slices = slice_by_depth(circuit, max_slice_depth=1)\n", "\n", - "# Define the maximum number of qwc groups allowed in the backpropagated observable, and the truncation error budget\n", + "# Define the maximum number of qwc groups allowed in the backpropagated observable,\n", + "# and the truncation error budget\n", "op_budget = OperatorBudget(max_qwc_groups=15)\n", "truncation_error_budget = setup_budget(\n", " max_error_total=0.03, max_error_per_slice=0.005\n", @@ -624,7 +625,8 @@ ")\n", "bp_circuit = combine_slices(remaining_slices)\n", "\n", - "# Now backpropagate with truncation, using the same operator budget and the defined truncation error budget\n", + "# Now backpropagate with truncation, using the same operator budget and\n", + "# the defined truncation error budget\n", "bp_obs_trunc, remaining_slices_trunc, metadata = backpropagate(\n", " observable,\n", " slices,\n", @@ -635,7 +637,8 @@ " remaining_slices_trunc, include_barriers=False\n", ")\n", "\n", - "# Now we transpile the original circuit and the two backpropagated circuits, and apply the layout to the corresponding observables\n", + "# Now we transpile the original circuit and the two backpropagated circuits,\n", + "# and apply the layout to the corresponding observables\n", "pm = generate_preset_pass_manager(optimization_level=3, backend=backend)\n", "\n", "isa_circuit = pm.run(circuit)\n", @@ -665,8 +668,8 @@ " (isa_bp_circuit_trunc, isa_bp_observable_trunc),\n", "]\n", "\n", - "# Now we instantiate the Estimator primitive for the hardware with ZNE and measurement error mitigation\n", - "# and compute the three circuits and observables\n", + "# Now we instantiate the Estimator primitive for the hardware with ZNE and measurement error\n", + "# mitigation and compute the three circuits and observables\n", "options = EstimatorOptions()\n", "options.default_precision = 0.01\n", "options.resilience_level = 2\n", diff --git a/docs/tutorials/probabilistic-error-amplification.ipynb b/docs/tutorials/probabilistic-error-amplification.ipynb index d72b1c7f6ce..2003ec0d319 100644 --- a/docs/tutorials/probabilistic-error-amplification.ipynb +++ b/docs/tutorials/probabilistic-error-amplification.ipynb @@ -1200,14 +1200,14 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "id": "2b96bdd2", "metadata": {}, "outputs": [], "source": [ "# Exact data computed using the methods described in the original reference\n", - "# Y. Kim et al. \"Evidence for the utility of quantum computing before fault tolerance\" (Nature 618, 500–505 (2023))\n", - "# Directly used here for brevity\n", + "# Y. Kim et al. \"Evidence for the utility of quantum computing before fault tolerance\" (Nature 618,\n", + "# 500–505 (2023)) Directly used here for brevity\n", "exact_data = np.array(\n", " [\n", " 1,\n", @@ -1312,7 +1312,6 @@ "metadata": {}, "source": [ "## Next steps\n", - "\n", "If you found this work interesting, you might be interested in the following material:\n", "- A [tutorial](/docs/tutorials/combine-error-mitigation-techniques) focused on combining error mitigation techniques.\n", diff --git a/docs/tutorials/projected-quantum-kernels.ipynb b/docs/tutorials/projected-quantum-kernels.ipynb index 9a8ca77a0b7..506d2e90362 100644 --- a/docs/tutorials/projected-quantum-kernels.ipynb +++ b/docs/tutorials/projected-quantum-kernels.ipynb @@ -849,8 +849,8 @@ "# Identity operator on all qubits\n", "id = \"I\" * num_qubits\n", "\n", - "# projections_train[i][j][k] will be the expectation value of the j-th Pauli operator (0: X, 1: Y, 2: Z)\n", - "# of datapoint i on qubit k\n", + "# projections_train[i][j][k] will be the expectation value of the j-th\n", + "# Pauli operator (0: X, 1: Y, 2: Z) of datapoint i on qubit k\n", "projections_train = []\n", "jobs_train = []\n", "\n", @@ -990,8 +990,8 @@ "# Identity operator on all qubits\n", "id = \"I\" * num_qubits\n", "\n", - "# projections_test[i][j][k] will be the expectation value of the j-th Pauli operator (0: X, 1: Y, 2: Z)\n", - "# of datapoint i on qubit k\n", + "# projections_test[i][j][k] will be the expectation value of the\n", + "# j-th Pauli operator (0: X, 1: Y, 2: Z) of datapoint i on qubit k\n", "projections_test = []\n", "jobs_test = []\n", "\n", diff --git a/learning/courses/quantum-chem-with-vqe/geometry.ipynb b/learning/courses/quantum-chem-with-vqe/geometry.ipynb index 0fdf43e9651..f912c646e57 100644 --- a/learning/courses/quantum-chem-with-vqe/geometry.ipynb +++ b/learning/courses/quantum-chem-with-vqe/geometry.ipynb @@ -281,8 +281,8 @@ "# Here, we select the least busy backend available:\n", "backend = service.least_busy(operational=True, simulator=False)\n", "print(backend)\n", - "# Or to select a specific real backend use the line below, and substitute 'ibm_strasbourg' for your chosen device.\n", - "# backend = service.get_backend('ibm_strasbourg')" + "# Or to select a specific real backend use the line below, and substitute 'ibm_strasbourg'\n", + "# for your chosen device. backend = service.get_backend('ibm_strasbourg')" ] }, { diff --git a/learning/courses/quantum-diagonalization-algorithms/krylov.ipynb b/learning/courses/quantum-diagonalization-algorithms/krylov.ipynb index 958c1a51091..0e7f50e45ea 100644 --- a/learning/courses/quantum-diagonalization-algorithms/krylov.ipynb +++ b/learning/courses/quantum-diagonalization-algorithms/krylov.ipynb @@ -88,7 +88,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "49017c87-a971-4d2d-b6dc-b8ec9b64184a", "metadata": {}, "outputs": [ @@ -104,7 +104,8 @@ } ], "source": [ - "# One might use linalg.eigh here, but later matrices may not be Hermitian. So we use linalg.eig in this lesson.\n", + "# One might use linalg.eigh here, but later matrices may not be Hermitian. So we use\n", + "# linalg.eig in this lesson.\n", "\n", "import numpy as np\n", "\n", @@ -509,12 +510,13 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "fb4f96ea-906c-4819-a9cb-8e9409cac051", "metadata": {}, "outputs": [], "source": [ - "# vknown is some established vector in our subspace. vnext is one we wish to add, which must be orthogonal to vknown.\n", + "# vknown is some established vector in our subspace. vnext is one we wish to add,\n", + "# which must be orthogonal to vknown.\n", "\n", "\n", "def orthog_pair(vknown, vnext):\n", @@ -546,7 +548,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "818cceee-bd88-472a-8af9-0a6c0dec5646", "metadata": {}, "outputs": [], @@ -591,7 +593,8 @@ " eigs.append(np.linalg.eig(Hs[j + 1]).eigenvalues)\n", " k_tot_times.append(time_mus() - t0)\n", "\n", - " # Return the Krylov vectors, the projected Hamiltonians, the eigenvalues, and the total time required.\n", + " # Return the Krylov vectors, the projected Hamiltonians, the eigenvalues,\n", + " # and the total time required.\n", " return (ks, Hs, eigs, k_tot_times)" ] }, @@ -605,7 +608,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "3e2aa939-b568-4982-b64a-9f2e4b0da2b5", "metadata": {}, "outputs": [], @@ -621,7 +624,8 @@ " ]\n", ")\n", "\n", - "# Give the test matrix and an initial guess as arguments in the function defined above. Calculate outputs.\n", + "# Give the test matrix and an initial guess as arguments in the function defined above.\n", + "# Calculate outputs.\n", "test_ks, test_Hs, test_eigs, text_k_tot_times = krylov_full_build(\n", " np.array([0.5, 0.5, 0, 0.5, 0.5]), test_matrix\n", ")" @@ -808,11 +812,13 @@ "metadata": {}, "outputs": [], "source": [ - "# Choose the absolute error you can tolerate, and make a list for tracking the Krylov subspace size at which that error is achieved.\n", + "# Choose the absolute error you can tolerate, and make a list for tracking the Krylov subspace size\n", + "# at which that error is achieved.\n", "abserr = 0.05\n", "accept_subspace_size = []\n", "\n", - "# Lists to store total time spent on the Krylov method, and the subset of that time spent on diagonalizing the projected matrix.\n", + "# Lists to store total time spent on the Krylov method, and the subset of that time spent on\n", + "# diagonalizing the projected matrix.\n", "matrix_krylov_tot_times = []\n", "matrix_krylov_dim = []\n", "\n", @@ -821,7 +827,8 @@ " test_ks, test_Hs, test_eigs, test_k_tot_times = krylov_full_build(\n", " np.ones(len(matrices[mm])), matrices[mm]\n", " )\n", - " # We have not yet found a Krylov subspace that produces our minimum eigenvalue to within the required error.\n", + " # We have not yet found a Krylov subspace that produces our minimum eigenvalue to\n", + " # within the required error.\n", " found = 0\n", " for j in range(0, len(matrices[mm]) - 1):\n", " # If we still haven't found the desired subspace...\n", @@ -879,7 +886,7 @@ }, { "cell_type": "code", - "execution_count": 95, + "execution_count": null, "id": "41ac6108-526d-476b-8255-db9bb52a30ca", "metadata": {}, "outputs": [], @@ -887,7 +894,8 @@ "smooth_numpy_times = []\n", "smooth_krylov_times = []\n", "\n", - "# Choose the number of adjacent points over which to average forward; the same will be used backward.\n", + "# Choose the number of adjacent points over which to average forward;\n", + "# the same will be used backward.\n", "smooth_steps = 10\n", "\n", "# We will do this smoothing for all points/matrix dimensions\n", @@ -896,7 +904,8 @@ " start = max(0, i - smooth_steps)\n", " end = min(len(matrix_krylov_tot_times) - 1, i + smooth_steps)\n", "\n", - " # Dummy variables for accumulating an average over adjacent points. This is done for both Krylov and the NumPy calculations.\n", + " # Dummy variables for accumulating an average over adjacent points. This is done for both Krylov\n", + " # and the NumPy calculations.\n", " smooth_count = 0\n", " smooth_numpy_sum = 0\n", " smooth_krylov_sum = 0\n", @@ -2162,7 +2171,8 @@ "metadata": {}, "outputs": [], "source": [ - "# This job required 17 minutes of QPU time to run on a Heron r2 processor. This is only an estimate. Your execution time may vary.\n", + "# This job required 17 minutes of QPU time to run on a Heron r2 processor. This is only an estimate.\n", + "# Your execution time may vary.\n", "\n", "with Batch(backend=backend) as batch:\n", " # Estimator\n", diff --git a/learning/courses/quantum-diagonalization-algorithms/sqd-implementation.ipynb b/learning/courses/quantum-diagonalization-algorithms/sqd-implementation.ipynb index 520df6f1767..06459280e9b 100644 --- a/learning/courses/quantum-diagonalization-algorithms/sqd-implementation.ipynb +++ b/learning/courses/quantum-diagonalization-algorithms/sqd-implementation.ipynb @@ -734,7 +734,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "a2d22e0b-8f51-42a0-858f-ad0297cb0bae", "metadata": {}, "outputs": [ @@ -806,7 +806,8 @@ " bs_mat_tmp = bitstring_matrix_full\n", " probs_arr_tmp = probs_arr_full\n", "\n", - " # If we have average orbital occupancy information, we use it to refine the full set of noisy configurations\n", + " # If we have average orbital occupancy information, we use it to refine\n", + " # the full set of noisy configurations.\n", " else:\n", " bs_mat_tmp, probs_arr_tmp = recover_configurations(\n", " bitstring_matrix_full,\n", diff --git a/learning/courses/quantum-diagonalization-algorithms/vqe.ipynb b/learning/courses/quantum-diagonalization-algorithms/vqe.ipynb index 9dd055a2cf7..4f62d53361b 100644 --- a/learning/courses/quantum-diagonalization-algorithms/vqe.ipynb +++ b/learning/courses/quantum-diagonalization-algorithms/vqe.ipynb @@ -225,7 +225,8 @@ "source": [ "# Example syntax for minimization\n", "# from scipy.optimize import minimize\n", - "# res = minimize(cost_func, x0, args=(ansatz, hamiltonian, estimator), method=\"cobyla\", options={'maxiter': 200})" + "# res = minimize(cost_func, x0, args=(ansatz, hamiltonian, estimator), method=\"cobyla\",\n", + "# options={'maxiter': 200})" ] }, { @@ -512,7 +513,8 @@ "# Pre-defined ansatz circuit and operator class for Hamiltonian\n", "from qiskit.circuit.library import efficient_su2\n", "\n", - "# Note that it is more common to place initial 'h' gates outside the ansatz. Here we specifically wanted this layer structure.\n", + "# Note that it is more common to place initial 'h' gates outside the ansatz.\n", + "# Here we specifically wanted this layer structure.\n", "ansatz = efficient_su2(\n", " hamiltonian.num_qubits, su2_gates=[\"h\", \"rz\", \"y\"], entanglement=\"circular\", reps=1\n", ")\n", diff --git a/learning/courses/quantum-machine-learning/data-encoding.ipynb b/learning/courses/quantum-machine-learning/data-encoding.ipynb index f48c9a8199f..94e0139e1cd 100644 --- a/learning/courses/quantum-machine-learning/data-encoding.ipynb +++ b/learning/courses/quantum-machine-learning/data-encoding.ipynb @@ -1548,7 +1548,8 @@ " su2cx = su2circuit.decompose().count_ops().get(\"cx\")\n", " pcx = pcircuit.decompose().count_ops().get(\"cx\")\n", "\n", - " # Appending the cx gate counts to the lists. We shift the zz and pauli data points, because they overlap.\n", + " # Appending the cx gate counts to the lists. We shift the zz and Pauli data points,\n", + " # because they overlap.\n", " n_data.append(n)\n", " zz2gates.append(zzcx - 0.5)\n", " z2gates.append(0)\n", diff --git a/learning/courses/quantum-machine-learning/introduction.ipynb b/learning/courses/quantum-machine-learning/introduction.ipynb index 57d54df0b48..c75d6bae901 100644 --- a/learning/courses/quantum-machine-learning/introduction.ipynb +++ b/learning/courses/quantum-machine-learning/introduction.ipynb @@ -261,11 +261,13 @@ "\n", "# Step 1: Map classical inputs to a quantum problem:\n", "\n", - "# Start by getting some appropriate data. The data imported below consist of 128 rows or data points.\n", + "# Start by getting some appropriate data.\n", + "# The data imported below consist of 128 rows or data points.\n", "# Each row has 14 columns that correspond to data features, and a 15th column with a label (+/-1).\n", "!wget https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv\n", "\n", - "# Import some required packages, and write a function to pull some training data out of the csv file you got above.\n", + "# Import some required packages, and write a function to pull some\n", + "# training data out of the csv file you got above.\n", "import pandas as pd\n", "import numpy as np\n", "\n", @@ -287,7 +289,8 @@ "num_samples = np.shape(X_train)[0]\n", "\n", "# Prepare feature map for computing overlap between two data points.\n", - "# This could be pre-built feature maps like ZZFeatureMap, or a custom quantum circuit, as shown here.\n", + "# This could be pre-built feature maps like ZZFeatureMap, or a custom quantum circuit,\n", + "# as shown here.\n", "num_features = np.shape(X_train)[1]\n", "num_qubits = int(num_features / 2)\n", "entangler_map = [[0, 2], [3, 4], [2, 5], [1, 4], [2, 3], [4, 6]]\n", diff --git a/learning/courses/quantum-machine-learning/quantum-kernel-methods.ipynb b/learning/courses/quantum-machine-learning/quantum-kernel-methods.ipynb index 6b9360c6e30..4d16c60f986 100644 --- a/learning/courses/quantum-machine-learning/quantum-kernel-methods.ipynb +++ b/learning/courses/quantum-machine-learning/quantum-kernel-methods.ipynb @@ -302,18 +302,24 @@ "outputs": [], "source": [ "# Run this for a simulator\n", - "# from qiskit.primitives import StatevectorSampler\n", "\n", + "# from qiskit.primitives import StatevectorSampler\n", "# from qiskit_ibm_runtime import Options, Session, Sampler\n", - "\n", "# num_shots = 10000\n", "\n", "# Evaluate the problem using state vector-based primitives from Qiskit\n", + "\n", "# sampler = StatevectorSampler()\n", "# results = sampler.run([overlap_circ], shots=num_shots).result()\n", - "# .get_counts() returns counts associated with a state labeled by bit results such as |001101...01>.\n", + "\n", + "# .get_counts() returns counts associated with a state labeled by bit results\n", + "# such as |001101...01>.\n", + "\n", "# counts_bit = results[0].data.meas.get_counts()\n", - "# .get_int_counts returns the same counts, but labeled by integer equivalent of the above bit string.\n", + "\n", + "# .get_int_counts returns the same counts, but labeled by integer equivalent\n", + "# of the above bit string.\n", + "\n", "# counts = results[0].data.meas.get_int_counts()" ] }, @@ -337,7 +343,8 @@ "results = sampler.run([overlap_ibm], shots=num_shots).result()\n", "# .get_counts() returns counts associated with a state labeled by bit results such as |001101...01>.\n", "counts_bit = results[0].data.meas.get_counts()\n", - "# .get_int_counts returns the same counts, but labeled by integer equivalent of the above bit string.\n", + "# .get_int_counts returns the same counts, but labeled by integer equivalent\n", + "# of the above bit string.\n", "counts = results[0].data.meas.get_int_counts()" ] }, @@ -654,7 +661,9 @@ "# To use a simulator\n", "from qiskit.primitives import StatevectorSampler\n", "\n", - "# Remember to insert your token in the QiskitRuntimeService constructor to use real quantum computers\n", + "# Remember to insert your token in the QiskitRuntimeService constructor\n", + "# to use real quantum computers\n", + "\n", "# service = QiskitRuntimeService()\n", "# backend = service.least_busy(\n", "# operational=True, simulator=False, min_num_qubits=fm.num_qubits\n", @@ -681,10 +690,12 @@ " .data.meas.get_int_counts()\n", " )\n", "\n", - " # Assign the probability of the 0 state to the kernel matrix, and the transposed element (since this is an inner product)\n", + " # Assign the probability of the 0 state to the kernel matrix, and the transposed element\n", + " # (since this is an inner product)\n", " kernel_matrix[x1, x2] = counts.get(0, 0.0) / num_shots\n", " kernel_matrix[x2, x1] = counts.get(0, 0.0) / num_shots\n", - " # Fill in on-diagonal elements with 1, again, since this is an inner-product corresponding to probability (or alter the code to check these entries and verify they yield 1)\n", + " # Fill in on-diagonal elements with 1, again, since this is an inner-product corresponding to\n", + " # probability (or alter the code to check these entries and verify they yield 1)\n", " kernel_matrix[x1, x1] = 1\n", "\n", "print(\"training done\")\n", @@ -744,7 +755,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "id": "d8708973-c97c-494c-af9c-e4a0fe430452", "metadata": {}, "outputs": [ @@ -757,10 +768,12 @@ } ], "source": [ - "# Feed in the pre-computed matrix and the labels of the training data. The classical algorithm gives you a fit.\n", + "# Feed in the pre-computed matrix and the labels of the training data.\n", + "# The classical algorithm gives you a fit.\n", "qml_svc.fit(kernel_matrix, train_labels)\n", "\n", - "# Now use the .score to test your data, using the matrix of test data, and test labels as your inputs.\n", + "# Now use the .score to test your data, using the matrix of test data,\n", + "# and test labels as your inputs.\n", "qml_score_precomputed_kernel = qml_svc.score(test_matrix, test_labels)\n", "print(f\"Precomputed kernel classification test score: {qml_score_precomputed_kernel}\")" ] diff --git a/learning/courses/quantum-machine-learning/qvc-qnn.ipynb b/learning/courses/quantum-machine-learning/qvc-qnn.ipynb index 2afc3b891ee..750a045424b 100644 --- a/learning/courses/quantum-machine-learning/qvc-qnn.ipynb +++ b/learning/courses/quantum-machine-learning/qvc-qnn.ipynb @@ -51,7 +51,8 @@ "size = 8\n", "# One dimension of the image (called vertical, but it doesn't matter). Must be a divisor of `size`\n", "vert_size = 2\n", - "# The length of the line to be detected (yellow). Must be less than or equal to the smallest dimension of the image (`<=min(vert_size,size/vert_size)`\n", + "# The length of the line to be detected (yellow). Must be less than or equal to the smallest\n", + "# dimension of the image (`<=min(vert_size,size/vert_size)`\n", "line_size = 2\n", "\n", "\n", @@ -68,7 +69,8 @@ " hor_array[j][i + p] = np.pi / 2\n", " j += 1\n", "\n", - " # Make two adjacent entries pi/2, then move down to the next row. Careful to avoid the \"pixels\" at size/vert_size - linesize, because we want to fold this list into a grid.\n", + " # Make two adjacent entries pi/2, then move down to the next row. Careful to avoid the \"pixels\"\n", + " # at size/vert_size - linesize, because we want to fold this list into a grid.\n", "\n", " j = 0\n", " for i in range(0, round(size / vert_size) * (vert_size - line_size + 1)):\n", @@ -76,7 +78,8 @@ " ver_array[j][i + p * round(size / vert_size)] = np.pi / 2\n", " j += 1\n", "\n", - " # Make entries pi/2, spaced by the length/rows, so that when folded, the entries appear on top of each other.\n", + " # Make entries pi/2, spaced by the length/rows, so that when folded,\n", + " # the entries appear on top of each other.\n", "\n", " for n in range(num_images):\n", " rng = np.random.randint(0, 2)\n", @@ -89,7 +92,8 @@ " labels.append(1)\n", " random_image = np.random.randint(0, len(ver_array))\n", " images.append(np.array(ver_array[random_image]))\n", - " # Randomly select 0 or 1 for a horizontal or vertical array, assign the corresponding label.\n", + " # Randomly select 0 or 1 for a horizontal or vertical array, assign the corresponding\n", + " # label.\n", "\n", " # Create noise\n", " for i in range(size):\n", @@ -228,7 +232,8 @@ "num_qubits = len(train_images[0])\n", "\n", "# Data encoding\n", - "# Note that qiskit orders parameters alphabetically. We assign the parameter prefix \"a\" to ensure our data encoding goes to the first part of the circuit, the feature mapping.\n", + "# Note that qiskit orders parameters alphabetically. We assign the parameter prefix \"a\" to ensure\n", + "# our data encoding goes to the first part of the circuit, the feature mapping.\n", "feature_map = z_feature_map(num_qubits, parameter_prefix=\"a\")" ] }, @@ -302,7 +307,8 @@ "for i in range(size):\n", " qnn_circuit.ry(params[i], i)\n", "\n", - "# Here is a list of qubit pairs between which we want CNOT gates. The choice of these is not yet obvious.\n", + "# Here is a list of qubit pairs between which we want CNOT gates.\n", + "# The choice of these is not yet obvious.\n", "qnn_cnot_list = [[0, 1], [1, 2], [2, 3]]\n", "\n", "for i in range(len(qnn_cnot_list)):\n", @@ -1005,7 +1011,8 @@ "for i in range(size):\n", " qnn_circuit.ry(params[i], i)\n", "\n", - "# Here is an extended list of qubit pairs between which we want CNOT gates. This now covers all pixels connected by horizontal lines.\n", + "# Here is an extended list of qubit pairs between which we want CNOT gates. This now covers all\n", + "# pixels connected by horizontal lines.\n", "qnn_cnot_list = [[0, 1], [1, 2], [2, 3], [4, 5], [5, 6], [6, 7]]\n", "\n", "for i in range(len(qnn_cnot_list)):\n", @@ -1295,7 +1302,8 @@ "size = 36\n", "# One dimension of the image (called vertical, but it doesn't matter). Must be a divisor of `size`\n", "vert_size = 6\n", - "# The length of the line to be detected (yellow). Must be less than or equal to the smallest dimension of the image (`<=min(vert_size,size/vert_size)`\n", + "# The length of the line to be detected (yellow). Must be less than or equal to the smallest\n", + "# dimension of the image (`<=min(vert_size,size/vert_size)`\n", "line_size = 6\n", "\n", "\n", @@ -1312,7 +1320,8 @@ " hor_array[j][i + p] = np.pi / 2\n", " j += 1\n", "\n", - " # Make two adjacent entries pi/2, then move down to the next row. Careful to avoid the \"pixels\" at size/vert_size - linesize, because we want to fold this list into a grid.\n", + " # Make two adjacent entries pi/2, then move down to the next row. Careful to avoid the \"pixels\"\n", + " # at size/vert_size - linesize, because we want to fold this list into a grid.\n", "\n", " j = 0\n", " for i in range(0, round(size / vert_size) * (vert_size - line_size + 1)):\n", @@ -1320,7 +1329,8 @@ " ver_array[j][i + p * round(size / vert_size)] = np.pi / 2\n", " j += 1\n", "\n", - " # Make entries pi/2, spaced by the length/rows, so that when folded, the entries appear on top of each other.\n", + " # Make entries pi/2, spaced by the length/rows, so that when folded,\n", + " # the entries appear on top of each other.\n", "\n", " for n in range(num_images):\n", " rng = np.random.randint(0, 2)\n", @@ -1355,7 +1365,7 @@ }, { "cell_type": "code", - "execution_count": 64, + "execution_count": null, "id": "474d3a43-268b-425c-a3a9-165a34589d72", "metadata": {}, "outputs": [], @@ -1363,7 +1373,8 @@ "from sklearn.model_selection import train_test_split\n", "\n", "np.random.seed(42)\n", - "# Here we specify a very small data set. Increase for realism, but monitor use of quantum computing time.\n", + "# Here we specify a very small data set. Increase for realism, but\n", + "# monitor use of quantum computing time.\n", "images, labels = generate_dataset(10)\n", "\n", "train_images, test_images, train_labels, test_labels = train_test_split(\n", @@ -1422,13 +1433,14 @@ "num_qubits = len(train_images[0])\n", "\n", "# Data encoding\n", - "# Note that qiskit orders parameters alphabetically. We assign the parameter prefix \"a\" to ensure our data encoding goes to the first part of the circuit, the feature mapping.\n", + "# Note that qiskit orders parameters alphabetically. We assign the parameter prefix \"a\" to ensure\n", + "# our data encoding goes to the first part of the circuit, the feature mapping.\n", "feature_map = z_feature_map(num_qubits, parameter_prefix=\"a\")" ] }, { "cell_type": "code", - "execution_count": 67, + "execution_count": null, "id": "34603184-d0cc-4ce2-87c4-a1e8b1bdbf7d", "metadata": {}, "outputs": [ @@ -1458,7 +1470,9 @@ " if j < hor_size - 1:\n", " qnn_circuit.cx((i * hor_size) + j, (i * hor_size) + j + 1)\n", "\n", - "# CNOT gates between vertically adjacent qubits, likely not necessary based on our preliminary simulation.\n", + "# CNOT gates between vertically adjacent qubits, likely not necessary\n", + "# based on our preliminary simulation.\n", + "\n", "# if i\n", - "\n", - "Toshinari Itoko (28 June 2024)\n", - "\n", - "[Download the pdf](https://ibm.ent.box.com/public/static/a0zgies7bh91hm2lwev9o0bfeybxc6n6.zip) of the original lecture. Note that some code snippets might become deprecated since these are static images.\n", - "\n", - "*Approximate QPU time to run this experiment is 1 m 40 s.*\n", - "\n", - "" - ] - }, - { - "cell_type": "markdown", - "id": "e936adb0-bc31-41a8-b988-02ce7e68b902", - "metadata": {}, - "source": [ - "## 1. Introduction\n", - "\n", - "Throughout this lesson, we will examine noise and how it can be mitigated on quantum computers. We will begin by looking at the effects of noise using a simulator that can simulate noise in a few ways, including using noise profiles from real quantum computers. Then we will move on to real quantum computers, in which noise is inherent. We will look at the effects of error mitigation, including combinations of things like zero-noise extrapolation (ZNE) and gate-twirling.\n", - "\n", - "We will start by loading some packages." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "38cf024b", - "metadata": {}, - "outputs": [], - "source": [ - "# !pip install qiskit qiskit_aer qiskit_ibm_runtime\n", - "# !pip install jupyter\n", - "# !pip install matplotlib pylatexenc" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "897008ea", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'2.0.2'" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import qiskit\n", - "\n", - "qiskit.__version__" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "486b4d35", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'0.17.1'" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import qiskit_aer\n", - "\n", - "qiskit_aer.__version__" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "994c44e5", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'0.40.1'" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import qiskit_ibm_runtime\n", - "\n", - "qiskit_ibm_runtime.__version__" - ] - }, - { - "cell_type": "markdown", - "id": "ee48a826-fda3-4fb4-b50b-d69c38eb887f", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "slide" - }, - "tags": [] - }, - "source": [ - "## 2. Noisy simulation without error mitigation\n", - "\n", - "\n", - "Qiskit Aer is a classical simulator for quantum computing. It can simulate not only ideal execution but also noisy execution of quantum circuits. This notebook demonstrates how to run noisy simulation using Qiskit Aer:\n", - "\n", - "1. Build a noise model\n", - "2. Build a noisy sampler (simulator) with the noise model\n", - "3. Run a quantum circuit on the noisy sampler\n", - "\n", - "```\n", - "noise_model = NoiseModel()\n", - "...\n", - "noisy_sampler = Sampler(options={\"backend_options\": {\"noise_model\": noise_model}})\n", - "job = noisy_sampler.run([circuit])\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "d1490ab8", - "metadata": {}, - "source": [ - "### 2.1 Build a test circuit\n", - "\n", - "We consider toy 1-qubit circuits which just repeat X gates `d` times (`d`=0 ... 100) and measure the `Z` observable." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "b4863c66", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from qiskit.circuit import QuantumCircuit\n", - "\n", - "MAX_DEPTH = 100\n", - "circuits = []\n", - "for d in range(MAX_DEPTH + 1):\n", - " circ = QuantumCircuit(1)\n", - " for _ in range(d):\n", - " circ.x(0)\n", - " circ.barrier(0)\n", - " circ.measure_all()\n", - " circuits.append(circ)\n", - "\n", - "display(circuits[3].draw(output=\"mpl\"))" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "a366502c", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "SparsePauliOp(['Z'],\n", - " coeffs=[1.+0.j])" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.quantum_info import SparsePauliOp\n", - "\n", - "obs = SparsePauliOp.from_list([(\"Z\", 1.0)])\n", - "obs" - ] - }, - { - "cell_type": "markdown", - "id": "6200c2ed", - "metadata": {}, - "source": [ - "### 2.2 Build a noise model\n", - "\n", - "To do noisy simulation, we need to specify `NoiseModel`. We show how to build `NoiseModel` in this section." - ] - }, - { - "cell_type": "markdown", - "id": "dae0c070", - "metadata": {}, - "source": [ - "We first need to define quantum (or readout) errors to add to a noise model." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "1e0f1c12", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit_aer.noise.errors import (\n", - " coherent_unitary_error,\n", - " amplitude_damping_error,\n", - " ReadoutError,\n", - ")\n", - "from qiskit.circuit.library import RXGate\n", - "\n", - "# Coherent (unitary) error: Over X-rotation error\n", - "# https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.noise.coherent_unitary_error.html#qiskit_aer.noise.coherent_unitary_error\n", - "OVER_ROTATION_ANGLE = 0.05\n", - "coherent_error = coherent_unitary_error(RXGate(OVER_ROTATION_ANGLE).to_matrix())\n", - "\n", - "# Incoherent error: Amplitude dumping error\n", - "# https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.noise.amplitude_damping_error.html#qiskit_aer.noise.amplitude_damping_error\n", - "AMPLITUDE_DAMPING_PARAM = 0.02 # in [0, 1] (0: no error)\n", - "incoherent_error = amplitude_damping_error(AMPLITUDE_DAMPING_PARAM)\n", - "\n", - "# Readout (measurement) error: Readout error\n", - "# https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.noise.ReadoutError.html#qiskit_aer.noise.ReadoutError\n", - "PREP0_MEAS1 = 0.03 # P(1|0): Probability of preparing 0 and measuring 1\n", - "PREP1_MEAS0 = 0.08 # P(0|1): Probability of preparing 1 and measuring 0\n", - "readout_error = ReadoutError(\n", - " [[1 - PREP0_MEAS1, PREP0_MEAS1], [PREP1_MEAS0, 1 - PREP1_MEAS0]]\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "e23c26ba", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit_aer.noise import NoiseModel\n", - "\n", - "noise_model = NoiseModel()\n", - "noise_model.add_quantum_error(coherent_error.compose(incoherent_error), \"x\", (0,))\n", - "noise_model.add_readout_error(readout_error, (0,))" - ] - }, - { - "cell_type": "markdown", - "id": "5a786224", - "metadata": {}, - "source": [ - "### 2.3 Build a noisy sampler with the noise model" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "f8aded6f", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit_aer.primitives import SamplerV2 as Sampler\n", - "\n", - "noisy_sampler = Sampler(options={\"backend_options\": {\"noise_model\": noise_model}})" - ] - }, - { - "cell_type": "markdown", - "id": "922ba1ac", - "metadata": {}, - "source": [ - "### 2.4 Run quantum circuits on the noisy sampler" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "4ae504e7", - "metadata": {}, - "outputs": [], - "source": [ - "job = noisy_sampler.run(circuits, shots=400)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "3b2fff25", - "metadata": {}, - "outputs": [], - "source": [ - "result = job.result()" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "4dc12337", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'0': 389, '1': 11}" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "result[0].data.meas.get_counts()" - ] - }, - { - "cell_type": "markdown", - "id": "7e654c5e", - "metadata": {}, - "source": [ - "### 2.5 Plot results" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1a25e394", - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "plt.title(\"Noisy simulation\")\n", - "ds = list(range(MAX_DEPTH + 1))\n", - "plt.plot(\n", - " ds,\n", - " [result[d].data.meas.expectation_values([\"Z\"]) for d in ds],\n", - " color=\"gray\",\n", - " linestyle=\"-\",\n", - ")\n", - "plt.scatter(ds, [result[d].data.meas.expectation_values([\"Z\"]) for d in ds], marker=\"o\")\n", - "plt.hlines(0, xmin=0, xmax=MAX_DEPTH, colors=\"black\")\n", - "plt.ylim(-1, 1)\n", - "plt.xlabel(\"Circuit depth\")\n", - "plt.ylabel(\"Measured \")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "09ef68bb", - "metadata": {}, - "source": [ - "### 2.6 Ideal simulation" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "041abc81", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "ideal_sampler = Sampler()\n", - "job_ideal = ideal_sampler.run(circuits)\n", - "result_ideal = job_ideal.result()\n", - "plt.title(\"Ideal simulation\")\n", - "ds = list(range(MAX_DEPTH + 1))\n", - "plt.plot(\n", - " ds,\n", - " [result_ideal[d].data.meas.expectation_values([\"Z\"]) for d in ds],\n", - " color=\"gray\",\n", - " linestyle=\"-\",\n", - ")\n", - "plt.scatter(\n", - " ds, [result_ideal[d].data.meas.expectation_values([\"Z\"]) for d in ds], marker=\"o\"\n", - ")\n", - "plt.hlines(0, xmin=0, xmax=MAX_DEPTH, colors=\"black\")\n", - "plt.xlabel(\"Circuit depth\")\n", - "plt.ylabel(\"Measured \")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "a1e1b6e6", - "metadata": {}, - "source": [ - "### 2.7 Exercise\n", - "\n", - "By tweaking the code below,\n", - "- [ ] Try 25x number of shots (= 10_000 shots) and ensure that a smoother plot is obtained\n", - "- [ ] Change noise parameters (OVER_ROTATION_ANGLE, AMPLITUDE_DAMPING_PARAM, PREP0_MEAS1, or PREP1_MEAS0) and see how the plot changes" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "502b9cfe", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "OVER_ROTATION_ANGLE = 0.05\n", - "coherent_error = coherent_unitary_error(RXGate(OVER_ROTATION_ANGLE).to_matrix())\n", - "AMPLITUDE_DAMPING_PARAM = 0.02 # in [0, 1] (0: no error)\n", - "incoherent_error = amplitude_damping_error(AMPLITUDE_DAMPING_PARAM)\n", - "PREP0_MEAS1 = 0.1 # P(1|0): Probability of preparing 0 and measuring 1\n", - "PREP1_MEAS0 = 0.05 # P(0|1): Probability of preparing 1 and measuring 0\n", - "readout_error = ReadoutError(\n", - " [[1 - PREP0_MEAS1, PREP0_MEAS1], [PREP1_MEAS0, 1 - PREP1_MEAS0]]\n", - ")\n", - "noise_model = NoiseModel()\n", - "noise_model.add_quantum_error(coherent_error.compose(incoherent_error), \"x\", (0,))\n", - "noise_model.add_readout_error(readout_error, (0,))\n", - "options = {\n", - " \"backend_options\": {\"noise_model\": noise_model},\n", - "}\n", - "noisy_sampler = Sampler(options=options)\n", - "job = noisy_sampler.run(circuits, shots=400)\n", - "result = job.result()\n", - "plt.title(\"Noisy simulation\")\n", - "ds = list(range(MAX_DEPTH + 1))\n", - "plt.plot(\n", - " ds,\n", - " [result[d].data.meas.expectation_values([\"Z\"]) for d in ds],\n", - " marker=\"o\",\n", - " linestyle=\"-\",\n", - ")\n", - "plt.hlines(0, xmin=0, xmax=MAX_DEPTH, colors=\"black\")\n", - "plt.ylim(-1, 1)\n", - "plt.xlabel(\"Depth\")\n", - "plt.ylabel(\"Measured \")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "3cfe1a93", - "metadata": {}, - "source": [ - "### 2.8 More realistic noisy simulation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8d607bb5", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit_aer import AerSimulator\n", - "from qiskit_ibm_runtime import SamplerV2 as Sampler, QiskitRuntimeService\n", - "\n", - "service = QiskitRuntimeService()\n", - "real_backend = service.least_busy(\n", - " operational=True, simulator=False, min_num_qubits=127\n", - ") # Eagle" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "81a67f2f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "aer = AerSimulator.from_backend(real_backend)\n", - "noisy_sampler = Sampler(mode=aer)\n", - "job = noisy_sampler.run(circuits)\n", - "result = job.result()\n", - "plt.title(\"Noisy simulation with noise model from real backend\")\n", - "ds = list(range(MAX_DEPTH + 1))\n", - "plt.plot(\n", - " ds,\n", - " [result[d].data.meas.expectation_values([\"Z\"]) for d in ds],\n", - " marker=\"o\",\n", - " linestyle=\"-\",\n", - ")\n", - "plt.hlines(0, xmin=0, xmax=MAX_DEPTH, colors=\"black\")\n", - "plt.ylim(-1, 1)\n", - "plt.xlabel(\"Depth\")\n", - "plt.ylabel(\"Measured \")\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "567d30e9", - "metadata": {}, - "source": [ - "## 3. Real quantum computation with error mitigation\n", - "\n", - "In this part, we demonstrate how to obtain error mitigated results (expectation values) using Qiskit Estimator.\n", - "We consider 6-qubit Trotterized circuits for simulating the time evolution of one dimensional Ising model and see how the error scales with respect to the number of time steps." - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "2d301499", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "backend = service.least_busy(\n", - " operational=True, simulator=False, min_num_qubits=127\n", - ") # Eagle\n", - "backend" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "6c90b89c", - "metadata": {}, - "outputs": [], - "source": [ - "NUM_QUBITS = 6\n", - "NUM_TIME_STEPS = list(range(8))\n", - "RX_ANGLE = 0.1\n", - "RZZ_ANGLE = 0.1" - ] - }, - { - "cell_type": "markdown", - "id": "b1489739", - "metadata": {}, - "source": [ - "### 3.1 Build circuits" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "1a77956e", - "metadata": {}, - "outputs": [], - "source": [ - "# Build circuits with different number of time steps\n", - "circuits = []\n", - "for n_steps in NUM_TIME_STEPS:\n", - " circ = QuantumCircuit(NUM_QUBITS)\n", - " for i in range(n_steps):\n", - " # rx layer\n", - " for q in range(NUM_QUBITS):\n", - " circ.rx(RX_ANGLE, q)\n", - " # 1st rzz layer\n", - " for q in range(1, NUM_QUBITS - 1, 2):\n", - " circ.rzz(RZZ_ANGLE, q, q + 1)\n", - " # 2nd rzz layer\n", - " for q in range(0, NUM_QUBITS - 1, 2):\n", - " circ.rzz(RZZ_ANGLE, q, q + 1)\n", - " circ.barrier() # need not to optimize the circuit\n", - " # Uncompute stage\n", - " for i in range(n_steps):\n", - " for q in range(0, NUM_QUBITS - 1, 2):\n", - " circ.rzz(-RZZ_ANGLE, q, q + 1)\n", - " for q in range(1, NUM_QUBITS - 1, 2):\n", - " circ.rzz(-RZZ_ANGLE, q, q + 1)\n", - " for q in range(NUM_QUBITS):\n", - " circ.rx(-RX_ANGLE, q)\n", - " circuits.append(circ)" - ] - }, - { - "cell_type": "markdown", - "id": "94eb9062", - "metadata": {}, - "source": [ - "To know the ideal output in advance, we use compute-uncompute circuits that consist of a first stage where the original circuit $U$ is applied, and a second stage where it is reversed $U^\\dagger$.\n", - "Note that the ideal outcome of such circuits will trivially be the input state $|000000\\rangle$, which has the trivial expectation values for any Pauli observables, for example, $\\langle IIIIIZ \\rangle = 1$." - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "20296b5a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Print the circuit with 2 time steps\n", - "circuits[2].draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "cde8d3ea", - "metadata": {}, - "source": [ - "Note: As shown above, the circuit with $k$ time steps will have $4k$ two-qubit gate layers." - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "id": "af0d03e9", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "SparsePauliOp(['IIIIIZ'],\n", - " coeffs=[1.+0.j])" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "obs = SparsePauliOp.from_sparse_list([(\"Z\", [0], 1.0)], num_qubits=NUM_QUBITS)\n", - "obs" - ] - }, - { - "cell_type": "markdown", - "id": "ebd6ea9b", - "metadata": {}, - "source": [ - "### 3.2 Transpile the circuits" - ] - }, - { - "cell_type": "markdown", - "id": "8eaa552e", - "metadata": {}, - "source": [ - "We transpile the circuits for the backend with optimization (`optimization_level=1`)." - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "id": "87b861e2", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "pm = generate_preset_pass_manager(optimization_level=1, backend=backend)\n", - "isa_circuits = pm.run(circuits)\n", - "display(isa_circuits[2].draw(\"mpl\", idle_wires=False, fold=-1))" - ] - }, - { - "cell_type": "markdown", - "id": "743ad427", - "metadata": {}, - "source": [ - "### 3.3 Execute using Estimator (with different resilience levels)\n", - "\n", - "Setting the resilienece level (`estimator.options.resilience_level`) is the easiest way to apply error mitigation when using Qiskit Estimator. Estimator supports the following resilience levels (as of 2024/06/28). See more details in the [Configure error mitigation](/docs/guides/error-mitigation-and-suppression-techniques) guide.\n", - "\n", - "![image.png](/learning/images/courses/utility-scale-quantum-computing/error-mitigation/res_level.avif)" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "id": "328f71f2", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Job ID (rl=0): d146vcnmya70008emprg\n", - "Job ID (rl=1): d146vdnqf56g0081sva0\n", - "Job ID (rl=2): d146ven5z6q00087c61g\n" - ] - } - ], - "source": [ - "from qiskit_ibm_runtime import Batch\n", - "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", - "\n", - "jobs = []\n", - "job_ids = []\n", - "with Batch(backend=backend):\n", - " for resilience_level in [0, 1, 2]:\n", - " estimator = Estimator()\n", - " estimator.options.resilience_level = resilience_level\n", - " job = estimator.run(\n", - " [(circ, obs.apply_layout(circ.layout)) for circ in isa_circuits]\n", - " )\n", - " job_ids.append(job.job_id())\n", - " print(f\"Job ID (rl={resilience_level}): {job.job_id()}\")\n", - " jobs.append(job)" - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "id": "1dd804e4-0d7d-4782-9559-7b3796cab121", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "DONE\n", - "DONE\n", - "DONE\n" - ] - } - ], - "source": [ - "# check job status\n", - "for job in jobs:\n", - " print(job.status())" - ] - }, - { - "cell_type": "code", - "execution_count": 32, - "id": "d138e1e9", - "metadata": {}, - "outputs": [], - "source": [ - "# REPLACE WITH YOUR OWN JOB IDS\n", - "jobs = [service.job(job_id) for job_id in job_ids]" - ] - }, - { - "cell_type": "code", - "execution_count": 33, - "id": "ea820e98", - "metadata": {}, - "outputs": [], - "source": [ - "# Get results\n", - "results = [job.result() for job in jobs]" - ] - }, - { - "cell_type": "markdown", - "id": "e3691462", - "metadata": {}, - "source": [ - "### 3.4 Plot results" - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "id": "7527976e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.title(\"Error mitigation with different resilience levels\")\n", - "labels = [\"0 (No mitigation)\", \"1 (TREX)\", \"2 (ZNE + Gate twirling)\"]\n", - "steps = NUM_TIME_STEPS\n", - "for result, label in zip(results, labels):\n", - " plt.errorbar(\n", - " x=steps,\n", - " y=[result[s].data.evs for s in steps],\n", - " yerr=[result[s].data.stds for s in steps],\n", - " marker=\"o\",\n", - " linestyle=\"-\",\n", - " capsize=4,\n", - " label=label,\n", - " )\n", - "plt.hlines(\n", - " 1.0, min(steps), max(steps), linestyle=\"dashed\", label=\"Ideal\", colors=\"black\"\n", - ")\n", - "plt.xlabel(\"Time steps\")\n", - "plt.ylabel(\"Mitigated \")\n", - "plt.legend()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "id": "e4def30b", - "metadata": {}, - "source": [ - "## 4. (Optional) Customize error mitigation options" - ] - }, - { - "cell_type": "markdown", - "id": "f099e16c", - "metadata": {}, - "source": [ - "We can customize the application of error mitigation techniques via options as shown below." - ] - }, - { - "cell_type": "code", - "execution_count": 35, - "id": "a22e6b8c", - "metadata": {}, - "outputs": [], - "source": [ - "# TREX\n", - "estimator.options.twirling.enable_measure = True\n", - "estimator.options.twirling.num_randomizations = \"auto\"\n", - "estimator.options.twirling.shots_per_randomization = \"auto\"\n", - "\n", - "# Gate twirling\n", - "estimator.options.twirling.enable_gates = True\n", - "# ZNE\n", - "estimator.options.resilience.zne_mitigation = True\n", - "estimator.options.resilience.zne.noise_factors = [1, 3, 5]\n", - "estimator.options.resilience.zne.extrapolator = (\"exponential\", \"linear\")\n", - "\n", - "# Dynamical decoupling\n", - "estimator.options.dynamical_decoupling.enable = True # Default: False\n", - "estimator.options.dynamical_decoupling.sequence_type = \"XX\"\n", - "\n", - "# Other options\n", - "estimator.options.default_shots = 10_000" - ] - }, - { - "cell_type": "markdown", - "id": "79dd06fb", - "metadata": {}, - "source": [ - "See the following guides and API reference for the details of error mitigation options.\n", - "- [Configure error mitigation](/docs/guides/error-mitigation-and-suppression-techniques)\n", - "- [Introduction to options](/docs/guides/runtime-options-overview)\n", - "- [EstimatorOptions](/docs/api/qiskit-ibm-runtime/options-estimator-options)\n", - "- [SamplerOptions](/docs/api/qiskit-ibm-runtime/options-sampler-options)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "fad91a68", + "metadata": {}, + "source": [ + "---\n", + "title: Error mitigation\n", + "description: Throughout this lesson, we will examine noise and how it can be mitigated on quantum computers.\n", + "---\n", + "\n", + "\n", + " # Quantum noise and error mitigation\n", + "\n", + "\n", + "\n", + "\n", + "Toshinari Itoko (28 June 2024)\n", + "\n", + "[Download the pdf](https://ibm.ent.box.com/public/static/a0zgies7bh91hm2lwev9o0bfeybxc6n6.zip) of the original lecture. Note that some code snippets might become deprecated since these are static images.\n", + "\n", + "*Approximate QPU time to run this experiment is 1 m 40 s.*\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "e936adb0-bc31-41a8-b988-02ce7e68b902", + "metadata": {}, + "source": [ + "## 1. Introduction\n", + "\n", + "Throughout this lesson, we will examine noise and how it can be mitigated on quantum computers. We will begin by looking at the effects of noise using a simulator that can simulate noise in a few ways, including using noise profiles from real quantum computers. Then we will move on to real quantum computers, in which noise is inherent. We will look at the effects of error mitigation, including combinations of things like zero-noise extrapolation (ZNE) and gate-twirling.\n", + "\n", + "We will start by loading some packages." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "38cf024b", + "metadata": {}, + "outputs": [], + "source": [ + "# !pip install qiskit qiskit_aer qiskit_ibm_runtime\n", + "# !pip install jupyter\n", + "# !pip install matplotlib pylatexenc" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "897008ea", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'2.0.2'" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import qiskit\n", + "\n", + "qiskit.__version__" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "486b4d35", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'0.17.1'" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import qiskit_aer\n", + "\n", + "qiskit_aer.__version__" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "994c44e5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'0.40.1'" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import qiskit_ibm_runtime\n", + "\n", + "qiskit_ibm_runtime.__version__" + ] + }, + { + "cell_type": "markdown", + "id": "ee48a826-fda3-4fb4-b50b-d69c38eb887f", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + }, + "tags": [] + }, + "source": [ + "## 2. Noisy simulation without error mitigation\n", + "\n", + "\n", + "Qiskit Aer is a classical simulator for quantum computing. It can simulate not only ideal execution but also noisy execution of quantum circuits. This notebook demonstrates how to run noisy simulation using Qiskit Aer:\n", + "\n", + "1. Build a noise model\n", + "2. Build a noisy sampler (simulator) with the noise model\n", + "3. Run a quantum circuit on the noisy sampler\n", + "\n", + "```\n", + "noise_model = NoiseModel()\n", + "...\n", + "noisy_sampler = Sampler(options={\"backend_options\": {\"noise_model\": noise_model}})\n", + "job = noisy_sampler.run([circuit])\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "d1490ab8", + "metadata": {}, + "source": [ + "### 2.1 Build a test circuit\n", + "\n", + "We consider toy 1-qubit circuits which just repeat X gates `d` times (`d`=0 ... 100) and measure the `Z` observable." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "b4863c66", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from qiskit.circuit import QuantumCircuit\n", + "\n", + "MAX_DEPTH = 100\n", + "circuits = []\n", + "for d in range(MAX_DEPTH + 1):\n", + " circ = QuantumCircuit(1)\n", + " for _ in range(d):\n", + " circ.x(0)\n", + " circ.barrier(0)\n", + " circ.measure_all()\n", + " circuits.append(circ)\n", + "\n", + "display(circuits[3].draw(output=\"mpl\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "a366502c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "SparsePauliOp(['Z'],\n", + " coeffs=[1.+0.j])" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.quantum_info import SparsePauliOp\n", + "\n", + "obs = SparsePauliOp.from_list([(\"Z\", 1.0)])\n", + "obs" + ] + }, + { + "cell_type": "markdown", + "id": "6200c2ed", + "metadata": {}, + "source": [ + "### 2.2 Build a noise model\n", + "\n", + "To do noisy simulation, we need to specify `NoiseModel`. We show how to build `NoiseModel` in this section." + ] + }, + { + "cell_type": "markdown", + "id": "dae0c070", + "metadata": {}, + "source": [ + "We first need to define quantum (or readout) errors to add to a noise model." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "1e0f1c12", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_aer.noise.errors import (\n", + " coherent_unitary_error,\n", + " amplitude_damping_error,\n", + " ReadoutError,\n", + ")\n", + "from qiskit.circuit.library import RXGate\n", + "\n", + "# Coherent (unitary) error: Over X-rotation error\n", + "# https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.noise.coherent_unitary_error.html#qiskit_aer.noise.coherent_unitary_error\n", + "OVER_ROTATION_ANGLE = 0.05\n", + "coherent_error = coherent_unitary_error(RXGate(OVER_ROTATION_ANGLE).to_matrix())\n", + "\n", + "# Incoherent error: Amplitude dumping error\n", + "# https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.noise.amplitude_damping_error.html#qiskit_aer.noise.amplitude_damping_error\n", + "AMPLITUDE_DAMPING_PARAM = 0.02 # in [0, 1] (0: no error)\n", + "incoherent_error = amplitude_damping_error(AMPLITUDE_DAMPING_PARAM)\n", + "\n", + "# Readout (measurement) error: Readout error\n", + "# https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.noise.ReadoutError.html#qiskit_aer.noise.ReadoutError\n", + "PREP0_MEAS1 = 0.03 # P(1|0): Probability of preparing 0 and measuring 1\n", + "PREP1_MEAS0 = 0.08 # P(0|1): Probability of preparing 1 and measuring 0\n", + "readout_error = ReadoutError(\n", + " [[1 - PREP0_MEAS1, PREP0_MEAS1], [PREP1_MEAS0, 1 - PREP1_MEAS0]]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "e23c26ba", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_aer.noise import NoiseModel\n", + "\n", + "noise_model = NoiseModel()\n", + "noise_model.add_quantum_error(coherent_error.compose(incoherent_error), \"x\", (0,))\n", + "noise_model.add_readout_error(readout_error, (0,))" + ] + }, + { + "cell_type": "markdown", + "id": "5a786224", + "metadata": {}, + "source": [ + "### 2.3 Build a noisy sampler with the noise model" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "f8aded6f", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_aer.primitives import SamplerV2 as Sampler\n", + "\n", + "noisy_sampler = Sampler(options={\"backend_options\": {\"noise_model\": noise_model}})" + ] + }, + { + "cell_type": "markdown", + "id": "922ba1ac", + "metadata": {}, + "source": [ + "### 2.4 Run quantum circuits on the noisy sampler" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "4ae504e7", + "metadata": {}, + "outputs": [], + "source": [ + "job = noisy_sampler.run(circuits, shots=400)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "3b2fff25", + "metadata": {}, + "outputs": [], + "source": [ + "result = job.result()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "4dc12337", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'0': 389, '1': 11}" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "result[0].data.meas.get_counts()" + ] + }, + { + "cell_type": "markdown", + "id": "7e654c5e", + "metadata": {}, + "source": [ + "### 2.5 Plot results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a25e394", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "plt.title(\"Noisy simulation\")\n", + "ds = list(range(MAX_DEPTH + 1))\n", + "plt.plot(\n", + " ds,\n", + " [result[d].data.meas.expectation_values([\"Z\"]) for d in ds],\n", + " color=\"gray\",\n", + " linestyle=\"-\",\n", + ")\n", + "plt.scatter(ds, [result[d].data.meas.expectation_values([\"Z\"]) for d in ds], marker=\"o\")\n", + "plt.hlines(0, xmin=0, xmax=MAX_DEPTH, colors=\"black\")\n", + "plt.ylim(-1, 1)\n", + "plt.xlabel(\"Circuit depth\")\n", + "plt.ylabel(\"Measured \")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "09ef68bb", + "metadata": {}, + "source": [ + "### 2.6 Ideal simulation" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "041abc81", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ideal_sampler = Sampler()\n", + "job_ideal = ideal_sampler.run(circuits)\n", + "result_ideal = job_ideal.result()\n", + "plt.title(\"Ideal simulation\")\n", + "ds = list(range(MAX_DEPTH + 1))\n", + "plt.plot(\n", + " ds,\n", + " [result_ideal[d].data.meas.expectation_values([\"Z\"]) for d in ds],\n", + " color=\"gray\",\n", + " linestyle=\"-\",\n", + ")\n", + "plt.scatter(\n", + " ds, [result_ideal[d].data.meas.expectation_values([\"Z\"]) for d in ds], marker=\"o\"\n", + ")\n", + "plt.hlines(0, xmin=0, xmax=MAX_DEPTH, colors=\"black\")\n", + "plt.xlabel(\"Circuit depth\")\n", + "plt.ylabel(\"Measured \")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "a1e1b6e6", + "metadata": {}, + "source": [ + "### 2.7 Exercise\n", + "\n", + "By tweaking the code below,\n", + "- [ ] Try 25x number of shots (= 10_000 shots) and ensure that a smoother plot is obtained\n", + "- [ ] Change noise parameters (OVER_ROTATION_ANGLE, AMPLITUDE_DAMPING_PARAM, PREP0_MEAS1, or PREP1_MEAS0) and see how the plot changes" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "502b9cfe", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "OVER_ROTATION_ANGLE = 0.05\n", + "coherent_error = coherent_unitary_error(RXGate(OVER_ROTATION_ANGLE).to_matrix())\n", + "AMPLITUDE_DAMPING_PARAM = 0.02 # in [0, 1] (0: no error)\n", + "incoherent_error = amplitude_damping_error(AMPLITUDE_DAMPING_PARAM)\n", + "PREP0_MEAS1 = 0.1 # P(1|0): Probability of preparing 0 and measuring 1\n", + "PREP1_MEAS0 = 0.05 # P(0|1): Probability of preparing 1 and measuring 0\n", + "readout_error = ReadoutError(\n", + " [[1 - PREP0_MEAS1, PREP0_MEAS1], [PREP1_MEAS0, 1 - PREP1_MEAS0]]\n", + ")\n", + "noise_model = NoiseModel()\n", + "noise_model.add_quantum_error(coherent_error.compose(incoherent_error), \"x\", (0,))\n", + "noise_model.add_readout_error(readout_error, (0,))\n", + "options = {\n", + " \"backend_options\": {\"noise_model\": noise_model},\n", + "}\n", + "noisy_sampler = Sampler(options=options)\n", + "job = noisy_sampler.run(circuits, shots=400)\n", + "result = job.result()\n", + "plt.title(\"Noisy simulation\")\n", + "ds = list(range(MAX_DEPTH + 1))\n", + "plt.plot(\n", + " ds,\n", + " [result[d].data.meas.expectation_values([\"Z\"]) for d in ds],\n", + " marker=\"o\",\n", + " linestyle=\"-\",\n", + ")\n", + "plt.hlines(0, xmin=0, xmax=MAX_DEPTH, colors=\"black\")\n", + "plt.ylim(-1, 1)\n", + "plt.xlabel(\"Depth\")\n", + "plt.ylabel(\"Measured \")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "3cfe1a93", + "metadata": {}, + "source": [ + "### 2.8 More realistic noisy simulation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8d607bb5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit_aer import AerSimulator\n", + "from qiskit_ibm_runtime import SamplerV2 as Sampler, QiskitRuntimeService\n", + "\n", + "service = QiskitRuntimeService()\n", + "real_backend = service.least_busy(\n", + " operational=True, simulator=False, min_num_qubits=127\n", + ") # Eagle" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "81a67f2f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "aer = AerSimulator.from_backend(real_backend)\n", + "noisy_sampler = Sampler(mode=aer)\n", + "job = noisy_sampler.run(circuits)\n", + "result = job.result()\n", + "plt.title(\"Noisy simulation with noise model from real backend\")\n", + "ds = list(range(MAX_DEPTH + 1))\n", + "plt.plot(\n", + " ds,\n", + " [result[d].data.meas.expectation_values([\"Z\"]) for d in ds],\n", + " marker=\"o\",\n", + " linestyle=\"-\",\n", + ")\n", + "plt.hlines(0, xmin=0, xmax=MAX_DEPTH, colors=\"black\")\n", + "plt.ylim(-1, 1)\n", + "plt.xlabel(\"Depth\")\n", + "plt.ylabel(\"Measured \")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "567d30e9", + "metadata": {}, + "source": [ + "## 3. Real quantum computation with error mitigation\n", + "\n", + "In this part, we demonstrate how to obtain error mitigated results (expectation values) using Qiskit Estimator.\n", + "We consider 6-qubit Trotterized circuits for simulating the time evolution of one dimensional Ising model and see how the error scales with respect to the number of time steps." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "2d301499", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "backend = service.least_busy(\n", + " operational=True, simulator=False, min_num_qubits=127\n", + ") # Eagle\n", + "backend" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "6c90b89c", + "metadata": {}, + "outputs": [], + "source": [ + "NUM_QUBITS = 6\n", + "NUM_TIME_STEPS = list(range(8))\n", + "RX_ANGLE = 0.1\n", + "RZZ_ANGLE = 0.1" + ] + }, + { + "cell_type": "markdown", + "id": "b1489739", + "metadata": {}, + "source": [ + "### 3.1 Build circuits" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "1a77956e", + "metadata": {}, + "outputs": [], + "source": [ + "# Build circuits with different number of time steps\n", + "circuits = []\n", + "for n_steps in NUM_TIME_STEPS:\n", + " circ = QuantumCircuit(NUM_QUBITS)\n", + " for i in range(n_steps):\n", + " # rx layer\n", + " for q in range(NUM_QUBITS):\n", + " circ.rx(RX_ANGLE, q)\n", + " # 1st rzz layer\n", + " for q in range(1, NUM_QUBITS - 1, 2):\n", + " circ.rzz(RZZ_ANGLE, q, q + 1)\n", + " # 2nd rzz layer\n", + " for q in range(0, NUM_QUBITS - 1, 2):\n", + " circ.rzz(RZZ_ANGLE, q, q + 1)\n", + " circ.barrier() # need not to optimize the circuit\n", + " # Uncompute stage\n", + " for i in range(n_steps):\n", + " for q in range(0, NUM_QUBITS - 1, 2):\n", + " circ.rzz(-RZZ_ANGLE, q, q + 1)\n", + " for q in range(1, NUM_QUBITS - 1, 2):\n", + " circ.rzz(-RZZ_ANGLE, q, q + 1)\n", + " for q in range(NUM_QUBITS):\n", + " circ.rx(-RX_ANGLE, q)\n", + " circuits.append(circ)" + ] + }, + { + "cell_type": "markdown", + "id": "94eb9062", + "metadata": {}, + "source": [ + "To know the ideal output in advance, we use compute-uncompute circuits that consist of a first stage where the original circuit $U$ is applied, and a second stage where it is reversed $U^\\dagger$.\n", + "Note that the ideal outcome of such circuits will trivially be the input state $|000000\\rangle$, which has the trivial expectation values for any Pauli observables, for example, $\\langle IIIIIZ \\rangle = 1$." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "20296b5a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Print the circuit with 2 time steps\n", + "circuits[2].draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "cde8d3ea", + "metadata": {}, + "source": [ + "Note: As shown above, the circuit with $k$ time steps will have $4k$ two-qubit gate layers." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "af0d03e9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "SparsePauliOp(['IIIIIZ'],\n", + " coeffs=[1.+0.j])" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "obs = SparsePauliOp.from_sparse_list([(\"Z\", [0], 1.0)], num_qubits=NUM_QUBITS)\n", + "obs" + ] + }, + { + "cell_type": "markdown", + "id": "ebd6ea9b", + "metadata": {}, + "source": [ + "### 3.2 Transpile the circuits" + ] + }, + { + "cell_type": "markdown", + "id": "8eaa552e", + "metadata": {}, + "source": [ + "We transpile the circuits for the backend with optimization (`optimization_level=1`)." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "87b861e2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "pm = generate_preset_pass_manager(optimization_level=1, backend=backend)\n", + "isa_circuits = pm.run(circuits)\n", + "display(isa_circuits[2].draw(\"mpl\", idle_wires=False, fold=-1))" + ] + }, + { + "cell_type": "markdown", + "id": "743ad427", + "metadata": {}, + "source": [ + "### 3.3 Execute using Estimator (with different resilience levels)\n", + "\n", + "Setting the resilienece level (`estimator.options.resilience_level`) is the easiest way to apply error mitigation when using Qiskit Estimator. Estimator supports the following resilience levels (as of 2024/06/28). See more details in the [Configure error mitigation](/docs/guides/error-mitigation-and-suppression-techniques) guide.\n", + "\n", + "![image.png](/learning/images/courses/utility-scale-quantum-computing/error-mitigation/res_level.avif)" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "328f71f2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Job ID (rl=0): d146vcnmya70008emprg\n", + "Job ID (rl=1): d146vdnqf56g0081sva0\n", + "Job ID (rl=2): d146ven5z6q00087c61g\n" + ] + } + ], + "source": [ + "from qiskit_ibm_runtime import Batch\n", + "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", + "\n", + "jobs = []\n", + "job_ids = []\n", + "with Batch(backend=backend):\n", + " for resilience_level in [0, 1, 2]:\n", + " estimator = Estimator()\n", + " estimator.options.resilience_level = resilience_level\n", + " job = estimator.run(\n", + " [(circ, obs.apply_layout(circ.layout)) for circ in isa_circuits]\n", + " )\n", + " job_ids.append(job.job_id())\n", + " print(f\"Job ID (rl={resilience_level}): {job.job_id()}\")\n", + " jobs.append(job)" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "1dd804e4-0d7d-4782-9559-7b3796cab121", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DONE\n", + "DONE\n", + "DONE\n" + ] + } + ], + "source": [ + "# check job status\n", + "for job in jobs:\n", + " print(job.status())" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "d138e1e9", + "metadata": {}, + "outputs": [], + "source": [ + "# REPLACE WITH YOUR OWN JOB IDS\n", + "jobs = [service.job(job_id) for job_id in job_ids]" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "ea820e98", + "metadata": {}, + "outputs": [], + "source": [ + "# Get results\n", + "results = [job.result() for job in jobs]" + ] + }, + { + "cell_type": "markdown", + "id": "e3691462", + "metadata": {}, + "source": [ + "### 3.4 Plot results" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "7527976e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.title(\"Error mitigation with different resilience levels\")\n", + "labels = [\"0 (No mitigation)\", \"1 (TREX)\", \"2 (ZNE + Gate twirling)\"]\n", + "steps = NUM_TIME_STEPS\n", + "for result, label in zip(results, labels):\n", + " plt.errorbar(\n", + " x=steps,\n", + " y=[result[s].data.evs for s in steps],\n", + " yerr=[result[s].data.stds for s in steps],\n", + " marker=\"o\",\n", + " linestyle=\"-\",\n", + " capsize=4,\n", + " label=label,\n", + " )\n", + "plt.hlines(\n", + " 1.0, min(steps), max(steps), linestyle=\"dashed\", label=\"Ideal\", colors=\"black\"\n", + ")\n", + "plt.xlabel(\"Time steps\")\n", + "plt.ylabel(\"Mitigated \")\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "e4def30b", + "metadata": {}, + "source": [ + "## 4. (Optional) Customize error mitigation options" + ] + }, + { + "cell_type": "markdown", + "id": "f099e16c", + "metadata": {}, + "source": [ + "We can customize the application of error mitigation techniques via options as shown below." + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "a22e6b8c", + "metadata": {}, + "outputs": [], + "source": [ + "# TREX\n", + "estimator.options.twirling.enable_measure = True\n", + "estimator.options.twirling.num_randomizations = \"auto\"\n", + "estimator.options.twirling.shots_per_randomization = \"auto\"\n", + "\n", + "# Gate twirling\n", + "estimator.options.twirling.enable_gates = True\n", + "# ZNE\n", + "estimator.options.resilience.zne_mitigation = True\n", + "estimator.options.resilience.zne.noise_factors = [1, 3, 5]\n", + "estimator.options.resilience.zne.extrapolator = (\"exponential\", \"linear\")\n", + "\n", + "# Dynamical decoupling\n", + "estimator.options.dynamical_decoupling.enable = True # Default: False\n", + "estimator.options.dynamical_decoupling.sequence_type = \"XX\"\n", + "\n", + "# Other options\n", + "estimator.options.default_shots = 10_000" + ] + }, + { + "cell_type": "markdown", + "id": "79dd06fb", + "metadata": {}, + "source": [ + "See the following guides and API reference for the details of error mitigation options.\n", + "- [Configure error mitigation](/docs/guides/error-mitigation-and-suppression-techniques)\n", + "- [Introduction to options](/docs/guides/runtime-options-overview)\n", + "- [EstimatorOptions](/docs/api/qiskit-ibm-runtime/options-estimator-options)\n", + "- [SamplerOptions](/docs/api/qiskit-ibm-runtime/options-sampler-options)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/learning/courses/utility-scale-quantum-computing/grovers-algorithm.ipynb b/learning/courses/utility-scale-quantum-computing/grovers-algorithm.ipynb index 55019fc30c9..26c680e1c0e 100644 --- a/learning/courses/utility-scale-quantum-computing/grovers-algorithm.ipynb +++ b/learning/courses/utility-scale-quantum-computing/grovers-algorithm.ipynb @@ -577,7 +577,8 @@ } ], "source": [ - "# If the Notebook session got disconnected you can also check your job status by running the following code\n", + "# If the Notebook session got disconnected you can also check your job status\n", + "# by running the following code\n", "job_real = service.job(job_id) # Input your job-id between the quotations\n", "job_real.status()" ] diff --git a/learning/courses/utility-scale-quantum-computing/hardware.ipynb b/learning/courses/utility-scale-quantum-computing/hardware.ipynb index 574ccc62b66..0442b680ead 100644 --- a/learning/courses/utility-scale-quantum-computing/hardware.ipynb +++ b/learning/courses/utility-scale-quantum-computing/hardware.ipynb @@ -175,7 +175,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "51b87458-a5e8-4e77-a34d-39fe425a5f01", "metadata": {}, "outputs": [ @@ -191,7 +191,8 @@ } ], "source": [ - "# This function requires that Graphviz is installed. If you need to install Graphviz you can refer to:\n", + "# This function requires that Graphviz is installed. If you need to install Graphviz\n", + "# you can refer to:\n", "# https://graphviz.org/download/#executable-packages for instructions.\n", "try:\n", " fig = backend.coupling_map.draw()\n", diff --git a/learning/courses/utility-scale-quantum-computing/quantum-circuit-optimization.ipynb b/learning/courses/utility-scale-quantum-computing/quantum-circuit-optimization.ipynb index 2a18f355aeb..35150b22c8a 100644 --- a/learning/courses/utility-scale-quantum-computing/quantum-circuit-optimization.ipynb +++ b/learning/courses/utility-scale-quantum-computing/quantum-circuit-optimization.ipynb @@ -1,2157 +1,2158 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "fad91a68", - "metadata": {}, - "source": [ - "---\n", - "title: Quantum circuit optimization\n", - "description: This lesson will address several aspects of circuit optimization in quantum computing.\n", - "---\n", - "\n", - "\n", - " # Quantum circuit optimization\n", - "\n", - "\n", - "\n", - "Toshinari Itoko (21 June 2024)\n", - "\n", - "[Download the pdf](https://ibm.ent.box.com/public/static/0hvvgr1gnwx64x2ukgk04sss6sxc4zko.zip) of the original lecture. Note that some code snippets might become deprecated since these are static images.\n", - "\n", - "*Approximate QPU time to run this experiment is 15 s.*\n", - "\n", - "(Note: Some cells of part 2 are copied from the notebook \"Qiskit Deep dive\", written by Matthew Treinish (Qiskit maintainer))\n", - "\n", - "" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "38cf024b", - "metadata": {}, - "outputs": [], - "source": [ - "# !pip install 'qiskit[visualization]'\n", - "# !pip install qiskit_ibm_runtime qiskit_aer\n", - "# !pip install jupyter\n", - "# !pip install matplotlib pylatexenc pydot pillow" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "5bc656fa-6376-436e-adfc-59676edc719b", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "'2.0.2'" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import qiskit\n", - "\n", - "qiskit.__version__" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "b5d0220b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'0.40.1'" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import qiskit_ibm_runtime\n", - "\n", - "qiskit_ibm_runtime.__version__" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "1032e247", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'0.17.1'" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import qiskit_aer\n", - "\n", - "qiskit_aer.__version__" - ] - }, - { - "cell_type": "markdown", - "id": "335804e7", - "metadata": {}, - "source": [ - "## 1. Introduction\n", - "\n", - "This lesson will address several aspects of circuit optimization in quantum computing. Specifically, we will see the value of circuit optimization by using optimization settings built into Qiskit. Then we will go a bit deeper and see what you can do as an expert in your particular application area to build circuits in a smart way. Finally, we will take a close look at what goes on during transpilation that helps us optimize our circuits." - ] - }, - { - "cell_type": "markdown", - "id": "187c0ab1", - "metadata": {}, - "source": [ - "## 2. Circuit optimization matters\n", - "\n", - "We first compare the results of running 5-qubit GHZ state ($\\frac{1}{\\sqrt{2}} \\left( |00000\\rangle + |11111\\rangle \\right)$) preparation circuits with and without optimization." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "b1570aa9", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.circuit import QuantumCircuit\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "from qiskit.primitives import BackendSamplerV2 as Sampler" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f6be6161", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit_ibm_runtime.fake_provider import FakeBrisbane\n", - "\n", - "backend = FakeBrisbane()" - ] - }, - { - "cell_type": "markdown", - "id": "9cb38fa1", - "metadata": {}, - "source": [ - "We first use a GHZ circuit naively synthesized as follows." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "262b97e5", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "num_qubits = 5\n", - "\n", - "ghz_circ = QuantumCircuit(num_qubits)\n", - "ghz_circ.h(0)\n", - "[ghz_circ.cx(0, i) for i in range(1, num_qubits)]\n", - "ghz_circ.measure_all()\n", - "ghz_circ.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "f5134657", - "metadata": {}, - "source": [ - "### 2.1 Optimization level\n", - "`optimization_level` has four options, from 0 to 3. The higher the optimization level, the more computational effort is spent to optimize the circuit. Level 0 performs no optimization and just does the minimal amount of work to make the circuit runnable on the selected backend. Level 3 spends the most amount if effort (and typically runtime) to try to optimize the circuit. Level 1 is the default optimization level." - ] - }, - { - "cell_type": "markdown", - "id": "ed964d46", - "metadata": {}, - "source": [ - "We transpile the circuit without optimization (`optimization_level=0`) and with optimization (`optimization_level=2`).\n", - "We see a big difference in the circuit length of transpiled circuits." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "042d2bbc", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "optimization_level=0:\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "optimization_level=2:\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "pm0 = generate_preset_pass_manager(\n", - " optimization_level=0, backend=backend, seed_transpiler=777\n", - ")\n", - "pm2 = generate_preset_pass_manager(\n", - " optimization_level=2, backend=backend, seed_transpiler=777\n", - ")\n", - "circ0 = pm0.run(ghz_circ)\n", - "circ2 = pm2.run(ghz_circ)\n", - "print(\"optimization_level=0:\")\n", - "display(circ0.draw(\"mpl\", idle_wires=False, fold=-1))\n", - "print(\"optimization_level=2:\")\n", - "display(circ2.draw(\"mpl\", idle_wires=False, fold=-1))" - ] - }, - { - "cell_type": "markdown", - "id": "acd14f8c", - "metadata": {}, - "source": [ - "### 2.2 Exercise\n", - "Try `optimization_level=1` as well and compare the resulting circuit with the above two. Try it by modifying the code above.\n", - "\n", - "__Solution:__" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "6e8389e1", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "optimization_level=1:\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "pm1 = generate_preset_pass_manager(\n", - " optimization_level=1, backend=backend, seed_transpiler=777\n", - ")\n", - "circ1 = pm1.run(ghz_circ)\n", - "print(\"optimization_level=1:\")\n", - "display(circ1.draw(\"mpl\", idle_wires=False, fold=-1))" - ] - }, - { - "cell_type": "markdown", - "id": "641ec604", - "metadata": {}, - "source": [ - "Run on a fake backend (noisy simulation). See Appendix 1 for how to run on a real backend." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "dfb1b0ad", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Job ID: 93a4ac70-e3ea-44ad-aea9-5045840c9076\n" - ] - } - ], - "source": [ - "# run the circuits on the fake backend (noisy simulator)\n", - "sampler = Sampler(backend=backend)\n", - "job = sampler.run([circ0, circ2], shots=10000)\n", - "print(f\"Job ID: {job.job_id()}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "fde8d64b", - "metadata": {}, - "outputs": [], - "source": [ - "# get results\n", - "result = job.result()\n", - "unoptimized_result = result[0].data.meas.get_counts()\n", - "optimized_result = result[1].data.meas.get_counts()" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "5d344bb9", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.visualization import plot_histogram\n", - "\n", - "# plot\n", - "sim_result = {\"0\" * 5: 0.5, \"1\" * 5: 0.5}\n", - "plot_histogram(\n", - " [result for result in [sim_result, unoptimized_result, optimized_result]],\n", - " bar_labels=False,\n", - " legend=[\n", - " \"ideal\",\n", - " \"no optimization\",\n", - " \"with optimization\",\n", - " ],\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "f6998d1e", - "metadata": {}, - "source": [ - "## 3. Circuit synthesis matters" - ] - }, - { - "cell_type": "markdown", - "id": "1f6208e0", - "metadata": {}, - "source": [ - "We next compare the results of running two differently synthesized 5-qubit GHZ state ($\\frac{1}{\\sqrt{2}} \\left( |00000\\rangle + |11111\\rangle \\right)$) preparation circuits." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "896dc520", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Original GHZ circuit (naive synthesis)\n", - "ghz_circ.draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "d27a9d9b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# A cleverly-synthesized GHZ circuit\n", - "ghz_circ2 = QuantumCircuit(5)\n", - "ghz_circ2.h(2)\n", - "ghz_circ2.cx(2, 1)\n", - "ghz_circ2.cx(2, 3)\n", - "ghz_circ2.cx(1, 0)\n", - "ghz_circ2.cx(3, 4)\n", - "ghz_circ2.measure_all()\n", - "ghz_circ2.draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "d4e16053", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "original synthesis:\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "new synthesis:\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# transpile both with the same optimization level 2\n", - "circ_org = pm2.run(ghz_circ)\n", - "circ_new = pm2.run(ghz_circ2)\n", - "print(\"original synthesis:\")\n", - "display(circ_org.draw(\"mpl\", idle_wires=False, fold=-1))\n", - "print(\"new synthesis:\")\n", - "display(circ_new.draw(\"mpl\", idle_wires=False, fold=-1))" - ] - }, - { - "cell_type": "markdown", - "id": "da0dbc8f", - "metadata": {}, - "source": [ - "The new synthesis produces a shallower circuit. Why?\n", - "\n", - "This is because the new circuit can be laid out on linearly connected qubits, so on IBM® Brisbane's heavy-hexagon coupling graph as well, while the original circuit requires star-shaped connectivity (a degree-4 node) and hence cannot be laid out on the heavy-hex coupling graph, which has nodes at most degree 3. As a result, the original circuit requires qubit routing that adds SWAP gates, increasing the gate count.\n", - "\n", - "What we have done in the new circuit can be seen as a manual \"coupling constraint-aware\" circuit synthesis. In other words: manually solving circuit synthesis and circuit mapping at the same time." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "5f884bba", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Job ID: 19d635b0-4d8b-44c2-a76e-49e4b9078b1b\n" - ] - } - ], - "source": [ - "# run the circuits\n", - "sampler = Sampler(backend=backend)\n", - "job = sampler.run([circ_org, circ_new], shots=10000)\n", - "print(f\"Job ID: {job.job_id()}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "3af5e682", - "metadata": {}, - "outputs": [], - "source": [ - "# get results\n", - "result = job.result()\n", - "synthesis_org_result = result[0].data.meas.get_counts()\n", - "synthesis_new_result = result[1].data.meas.get_counts()" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "80f8ef81", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# plot\n", - "sim_result = {\"0\" * 5: 0.5, \"1\" * 5: 0.5}\n", - "plot_histogram(\n", - " [\n", - " result\n", - " for result in [\n", - " sim_result,\n", - " unoptimized_result,\n", - " synthesis_org_result,\n", - " synthesis_new_result,\n", - " ]\n", - " ],\n", - " bar_labels=False,\n", - " legend=[\n", - " \"ideal\",\n", - " \"no optimization\",\n", - " \"synthesis_org\",\n", - " \"synthesis_new\",\n", - " ],\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "96746945", - "metadata": {}, - "source": [ - "In general, circuit synthesis depends on application and it's too difficult for a software to cover all possible applications. Qiskit transpiler happens to have no functions of synthesizing GHZ state preparation circuit. In such a case, manual circuit synthesis as shown above is worth considering." - ] - }, - { - "cell_type": "markdown", - "id": "73844ec5", - "metadata": {}, - "source": [ - "In this section, we look into the details of how Qiskit transpiler works using the following toy example circuit." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f2228937", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Build a toy example circuit\n", - "from math import pi\n", - "import itertools\n", - "from qiskit.circuit import QuantumCircuit\n", - "from qiskit.circuit.library import excitation_preserving\n", - "\n", - "circuit = QuantumCircuit(4, name=\"Example circuit\")\n", - "circuit.append(excitation_preserving(4, reps=1, flatten=True), range(4))\n", - "circuit.measure_all()\n", - "\n", - "value_cycle = itertools.cycle([0, pi / 4, pi / 2, 3 * pi / 4, pi, 2 * pi])\n", - "circuit.assign_parameters(\n", - " [x[1] for x in zip(range(len(circuit.parameters)), value_cycle)], inplace=True\n", - ")\n", - "circuit.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "aacd9748", - "metadata": {}, - "source": [ - "### 3.1 Draw the entire Qiskit transpilation flow" - ] - }, - { - "cell_type": "markdown", - "id": "c5e8511a", - "metadata": {}, - "source": [ - "We look into the transpiler passes (tasks) for `optimization_level=1`." - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "74bd20af", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "\n", - "# There is no need to read this entire image, but this outputs all the steps in the transpile() call\n", - "# for optimization level 1\n", - "pm = generate_preset_pass_manager(1, backend, seed_transpiler=42)\n", - "pm.draw()" - ] - }, - { - "cell_type": "markdown", - "id": "833a8bcc", - "metadata": {}, - "source": [ - "The flow consists of six stages:" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "f58a6711", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "('init', 'layout', 'routing', 'translation', 'optimization', 'scheduling')\n" - ] - } - ], - "source": [ - "print(pm.stages)" - ] - }, - { - "cell_type": "markdown", - "id": "f34ba488-1f4a-429e-8c1c-3b623ae4826c", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "slide" - }, - "tags": [] - }, - "source": [ - "### 3.2 Draw an individual stage\n", - "\n", - "First, let's draw all the tasks (transpiler passes) done in the `init` stage." - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "09b4ffbe", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "pm.init.draw()" - ] - }, - { - "cell_type": "markdown", - "id": "e7e4329f", - "metadata": {}, - "source": [ - "We can run each individual stage. Let's run `init` stage for our circuit. By enabling logger, we can see the details of the run." - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "a139da85-e5b4-4c7c-900f-da8a0b8a5989", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "subslide" - }, - "tags": [] - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO:qiskit.passmanager.base_tasks:Pass: UnitarySynthesis - 0.03576 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: HighLevelSynthesis - 0.16618 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: BasisTranslator - 0.07176 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: InverseCancellation - 0.27299 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: ContractIdleWiresInControlFlow - 0.00811 (ms)\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import logging\n", - "\n", - "logger = logging.getLogger()\n", - "logger.setLevel(\"INFO\")\n", - "\n", - "init_out = pm.init.run(circuit)\n", - "init_out.draw(\"mpl\", fold=-1)" - ] - }, - { - "cell_type": "markdown", - "id": "c97816d8", - "metadata": {}, - "source": [ - "### 3.3 Exercise\n", - "Draw `layout` stage passes and run the stage for the output circuit of the `init` stage (`init_out`), by modifying cells used above.\n", - "\n", - "__Solution:__" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "56024db6", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO:qiskit.passmanager.base_tasks:Pass: SetLayout - 0.01001 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: TrivialLayout - 0.07129 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: CheckMap - 0.08917 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: VF2Layout - 1.24431 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: BarrierBeforeFinalMeasurements - 0.02599 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: SabreLayout - 5.11169 (ms)\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 23, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "display(pm.layout.draw())\n", - "layout_out = pm.layout.run(init_out)\n", - "layout_out.draw(\"mpl\", idle_wires=False, fold=-1)" - ] - }, - { - "cell_type": "markdown", - "id": "6db6618a", - "metadata": {}, - "source": [ - "Do the same thing for `translation` stage.\n", - "\n", - "__Solution:__" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "id": "fd7cec6b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO:qiskit.passmanager.base_tasks:Pass: UnitarySynthesis - 0.03386 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: HighLevelSynthesis - 0.02718 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: BasisTranslator - 2.64192 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: CheckGateDirection - 0.02217 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: GateDirection - 0.36502 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: BasisTranslator - 0.64778 (ms)\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "display(pm.translation.draw())\n", - "basis_out = pm.translation.run(layout_out)\n", - "basis_out.draw(\"mpl\", idle_wires=False, fold=-1)" - ] - }, - { - "cell_type": "markdown", - "id": "e3d78127-46f4-498a-958b-7c8ba107ae9d", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "slide" - }, - "tags": [] - }, - "source": [ - "Note: Any individual stage cannot always be run independently (as some of them need to carry over information from one previous stage)." - ] - }, - { - "cell_type": "markdown", - "id": "ff7370b9-6588-49c0-b82f-dbef823a973c", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "slide" - }, - "tags": [] - }, - "source": [ - "### 3.4 Optimization Stage\n", - "\n", - "The last default stage in the pipeline is optimization. After we've embedded the circuit for the target the circuit has expanded quite a bit. Most of this is due to inefficiencies in the equivalence relationships from basis translation and swap insertion. The optimization stage is used to try and minimize the size and depth of the circuit. It runs a series of passes in a ```do while``` loop until it reaches a steady output." - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "id": "f86b9045", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 25, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# pm.pre_optimization.draw()\n", - "pm.optimization.draw()" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "id": "c2ee9c96-b595-4882-a581-1dbd28ac980e", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "subslide" - }, - "tags": [] - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "INFO:qiskit.passmanager.base_tasks:Pass: Depth - 0.30112 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.03195 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: Size - 0.01216 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.01001 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: Optimize1qGatesDecomposition - 0.63729 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: InverseCancellation - 0.41723 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: ContractIdleWiresInControlFlow - 0.01192 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: GatesInBasis - 0.05484 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: Depth - 0.08583 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.20599 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: Size - 0.00787 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.00715 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: Optimize1qGatesDecomposition - 0.16809 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: InverseCancellation - 0.17190 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: ContractIdleWiresInControlFlow - 0.00691 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: GatesInBasis - 0.02408 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: Depth - 0.04935 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.00525 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: Size - 0.00620 (ms)\n", - "INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.00286 (ms)\n" - ] - } - ], - "source": [ - "logger = logging.getLogger()\n", - "logger.setLevel(\"INFO\")\n", - "opt_out = pm.optimization.run(basis_out)" - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "id": "65d650b0-ec27-4b1b-a121-f1bb958b18e2", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "" - }, - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 28, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "opt_out.draw(\"mpl\", idle_wires=False, fold=-1)" - ] - }, - { - "cell_type": "markdown", - "id": "24c2abce-393a-41c5-9d1d-6273ad94a707", - "metadata": { - "editable": true, - "slideshow": { - "slide_type": "subslide" - }, - "tags": [] - }, - "source": [ - "## 4. In-depth examples\n", - "### 4.1 Two-qubit block optimization using two-qubit unitary synthesis\n", - "\n", - "For level 2 and 3, we have more passes (`Collect2qBlocks`, `ConsolidateBlocks`, `UnitarySynthesis`) for more optimization, namely two-qubit block optimization. (Compare the optimization stage flow for level 2 with that above for level 1)\n", - "\n", - "The two-qubit block optimization is composed of two steps: Collecting and consolidating 2-qubit blocks and synthesizing the 2-qubit unitary matrices." - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "id": "179b1440", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 29, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "pm2 = generate_preset_pass_manager(2, backend, seed_transpiler=42)\n", - "pm2.optimization.draw()" - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "id": "e3f9e358-cc38-46cc-b2f7-f3bb9b9b179d", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.transpiler import PassManager\n", - "from qiskit.transpiler.passes import (\n", - " Collect2qBlocks,\n", - " ConsolidateBlocks,\n", - " UnitarySynthesis,\n", - ")\n", - "\n", - "# Collect 2q blocks and consolidate to unitary when we expect that we can reduce the 2q gate count for that unitary\n", - "consolidate_pm = PassManager(\n", - " [\n", - " Collect2qBlocks(),\n", - " ConsolidateBlocks(target=backend.target),\n", - " ]\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "id": "bbf4fa9a-6b49-4833-82fd-b3821f6bcb78", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 36, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "display(basis_out.draw(\"mpl\", idle_wires=False, fold=-1))\n", - "\n", - "consolidated = consolidate_pm.run(basis_out)\n", - "consolidated.draw(\"mpl\", idle_wires=False, fold=-1)" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "id": "7e7e0d3b-d267-4b1c-b207-42556d1ff3f2", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 37, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Synthesize unitaries\n", - "UnitarySynthesis(target=backend.target)(consolidated).draw(\n", - " \"mpl\", idle_wires=False, fold=-1\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "id": "a5c6dcec", - "metadata": {}, - "outputs": [], - "source": [ - "logger.setLevel(\"WARNING\")" - ] - }, - { - "cell_type": "markdown", - "id": "59a51954-c737-4ee2-b1be-fee896388c83", - "metadata": {}, - "source": [ - "We saw in Part 2 that the real quantum compiler flow is not that simple and is composed of many passes (tasks). This is mainly due to the software engineering required to ensure performance for a wide range of application circuits and maintainability of the software. Qiskit transpiler would work well in most cases but if you happen to see your circuit is not well optimized by Qiskit transpiler, it would be a good opportunity to research your own application-specific circuit optimization as shown in Part 1. Transpiler technology is evolving, your R&D contribution is welcome." - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "id": "86452399", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.circuit import QuantumCircuit\n", - "from qiskit_ibm_runtime import QiskitRuntimeService, Sampler" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1ff499b3", - "metadata": {}, - "outputs": [], - "source": [ - "service = QiskitRuntimeService()\n", - "backend = service.backend(\"ibm_brisbane\")\n", - "sampler = Sampler(backend)" - ] - }, - { - "cell_type": "code", - "execution_count": 43, - "id": "30a84ca1", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 43, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "circ = QuantumCircuit(3)\n", - "circ.ccx(0, 1, 2)\n", - "circ.measure_all()\n", - "circ.draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 44, - "id": "f58d9cb9", - "metadata": {}, - "outputs": [ - { - "ename": "IBMInputValueError", - "evalue": "'The instruction ccx on qubits (0, 1, 2) is not supported by the target system. Circuits that do not match the target hardware definition are no longer supported after March 4, 2024. See the transpilation documentation (/docs/guides/transpile) for instructions to transform circuits and the primitive examples (/docs/guides/sampler-examples) to see this coupled with operator transformations.'", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mIBMInputValueError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[44]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[43msampler\u001b[49m\u001b[43m.\u001b[49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\u001b[43m[\u001b[49m\u001b[43mcirc\u001b[49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;66;03m# IBMInputValueError will be raised\u001b[39;00m\n", - "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Caskroom/miniforge/base/envs/doc/lib/python3.11/site-packages/qiskit_ibm_runtime/sampler.py:111\u001b[39m, in \u001b[36mSamplerV2.run\u001b[39m\u001b[34m(self, pubs, shots)\u001b[39m\n\u001b[32m 107\u001b[39m coerced_pubs = [SamplerPub.coerce(pub, shots) \u001b[38;5;28;01mfor\u001b[39;00m pub \u001b[38;5;129;01min\u001b[39;00m pubs]\n\u001b[32m 109\u001b[39m validate_classical_registers(coerced_pubs)\n\u001b[32m--> \u001b[39m\u001b[32m111\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_run\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcoerced_pubs\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Caskroom/miniforge/base/envs/doc/lib/python3.11/site-packages/qiskit_ibm_runtime/base_primitive.py:158\u001b[39m, in \u001b[36mBasePrimitiveV2._run\u001b[39m\u001b[34m(self, pubs)\u001b[39m\n\u001b[32m 156\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m pub \u001b[38;5;129;01min\u001b[39;00m pubs:\n\u001b[32m 157\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mgetattr\u001b[39m(\u001b[38;5;28mself\u001b[39m._backend, \u001b[33m\"\u001b[39m\u001b[33mtarget\u001b[39m\u001b[33m\"\u001b[39m, \u001b[38;5;28;01mNone\u001b[39;00m) \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m is_simulator(\u001b[38;5;28mself\u001b[39m._backend):\n\u001b[32m--> \u001b[39m\u001b[32m158\u001b[39m \u001b[43mvalidate_isa_circuits\u001b[49m\u001b[43m(\u001b[49m\u001b[43m[\u001b[49m\u001b[43mpub\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcircuit\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_backend\u001b[49m\u001b[43m.\u001b[49m\u001b[43mtarget\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 160\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(\u001b[38;5;28mself\u001b[39m._backend, IBMBackend):\n\u001b[32m 161\u001b[39m \u001b[38;5;28mself\u001b[39m._backend.check_faulty(pub.circuit)\n", - "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Caskroom/miniforge/base/envs/doc/lib/python3.11/site-packages/qiskit_ibm_runtime/utils/validations.py:96\u001b[39m, in \u001b[36mvalidate_isa_circuits\u001b[39m\u001b[34m(circuits, target)\u001b[39m\n\u001b[32m 94\u001b[39m message = is_isa_circuit(circuit, target)\n\u001b[32m 95\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m message:\n\u001b[32m---> \u001b[39m\u001b[32m96\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m IBMInputValueError(\n\u001b[32m 97\u001b[39m message\n\u001b[32m 98\u001b[39m + \u001b[33m\"\u001b[39m\u001b[33m Circuits that do not match the target hardware definition are no longer \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 99\u001b[39m \u001b[33m\"\u001b[39m\u001b[33msupported after March 4, 2024. See the transpilation documentation \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 100\u001b[39m \u001b[33m\"\u001b[39m\u001b[33m(https://quantum.cloud.ibm.com/docs/guides/transpile) for instructions \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 101\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mto transform circuits and the primitive examples \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 102\u001b[39m \u001b[33m\"\u001b[39m\u001b[33m(https://quantum.cloud.ibm.com/docs/guides/primitives-examples) to see \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 103\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mthis coupled with operator transformations.\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 104\u001b[39m )\n", - "\u001b[31mIBMInputValueError\u001b[39m: 'The instruction ccx on qubits (0, 1, 2) is not supported by the target system. Circuits that do not match the target hardware definition are no longer supported after March 4, 2024. See the transpilation documentation (https://quantum.cloud.ibm.com/docs/guides/transpile) for instructions to transform circuits and the primitive examples (https://quantum.cloud.ibm.com/docs/guides/primitives-examples) to see this coupled with operator transformations.'" - ] - } - ], - "source": [ - "sampler.run([circ]) # IBMInputValueError will be raised" - ] - }, - { - "cell_type": "markdown", - "id": "2a261d51", - "metadata": {}, - "source": [ - "### 4.2 Circuit optimization matters\n", - "\n", - "We first compare the results of running 5-qubit GHZ state ($\\frac{1}{\\sqrt{2}} \\left( |00000\\rangle + |11111\\rangle \\right)$) preparation circuits with and without optimization." - ] - }, - { - "cell_type": "code", - "execution_count": 45, - "id": "9483b736", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit.circuit import QuantumCircuit\n", - "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", - "from qiskit_ibm_runtime import QiskitRuntimeService, Sampler" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "52e99762", - "metadata": {}, - "outputs": [], - "source": [ - "service = QiskitRuntimeService()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "270db9aa", - "metadata": {}, - "outputs": [], - "source": [ - "# backend = service.backend('ibm_brisbane')\n", - "backend = service.least_busy(\n", - " operational=True, simulator=False, min_num_qubits=127\n", - ") # Eagle\n", - "backend" - ] - }, - { - "cell_type": "markdown", - "id": "29d29a39", - "metadata": {}, - "source": [ - "We first use a GHZ circuit naively synthesized as follows." - ] - }, - { - "cell_type": "code", - "execution_count": 55, - "id": "485b8ce6", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 55, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "num_qubits = 5\n", - "\n", - "ghz_circ = QuantumCircuit(num_qubits)\n", - "ghz_circ.h(0)\n", - "[ghz_circ.cx(0, i) for i in range(1, num_qubits)]\n", - "ghz_circ.measure_all()\n", - "ghz_circ.draw(\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "8eaa552e", - "metadata": {}, - "source": [ - "We transpile the circuit without optimization (`optimization_level=0`) and with optimization (`optimization_level=2`).\n", - "As you can see, there is a big difference in the circuit length of transpiled circuits." - ] - }, - { - "cell_type": "code", - "execution_count": 56, - "id": "87b861e2", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "optimization_level=0:\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "optimization_level=2:\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "pm0 = generate_preset_pass_manager(\n", - " optimization_level=0, backend=backend, seed_transpiler=777\n", - ")\n", - "pm2 = generate_preset_pass_manager(\n", - " optimization_level=2, backend=backend, seed_transpiler=777\n", - ")\n", - "circ0 = pm0.run(ghz_circ)\n", - "circ2 = pm2.run(ghz_circ)\n", - "print(\"optimization_level=0:\")\n", - "display(circ0.draw(\"mpl\", idle_wires=False, fold=-1))\n", - "print(\"optimization_level=2:\")\n", - "display(circ2.draw(\"mpl\", idle_wires=False, fold=-1))" - ] - }, - { - "cell_type": "code", - "execution_count": 57, - "id": "328f71f2", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Job ID: d13rnnemya70008ek1zg\n" - ] - } - ], - "source": [ - "# run the circuits\n", - "sampler = Sampler(backend)\n", - "job = sampler.run([circ0, circ2], shots=10000)\n", - "job_id = job.job_id()\n", - "print(f\"Job ID: {job_id}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 58, - "id": "d138e1e9", - "metadata": {}, - "outputs": [], - "source": [ - "# REPLACE WITH YOUR OWN JOB IDS\n", - "job = service.job(job_id)" - ] - }, - { - "cell_type": "code", - "execution_count": 59, - "id": "c137e06a", - "metadata": {}, - "outputs": [], - "source": [ - "# get results\n", - "result = job.result()\n", - "unoptimized_result = result[0].data.meas.get_counts()\n", - "optimized_result = result[1].data.meas.get_counts()" - ] - }, - { - "cell_type": "code", - "execution_count": 60, - "id": "7527976e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 60, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.visualization import plot_histogram\n", - "\n", - "# plot\n", - "sim_result = {\"0\" * 5: 0.5, \"1\" * 5: 0.5}\n", - "plot_histogram(\n", - " [result for result in [sim_result, unoptimized_result, optimized_result]],\n", - " bar_labels=False,\n", - " legend=[\n", - " \"ideal\",\n", - " \"no optimization\",\n", - " \"with optimization\",\n", - " ],\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "d0d49ab7", - "metadata": {}, - "source": [ - "### 4.3 Circuit synthesis matters" - ] - }, - { - "cell_type": "markdown", - "id": "c6643c6d", - "metadata": {}, - "source": [ - "We next compare the results of running two differently synthesized 5-qubit GHZ state ($\\frac{1}{\\sqrt{2}} \\left( |00000\\rangle + |11111\\rangle \\right)$) preparation circuits." - ] - }, - { - "cell_type": "code", - "execution_count": 61, - "id": "886d9b45", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 61, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Original GHZ circuit (naive synthesis)\n", - "ghz_circ.draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 62, - "id": "3b559186", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 62, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# A better GHZ circuit (smarter synthesis), you learned in a previous lecture\n", - "ghz_circ2 = QuantumCircuit(5)\n", - "ghz_circ2.h(2)\n", - "ghz_circ2.cx(2, 1)\n", - "ghz_circ2.cx(2, 3)\n", - "ghz_circ2.cx(1, 0)\n", - "ghz_circ2.cx(3, 4)\n", - "ghz_circ2.measure_all()\n", - "ghz_circ2.draw(\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 63, - "id": "054890b6", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "original synthesis:\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "new synthesis:\n" - ] - }, - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "circ_org = pm2.run(ghz_circ)\n", - "circ_new = pm2.run(ghz_circ2)\n", - "print(\"original synthesis:\")\n", - "display(circ_org.draw(\"mpl\", idle_wires=False, fold=-1))\n", - "print(\"new synthesis:\")\n", - "display(circ_new.draw(\"mpl\", idle_wires=False, fold=-1))" - ] - }, - { - "cell_type": "code", - "execution_count": 64, - "id": "de0c8577", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Job ID: d13rp283grvg008j12fg\n" - ] - } - ], - "source": [ - "# run the circuits\n", - "sampler = Sampler(backend)\n", - "job = sampler.run([circ_org, circ_new], shots=10000)\n", - "job_id = job.job_id()\n", - "print(f\"Job ID: {job_id}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 66, - "id": "a6fcb968", - "metadata": {}, - "outputs": [], - "source": [ - "# REPLACE WITH YOUR OWN JOB IDS\n", - "job = service.job(job_id)" - ] - }, - { - "cell_type": "code", - "execution_count": 67, - "id": "82165302", - "metadata": {}, - "outputs": [], - "source": [ - "# get results\n", - "result = job.result()\n", - "synthesis_org_result = result[0].data.meas.get_counts()\n", - "synthesis_new_result = result[1].data.meas.get_counts()" - ] - }, - { - "cell_type": "code", - "execution_count": 68, - "id": "b9021da5", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 68, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# plot\n", - "sim_result = {\"0\" * 5: 0.5, \"1\" * 5: 0.5}\n", - "plot_histogram(\n", - " [result for result in [sim_result, synthesis_org_result, synthesis_new_result]],\n", - " bar_labels=False,\n", - " legend=[\n", - " \"ideal\",\n", - " \"synthesis_org\",\n", - " \"synthesis_new\",\n", - " ],\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "07f7638d", - "metadata": {}, - "source": [ - "### 4.4 General 1-qubit gate decomposition" - ] - }, - { - "cell_type": "code", - "execution_count": 69, - "id": "f08c76bb", - "metadata": {}, - "outputs": [], - "source": [ - "from qiskit import QuantumCircuit, transpile\n", - "from qiskit.circuit import Parameter\n", - "from qiskit.circuit.library.standard_gates import UGate\n", - "\n", - "phi, theta, lam = Parameter(\"φ\"), Parameter(\"θ\"), Parameter(\"λ\")" - ] - }, - { - "cell_type": "code", - "execution_count": 70, - "id": "ed93f69a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 70, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(1)\n", - "qc.append(UGate(theta, phi, lam), [0])\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 42, - "id": "2fa17bd2", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 42, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "transpile(qc, basis_gates=[\"rz\", \"sx\"]).draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "d8af1753", - "metadata": {}, - "source": [ - "### 4.5 One-qubit block optimization" - ] - }, - { - "cell_type": "code", - "execution_count": 71, - "id": "6f64d07d", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 71, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit import QuantumCircuit\n", - "\n", - "qc = QuantumCircuit(1)\n", - "qc.x(0)\n", - "qc.y(0)\n", - "qc.z(0)\n", - "qc.rx(1.23, 0)\n", - "qc.ry(1.23, 0)\n", - "qc.rz(1.23, 0)\n", - "qc.h(0)\n", - "qc.s(0)\n", - "qc.t(0)\n", - "qc.sx(0)\n", - "qc.sdg(0)\n", - "qc.tdg(0)\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 72, - "id": "0aa1b908", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Operator([[ 0.45292511-0.57266982j, -0.66852684-0.14135058j],\n", - " [ 0.14135058+0.66852684j, -0.57266982+0.45292511j]],\n", - " input_dims=(2,), output_dims=(2,))\n" - ] - } - ], - "source": [ - "from qiskit.quantum_info import Operator\n", - "\n", - "Operator(qc)" - ] - }, - { - "cell_type": "code", - "execution_count": 73, - "id": "c06f5e75", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 73, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit import transpile\n", - "\n", - "qc_opt = transpile(qc, basis_gates=[\"rz\", \"sx\"])\n", - "qc_opt.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 74, - "id": "a9ec0568", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Operator([[ 0.45292511-0.57266982j, -0.66852684-0.14135058j],\n", - " [ 0.14135058+0.66852684j, -0.57266982+0.45292511j]],\n", - " input_dims=(2,), output_dims=(2,))\n" - ] - } - ], - "source": [ - "Operator(qc_opt)" - ] - }, - { - "cell_type": "code", - "execution_count": 75, - "id": "e83779af", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 75, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "Operator(qc).equiv(Operator(qc_opt))" - ] - }, - { - "cell_type": "markdown", - "id": "1ebdc297", - "metadata": {}, - "source": [ - "### 4.6 Toffoli decomposition" - ] - }, - { - "cell_type": "code", - "execution_count": 76, - "id": "f802c5df", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 76, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(3)\n", - "qc.ccx(0, 1, 2)\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 77, - "id": "330cea7e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 77, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit import QuantumCircuit, transpile\n", - "\n", - "qc = QuantumCircuit(3)\n", - "qc.ccx(0, 1, 2)\n", - "qc = transpile(qc, basis_gates=[\"rz\", \"sx\", \"cx\"])\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "24004df8", - "metadata": {}, - "source": [ - "### 4.7 CU gate decomposition" - ] - }, - { - "cell_type": "code", - "execution_count": 78, - "id": "1df5876d", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 78, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.circuit.library.standard_gates import CUGate\n", - "\n", - "phi, theta, lam, gamma = Parameter(\"φ\"), Parameter(\"θ\"), Parameter(\"λ\"), Parameter(\"γ\")\n", - "qc = QuantumCircuit(2)\n", - "# qc.cu(theta, phi, lam, gamma, 0, 1)\n", - "qc.append(CUGate(theta, phi, lam, gamma), [0, 1])\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "code", - "execution_count": 79, - "id": "64f7e5f3", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 79, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from qiskit.circuit.library.standard_gates import CUGate\n", - "\n", - "phi, theta, lam, gamma = Parameter(\"φ\"), Parameter(\"θ\"), Parameter(\"λ\"), Parameter(\"γ\")\n", - "qc = QuantumCircuit(2)\n", - "qc.append(CUGate(theta, phi, lam, gamma), [0, 1])\n", - "qc = transpile(qc, basis_gates=[\"rz\", \"sx\", \"cx\"])\n", - "qc.draw(output=\"mpl\")" - ] - }, - { - "cell_type": "markdown", - "id": "88e7a028", - "metadata": {}, - "source": [ - "### 4.8 CX, ECR, CZ equal up to local Cliffords\n", - "\n", - "Note that $H$(Hadamard), $S$($\\pi/2$ Z-rotation), $S^\\dagger$($-\\pi/2$ Z-rotation), $X$(Pauli X) are all Clifford gates." - ] - }, - { - "cell_type": "code", - "execution_count": 80, - "id": "f5b362b6", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 80, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(2)\n", - "qc.cx(0, 1)\n", - "qc.draw(output=\"mpl\", style=\"bw\")" - ] - }, - { - "cell_type": "code", - "execution_count": 81, - "id": "8740d07b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 81, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(2)\n", - "qc.cx(0, 1)\n", - "transpile(qc, basis_gates=[\"x\", \"s\", \"h\", \"sdg\", \"ecr\"]).draw(output=\"mpl\", style=\"bw\")" - ] - }, - { - "cell_type": "code", - "execution_count": 82, - "id": "5113a7c5", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 82, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(2)\n", - "qc.cx(0, 1)\n", - "transpile(qc, basis_gates=[\"h\", \"cz\"]).draw(output=\"mpl\", style=\"bw\")" - ] - }, - { - "cell_type": "markdown", - "id": "f19867b3", - "metadata": {}, - "source": [ - "Using IBM backend 1q basis gates \"rz\", \"sx\" and \"x\"." - ] - }, - { - "cell_type": "code", - "execution_count": 83, - "id": "9d9b54d4", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 83, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(2)\n", - "qc.cx(0, 1)\n", - "transpile(qc, basis_gates=[\"rz\", \"sx\", \"x\", \"ecr\"]).draw(output=\"mpl\", style=\"bw\")" - ] - }, - { - "cell_type": "code", - "execution_count": 84, - "id": "c395cd24", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\"Output" - ] - }, - "execution_count": 84, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "qc = QuantumCircuit(2)\n", - "qc.cx(0, 1)\n", - "transpile(qc, basis_gates=[\"rz\", \"sx\", \"x\", \"cz\"]).draw(output=\"mpl\", style=\"bw\")" - ] - }, - { - "cell_type": "code", - "execution_count": 85, - "id": "4eab683f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'2.0.2'" - ] - }, - "execution_count": 85, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Check Qiskit version\n", - "import qiskit\n", - "\n", - "qiskit.__version__" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "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" - }, - "widgets": { - "application/vnd.jupyter.widget-state+json": { - "state": {}, - "version_major": 2, - "version_minor": 0 - } - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} +{ + "cells": [ + { + "cell_type": "markdown", + "id": "fad91a68", + "metadata": {}, + "source": [ + "---\n", + "title: Quantum circuit optimization\n", + "description: This lesson will address several aspects of circuit optimization in quantum computing.\n", + "---\n", + "\n", + "\n", + " # Quantum circuit optimization\n", + "\n", + "\n", + "\n", + "Toshinari Itoko (21 June 2024)\n", + "\n", + "[Download the pdf](https://ibm.ent.box.com/public/static/0hvvgr1gnwx64x2ukgk04sss6sxc4zko.zip) of the original lecture. Note that some code snippets might become deprecated since these are static images.\n", + "\n", + "*Approximate QPU time to run this experiment is 15 s.*\n", + "\n", + "(Note: Some cells of part 2 are copied from the notebook \"Qiskit Deep dive\", written by Matthew Treinish (Qiskit maintainer))\n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "38cf024b", + "metadata": {}, + "outputs": [], + "source": [ + "# !pip install 'qiskit[visualization]'\n", + "# !pip install qiskit_ibm_runtime qiskit_aer\n", + "# !pip install jupyter\n", + "# !pip install matplotlib pylatexenc pydot pillow" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "5bc656fa-6376-436e-adfc-59676edc719b", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'2.0.2'" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import qiskit\n", + "\n", + "qiskit.__version__" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "b5d0220b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'0.40.1'" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import qiskit_ibm_runtime\n", + "\n", + "qiskit_ibm_runtime.__version__" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "1032e247", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'0.17.1'" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import qiskit_aer\n", + "\n", + "qiskit_aer.__version__" + ] + }, + { + "cell_type": "markdown", + "id": "335804e7", + "metadata": {}, + "source": [ + "## 1. Introduction\n", + "\n", + "This lesson will address several aspects of circuit optimization in quantum computing. Specifically, we will see the value of circuit optimization by using optimization settings built into Qiskit. Then we will go a bit deeper and see what you can do as an expert in your particular application area to build circuits in a smart way. Finally, we will take a close look at what goes on during transpilation that helps us optimize our circuits." + ] + }, + { + "cell_type": "markdown", + "id": "187c0ab1", + "metadata": {}, + "source": [ + "## 2. Circuit optimization matters\n", + "\n", + "We first compare the results of running 5-qubit GHZ state ($\\frac{1}{\\sqrt{2}} \\left( |00000\\rangle + |11111\\rangle \\right)$) preparation circuits with and without optimization." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "b1570aa9", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.circuit import QuantumCircuit\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "from qiskit.primitives import BackendSamplerV2 as Sampler" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f6be6161", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_ibm_runtime.fake_provider import FakeBrisbane\n", + "\n", + "backend = FakeBrisbane()" + ] + }, + { + "cell_type": "markdown", + "id": "9cb38fa1", + "metadata": {}, + "source": [ + "We first use a GHZ circuit naively synthesized as follows." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "262b97e5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "num_qubits = 5\n", + "\n", + "ghz_circ = QuantumCircuit(num_qubits)\n", + "ghz_circ.h(0)\n", + "[ghz_circ.cx(0, i) for i in range(1, num_qubits)]\n", + "ghz_circ.measure_all()\n", + "ghz_circ.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "f5134657", + "metadata": {}, + "source": [ + "### 2.1 Optimization level\n", + "`optimization_level` has four options, from 0 to 3. The higher the optimization level, the more computational effort is spent to optimize the circuit. Level 0 performs no optimization and just does the minimal amount of work to make the circuit runnable on the selected backend. Level 3 spends the most amount if effort (and typically runtime) to try to optimize the circuit. Level 1 is the default optimization level." + ] + }, + { + "cell_type": "markdown", + "id": "ed964d46", + "metadata": {}, + "source": [ + "We transpile the circuit without optimization (`optimization_level=0`) and with optimization (`optimization_level=2`).\n", + "We see a big difference in the circuit length of transpiled circuits." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "042d2bbc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "optimization_level=0:\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "optimization_level=2:\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "pm0 = generate_preset_pass_manager(\n", + " optimization_level=0, backend=backend, seed_transpiler=777\n", + ")\n", + "pm2 = generate_preset_pass_manager(\n", + " optimization_level=2, backend=backend, seed_transpiler=777\n", + ")\n", + "circ0 = pm0.run(ghz_circ)\n", + "circ2 = pm2.run(ghz_circ)\n", + "print(\"optimization_level=0:\")\n", + "display(circ0.draw(\"mpl\", idle_wires=False, fold=-1))\n", + "print(\"optimization_level=2:\")\n", + "display(circ2.draw(\"mpl\", idle_wires=False, fold=-1))" + ] + }, + { + "cell_type": "markdown", + "id": "acd14f8c", + "metadata": {}, + "source": [ + "### 2.2 Exercise\n", + "Try `optimization_level=1` as well and compare the resulting circuit with the above two. Try it by modifying the code above.\n", + "\n", + "__Solution:__" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "6e8389e1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "optimization_level=1:\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "pm1 = generate_preset_pass_manager(\n", + " optimization_level=1, backend=backend, seed_transpiler=777\n", + ")\n", + "circ1 = pm1.run(ghz_circ)\n", + "print(\"optimization_level=1:\")\n", + "display(circ1.draw(\"mpl\", idle_wires=False, fold=-1))" + ] + }, + { + "cell_type": "markdown", + "id": "641ec604", + "metadata": {}, + "source": [ + "Run on a fake backend (noisy simulation). See Appendix 1 for how to run on a real backend." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "dfb1b0ad", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Job ID: 93a4ac70-e3ea-44ad-aea9-5045840c9076\n" + ] + } + ], + "source": [ + "# run the circuits on the fake backend (noisy simulator)\n", + "sampler = Sampler(backend=backend)\n", + "job = sampler.run([circ0, circ2], shots=10000)\n", + "print(f\"Job ID: {job.job_id()}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "fde8d64b", + "metadata": {}, + "outputs": [], + "source": [ + "# get results\n", + "result = job.result()\n", + "unoptimized_result = result[0].data.meas.get_counts()\n", + "optimized_result = result[1].data.meas.get_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "5d344bb9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.visualization import plot_histogram\n", + "\n", + "# plot\n", + "sim_result = {\"0\" * 5: 0.5, \"1\" * 5: 0.5}\n", + "plot_histogram(\n", + " [result for result in [sim_result, unoptimized_result, optimized_result]],\n", + " bar_labels=False,\n", + " legend=[\n", + " \"ideal\",\n", + " \"no optimization\",\n", + " \"with optimization\",\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "f6998d1e", + "metadata": {}, + "source": [ + "## 3. Circuit synthesis matters" + ] + }, + { + "cell_type": "markdown", + "id": "1f6208e0", + "metadata": {}, + "source": [ + "We next compare the results of running two differently synthesized 5-qubit GHZ state ($\\frac{1}{\\sqrt{2}} \\left( |00000\\rangle + |11111\\rangle \\right)$) preparation circuits." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "896dc520", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Original GHZ circuit (naive synthesis)\n", + "ghz_circ.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "d27a9d9b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# A cleverly-synthesized GHZ circuit\n", + "ghz_circ2 = QuantumCircuit(5)\n", + "ghz_circ2.h(2)\n", + "ghz_circ2.cx(2, 1)\n", + "ghz_circ2.cx(2, 3)\n", + "ghz_circ2.cx(1, 0)\n", + "ghz_circ2.cx(3, 4)\n", + "ghz_circ2.measure_all()\n", + "ghz_circ2.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "d4e16053", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "original synthesis:\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "new synthesis:\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# transpile both with the same optimization level 2\n", + "circ_org = pm2.run(ghz_circ)\n", + "circ_new = pm2.run(ghz_circ2)\n", + "print(\"original synthesis:\")\n", + "display(circ_org.draw(\"mpl\", idle_wires=False, fold=-1))\n", + "print(\"new synthesis:\")\n", + "display(circ_new.draw(\"mpl\", idle_wires=False, fold=-1))" + ] + }, + { + "cell_type": "markdown", + "id": "da0dbc8f", + "metadata": {}, + "source": [ + "The new synthesis produces a shallower circuit. Why?\n", + "\n", + "This is because the new circuit can be laid out on linearly connected qubits, so on IBM® Brisbane's heavy-hexagon coupling graph as well, while the original circuit requires star-shaped connectivity (a degree-4 node) and hence cannot be laid out on the heavy-hex coupling graph, which has nodes at most degree 3. As a result, the original circuit requires qubit routing that adds SWAP gates, increasing the gate count.\n", + "\n", + "What we have done in the new circuit can be seen as a manual \"coupling constraint-aware\" circuit synthesis. In other words: manually solving circuit synthesis and circuit mapping at the same time." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "5f884bba", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Job ID: 19d635b0-4d8b-44c2-a76e-49e4b9078b1b\n" + ] + } + ], + "source": [ + "# run the circuits\n", + "sampler = Sampler(backend=backend)\n", + "job = sampler.run([circ_org, circ_new], shots=10000)\n", + "print(f\"Job ID: {job.job_id()}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "3af5e682", + "metadata": {}, + "outputs": [], + "source": [ + "# get results\n", + "result = job.result()\n", + "synthesis_org_result = result[0].data.meas.get_counts()\n", + "synthesis_new_result = result[1].data.meas.get_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "80f8ef81", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# plot\n", + "sim_result = {\"0\" * 5: 0.5, \"1\" * 5: 0.5}\n", + "plot_histogram(\n", + " [\n", + " result\n", + " for result in [\n", + " sim_result,\n", + " unoptimized_result,\n", + " synthesis_org_result,\n", + " synthesis_new_result,\n", + " ]\n", + " ],\n", + " bar_labels=False,\n", + " legend=[\n", + " \"ideal\",\n", + " \"no optimization\",\n", + " \"synthesis_org\",\n", + " \"synthesis_new\",\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "96746945", + "metadata": {}, + "source": [ + "In general, circuit synthesis depends on application and it's too difficult for a software to cover all possible applications. Qiskit transpiler happens to have no functions of synthesizing GHZ state preparation circuit. In such a case, manual circuit synthesis as shown above is worth considering." + ] + }, + { + "cell_type": "markdown", + "id": "73844ec5", + "metadata": {}, + "source": [ + "In this section, we look into the details of how Qiskit transpiler works using the following toy example circuit." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f2228937", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Build a toy example circuit\n", + "from math import pi\n", + "import itertools\n", + "from qiskit.circuit import QuantumCircuit\n", + "from qiskit.circuit.library import excitation_preserving\n", + "\n", + "circuit = QuantumCircuit(4, name=\"Example circuit\")\n", + "circuit.append(excitation_preserving(4, reps=1, flatten=True), range(4))\n", + "circuit.measure_all()\n", + "\n", + "value_cycle = itertools.cycle([0, pi / 4, pi / 2, 3 * pi / 4, pi, 2 * pi])\n", + "circuit.assign_parameters(\n", + " [x[1] for x in zip(range(len(circuit.parameters)), value_cycle)], inplace=True\n", + ")\n", + "circuit.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "aacd9748", + "metadata": {}, + "source": [ + "### 3.1 Draw the entire Qiskit transpilation flow" + ] + }, + { + "cell_type": "markdown", + "id": "c5e8511a", + "metadata": {}, + "source": [ + "We look into the transpiler passes (tasks) for `optimization_level=1`." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "74bd20af", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "\n", + "# There is no need to read this entire image, but this outputs all the steps in the transpile() call\n", + "# for optimization level 1\n", + "pm = generate_preset_pass_manager(1, backend, seed_transpiler=42)\n", + "pm.draw()" + ] + }, + { + "cell_type": "markdown", + "id": "833a8bcc", + "metadata": {}, + "source": [ + "The flow consists of six stages:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "f58a6711", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "('init', 'layout', 'routing', 'translation', 'optimization', 'scheduling')\n" + ] + } + ], + "source": [ + "print(pm.stages)" + ] + }, + { + "cell_type": "markdown", + "id": "f34ba488-1f4a-429e-8c1c-3b623ae4826c", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + }, + "tags": [] + }, + "source": [ + "### 3.2 Draw an individual stage\n", + "\n", + "First, let's draw all the tasks (transpiler passes) done in the `init` stage." + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "09b4ffbe", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pm.init.draw()" + ] + }, + { + "cell_type": "markdown", + "id": "e7e4329f", + "metadata": {}, + "source": [ + "We can run each individual stage. Let's run `init` stage for our circuit. By enabling logger, we can see the details of the run." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "a139da85-e5b4-4c7c-900f-da8a0b8a5989", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "subslide" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:qiskit.passmanager.base_tasks:Pass: UnitarySynthesis - 0.03576 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: HighLevelSynthesis - 0.16618 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: BasisTranslator - 0.07176 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: InverseCancellation - 0.27299 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: ContractIdleWiresInControlFlow - 0.00811 (ms)\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import logging\n", + "\n", + "logger = logging.getLogger()\n", + "logger.setLevel(\"INFO\")\n", + "\n", + "init_out = pm.init.run(circuit)\n", + "init_out.draw(\"mpl\", fold=-1)" + ] + }, + { + "cell_type": "markdown", + "id": "c97816d8", + "metadata": {}, + "source": [ + "### 3.3 Exercise\n", + "Draw `layout` stage passes and run the stage for the output circuit of the `init` stage (`init_out`), by modifying cells used above.\n", + "\n", + "__Solution:__" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "56024db6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:qiskit.passmanager.base_tasks:Pass: SetLayout - 0.01001 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: TrivialLayout - 0.07129 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: CheckMap - 0.08917 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: VF2Layout - 1.24431 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: BarrierBeforeFinalMeasurements - 0.02599 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: SabreLayout - 5.11169 (ms)\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "display(pm.layout.draw())\n", + "layout_out = pm.layout.run(init_out)\n", + "layout_out.draw(\"mpl\", idle_wires=False, fold=-1)" + ] + }, + { + "cell_type": "markdown", + "id": "6db6618a", + "metadata": {}, + "source": [ + "Do the same thing for `translation` stage.\n", + "\n", + "__Solution:__" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "fd7cec6b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:qiskit.passmanager.base_tasks:Pass: UnitarySynthesis - 0.03386 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: HighLevelSynthesis - 0.02718 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: BasisTranslator - 2.64192 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: CheckGateDirection - 0.02217 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: GateDirection - 0.36502 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: BasisTranslator - 0.64778 (ms)\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "display(pm.translation.draw())\n", + "basis_out = pm.translation.run(layout_out)\n", + "basis_out.draw(\"mpl\", idle_wires=False, fold=-1)" + ] + }, + { + "cell_type": "markdown", + "id": "e3d78127-46f4-498a-958b-7c8ba107ae9d", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + }, + "tags": [] + }, + "source": [ + "Note: Any individual stage cannot always be run independently (as some of them need to carry over information from one previous stage)." + ] + }, + { + "cell_type": "markdown", + "id": "ff7370b9-6588-49c0-b82f-dbef823a973c", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "slide" + }, + "tags": [] + }, + "source": [ + "### 3.4 Optimization Stage\n", + "\n", + "The last default stage in the pipeline is optimization. After we've embedded the circuit for the target the circuit has expanded quite a bit. Most of this is due to inefficiencies in the equivalence relationships from basis translation and swap insertion. The optimization stage is used to try and minimize the size and depth of the circuit. It runs a series of passes in a ```do while``` loop until it reaches a steady output." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "f86b9045", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# pm.pre_optimization.draw()\n", + "pm.optimization.draw()" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "c2ee9c96-b595-4882-a581-1dbd28ac980e", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "subslide" + }, + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO:qiskit.passmanager.base_tasks:Pass: Depth - 0.30112 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.03195 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: Size - 0.01216 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.01001 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: Optimize1qGatesDecomposition - 0.63729 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: InverseCancellation - 0.41723 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: ContractIdleWiresInControlFlow - 0.01192 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: GatesInBasis - 0.05484 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: Depth - 0.08583 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.20599 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: Size - 0.00787 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.00715 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: Optimize1qGatesDecomposition - 0.16809 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: InverseCancellation - 0.17190 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: ContractIdleWiresInControlFlow - 0.00691 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: GatesInBasis - 0.02408 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: Depth - 0.04935 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.00525 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: Size - 0.00620 (ms)\n", + "INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.00286 (ms)\n" + ] + } + ], + "source": [ + "logger = logging.getLogger()\n", + "logger.setLevel(\"INFO\")\n", + "opt_out = pm.optimization.run(basis_out)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "65d650b0-ec27-4b1b-a121-f1bb958b18e2", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "" + }, + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "opt_out.draw(\"mpl\", idle_wires=False, fold=-1)" + ] + }, + { + "cell_type": "markdown", + "id": "24c2abce-393a-41c5-9d1d-6273ad94a707", + "metadata": { + "editable": true, + "slideshow": { + "slide_type": "subslide" + }, + "tags": [] + }, + "source": [ + "## 4. In-depth examples\n", + "### 4.1 Two-qubit block optimization using two-qubit unitary synthesis\n", + "\n", + "For level 2 and 3, we have more passes (`Collect2qBlocks`, `ConsolidateBlocks`, `UnitarySynthesis`) for more optimization, namely two-qubit block optimization. (Compare the optimization stage flow for level 2 with that above for level 1)\n", + "\n", + "The two-qubit block optimization is composed of two steps: Collecting and consolidating 2-qubit blocks and synthesizing the 2-qubit unitary matrices." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "179b1440", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pm2 = generate_preset_pass_manager(2, backend, seed_transpiler=42)\n", + "pm2.optimization.draw()" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "e3f9e358-cc38-46cc-b2f7-f3bb9b9b179d", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.transpiler import PassManager\n", + "from qiskit.transpiler.passes import (\n", + " Collect2qBlocks,\n", + " ConsolidateBlocks,\n", + " UnitarySynthesis,\n", + ")\n", + "\n", + "# Collect 2q blocks and consolidate to unitary when we expect that we can reduce the 2q gate count\n", + "# for that unitary\n", + "consolidate_pm = PassManager(\n", + " [\n", + " Collect2qBlocks(),\n", + " ConsolidateBlocks(target=backend.target),\n", + " ]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "bbf4fa9a-6b49-4833-82fd-b3821f6bcb78", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 36, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "display(basis_out.draw(\"mpl\", idle_wires=False, fold=-1))\n", + "\n", + "consolidated = consolidate_pm.run(basis_out)\n", + "consolidated.draw(\"mpl\", idle_wires=False, fold=-1)" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "7e7e0d3b-d267-4b1c-b207-42556d1ff3f2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Synthesize unitaries\n", + "UnitarySynthesis(target=backend.target)(consolidated).draw(\n", + " \"mpl\", idle_wires=False, fold=-1\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "a5c6dcec", + "metadata": {}, + "outputs": [], + "source": [ + "logger.setLevel(\"WARNING\")" + ] + }, + { + "cell_type": "markdown", + "id": "59a51954-c737-4ee2-b1be-fee896388c83", + "metadata": {}, + "source": [ + "We saw in Part 2 that the real quantum compiler flow is not that simple and is composed of many passes (tasks). This is mainly due to the software engineering required to ensure performance for a wide range of application circuits and maintainability of the software. Qiskit transpiler would work well in most cases but if you happen to see your circuit is not well optimized by Qiskit transpiler, it would be a good opportunity to research your own application-specific circuit optimization as shown in Part 1. Transpiler technology is evolving, your R&D contribution is welcome." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "86452399", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.circuit import QuantumCircuit\n", + "from qiskit_ibm_runtime import QiskitRuntimeService, Sampler" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1ff499b3", + "metadata": {}, + "outputs": [], + "source": [ + "service = QiskitRuntimeService()\n", + "backend = service.backend(\"ibm_brisbane\")\n", + "sampler = Sampler(backend)" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "id": "30a84ca1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "circ = QuantumCircuit(3)\n", + "circ.ccx(0, 1, 2)\n", + "circ.measure_all()\n", + "circ.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "id": "f58d9cb9", + "metadata": {}, + "outputs": [ + { + "ename": "IBMInputValueError", + "evalue": "'The instruction ccx on qubits (0, 1, 2) is not supported by the target system. Circuits that do not match the target hardware definition are no longer supported after March 4, 2024. See the transpilation documentation (/docs/guides/transpile) for instructions to transform circuits and the primitive examples (/docs/guides/sampler-examples) to see this coupled with operator transformations.'", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mIBMInputValueError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[44]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[43msampler\u001b[49m\u001b[43m.\u001b[49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\u001b[43m[\u001b[49m\u001b[43mcirc\u001b[49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;66;03m# IBMInputValueError will be raised\u001b[39;00m\n", + "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Caskroom/miniforge/base/envs/doc/lib/python3.11/site-packages/qiskit_ibm_runtime/sampler.py:111\u001b[39m, in \u001b[36mSamplerV2.run\u001b[39m\u001b[34m(self, pubs, shots)\u001b[39m\n\u001b[32m 107\u001b[39m coerced_pubs = [SamplerPub.coerce(pub, shots) \u001b[38;5;28;01mfor\u001b[39;00m pub \u001b[38;5;129;01min\u001b[39;00m pubs]\n\u001b[32m 109\u001b[39m validate_classical_registers(coerced_pubs)\n\u001b[32m--> \u001b[39m\u001b[32m111\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_run\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcoerced_pubs\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Caskroom/miniforge/base/envs/doc/lib/python3.11/site-packages/qiskit_ibm_runtime/base_primitive.py:158\u001b[39m, in \u001b[36mBasePrimitiveV2._run\u001b[39m\u001b[34m(self, pubs)\u001b[39m\n\u001b[32m 156\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m pub \u001b[38;5;129;01min\u001b[39;00m pubs:\n\u001b[32m 157\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mgetattr\u001b[39m(\u001b[38;5;28mself\u001b[39m._backend, \u001b[33m\"\u001b[39m\u001b[33mtarget\u001b[39m\u001b[33m\"\u001b[39m, \u001b[38;5;28;01mNone\u001b[39;00m) \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m is_simulator(\u001b[38;5;28mself\u001b[39m._backend):\n\u001b[32m--> \u001b[39m\u001b[32m158\u001b[39m \u001b[43mvalidate_isa_circuits\u001b[49m\u001b[43m(\u001b[49m\u001b[43m[\u001b[49m\u001b[43mpub\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcircuit\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_backend\u001b[49m\u001b[43m.\u001b[49m\u001b[43mtarget\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 160\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(\u001b[38;5;28mself\u001b[39m._backend, IBMBackend):\n\u001b[32m 161\u001b[39m \u001b[38;5;28mself\u001b[39m._backend.check_faulty(pub.circuit)\n", + "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Caskroom/miniforge/base/envs/doc/lib/python3.11/site-packages/qiskit_ibm_runtime/utils/validations.py:96\u001b[39m, in \u001b[36mvalidate_isa_circuits\u001b[39m\u001b[34m(circuits, target)\u001b[39m\n\u001b[32m 94\u001b[39m message = is_isa_circuit(circuit, target)\n\u001b[32m 95\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m message:\n\u001b[32m---> \u001b[39m\u001b[32m96\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m IBMInputValueError(\n\u001b[32m 97\u001b[39m message\n\u001b[32m 98\u001b[39m + \u001b[33m\"\u001b[39m\u001b[33m Circuits that do not match the target hardware definition are no longer \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 99\u001b[39m \u001b[33m\"\u001b[39m\u001b[33msupported after March 4, 2024. See the transpilation documentation \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 100\u001b[39m \u001b[33m\"\u001b[39m\u001b[33m(https://quantum.cloud.ibm.com/docs/guides/transpile) for instructions \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 101\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mto transform circuits and the primitive examples \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 102\u001b[39m \u001b[33m\"\u001b[39m\u001b[33m(https://quantum.cloud.ibm.com/docs/guides/primitives-examples) to see \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 103\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mthis coupled with operator transformations.\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 104\u001b[39m )\n", + "\u001b[31mIBMInputValueError\u001b[39m: 'The instruction ccx on qubits (0, 1, 2) is not supported by the target system. Circuits that do not match the target hardware definition are no longer supported after March 4, 2024. See the transpilation documentation (https://quantum.cloud.ibm.com/docs/guides/transpile) for instructions to transform circuits and the primitive examples (https://quantum.cloud.ibm.com/docs/guides/primitives-examples) to see this coupled with operator transformations.'" + ] + } + ], + "source": [ + "sampler.run([circ]) # IBMInputValueError will be raised" + ] + }, + { + "cell_type": "markdown", + "id": "2a261d51", + "metadata": {}, + "source": [ + "### 4.2 Circuit optimization matters\n", + "\n", + "We first compare the results of running 5-qubit GHZ state ($\\frac{1}{\\sqrt{2}} \\left( |00000\\rangle + |11111\\rangle \\right)$) preparation circuits with and without optimization." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "9483b736", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.circuit import QuantumCircuit\n", + "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n", + "from qiskit_ibm_runtime import QiskitRuntimeService, Sampler" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52e99762", + "metadata": {}, + "outputs": [], + "source": [ + "service = QiskitRuntimeService()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "270db9aa", + "metadata": {}, + "outputs": [], + "source": [ + "# backend = service.backend('ibm_brisbane')\n", + "backend = service.least_busy(\n", + " operational=True, simulator=False, min_num_qubits=127\n", + ") # Eagle\n", + "backend" + ] + }, + { + "cell_type": "markdown", + "id": "29d29a39", + "metadata": {}, + "source": [ + "We first use a GHZ circuit naively synthesized as follows." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "485b8ce6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "num_qubits = 5\n", + "\n", + "ghz_circ = QuantumCircuit(num_qubits)\n", + "ghz_circ.h(0)\n", + "[ghz_circ.cx(0, i) for i in range(1, num_qubits)]\n", + "ghz_circ.measure_all()\n", + "ghz_circ.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "8eaa552e", + "metadata": {}, + "source": [ + "We transpile the circuit without optimization (`optimization_level=0`) and with optimization (`optimization_level=2`).\n", + "As you can see, there is a big difference in the circuit length of transpiled circuits." + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "id": "87b861e2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "optimization_level=0:\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "optimization_level=2:\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "pm0 = generate_preset_pass_manager(\n", + " optimization_level=0, backend=backend, seed_transpiler=777\n", + ")\n", + "pm2 = generate_preset_pass_manager(\n", + " optimization_level=2, backend=backend, seed_transpiler=777\n", + ")\n", + "circ0 = pm0.run(ghz_circ)\n", + "circ2 = pm2.run(ghz_circ)\n", + "print(\"optimization_level=0:\")\n", + "display(circ0.draw(\"mpl\", idle_wires=False, fold=-1))\n", + "print(\"optimization_level=2:\")\n", + "display(circ2.draw(\"mpl\", idle_wires=False, fold=-1))" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "id": "328f71f2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Job ID: d13rnnemya70008ek1zg\n" + ] + } + ], + "source": [ + "# run the circuits\n", + "sampler = Sampler(backend)\n", + "job = sampler.run([circ0, circ2], shots=10000)\n", + "job_id = job.job_id()\n", + "print(f\"Job ID: {job_id}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "id": "d138e1e9", + "metadata": {}, + "outputs": [], + "source": [ + "# REPLACE WITH YOUR OWN JOB IDS\n", + "job = service.job(job_id)" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "id": "c137e06a", + "metadata": {}, + "outputs": [], + "source": [ + "# get results\n", + "result = job.result()\n", + "unoptimized_result = result[0].data.meas.get_counts()\n", + "optimized_result = result[1].data.meas.get_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "id": "7527976e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 60, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.visualization import plot_histogram\n", + "\n", + "# plot\n", + "sim_result = {\"0\" * 5: 0.5, \"1\" * 5: 0.5}\n", + "plot_histogram(\n", + " [result for result in [sim_result, unoptimized_result, optimized_result]],\n", + " bar_labels=False,\n", + " legend=[\n", + " \"ideal\",\n", + " \"no optimization\",\n", + " \"with optimization\",\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "d0d49ab7", + "metadata": {}, + "source": [ + "### 4.3 Circuit synthesis matters" + ] + }, + { + "cell_type": "markdown", + "id": "c6643c6d", + "metadata": {}, + "source": [ + "We next compare the results of running two differently synthesized 5-qubit GHZ state ($\\frac{1}{\\sqrt{2}} \\left( |00000\\rangle + |11111\\rangle \\right)$) preparation circuits." + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "id": "886d9b45", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 61, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Original GHZ circuit (naive synthesis)\n", + "ghz_circ.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "id": "3b559186", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 62, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# A better GHZ circuit (smarter synthesis), you learned in a previous lecture\n", + "ghz_circ2 = QuantumCircuit(5)\n", + "ghz_circ2.h(2)\n", + "ghz_circ2.cx(2, 1)\n", + "ghz_circ2.cx(2, 3)\n", + "ghz_circ2.cx(1, 0)\n", + "ghz_circ2.cx(3, 4)\n", + "ghz_circ2.measure_all()\n", + "ghz_circ2.draw(\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "id": "054890b6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "original synthesis:\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "new synthesis:\n" + ] + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "circ_org = pm2.run(ghz_circ)\n", + "circ_new = pm2.run(ghz_circ2)\n", + "print(\"original synthesis:\")\n", + "display(circ_org.draw(\"mpl\", idle_wires=False, fold=-1))\n", + "print(\"new synthesis:\")\n", + "display(circ_new.draw(\"mpl\", idle_wires=False, fold=-1))" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "id": "de0c8577", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Job ID: d13rp283grvg008j12fg\n" + ] + } + ], + "source": [ + "# run the circuits\n", + "sampler = Sampler(backend)\n", + "job = sampler.run([circ_org, circ_new], shots=10000)\n", + "job_id = job.job_id()\n", + "print(f\"Job ID: {job_id}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "id": "a6fcb968", + "metadata": {}, + "outputs": [], + "source": [ + "# REPLACE WITH YOUR OWN JOB IDS\n", + "job = service.job(job_id)" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "id": "82165302", + "metadata": {}, + "outputs": [], + "source": [ + "# get results\n", + "result = job.result()\n", + "synthesis_org_result = result[0].data.meas.get_counts()\n", + "synthesis_new_result = result[1].data.meas.get_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "id": "b9021da5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 68, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# plot\n", + "sim_result = {\"0\" * 5: 0.5, \"1\" * 5: 0.5}\n", + "plot_histogram(\n", + " [result for result in [sim_result, synthesis_org_result, synthesis_new_result]],\n", + " bar_labels=False,\n", + " legend=[\n", + " \"ideal\",\n", + " \"synthesis_org\",\n", + " \"synthesis_new\",\n", + " ],\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "07f7638d", + "metadata": {}, + "source": [ + "### 4.4 General 1-qubit gate decomposition" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "id": "f08c76bb", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit import QuantumCircuit, transpile\n", + "from qiskit.circuit import Parameter\n", + "from qiskit.circuit.library.standard_gates import UGate\n", + "\n", + "phi, theta, lam = Parameter(\"φ\"), Parameter(\"θ\"), Parameter(\"λ\")" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "id": "ed93f69a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 70, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(1)\n", + "qc.append(UGate(theta, phi, lam), [0])\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "2fa17bd2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "transpile(qc, basis_gates=[\"rz\", \"sx\"]).draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "d8af1753", + "metadata": {}, + "source": [ + "### 4.5 One-qubit block optimization" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "id": "6f64d07d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 71, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit import QuantumCircuit\n", + "\n", + "qc = QuantumCircuit(1)\n", + "qc.x(0)\n", + "qc.y(0)\n", + "qc.z(0)\n", + "qc.rx(1.23, 0)\n", + "qc.ry(1.23, 0)\n", + "qc.rz(1.23, 0)\n", + "qc.h(0)\n", + "qc.s(0)\n", + "qc.t(0)\n", + "qc.sx(0)\n", + "qc.sdg(0)\n", + "qc.tdg(0)\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "id": "0aa1b908", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Operator([[ 0.45292511-0.57266982j, -0.66852684-0.14135058j],\n", + " [ 0.14135058+0.66852684j, -0.57266982+0.45292511j]],\n", + " input_dims=(2,), output_dims=(2,))\n" + ] + } + ], + "source": [ + "from qiskit.quantum_info import Operator\n", + "\n", + "Operator(qc)" + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "id": "c06f5e75", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 73, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit import transpile\n", + "\n", + "qc_opt = transpile(qc, basis_gates=[\"rz\", \"sx\"])\n", + "qc_opt.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "id": "a9ec0568", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Operator([[ 0.45292511-0.57266982j, -0.66852684-0.14135058j],\n", + " [ 0.14135058+0.66852684j, -0.57266982+0.45292511j]],\n", + " input_dims=(2,), output_dims=(2,))\n" + ] + } + ], + "source": [ + "Operator(qc_opt)" + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "id": "e83779af", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 75, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Operator(qc).equiv(Operator(qc_opt))" + ] + }, + { + "cell_type": "markdown", + "id": "1ebdc297", + "metadata": {}, + "source": [ + "### 4.6 Toffoli decomposition" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "id": "f802c5df", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 76, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(3)\n", + "qc.ccx(0, 1, 2)\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "id": "330cea7e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 77, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit import QuantumCircuit, transpile\n", + "\n", + "qc = QuantumCircuit(3)\n", + "qc.ccx(0, 1, 2)\n", + "qc = transpile(qc, basis_gates=[\"rz\", \"sx\", \"cx\"])\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "24004df8", + "metadata": {}, + "source": [ + "### 4.7 CU gate decomposition" + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "id": "1df5876d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 78, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.circuit.library.standard_gates import CUGate\n", + "\n", + "phi, theta, lam, gamma = Parameter(\"φ\"), Parameter(\"θ\"), Parameter(\"λ\"), Parameter(\"γ\")\n", + "qc = QuantumCircuit(2)\n", + "# qc.cu(theta, phi, lam, gamma, 0, 1)\n", + "qc.append(CUGate(theta, phi, lam, gamma), [0, 1])\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "id": "64f7e5f3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 79, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from qiskit.circuit.library.standard_gates import CUGate\n", + "\n", + "phi, theta, lam, gamma = Parameter(\"φ\"), Parameter(\"θ\"), Parameter(\"λ\"), Parameter(\"γ\")\n", + "qc = QuantumCircuit(2)\n", + "qc.append(CUGate(theta, phi, lam, gamma), [0, 1])\n", + "qc = transpile(qc, basis_gates=[\"rz\", \"sx\", \"cx\"])\n", + "qc.draw(output=\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "88e7a028", + "metadata": {}, + "source": [ + "### 4.8 CX, ECR, CZ equal up to local Cliffords\n", + "\n", + "Note that $H$(Hadamard), $S$($\\pi/2$ Z-rotation), $S^\\dagger$($-\\pi/2$ Z-rotation), $X$(Pauli X) are all Clifford gates." + ] + }, + { + "cell_type": "code", + "execution_count": 80, + "id": "f5b362b6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 80, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(2)\n", + "qc.cx(0, 1)\n", + "qc.draw(output=\"mpl\", style=\"bw\")" + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "id": "8740d07b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 81, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(2)\n", + "qc.cx(0, 1)\n", + "transpile(qc, basis_gates=[\"x\", \"s\", \"h\", \"sdg\", \"ecr\"]).draw(output=\"mpl\", style=\"bw\")" + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "id": "5113a7c5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 82, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(2)\n", + "qc.cx(0, 1)\n", + "transpile(qc, basis_gates=[\"h\", \"cz\"]).draw(output=\"mpl\", style=\"bw\")" + ] + }, + { + "cell_type": "markdown", + "id": "f19867b3", + "metadata": {}, + "source": [ + "Using IBM backend 1q basis gates \"rz\", \"sx\" and \"x\"." + ] + }, + { + "cell_type": "code", + "execution_count": 83, + "id": "9d9b54d4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 83, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(2)\n", + "qc.cx(0, 1)\n", + "transpile(qc, basis_gates=[\"rz\", \"sx\", \"x\", \"ecr\"]).draw(output=\"mpl\", style=\"bw\")" + ] + }, + { + "cell_type": "code", + "execution_count": 84, + "id": "c395cd24", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "execution_count": 84, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qc = QuantumCircuit(2)\n", + "qc.cx(0, 1)\n", + "transpile(qc, basis_gates=[\"rz\", \"sx\", \"x\", \"cz\"]).draw(output=\"mpl\", style=\"bw\")" + ] + }, + { + "cell_type": "code", + "execution_count": 85, + "id": "4eab683f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'2.0.2'" + ] + }, + "execution_count": 85, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Check Qiskit version\n", + "import qiskit\n", + "\n", + "qiskit.__version__" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/learning/courses/utility-scale-quantum-computing/teleportation.ipynb b/learning/courses/utility-scale-quantum-computing/teleportation.ipynb index b123ec4921a..8a6ec94f9a8 100644 --- a/learning/courses/utility-scale-quantum-computing/teleportation.ipynb +++ b/learning/courses/utility-scale-quantum-computing/teleportation.ipynb @@ -1299,7 +1299,9 @@ } ], "source": [ - "# If the Notebook session got disconnected you can also check your job status by running the following code\n", + "# If the Notebook session got disconnected you can also check your job status\n", + "# by running the following code\n", + "\n", "from qiskit_ibm_runtime import QiskitRuntimeService\n", "\n", "service = QiskitRuntimeService()\n", @@ -1732,7 +1734,9 @@ } ], "source": [ - "# If the Notebook session got disconnected you can also check your job status by running the following code\n", + "# If the Notebook session got disconnected you can also check your job status\n", + "# by running the following code\n", + "\n", "# from qiskit_ibm_runtime import QiskitRuntimeService\n", "# service = QiskitRuntimeService()\n", "job = service.job(job_id) # Input your job-id between the quotations\n", diff --git a/learning/courses/variational-algorithm-design/cost-functions.ipynb b/learning/courses/variational-algorithm-design/cost-functions.ipynb index c35d1081522..0ef48bd9001 100644 --- a/learning/courses/variational-algorithm-design/cost-functions.ipynb +++ b/learning/courses/variational-algorithm-design/cost-functions.ipynb @@ -334,7 +334,8 @@ " {outcome: freq / shots for outcome, freq in counts.items()}\n", " )\n", "\n", - " # Use the probabilities and known eigenvalues of Pauli operators to estimate the expectation value.\n", + " # Use the probabilities and known eigenvalues of Pauli operators to estimate\n", + " # the expectation value.\n", " val = 0\n", "\n", " if str(pauli) == \"X\":\n", diff --git a/learning/courses/variational-algorithm-design/examples-and-applications.ipynb b/learning/courses/variational-algorithm-design/examples-and-applications.ipynb index 8a827f218c2..18dcd495fe0 100644 --- a/learning/courses/variational-algorithm-design/examples-and-applications.ipynb +++ b/learning/courses/variational-algorithm-design/examples-and-applications.ipynb @@ -1366,7 +1366,8 @@ "metadata": {}, "outputs": [], "source": [ - "# Estimated compute resource usage: 25 minutes. Benchmarked at 24 min, 30 sec on an Eagle r3 processor on 5-30-24\n", + "# Estimated compute resource usage: 25 minutes.\n", + "# Benchmarked at 24 min, 30 sec on an Eagle r3 processor on 5-30-24\n", "\n", "k = 2\n", "betas = [30, 50, 80]\n", diff --git a/learning/modules/computer-science/deutsch-jozsa.ipynb b/learning/modules/computer-science/deutsch-jozsa.ipynb index fc465c5f2e3..922c15845f0 100644 --- a/learning/modules/computer-science/deutsch-jozsa.ipynb +++ b/learning/modules/computer-science/deutsch-jozsa.ipynb @@ -139,7 +139,9 @@ "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", "\n", "# Syntax for first saving your token. Delete these lines after saving your credentials.\n", - "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform', instance = '', token='', overwrite=True, set_as_default=True)\n", + "\n", + "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform',\n", + "# instance = '', token='', overwrite=True, set_as_default=True)\n", "# service = QiskitRuntimeService(channel='ibm_quantum_platform')\n", "\n", "# Load saved credentials\n", @@ -200,7 +202,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "5e67183b-42b9-44c2-bd4b-b5e2d192a796", "metadata": {}, "outputs": [ @@ -238,8 +240,12 @@ " return f\n", "\n", "\n", - "# first, convert oracle circuit (above) to a single gate for drawing purposes. otherwise, the circuit is too large to display\n", - "# blackbox = twobit_function(2).to_gate() # you may edit the number inside \"twobit_function()\" to select among the four valid functions\n", + "# first, convert oracle circuit (above) to a single gate for drawing purposes. otherwise, the\n", + "# circuit is too large to display\n", + "\n", + "# you may edit the number inside \"twobit_function()\" to select among the four valid functions:\n", + "# blackbox = twobit_function(2).to_gate()\n", + "\n", "# blackbox.label = \"$U_f$\"\n", "\n", "qc.h(0)\n", @@ -451,7 +457,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "4d9129df-f2ef-4f94-9508-21ed986fd823", "metadata": {}, "outputs": [ @@ -471,10 +477,12 @@ "\n", "## Step 1: Map the problem\n", "\n", - "# first, convert oracle circuit (above) to a single gate for drawing purposes. otherwise, the circuit is too large to display\n", + "# first, convert oracle circuit (above) to a single gate for drawing purposes.\n", + "# otherwise, the circuit is too large to display\n", "blackbox = twobit_function(\n", " 3\n", - ").to_gate() # you may edit the number (1-4) inside \"twobit_function()\" to select among the four valid functions\n", + " # you may edit the number (1-4) inside \"twobit_function()\" to select among the four valid functions\n", + ").to_gate()\n", "blackbox.label = \"$U_f$\"\n", "\n", "\n", @@ -620,7 +628,8 @@ " return qc_dj\n", "\n", " for state in on_states:\n", - " # qc_dj.barrier() # Barriers are added to help visualize how the functions are created. They can safely be removed.\n", + " # qc_dj.barrier() # Barriers are added to help visualize how the functions are created.\n", + " # They can safely be removed.\n", " qc_dj = add_cx(qc_dj, f\"{state:0b}\")\n", " qc_dj.mcx(list(range(num_qubits)), num_qubits)\n", " qc_dj = add_cx(qc_dj, f\"{state:0b}\")\n", diff --git a/learning/modules/computer-science/grovers.ipynb b/learning/modules/computer-science/grovers.ipynb index 747f1b6340b..b3a021a3854 100644 --- a/learning/modules/computer-science/grovers.ipynb +++ b/learning/modules/computer-science/grovers.ipynb @@ -274,14 +274,14 @@ "\n", "The two-qubit example above showed how the algebra works out for a small case, but there is a much more intuitive way to understand Grover's algorithm: as a sequence of geometric reflections in a two-dimensional plane. Below we describe this picture. You can also see John Watrous's course [Fundamentals of Quantum Algorithms](/learning/courses/fundamentals-of-quantum-algorithms/grover-algorithm/analysis) for more details.\n", "\n", - "**Setting up the plane.** We can decompose the initial superposition state $|\\psi\\rangle$ into two components. The correct state — the one we're searching for — we call $|A_1\\rangle$. Every other state, lumped together, we call $|A_0\\rangle$. By definition, $|A_1\\rangle$ and $|A_0\\rangle$ are orthogonal to one another, so we can plot them as perpendicular axes in an abstract, two-dimensional space. Since $|\\psi\\rangle$ is a linear combination of these two components, it sits at some small angle $\\theta$ to the $|A_0\\rangle$ axis — close to $|A_0\\rangle$, because at the start, only a tiny fraction of the state is in the correct component $|A_1\\rangle$.\n", + "**Setting up the plane.** We can decompose the initial superposition state $|\\psi\\rangle$ into two components. The correct state — the one we're searching for — we call $|A_1\\rangle$. Every other state, lumped together, we call $|A_0\\rangle$. By definition, $|A_1\\rangle$ and $|A_0\\rangle$ are orthogonal to one another, so we can plot them as perpendicular axes in an abstract, two-dimensional space. Since $|\\psi\\rangle$ is a linear combination of these two components, it sits at some small angle $\\theta$ to the $|A_0\\rangle$ axis — close to $|A_0\\rangle$, because at the start, only a tiny fraction of the state is in the correct component $|A_1\\rangle$.\n", "\n", "\n", "**Reflections.** The key mathematical fact we need is that an operator of the form\n", "$$\n", "2|v\\rangle\\langle v| - I\n", "$$\n", - "reflects any state about the axis defined by $|v\\rangle.$ To see why, consider two cases: a state along $|v\\rangle$ is left unchanged, and a state perpendicular to $|v\\rangle$ gets its sign flipped. Any other state can be decomposed into these two components, and the operator acts on each accordingly — which is exactly a reflection about $|v\\rangle$.\n", + "reflects any state about the axis defined by $|v\\rangle.$ To see why, consider two cases: a state along $|v\\rangle$ is left unchanged, and a state perpendicular to $|v\\rangle$ gets its sign flipped. Any other state can be decomposed into these two components, and the operator acts on each accordingly — which is exactly a reflection about $|v\\rangle$.\n", "\n", "It turns out that both the oracle and the diffusion steps in Grover's algorithm can be expressed as reflections in this geometric picture.\n", "\n", @@ -293,17 +293,17 @@ "$$\n", "H^{\\otimes n}\\, Z_{\\text{OR}}\\, H^{\\otimes n}\n", "$$\n", - "$Z_{\\text{OR}}$ by itself is a reflection about the all-zero state, since it flips the sign of every state that is not $|0\\rangle^{\\otimes n}$. This can be written as $2|0\\rangle\\langle 0| - I$. The surrounding Hadamard layers effectively perform a change of basis, transforming the axis of reflection. Recall that $H^{\\otimes n}$ maps $|0\\rangle^{\\otimes n}$ to the uniform superposition $|u\\rangle = \\frac{1}{\\sqrt{N}}\\sum_{x}|x\\rangle$. Since the Hadamard is its own inverse, the full expression becomes\n", + "$Z_{\\text{OR}}$ by itself is a reflection about the all-zero state, since it flips the sign of every state that is not $|0\\rangle^{\\otimes n}$. This can be written as $2|0\\rangle\\langle 0| - I$. The surrounding Hadamard layers effectively perform a change of basis, transforming the axis of reflection. Recall that $H^{\\otimes n}$ maps $|0\\rangle^{\\otimes n}$ to the uniform superposition $|u\\rangle = \\frac{1}{\\sqrt{N}}\\sum_{x}|x\\rangle$. Since the Hadamard is its own inverse, the full expression becomes\n", "$$\n", "H^{\\otimes n}\\left(2|0\\rangle\\langle 0| - I\\right)H^{\\otimes n} = 2|u\\rangle\\langle u| - I\n", "$$\n", - "which is a reflection about $|u\\rangle$. Since $|u\\rangle$ is very close to $|\\psi\\rangle$ (both are nearly along $|A_0\\rangle$), this second reflection sends the state to an angle $2\\theta$ from where it started.\n", + "which is a reflection about $|u\\rangle$. Since $|u\\rangle$ is very close to $|\\psi\\rangle$ (both are nearly along $|A_0\\rangle$), this second reflection sends the state to an angle $2\\theta$ from where it started.\n", "\n", "![Geometric interpretation of the Grover operator as a rotation.](/learning/images/modules/computer-science/grovers/grover-geometric-reflections.avif)\n", "\n", - "**Rotation by $2\\theta$.** The combined effect of these two reflections is a rotation by $2\\theta$ toward $|A_1\\rangle$. Each successive iteration of the Grover operator rotates the state by another $2\\theta.$\n", + "**Rotation by $2\\theta$.** The combined effect of these two reflections is a rotation by $2\\theta$ toward $|A_1\\rangle$. Each successive iteration of the Grover operator rotates the state by another $2\\theta.$\n", "\n", - "**Optimal number of iterations.** Our goal is to rotate the state as close to $|A_1\\rangle$ as possible, which means rotating by a total of approximately $\\pi/2$ radians (a quarter turn). If each iteration contributes $2\\theta$, the optimal number of iterations $t$ satisfies\n", + "**Optimal number of iterations.** Our goal is to rotate the state as close to $|A_1\\rangle$ as possible, which means rotating by a total of approximately $\\pi/2$ radians (a quarter turn). If each iteration contributes $2\\theta$, the optimal number of iterations $t$ satisfies\n", "$$\n", "(2t + 1)\\theta \\approx \\frac{\\pi}{2}\n", "$$\n", @@ -328,7 +328,7 @@ "source": [ "### Why is Grover's algorithm useful?\n", "\n", - "At this point you may be wondering: we just built an oracle that marks a target state — but to build it, we had to know the target state. So what are we actually searching for?\n", + "At this point you may be wondering: we just built an oracle that marks a target state — but to build it, we had to know the target state. So what are we actually searching for?\n", "\n", "This is a fair question, and there are several good answers.\n", "\n", @@ -336,11 +336,11 @@ "\n", "- You can also think of it as a **two-party activity**: one person knows the target state and builds the oracle; the other person's job is to find the answer using the oracle as a black box, with no peeking inside. In Activity 2 below, you will do exactly this with a partner.\n", "\n", - "- **Amplitude amplification is a broadly useful subroutine.** Even if this first demonstration seems circular, the underlying mechanism — called *amplitude amplification* — shows up again and again in quantum computing. What we are really building here is an intuition for a tool that appears as a subroutine in many more complex quantum algorithms.\n", + "- **Amplitude amplification is a broadly useful subroutine.** Even if this first demonstration seems circular, the underlying mechanism — called *amplitude amplification* — shows up again and again in quantum computing. What we are really building here is an intuition for a tool that appears as a subroutine in many more complex quantum algorithms.\n", "\n", - "- **There are problems where you can build an oracle without knowing the answer.** The key insight is that there exists a whole class of problems for which it is very hard to *find* a solution, but very easy to *check* that a given solution is correct. Factoring is one example: given a product of two large primes, it is extremely difficult to determine what those primes are, but once you have them, you can easily multiply them together to verify. (We have a better algorithm than Grover's for factoring specifically — see Shor's algorithm — but this is far from the only problem with this feature.) Sudoku, constraint satisfaction, and even the classic game of Minesweeper are all problems that are difficult to solve but easy to check.\n", + "- **There are problems where you can build an oracle without knowing the answer.** The key insight is that there exists a whole class of problems for which it is very hard to *find* a solution, but very easy to *check* that a given solution is correct. Factoring is one example: given a product of two large primes, it is extremely difficult to determine what those primes are, but once you have them, you can easily multiply them together to verify. (We have a better algorithm than Grover's for factoring specifically — see Shor's algorithm — but this is far from the only problem with this feature.) Sudoku, constraint satisfaction, and even the classic game of Minesweeper are all problems that are difficult to solve but easy to check.\n", "\n", - "Why is that relevant? It means we can know all of the *conditions* and *requirements* that a solution must satisfy, and we can encode those requirements into a quantum circuit that serves as the oracle — even though we do not know the solution itself. Grover's algorithm will find it for us.\n", + "Why is that relevant? It means we can know all of the *conditions* and *requirements* that a solution must satisfy, and we can encode those requirements into a quantum circuit that serves as the oracle — even though we do not know the solution itself. Grover's algorithm will find it for us.\n", "\n", "With these ideas in mind, let us work through several examples. We will begin with an example in which the solution state is clearly specified so we can follow the logic of the algorithm. We will then move on to a two-party activity, and finally to an example in which the oracle is built from problem constraints rather than from knowledge of the answer." ] @@ -559,11 +559,11 @@ "id": "fb293e00", "metadata": {}, "source": [ - "As we discussed in the geometric picture above, we might need to apply the Grover operator multiple times. The optimal number of iterations $t$ to maximize the amplitude of the target state in the absence of noise is\n", + "As we discussed in the geometric picture above, we might need to apply the Grover operator multiple times. The optimal number of iterations $t$ to maximize the amplitude of the target state in the absence of noise is\n", "$$\n", "t\\approx \\frac{\\pi}{4} \\sqrt{\\frac{N}{|A_1|}}-\\frac{1}{2}\n", "$$\n", - "where $|A_1|$ is the number of solution states and $N=2^n$ is the total number of states. On modern noisy quantum computers, the experimentally optimal number of iterations might be different — but here we calculate and use this theoretical, optimal number using $|A_1|=1$." + "where $|A_1|$ is the number of solution states and $N=2^n$ is the total number of states. On modern noisy quantum computers, the experimentally optimal number of iterations might be different — but here we calculate and use this theoretical, optimal number using $|A_1|=1$." ] }, { @@ -666,7 +666,9 @@ "from qiskit_ibm_runtime import QiskitRuntimeService\n", "\n", "# Syntax for first saving your token. Delete these lines after saving your credentials.\n", - "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform', instance = '', token='', overwrite=True, set_as_default=True)\n", + "\n", + "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform',\n", + "# instance = '', token='', overwrite=True, set_as_default=True)\n", "# service = QiskitRuntimeService(channel='ibm_quantum_platform')\n", "\n", "# Load saved credentials\n", @@ -746,12 +748,13 @@ }, { "cell_type": "code", - "execution_count": 96, + "execution_count": null, "id": "2a272d9e", "metadata": {}, "outputs": [], "source": [ - "# To run on a real quantum computer (this was tested on a Heron r2 processor and used 4 sec. of QPU time)\n", + "# To run on a real quantum computer (this was tested on a Heron r2 processor and\n", + "# used 4 sec. of QPU time)\n", "\n", "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", "\n", @@ -1015,7 +1018,8 @@ "metadata": {}, "outputs": [], "source": [ - "# To run on a real quantum computer (this was tested on a Heron r2 processor and used 4 seconds of QPU time)\n", + "# To run on a real quantum computer (this was tested on a Heron r2 processor and used\n", + "# 4 seconds of QPU time)\n", "\n", "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", "\n", @@ -1115,11 +1119,11 @@ "id": "mine_intro_01", "metadata": {}, "source": [ - "## Activity 3: Solve a Minesweeper grid with Grover's algorithm\n", - "\n", - "In the previous section, we noted that Grover's algorithm becomes genuinely useful when we can build an oracle from the *constraints* of a problem, rather than from knowledge of the answer. Minesweeper is a perfect example: the numbered cells tell us how many mines are adjacent, and those constraints fully determine where the mines must be — but finding the configuration requires search.\n", - "\n", - "Minesweeper has been proven to be NP-complete: it is hard to solve but easy to check. That makes it a natural candidate for Grover's algorithm. Of course, we cannot yet solve a full 9$\\times$9 grid on a noisy quantum computer — the circuits would be far too deep. Instead, we will use a tiny grid as a toy demonstration of how one would approach a larger board on a future fault-tolerant machine.\n", + "## Activity 3: Solve a Minesweeper grid with Grover's algorithm\n", + "\n", + "In the previous section, we noted that Grover's algorithm becomes genuinely useful when we can build an oracle from the *constraints* of a problem, rather than from knowledge of the answer. Minesweeper is a perfect example: the numbered cells tell us how many mines are adjacent, and those constraints fully determine where the mines must be — but finding the configuration requires search.\n", + "\n", + "Minesweeper has been proven to be NP-complete: it is hard to solve but easy to check. That makes it a natural candidate for Grover's algorithm. Of course, we cannot yet solve a full 9$\\times$9 grid on a noisy quantum computer — the circuits would be far too deep. Instead, we will use a tiny grid as a toy demonstration of how one would approach a larger board on a future fault-tolerant machine.\n", "\n", "A few important caveats. Grover's algorithm provides only a quadratic speedup over *unstructured* classical search. Minesweeper almost certainly has exploitable structure that a clever classical algorithm could use. And for an exponentially growing search space, even the $\\sqrt{N}$ improvement only goes so far. But let us set those concerns aside and use this toy problem to illustrate how problem constraints get encoded into a quantum oracle.\n", "\n", @@ -1127,11 +1131,11 @@ "\n", "Here is our baby minesweeper grid:\n", "\n", - "![A simple Minesweeper grid with three blank cells and three numbered cells.](/learning/images/modules/computer-science/grovers/minesweeper-grid.avif)\n", + "![A simple Minesweeper grid with three blank cells and three numbered cells.](/learning/images/modules/computer-science/grovers/minesweeper-grid.avif)\n", "\n", "Each blank cell can be represented by a binary variable indicating whether it contains a mine. We label these $x_0$, $x_1$, and $x_2$, where $x_i = 1$ means there is a mine on that cell and $x_i = 0$ means there is not:\n", "\n", - "![The same Minesweeper grid with variables x0, x1, x2 labeling the blank cells.](/learning/images/modules/computer-science/grovers/minesweeper-grid-labeled.avif)\n", + "![The same Minesweeper grid with variables x0, x1, x2 labeling the blank cells.](/learning/images/modules/computer-science/grovers/minesweeper-grid-labeled.avif)\n", "\n", "We could solve this in our heads in about half a second, but we are using this toy problem to illustrate how a much harder board could be approached with a quantum computer." ] @@ -1176,9 +1180,9 @@ "\n", "Now we need to encode this Boolean expression into a quantum circuit that serves as the oracle. The quantum version of XOR can be accomplished with CX (CNOT) gates: applying two CX gates from the data qubits to a workspace (ancilla) qubit effectively computes their XOR and stores the result in the ancilla.\n", "\n", - "We introduce three workspace qubits — one for each clause. We store the result of each Boolean expression in its corresponding workspace qubit, then use a multi-controlled Z gate to flip the phase of the three-qubit state that makes all three workspace qubits $|1\\rangle$ (meaning all clauses are satisfied simultaneously).\n", - "\n", - "In the first code cell below, we build the \"compute\" half of the oracle — the part that evaluates each clause and writes the result into the workspace qubits." + "We introduce three workspace qubits — one for each clause. We store the result of each Boolean expression in its corresponding workspace qubit, then use a multi-controlled Z gate to flip the phase of the three-qubit state that makes all three workspace qubits $|1\\rangle$ (meaning all clauses are satisfied simultaneously).\n", + "\n", + "In the first code cell below, we build the \"compute\" half of the oracle — the part that evaluates each clause and writes the result into the workspace qubits." ] }, { @@ -1216,7 +1220,7 @@ "source": [ "At this point, the result of each clause is stored in its corresponding workspace qubit. Now we need the three-qubit data state that makes all three workspace qubits $|1\\rangle$ to pick up a minus sign. We do this with a multi-controlled Z gate (implemented as an MCX gate sandwiched by Hadamard gates on the target).\n", "\n", - "After applying the phase flip, we must **uncompute** — undo all the clause-evaluation steps in reverse order — to reset the workspace qubits back to $|0\\rangle.$ This is essential so that the workspace qubits are clean for subsequent iterations of the Grover operator." + "After applying the phase flip, we must **uncompute** — undo all the clause-evaluation steps in reverse order — to reset the workspace qubits back to $|0\\rangle.$ This is essential so that the workspace qubits are clean for subsequent iterations of the Grover operator." ] }, { @@ -1253,7 +1257,7 @@ "id": "mine_groverop_01", "metadata": {}, "source": [ - "This circuit is our oracle: it flips the phase of the data-qubit state that satisfies all three Minesweeper constraints, and leaves the workspace qubits back in $|0\\rangle.$\n", + "This circuit is our oracle: it flips the phase of the data-qubit state that satisfies all three Minesweeper constraints, and leaves the workspace qubits back in $|0\\rangle.$\n", "\n", "Now we construct the full Grover operator from this oracle. Note the `reflection_qubits` argument: we pass only the data qubits `x`, because the workspace qubits are not part of the search space. Their job is done once the oracle has been applied." ] @@ -1274,7 +1278,7 @@ "id": "mine_circuit_01", "metadata": {}, "source": [ - "With three data qubits and one solution state, the optimal number of Grover iterations is $t \\approx \\frac{\\pi}{4}\\sqrt{8} - \\frac{1}{2} \\approx 1.7$, so we use two iterations. We apply Hadamard gates to the data qubits to create the initial superposition, compose the Grover operator twice, and measure only the data qubits." + "With three data qubits and one solution state, the optimal number of Grover iterations is $t \\approx \\frac{\\pi}{4}\\sqrt{8} - \\frac{1}{2} \\approx 1.7$, so we use two iterations. We apply Hadamard gates to the data qubits to create the initial superposition, compose the Grover operator twice, and measure only the data qubits." ] }, { @@ -1329,7 +1333,7 @@ "id": "mine_depth_01", "metadata": {}, "source": [ - "Now we can check the depth of the transpiled circuit. Because the Minesweeper oracle uses workspace qubits and multiple CX gates, the transpiled circuit will be deeper than those in the previous activities." + "Now we can check the depth of the transpiled circuit. Because the Minesweeper oracle uses workspace qubits and multiple CX gates, the transpiled circuit will be deeper than those in the previous activities." ] }, { @@ -1361,7 +1365,8 @@ "metadata": {}, "outputs": [], "source": [ - "# To run on a real quantum computer (this was tested on a Heron r2 processor and used 4 sec. of QPU time)\n", + "# To run on a real quantum computer (this was tested on a Heron r2 processor and\n", + "# used 4 sec. of QPU time)\n", "\n", "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", "\n", @@ -1408,9 +1413,9 @@ "id": "mine_conclusion_01", "metadata": {}, "source": [ - "The `101` state should appear with far higher probability than any other, indicating that mines are located at $x_0$ and $x_2$. We have used a quantum computer to solve a tiny game of Minesweeper!\n", - "\n", - "Of course, the best classical algorithms for minesweeper are better than a brute-force search through all possible mine configurations — they exploit the structure of the grid. Grover's algorithm would only offer an advantage on extremely difficult boards designed to be maximally ambiguous, and even then, the quadratic speedup means it cannot keep pace with exponential growth indefinitely. But the real takeaway is the technique: encoding problem constraints into a quantum oracle is a powerful pattern that extends to constraint satisfaction, combinatorial optimization, and many other domains." + "The `101` state should appear with far higher probability than any other, indicating that mines are located at $x_0$ and $x_2$. We have used a quantum computer to solve a tiny game of Minesweeper!\n", + "\n", + "Of course, the best classical algorithms for minesweeper are better than a brute-force search through all possible mine configurations — they exploit the structure of the grid. Grover's algorithm would only offer an advantage on extremely difficult boards designed to be maximally ambiguous, and even then, the quadratic speedup means it cannot keep pace with exponential growth indefinitely. But the real takeaway is the technique: encoding problem constraints into a quantum oracle is a powerful pattern that extends to constraint satisfaction, combinatorial optimization, and many other domains." ] }, { @@ -1428,7 +1433,7 @@ "- Grover's algorithm involves repeating a series of operations (commonly called the \"Grover operator\") a number of times $t,$ chosen to make the target states optimally likely to be measured.\n", "- Grover's algorithm can be run with fewer than $t$ iterations and still amplify the target states.\n", "- Grover's algorithm fits into the query model of computation and makes the most sense when one person controls the search and another controls/constructs the oracle. It may also be useful as a subroutine in other quantum computations.\n", - "- An oracle can be built from *problem constraints* rather than from knowledge of the solution, as demonstrated with the Minesweeper example.\n", + "- An oracle can be built from *problem constraints* rather than from knowledge of the solution, as demonstrated with the Minesweeper example.\n", "\n", "\n", "### T/F questions:\n", diff --git a/learning/modules/computer-science/quantum-key-distribution.ipynb b/learning/modules/computer-science/quantum-key-distribution.ipynb index 09aee245f70..63f56fb3720 100644 --- a/learning/modules/computer-science/quantum-key-distribution.ipynb +++ b/learning/modules/computer-science/quantum-key-distribution.ipynb @@ -396,7 +396,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "23a8a5c3-8f59-4130-8f2c-8b2bd50ee698", "metadata": {}, "outputs": [], @@ -407,7 +407,8 @@ "import numpy as np\n", "from qiskit import QuantumCircuit\n", "\n", - "# Set up a random number generator and a quantum circuit. We choose to start with 20 bits, though any number <30 should be fine.\n", + "# Set up a random number generator and a quantum circuit. We choose to start with 20 bits, though\n", + "# any number <30 should be fine.\n", "\n", "rng = np.random.default_rng()\n", "bit_num = 20\n", @@ -418,7 +419,8 @@ "\n", "abits = np.round(rng.random(bit_num))\n", "\n", - "# generate Alice's random measurement bases. Here we will associate a \"0\" with the Z basis, and a \"1\" with the X basis.\n", + "# generate Alice's random measurement bases. Here we will associate a \"0\" with the Z basis, and a\n", + "# \"1\" with the X basis.\n", "\n", "abase = np.round(rng.random(bit_num))\n", "\n", @@ -442,7 +444,8 @@ "\n", "bbase = np.round(rng.random(bit_num))\n", "\n", - "# Note that if Bob measures in Z no gates are necessary, since IBM Quantum computers measure in Z by default.\n", + "# Note that if Bob measures in Z no gates are necessary, since IBM Quantum computers\n", + "# measure in Z by default.\n", "# If Bob measures in the X basis, we implement a hadamard gate qc.h to facilitate the measurement.\n", "\n", "for m in range(bit_num):\n", @@ -527,7 +530,8 @@ "# Load the Qiskit Runtime service\n", "\n", "# Syntax for first saving your token. Delete these lines after saving your credentials.\n", - "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform', instance = '', token='', overwrite=True, set_as_default=True)\n", + "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform',\n", + "# instance = '', token='', overwrite=True, set_as_default=True)\n", "# service = QiskitRuntimeService(channel='ibm_quantum_platform')\n", "\n", "# Load saved credentials\n", @@ -989,7 +993,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "id": "2697eace-2faf-49cc-99f2-d8d7bda28588", "metadata": {}, "outputs": [ @@ -1007,7 +1011,8 @@ "source": [ "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", "\n", - "# This calculation was run on an Eagle r3 processor on 11-7-24 and required 3 sec to run, with 127 qubits.\n", + "# This calculation was run on an Eagle r3 processor on 11-7-24 and\n", + "# required 3 sec to run, with 127 qubits.\n", "# Qiskit patterns step 1: Mapping your problem to a quantum circuit\n", "\n", "bit_num = 127\n", @@ -1098,7 +1103,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "id": "b311cf3a-ccea-43fe-8bcd-a054296e113e", "metadata": {}, "outputs": [ @@ -1116,7 +1121,8 @@ "source": [ "from qiskit_ibm_runtime import SamplerV2 as Sampler\n", "\n", - "# This calculation was run on an Eagle r3 processor on 11-7-24 and required 2 s to run, with 127 qubits.\n", + "# This calculation was run on an Eagle r3 processor on 11-7-24 and\n", + "# required 2 s to run, with 127 qubits.\n", "# Qiskit patterns step 1: Mapping your problem to a quantum circuit\n", "\n", "bit_num = 127\n", diff --git a/learning/modules/computer-science/quantum-teleportation.ipynb b/learning/modules/computer-science/quantum-teleportation.ipynb index c11384c99f2..831c82832bf 100644 --- a/learning/modules/computer-science/quantum-teleportation.ipynb +++ b/learning/modules/computer-science/quantum-teleportation.ipynb @@ -476,7 +476,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "6e3e0d07-b52f-4e17-9233-0a00065f2d77", "metadata": {}, "outputs": [ @@ -507,15 +507,18 @@ "\n", "qc = QuantumCircuit(secret, Alice, Bob, cr)\n", "\n", - "# We entangle Alice's and Bob's qubits as in our work above. We apply a Hadamard gate and then a CNOT gate.\n", + "# We entangle Alice's and Bob's qubits as in our work above.\n", + "# We apply a Hadamard gate and then a CNOT gate.\n", "# Note that the second argument in the CNOT gate is the target.\n", "qc.h(Alice)\n", "qc.cx(Alice, Bob)\n", "\n", - "# Inserting a barrier changes nothing about the logic. It just allows us to force gates to be positioned in \"layers\".\n", + "# Inserting a barrier changes nothing about the logic. It just allows us to\n", + "# force gates to be positioned in \"layers\".\n", "qc.barrier()\n", "\n", - "# Now we will use random variables to create the secret state. Don't worry about the \"u\" gate and the details.\n", + "# Now we will use random variables to create the secret state.\n", + "# Don't worry about the \"u\" gate and the details.\n", "np.random.seed(42) # fixing seed for repeatability\n", "theta = np.random.uniform(0.0, 1.0) * np.pi # from 0 to pi\n", "varphi = np.random.uniform(0.0, 2.0) * np.pi # from 0 to 2*pi\n", @@ -533,7 +536,8 @@ "qc.measure(Alice, cr[1])\n", "qc.measure(secret, cr[0])\n", "\n", - "# Now we insert some conditional logic. If Alice measures Q in a \"1\" we need a Z gate, and if Alice measures A in a \"1\" we need an X gate (see the table).\n", + "# Now we insert some conditional logic. If Alice measures Q in a \"1\" we need a Z gate, and if Alice\n", + "# measures A in a \"1\" we need an X gate (see the table).\n", "with qc.if_test((cr[1], 1)):\n", " qc.x(Bob)\n", "with qc.if_test((cr[0], 1)):\n", @@ -614,7 +618,8 @@ "# Load the Qiskit Runtime service\n", "\n", "# Syntax for first saving your token. Delete these lines after saving your credentials.\n", - "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform', instance = '', token='', overwrite=True, set_as_default=True)\n", + "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform',\n", + "# instance = '', token='', overwrite=True, set_as_default=True)\n", "# service = QiskitRuntimeService(channel='ibm_quantum_platform')\n", "\n", "# Load saved credentials\n", @@ -816,7 +821,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "7d7742fa-e9a2-4ca0-9d9b-f29d73e07cd9", "metadata": {}, "outputs": [ @@ -855,7 +860,8 @@ "qc.h(ebitsa[Alice])\n", "qc.cx(ebitsa[Alice], ebitsb[Bob])\n", "\n", - "# Starting with Bob and Alice in the center, we swap their information onto adjacent qubits, until the information is on distant qubits.\n", + "# Starting with Bob and Alice in the center, we swap their information onto adjacent qubits,\n", + "# until the information is on distant qubits.\n", "\n", "for n in range(Alice):\n", " qc.swap(ebitsb[Bob], ebitsb[Bob + 1])\n", diff --git a/learning/modules/computer-science/vqe.ipynb b/learning/modules/computer-science/vqe.ipynb index 5f5f303e4bc..e0f67ce9c0e 100644 --- a/learning/modules/computer-science/vqe.ipynb +++ b/learning/modules/computer-science/vqe.ipynb @@ -366,7 +366,8 @@ "from qiskit_ibm_runtime import EstimatorV2 as Estimator\n", "\n", "# Syntax for first saving your token. Delete these lines after saving your credentials.\n", - "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform', instance = '', token='', overwrite=True, set_as_default=True)\n", + "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform',\n", + "# instance = '', token='', overwrite=True, set_as_default=True)\n", "# service = QiskitRuntimeService(channel='ibm_quantum_platform')\n", "\n", "# Load saved credentials\n", diff --git a/learning/modules/quantum-mechanics/bells-inequality-with-qiskit.ipynb b/learning/modules/quantum-mechanics/bells-inequality-with-qiskit.ipynb index e9a72eaef44..c233993a60e 100644 --- a/learning/modules/quantum-mechanics/bells-inequality-with-qiskit.ipynb +++ b/learning/modules/quantum-mechanics/bells-inequality-with-qiskit.ipynb @@ -673,12 +673,13 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "63858607-086b-408e-958a-699bf705c5ec", "metadata": {}, "outputs": [], "source": [ - "# We'll begin by importing qiskit and a visualization module so that we can plot a histogram of our results.\n", + "# We'll begin by importing qiskit and a visualization module so that we can plot a\n", + "# histogram of our results.\n", "\n", "from qiskit.visualization import plot_histogram" ] @@ -709,7 +710,8 @@ } ], "source": [ - "# We start by declaring our first quantum circuit, and giving it two qubits (the first \"2\") and two classical bits for storing outputs (the second \"2\")\n", + "# We start by declaring our first quantum circuit, and giving it two qubits (the first \"2\") and two\n", + "# classical bits for storing outputs (the second \"2\")\n", "# Define registers\n", "from qiskit import ClassicalRegister, QuantumRegister\n", "\n", @@ -721,11 +723,13 @@ "qc1.x([0, 1])\n", "# We need a Hadamard gate acting on Lucas's qubit, which we're calling the 0th qubit.\n", "qc1.h(0)\n", - "# The controlled-NOT gate uses the 0th qubit (Lucas's) as the control and the 1st qubit (Rihanna's) as the target.\n", + "# The controlled-NOT gate uses the 0th qubit (Lucas's) as the control and the 1st qubit (Rihanna's)\n", + "# as the target.\n", "qc1.cx(0, 1)\n", "# The rotation gate acts on the 1st qubit (Rihanna's) and has an argument of -2 pi/3\n", "qc1.ry(-2 * pi / 3, 1)\n", - "# Finally, we want to measure all the qubits in the circuit to obtain measurement probabilities, and store the results in the classical bits.\n", + "# Finally, we want to measure all the qubits in the circuit to obtain measurement probabilities, and\n", + "# store the results in the classical bits.\n", "qc1.measure([0, 1], [0, 1])\n", "# Now we can draw the first of the three circuits that will check Bell's inequality for us.\n", "qc1.draw(output=\"mpl\")" @@ -855,7 +859,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "59ba69f8-477a-4429-b831-9a8e088160f3", "metadata": {}, "outputs": [], @@ -869,7 +873,8 @@ " if counts_list[i].get(outcomes[j]) is None:\n", " counts_list[i].update({outcomes[j]: 0})\n", "\n", - "# Here we create a dictionary that holds all the outcomes and sums over their appearances in each of the circuits.\n", + "# Here we create a dictionary that holds all the outcomes and sums over their appearances\n", + "# in each of the circuits.\n", "\n", "total_counts = {}\n", "for i in range(0, len(outcomes)):\n", @@ -969,7 +974,8 @@ "from qiskit_ibm_runtime import QiskitRuntimeService\n", "\n", "# Syntax for first saving your token. Delete these lines after saving your credentials.\n", - "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform', instance = '', token='', overwrite=True, set_as_default=True)\n", + "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform',\n", + "# instance = '', token='', overwrite=True, set_as_default=True)\n", "# service = QiskitRuntimeService(channel='ibm_quantum_platform')\n", "\n", "# Load saved credentials\n", @@ -1042,7 +1048,8 @@ "# sampler.options.default_shots = 1000\n", "\n", "# Start a job that will return shots for all 100 parameter value sets.\n", - "# The best practice is to use a session as shown below. This is available to Premium Plan, Flex Plan, and On-Prem (IBM Quantum Platform API) Plan users.\n", + "# The best practice is to use a session as shown below. This is available to Premium Plan, Flex\n", + "# Plan, and On-Prem (IBM Quantum Platform API) Plan users.\n", "# result_list = [None] * len(qcs)\n", "# real_counts_list = [None] * len(qcs)\n", "# with Session(backend=backend) as session:\n", @@ -1059,7 +1066,8 @@ "# real_counts_list[i] = counts\n", "# # plot_histogram(counts)\n", "\n", - "# Open users can still carry out this experiment, but without reserving a session of use, meaning repeated queuing is possible.\n", + "# Open users can still carry out this experiment, but without reserving a session of use, meaning\n", + "# repeated queuing is possible.\n", "from qiskit_ibm_runtime import Batch\n", "\n", "batch = Batch(backend=backend)\n", @@ -1084,7 +1092,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "id": "c7ee41f3-b642-4694-873f-bea0c47bacce", "metadata": {}, "outputs": [ @@ -1116,7 +1124,8 @@ " if real_counts_list[i].get(outcomes[j]) is None:\n", " real_counts_list[i].update({outcomes[j]: 0})\n", "\n", - "# Here we create a dictionary that holds all the outcomes and sums over their appearances in each of the circuits.\n", + "# Here we create a dictionary that holds all the outcomes and sums over their appearances\n", + "# in each of the circuits.\n", "\n", "real_total_counts = {}\n", "for i in range(0, len(outcomes)):\n", @@ -1135,8 +1144,10 @@ "metadata": {}, "outputs": [], "source": [ - "# This syntax allows you to run the job on a simulator, in case you have exhausted your allotted time on real IBM quantum computers.\n", - "# But we strongly advise running this on real quantum computers, since this is meant to be a check of the behavior of real quantum systems.\n", + "# This syntax allows you to run the job on a simulator, in case you have exhausted your allotted\n", + "# time on real IBM quantum computers.\n", + "# But we strongly advise running this on real quantum computers, since this is meant to be a check\n", + "# of the behavior of real quantum systems.\n", "\n", "# This uses a local simulator\n", "# from qiskit_aer import AerSimulator\n", @@ -1170,7 +1181,8 @@ "# if counts_list[i].get(outcomes[j]) is None:\n", "# counts_list[i].update({outcomes[j]: 0})\n", "\n", - "# Here we create a dictionary that holds all the outcomes and sums over their appearances in each of the circuits.\n", + "# Here we create a dictionary that holds all the outcomes and sums over their appearances\n", + "# in each of the circuits.\n", "\n", "# total_counts = {}\n", "# for i in range(0, len(outcomes)):\n", diff --git a/learning/modules/quantum-mechanics/exploring-uncertainty-with-qiskit.ipynb b/learning/modules/quantum-mechanics/exploring-uncertainty-with-qiskit.ipynb index 625d954ce7b..9b881d99fcc 100644 --- a/learning/modules/quantum-mechanics/exploring-uncertainty-with-qiskit.ipynb +++ b/learning/modules/quantum-mechanics/exploring-uncertainty-with-qiskit.ipynb @@ -117,7 +117,8 @@ "from qiskit_ibm_runtime import QiskitRuntimeService\n", "\n", "# Syntax for first saving your token. Delete these lines after saving your credentials.\n", - "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform', instance = '', token='', overwrite=True, set_as_default=True)\n", + "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform',\n", + "# instance = '', token='', overwrite=True, set_as_default=True)\n", "# service = QiskitRuntimeService(channel='ibm_quantum_platform')\n", "\n", "# Load saved credentials\n", @@ -195,7 +196,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "8baef7f9-78bf-48c8-8359-efd65f943d96", "metadata": {}, "outputs": [ @@ -225,7 +226,8 @@ "qc.measure(qr, cr[0])\n", "qc.barrier()\n", "\n", - "# Change basis so that measurements made on quantum computer which normally tell us about z, now tell us about x.\n", + "# Change basis so that measurements made on quantum computer which normally tell us about z,\n", + "# now tell us about x.\n", "qc.h(qr)\n", "\n", "# Add a second measurement\n", diff --git a/learning/modules/quantum-mechanics/get-started-with-qiskit.ipynb b/learning/modules/quantum-mechanics/get-started-with-qiskit.ipynb index 84a11449d82..919f4783da8 100644 --- a/learning/modules/quantum-mechanics/get-started-with-qiskit.ipynb +++ b/learning/modules/quantum-mechanics/get-started-with-qiskit.ipynb @@ -927,7 +927,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "bdc5e025-8bf4-4fbc-af80-4404e2577511", "metadata": {}, "outputs": [ @@ -946,7 +946,8 @@ "# Load the Qiskit Runtime service\n", "\n", "# Syntax for first saving your token. Delete these lines after saving your credentials.\n", - "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform', instance = '', token='', overwrite=True, set_as_default=True)\n", + "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform',\n", + "# instance = '', token='', overwrite=True, set_as_default=True)\n", "# service = QiskitRuntimeService(channel='ibm_quantum_platform')\n", "\n", "# Load saved credentials\n", diff --git a/learning/modules/quantum-mechanics/stern-gerlach-measurements-with-qiskit.ipynb b/learning/modules/quantum-mechanics/stern-gerlach-measurements-with-qiskit.ipynb index ad21a379ae6..b3135041071 100644 --- a/learning/modules/quantum-mechanics/stern-gerlach-measurements-with-qiskit.ipynb +++ b/learning/modules/quantum-mechanics/stern-gerlach-measurements-with-qiskit.ipynb @@ -336,11 +336,13 @@ "from qiskit_ibm_runtime import QiskitRuntimeService\n", "\n", "# Syntax for first saving your token. Delete these lines after saving your credentials.\n", - "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform', instance = '', token='', overwrite=True, set_as_default=True)\n", + "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform',\n", + "# instance = '', token='', overwrite=True, set_as_default=True)\n", "# service = QiskitRuntimeService(channel='ibm_quantum_platform')\n", "\n", "# Syntax for specifying a channel and instance (if you need to change from the default set above)\n", - "# service = QiskitRuntimeService(channel='', instance=\"\")\n", + "# service = QiskitRuntimeService(channel='',\n", + "# instance=\"\")\n", "\n", "# Load saved credentials\n", "service = QiskitRuntimeService()" @@ -409,18 +411,21 @@ "metadata": {}, "outputs": [], "source": [ - "# Specify that we want to use only a single shot, to represent a single measurement of a spin in a SG device.\n", + "# Specify that we want to use only a single shot, to represent a single measurement of a spin in a\n", + "# SG device.\n", "num_shots = 1\n", "\n", "# Evaluate the problem using a QPU via Qiskit IBM Runtime\n", - "# The best practice is to use a session as shown below. This is available to Premium Plan, Flex Plan, and On-Prem (IBM Quantum Platform API) Plan users.\n", + "# The best practice is to use a session as shown below. This is available to Premium Plan, Flex\n", + "# Plan, and On-Prem (IBM Quantum Platform API) Plan users.\n", "with Session(backend=backend) as session:\n", " sampler = Sampler(mode=session)\n", " dist = sampler.run([qc_ibm], shots=num_shots).result()\n", "session.close()\n", "counts = dist[0].data.c.get_counts()\n", "\n", - "# Open users can still carry out this experiment, but without making use of a session, meaning repeated queuing is possible.\n", + "# Open users can still carry out this experiment, but without making use of a session, meaning\n", + "# repeated queuing is possible.\n", "# from qiskit_ibm_runtime import Batch,\n", "# batch = Batch(backend=backend)\n", "# sampler = Sampler(mode=batch)\n", @@ -543,14 +548,16 @@ "num_shots = 100\n", "\n", "# Evaluate the problem using a QPU via Qiskit IBM Runtime\n", - "# The best practice is to use a session as shown below. This is available to Premium Plan, Flex Plan, and On-Prem (IBM Quantum Platform API) Plan users.\n", + "# The best practice is to use a session as shown below. This is available to Premium Plan, Flex\n", + "# Plan, and On-Prem (IBM Quantum Platform API) Plan users.\n", "with Session(backend=backend) as session:\n", " sampler = Sampler(mode=session)\n", " dist = sampler.run([qc_ibm], shots=num_shots).result()\n", "session.close()\n", "counts = dist[0].data.c.get_counts()\n", "\n", - "# Open users can still carry out this experiment, but without making use of a session, meaning repeated queuing is possible.\n", + "# Open users can still carry out this experiment, but without making use of a session, meaning\n", + "# repeated queuing is possible.\n", "# batch = Batch(backend=backend)\n", "# sampler = Sampler(mode=batch)\n", "# dist = sampler.run([qc_ibm], shots=num_shots).result()\n", diff --git a/learning/modules/quantum-mechanics/superposition-with-qiskit.ipynb b/learning/modules/quantum-mechanics/superposition-with-qiskit.ipynb index 830c3b9eeee..acf847d7cc3 100644 --- a/learning/modules/quantum-mechanics/superposition-with-qiskit.ipynb +++ b/learning/modules/quantum-mechanics/superposition-with-qiskit.ipynb @@ -195,7 +195,8 @@ "from qiskit_ibm_runtime import QiskitRuntimeService\n", "\n", "# Syntax for first saving your token. Delete these lines after saving your credentials.\n", - "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform', instance = '', token='', overwrite=True, set_as_default=True)\n", + "# QiskitRuntimeService.save_account(channel='ibm_quantum_platform',\n", + "# instance = '', token='', overwrite=True, set_as_default=True)\n", "# service = QiskitRuntimeService(channel='ibm_quantum_platform')\n", "\n", "# Load saved credentials\n",