Skip to content

Commit

Permalink
Merge pull request #16 from qiqb-osaka/sse_tutorial
Browse files Browse the repository at this point in the history
add tutorial of SSE
  • Loading branch information
snuffkin authored Jun 14, 2024
2 parents 9338bb5 + 45ae1a9 commit 39d3464
Show file tree
Hide file tree
Showing 3 changed files with 320 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]

nbsphinx_execute = "never"

# -- Extension configuration -------------------------------------------------
autodoc_member_order = "bysource"
1 change: 1 addition & 0 deletions docs/tutorials.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ Tutorials
:maxdepth: 1

tutorials/sampling_on_riqu_server
tutorials/qaoa_with_server_side_execution
318 changes: 318 additions & 0 deletions docs/tutorials/qaoa_with_server_side_execution.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# QAOA with Server Side Execution\n",
"\n",
"This section describes how to run QAOA with Server Side Execution feature (SSE).\n",
"Suppose you can run QURI Parts riqu using the method described in [Sampling on riqu server](./sampling_on_riqu_server.ipynb)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## What is SSE?\n",
"\n",
"Consider the case when you run a job that repeats classical and quantum programs, such as classical-quantum hybrid algorithms.\n",
"If each quantum program waits in a job queue, the entire job takes a long time to complete.\n",
"\n",
"SSE is a feature that reduces latency by executing Python programs that contain both classical and quantum programs at a location closer to the device than the job key.\n",
"\n",
"In SSE, the device is occupied while the job is running."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Problem\n",
"\n",
"In this tutorial, we use SSE to solve the Maxcut problem with QAOA for four vertices, which is introduced in [Quantum Native Dojo](https://dojo.qulacs.org/en/qp_main/notebooks/5.3_quantum_approximate_optimazation_algorithm.html)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## QAOA Code\n",
"\n",
"This code executes QAOA using QURI Parts.\n",
"In this tutorial, this code is named `qaoa.py`.\n",
"Details are explained after the code. "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import traceback\n",
"\n",
"import numpy as np\n",
"from scipy.optimize import minimize\n",
"\n",
"from quri_parts.circuit import LinearMappedUnboundParametricQuantumCircuit\n",
"from quri_parts.core.estimator import QuantumEstimator\n",
"from quri_parts.core.estimator.sampling import create_sampling_estimator\n",
"from quri_parts.core.measurement import bitwise_commuting_pauli_measurement\n",
"from quri_parts.core.operator import Operator, pauli_label\n",
"from quri_parts.core.sampling import create_concurrent_sampler_from_sampling_backend\n",
"from quri_parts.core.sampling.shots_allocator import create_equipartition_shots_allocator\n",
"from quri_parts.core.state import quantum_state\n",
"\n",
"\n",
"# create estimator\n",
"def create_estimator(n_shots: int) -> QuantumEstimator:\n",
" from quri_parts.riqu.backend import RiquSamplingBackend\n",
" backend = RiquSamplingBackend()\n",
" return create_estimator_from_backend(backend, n_shots)\n",
"\n",
"\n",
"def create_estimator_from_backend(backend, n_shots: int):\n",
" sampler = create_concurrent_sampler_from_sampling_backend(backend)\n",
" allocator = create_equipartition_shots_allocator()\n",
" estimator = create_sampling_estimator(\n",
" n_shots, sampler, bitwise_commuting_pauli_measurement, allocator\n",
" )\n",
" return estimator\n",
"\n",
"\n",
"# A function to add U_C(gamma) to a circuit\n",
"def add_U_C(\n",
" n_vertices: int,\n",
" circuit: LinearMappedUnboundParametricQuantumCircuit,\n",
" gamma_idx: int,\n",
") -> None:\n",
" gamma = circuit.add_parameter(f\"gamma_{gamma_idx}\")\n",
" for i in range(n_vertices):\n",
" j = (i + 1) % n_vertices\n",
" circuit.add_CNOT_gate(i, j)\n",
" ## With QURI Parts, RZ(theta)=e^{-i*theta/2*Z}\n",
" circuit.add_ParametricRZ_gate(j, {gamma: 2})\n",
" circuit.add_CNOT_gate(i, j)\n",
"\n",
"\n",
"# A function to add U_X(beta) to a circuit\n",
"def add_U_X(\n",
" n_vertices: int,\n",
" circuit: LinearMappedUnboundParametricQuantumCircuit,\n",
" beta_idx: int,\n",
") -> None:\n",
" beta = circuit.add_parameter(f\"beta_{beta_idx}\")\n",
" for i in range(n_vertices):\n",
" circuit.add_ParametricRX_gate(i, {beta: 2})\n",
" return circuit\n",
"\n",
"\n",
"def generate_parameter_order(n_layer: int):\n",
" array = [0] * (2 * n_layer) \n",
" for i in range(n_layer):\n",
" array[i * 2] = n_layer + i\n",
" array[i * 2 + 1] = i\n",
" return array\n",
"\n",
"\n",
"def cost_function(\n",
" n_vertices: int,\n",
" observable: Operator,\n",
" estimator: QuantumEstimator,\n",
" n_layers: int,\n",
" parameter_order: list[int],\n",
"):\n",
" def qaoa_function(\n",
" x: np.ndarray[float],\n",
" ) -> float:\n",
" circuit = LinearMappedUnboundParametricQuantumCircuit(n_vertices)\n",
" ## to create superposition, apply Hadamard gate\n",
" for i in range(n_vertices):\n",
" circuit.add_H_gate(i)\n",
"\n",
" ## apply U_C, U_X\n",
" for i in range(n_layers):\n",
" add_U_C(n_vertices, circuit, i)\n",
" add_U_X(n_vertices, circuit, i)\n",
"\n",
" # Sorting the input x to x[[2, 0, 3, 1]] is for\n",
" # making the parameter order consistent with the\n",
" # circuit parameter order [gamma0, beta0, gamma1, beta1].\n",
" # You may check the circuit parameter order by running\n",
" # ```circuit.param_mapping.in_params````\n",
" bound_circuit = circuit.bind_parameters(x[parameter_order])\n",
"\n",
" ## prepare |beta, gamma>\n",
" state = quantum_state(n_vertices, circuit=bound_circuit)\n",
" return estimator(observable, state).value.real\n",
" \n",
" return qaoa_function\n",
"\n",
"\n",
"try:\n",
" # setting\n",
" n_vertices = 4\n",
" n_layers = 2\n",
" n_shots = 10000\n",
"\n",
" # initial parameter\n",
" x0 = np.array([0.1, 0.1, 0.2, 0.3])\n",
"\n",
" ## observable and cost function\n",
" cost_observable = Operator({pauli_label(f\"Z{i} Z{(i+1) % n_vertices}\"): 0.5 for i in range(n_vertices)})\n",
" estimator = create_estimator(n_shots)\n",
" parameter_order = generate_parameter_order(n_layers)\n",
" cost_fun = cost_function(n_vertices, cost_observable, estimator, n_layers, parameter_order)\n",
"\n",
" ## minimize with scipy.minimize\n",
" cost_history = [cost_fun(x0)]\n",
" result = minimize(\n",
" cost_fun,\n",
" x0,\n",
" method=\"powell\",\n",
" callback=lambda x: cost_history.append(cost_fun(x)),\n",
" options={\"maxiter\": 500},\n",
" )\n",
" print(\"QAOA Cost:\", result.fun) # value after optimization\n",
" print(\"Cost History:\", cost_history) # value after optimization\n",
" print(\"Optimized Parameter:\", result.x) # (beta, gamma) after optimization\n",
"except Exception as e:\n",
" print(\"Exception:\", e)\n",
" traceback.print_exc()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In this tutorial, the number of layers `n_layers` = 2.\n",
"$x_0 = [\\gamma_0, \\beta_0, \\gamma_1, \\beta_1]$ is the initial value of the rotation angle parameters to be embedded in the quantum circuit.\n",
"The number of parameters in QAOA is 2 * `n_layers`, so if the number of layers is changed, $x_0$ must also be changed.\n",
"\n",
"To execute Python script with SSE, you must use RiquSamplingBackend in `create_estimator` function.\n",
"You can rewrite `create_estimator` function to use any `backend` you like.\n",
"For example, if you want to run using Qiskit, use `QiskitSamplingBackend`.\n",
"You should first make sure it works on your PC before using `RiquSamplingBackend`.\n",
"\n",
"The user must write the program in a single Python script.\n",
"Libraries such as numpy are available.\n",
"\n",
"In SSE, the QPU is occupied while the Python script is running.\n",
"However, there is a defined execution time limit, and if the limit is exceeded, the script will be forced to terminate.\n",
"Please contact the administrator of the Quantum Computing Cloud Service for the running time limit.\n",
"\n",
"The following information can be obtained by the user after the SSE execution is complete.\n",
"\n",
"- `counts` and `properties` of the last quantum circuit executed\n",
"- Python script's output from `print` function as a log file.\n",
"\n",
"It is useful to print the history of QAOA cost values and the final cost value, as shown in the code above.\n",
"The code also handles `Exception` and prints a stack trace for debugging if an error occurs at runtime."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Run SSE jobs"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To execute `qaoa.py` with SSE, here is the code to execute on your PC."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from quri_parts.riqu.backend import RiquSseJob, RiquConfig\n",
"\n",
"sse = RiquSseJob(RiquConfig.from_file(\"default\"))\n",
"job = sse.run_sse(\"<path to qaoa.py>\", remark=\"qaoa with sse\")\n",
"job_id = job.id\n",
"print(f\"job_id={job_id}\")\n",
"\n",
"try:\n",
" result = job.result()\n",
" print(f\"counts of last circuit: {result.counts}\")\n",
"except Exception as e:\n",
" print(e)\n",
"\n",
"print(f\"log file: {sse.download_log()}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To run SSE, you need to instantiate `RiquSseJob`.\n",
"To read authentication information from `~/.riqu`, specify the section name in `~/.riqu` as the argument of the `RiquConfig.from_file` function.\n",
"If the `RiquSseJob` argument is omitted, the `default` section is read.\n",
"\n",
"The argument of the `run_sse` function is the path to the Python script to be executed by SSE.\n",
"The attribute `remark` of the job can be specified and `remark` is optional.\n",
"\n",
"If you call `result` function after you call `run_sse` function, you will get the result after the job is completed.\n",
"Although multiple quantum circuits may be executed in a single job in SSE, the result of the last executed quantum circuit is set to `counts` and `properties`.\n",
"\n",
"Calling `download_log` function downloads the contents of the Python script's print to the user's PC as a zipped log file.\n",
"The return value of `download_log` function is the path to the zipped file.\n",
"\n",
"When `download_log` function is called, the contents of the Python script's `print` is downloaded to the user's PC as a zipped log file.\n",
"The return value of the `download_log` function is the path to the zipped file."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## How to get information about a job later\n",
"\n",
"If you know `job_id`, you can retrieve information about SSE job at a later date.\n",
"Execute the following code:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from quri_parts.riqu.backend import RiquSamplingBackend, RiquConfig\n",
"\n",
"# retrieve job information\n",
"backend = RiquSamplingBackend(RiquConfig.from_file(\"default\"))\n",
"job = backend.retrieve_job(\"<job id>\")\n",
"result = job.result()\n",
"print(f\"counts of last circuit: {result.counts}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from quri_parts.riqu.backend import RiquSseJob, RiquConfig\n",
"\n",
"# download log\n",
"sse = RiquSseJob(RiquConfig.from_file(\"default\"))\n",
"log_file_path = sse.download_log(\"<job id>\")\n",
"print(f\"log file: {log_file_path}\")"
]
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

0 comments on commit 39d3464

Please sign in to comment.