diff --git a/quantum/qiskit-jupyter-app/.ceignore b/quantum/qiskit-jupyter-app/.ceignore new file mode 100644 index 00000000..5c3da25f --- /dev/null +++ b/quantum/qiskit-jupyter-app/.ceignore @@ -0,0 +1,4 @@ +README.md +.bob +setup.env.local +.setup.env.local diff --git a/quantum/qiskit-jupyter-app/.gitignore b/quantum/qiskit-jupyter-app/.gitignore new file mode 100644 index 00000000..3efa163e --- /dev/null +++ b/quantum/qiskit-jupyter-app/.gitignore @@ -0,0 +1,3 @@ +.bob +.setup.env.local +setup.env.local diff --git a/quantum/qiskit-jupyter-app/Dockerfile b/quantum/qiskit-jupyter-app/Dockerfile new file mode 100644 index 00000000..ce779baa --- /dev/null +++ b/quantum/qiskit-jupyter-app/Dockerfile @@ -0,0 +1,24 @@ +# Use the official Jupyter base-notebook image. +# Runs as non-root user "jovyan" (uid 1000) — satisfies IBM security policy. +FROM quay.io/jupyter/base-notebook:latest + +# Install project dependencies. +RUN pip install --no-cache-dir \ + pylatexenc \ + matplotlib \ + qiskit-ibm-runtime \ + jupyter + +# Pre-load the hello-world notebook into the work directory. +# At runtime the COS persistent data store is mounted over /home/jovyan/work; +# this copy is only visible when the bucket is empty (first launch). +COPY hello-world.ipynb /home/jovyan/hello-world.ipynb + +# Copy and install the startup script. +USER root +COPY start.sh /usr/local/bin/start.sh +RUN chmod +x /usr/local/bin/start.sh +USER jovyan + +# Override the base image entrypoint with our startup script. +CMD ["/usr/local/bin/start.sh"] diff --git a/quantum/qiskit-jupyter-app/README.md b/quantum/qiskit-jupyter-app/README.md new file mode 100644 index 00000000..03170424 --- /dev/null +++ b/quantum/qiskit-jupyter-app/README.md @@ -0,0 +1 @@ +See ![](../../content/posts/qiskit-jupyterlab-on-code-engine/) \ No newline at end of file diff --git a/quantum/qiskit-jupyter-app/hello-world.ipynb b/quantum/qiskit-jupyter-app/hello-world.ipynb new file mode 100644 index 00000000..a9057171 --- /dev/null +++ b/quantum/qiskit-jupyter-app/hello-world.ipynb @@ -0,0 +1,589 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "552b1077", + "metadata": {}, + "source": [ + "---\n", + "title: Run your first circuit on hardware\n", + "description: Get started using Qiskit with IBM Quantum hardware in this Hello World example\n", + "---\n", + "\n", + "# Run your first circuit on hardware\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "1a3c196d-545d-417f-b34e-fa422aa9a394", + "metadata": {}, + "source": [ + "This example contains two parts. You will first create a simple \"Hello world\" quantum program and run it on a quantum processing unit (QPU). Because actual quantum research requires much more robust programs, in the second section ([Scale to large numbers of qubits](#scale-to-large-numbers-of-qubits)), you will scale the simple program up to utility level.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "80471a0a", + "metadata": {}, + "source": [ + "## Create and run a simple quantum program\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "85fe979e", + "metadata": { + "raw_mimetype": "text/restructuredtext" + }, + "source": [ + "The four steps to writing a quantum program using Qiskit patterns are:\n", + "\n", + "1. Map the problem to a quantum-native format.\n", + "\n", + "2. Optimize the circuits and operators.\n", + "\n", + "3. Execute using a quantum primitive function.\n", + "\n", + "4. Analyze the results.\n", + "\n", + "### Step 1. Map the problem to a quantum-native format\n", + "\n", + "In a quantum program, *quantum circuits* are the native format in which to represent quantum instructions, and *operators* represent the observables to be measured. When creating a circuit, you'll usually create a new [`QuantumCircuit`](/docs/api/qiskit/qiskit.circuit.QuantumCircuit#quantumcircuit-class) object, then add instructions to it in sequence.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "21f7a26c", + "metadata": {}, + "source": [ + "The following code cell creates a circuit that produces a *Bell state,* which is a state wherein two qubits are fully entangled with each other.\n", + "\n", + "\n", + " The Qiskit SDK uses the LSb 0 bit numbering where the $n^{th}$ digit has value $1 \\ll n$ or $2^n$. For more details, see the [Bit-ordering in the Qiskit SDK](/docs/guides/bit-ordering) topic.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "930ca3b6", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from qiskit import QuantumCircuit\n", + "from qiskit.quantum_info import SparsePauliOp\n", + "from qiskit.transpiler import generate_preset_pass_manager\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", + "from matplotlib import pyplot as plt\n", + "\n", + "# Legacy server-side primitive before qiskit-ibm-runtime 0.50.0\n", + "# from qiskit_ibm_runtime import EstimatorOptions\n", + "# from qiskit_ibm_runtime import Estimator\n", + "\n", + "# Client-side primitive introduced in qiskit-ibm-runtime 0.50.0\n", + "from qiskit_ibm_runtime.options_models import EstimatorOptions\n", + "from qiskit_ibm_runtime.executor_estimator import Estimator\n", + "\n", + "# Uncomment the next line if you want to use a simulator:\n", + "# from qiskit_ibm_runtime.fake_provider import FakeBelemV2\n", + "\n", + "\n", + "# Create a new circuit with two qubits\n", + "qc = QuantumCircuit(2)\n", + "\n", + "# Add a Hadamard gate to qubit 0\n", + "qc.h(0)\n", + "\n", + "# Perform a controlled-X gate on qubit 1, controlled by qubit 0\n", + "qc.cx(0, 1)\n", + "\n", + "# Return a drawing of the circuit using MatPlotLib (\"mpl\").\n", + "# These guides are written by using Jupyter notebooks, which\n", + "# display the output of the last line of each cell.\n", + "# If you're running this in a script, use `print(qc.draw())` to\n", + "# print a text drawing.\n", + "qc.draw(\"mpl\")" + ] + }, + { + "cell_type": "markdown", + "id": "0c957de9", + "metadata": { + "raw_mimetype": "text/restructuredtext" + }, + "source": [ + "See [`QuantumCircuit`](/docs/api/qiskit/qiskit.circuit.QuantumCircuit#quantumcircuit-class) in the documentation for all available operations.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "f3ef4248-7938-44c1-85f1-edc997f0edcd", + "metadata": {}, + "source": [ + "When creating quantum circuits, you must also consider what type of data you want returned after execution. Qiskit provides two ways to return data: you can obtain sampling output for a set of qubits you choose to measure, or you can obtain the expectation value of an observable. Prepare your workload to measure your circuit in one of these two ways with primitives (explained in detail in [Step 3](#step-3-execute-using-the-quantum-primitives)).\n", + "\n", + "This example measures expectation values by using the `qiskit.quantum_info` submodule, which is specified by using operators (mathematical objects used to represent an action or process that changes a quantum state). The following code cell creates six two-qubit Pauli operators: `IZ`, `IX`, `ZI`, `XI`, `ZZ`, and `XX`.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c57b261c-b757-4432-beab-61b526c98a41", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Set up six different observables.\n", + "\n", + "observables_labels = [\"IZ\", \"IX\", \"ZI\", \"XI\", \"ZZ\", \"XX\"]\n", + "observables = [SparsePauliOp(label) for label in observables_labels]" + ] + }, + { + "cell_type": "markdown", + "id": "47150779", + "metadata": {}, + "source": [ + "\n", + " Here, something like the `ZZ` operator is a shorthand for the tensor product $Z\\otimes Z$, which means measuring Z on qubit 1 and Z on qubit 0 together, and obtaining information about the correlation between qubit 1 and qubit 0. Expectation values like this are also typically written as $\\langle Z_1 Z_0 \\rangle$.\n", + "\n", + " If the state is entangled, then the measurement of $\\langle Z_1 Z_0 \\rangle$ should be different from the measurement of $\\langle I_1 \\otimes Z_0 \\rangle \\langle Z_1 \\otimes I_0 \\rangle$. For the specific entangled state created by our circuit described above, the measurement of $\\langle Z_1 Z_0 \\rangle$ should be 1 and the measurement of $\\langle I_1 \\otimes Z_0 \\rangle \\langle Z_1 \\otimes I_0 \\rangle$ should be zero.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "83bf9151-3bc9-40d2-8615-31570238b08e", + "metadata": {}, + "source": [ + "\n", + "\n", + "### Step 2. Optimize the circuits and operators\n", + "\n", + "When executing circuits on a device, it is important to optimize the set of instructions that the circuit contains and minimize the overall depth (roughly the number of instructions) of the circuit. This ensures that you obtain the best results possible by reducing the effects of error and noise. Additionally, the circuit's instructions must conform to a backend device's [Instruction Set Architecture (ISA)](/docs/guides/transpile#instruction-set-architecture) and must consider the device's basis gates and qubit connectivity.\n", + "\n", + "The following code instantiates a real device to submit a job to and transforms the circuit and observables to match that backend's ISA. It requires that you have already [saved your credentials](/docs/guides/cloud-setup)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9a901271", + "metadata": {}, + "outputs": [], + "source": [ + "service = QiskitRuntimeService()\n", + "\n", + "backend = service.least_busy(simulator=False, operational=True)\n", + "\n", + "# Convert to an ISA circuit and layout-mapped observables.\n", + "pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", + "isa_circuit = pm.run(qc)\n", + "\n", + "isa_circuit.draw(\"mpl\", idle_wires=False)" + ] + }, + { + "cell_type": "markdown", + "id": "9acac1d4", + "metadata": {}, + "source": [ + "### Step 3. Execute using the quantum primitives\n", + "\n", + "Quantum computers can produce random results, so you usually collect a sample of the outputs by running the circuit many times. You can estimate the value of the observable by using the `Estimator` class. `Estimator` is one of two [primitives](/docs/guides/get-started-with-estimator); the other is `Sampler`, which can be used to get data from a quantum computer. These objects possess a `run()` method that executes the selection of circuits, observables, and parameters (if applicable), using a [primitive unified bloc (PUB)](/docs/guides/get-started-with-sampler).\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "62c4ca44", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Construct the Estimator instance.\n", + "\n", + "estimator = Estimator(mode=backend)\n", + "estimator.options.resilience_level = 1\n", + "estimator.options.default_shots = 5000\n", + "\n", + "mapped_observables = [\n", + " observable.apply_layout(isa_circuit.layout) for observable in observables\n", + "]\n", + "\n", + "# One pub, with one circuit to run against five different observables.\n", + "job = estimator.run([(isa_circuit, mapped_observables)])\n", + "\n", + "# Use the job ID to retrieve your job data later\n", + "print(f\">>> Job ID: {job.job_id()}\")" + ] + }, + { + "cell_type": "markdown", + "id": "47479e76", + "metadata": {}, + "source": [ + "After a job is submitted, you can wait until either the job is completed within your current python instance, or use the `job_id` to retrieve the data at a later time. (See the [section on retrieving jobs](/docs/guides/monitor-job#retrieve-job-results-at-a-later-time) for details.)\n", + "\n", + "After the job completes, examine its output through the job's `result()` attribute.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "792d2f01", + "metadata": {}, + "outputs": [], + "source": [ + "# This is the result of the entire submission. You submitted one Pub,\n", + "# so this contains one inner result (and some metadata of its own).\n", + "job_result = job.result()\n", + "\n", + "# This is the result from our single pub, which had six observables,\n", + "# so contains information on all six.\n", + "pub_result = job.result()[0]" + ] + }, + { + "cell_type": "markdown", + "id": "48317a25", + "metadata": {}, + "source": [ + "\n", + " When you run your quantum program on a real device, your workload must wait in a queue before it runs. To save time, you can instead use the following code to run this small workload on the [`fake_provider`](/docs/api/qiskit-ibm-runtime/fake-provider) with the `qiskit-ibm-runtime` local testing mode. Note that this is only possible for a small circuit. When you scale up in the next section, you will need to use a real device.\n", + "\n", + " ```python\n", + "\n", + " # Use the following code instead if you want to run on a simulator:\n", + "\n", + " from qiskit_ibm_runtime.fake_provider import FakeBelemV2\n", + " backend = FakeBelemV2()\n", + " estimator = Estimator(backend)\n", + "\n", + " # Convert to an ISA circuit and layout-mapped observables.\n", + "\n", + " pm = generate_preset_pass_manager(backend=backend, optimization_level=1)\n", + " isa_circuit = pm.run(qc)\n", + " mapped_observables = [\n", + " observable.apply_layout(isa_circuit.layout) for observable in observables\n", + " ]\n", + "\n", + " job = estimator.run([(isa_circuit, mapped_observables)])\n", + " result = job.result()\n", + "\n", + " # This is the result of the entire submission. You submitted one Pub,\n", + " # so this contains one inner result (and some metadata of its own).\n", + "\n", + " job_result = job.result()\n", + "\n", + " # This is the result from our single pub, which had five observables,\n", + " # so contains information on all five.\n", + "\n", + " pub_result = job.result()[0]\n", + " ```\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "d200d1f8", + "metadata": {}, + "source": [ + "### Step 4. Analyze the results\n", + "\n", + "The analyze step is typically where you might post-process your results using, for example, measurement error mitigation or zero noise extrapolation (ZNE). You might feed these results into another workflow for further analysis or prepare a plot of the key values and data. In general, this step is specific to your problem. For this example, plot each of the expectation values that were measured for our circuit.\n", + "\n", + "The expectation values and standard deviations for the observables you specified to Estimator are accessed through the job result's `PubResult.data.evs` and `PubResult.data.stds` attributes. To obtain the results from Sampler, use the `PubResult.data.meas.get_counts()` function, which will return a `dict` of measurements in the form of bitstrings as keys and counts as their corresponding values. For more information, see the [Sampler quickstart](/docs/guides/get-started-with-sampler) guide.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "87143fcc", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Plot the result\n", + "\n", + "values = pub_result.data.evs\n", + "\n", + "errors = pub_result.data.stds\n", + "\n", + "# plotting graph\n", + "plt.plot(observables_labels, values, \"-o\")\n", + "plt.xlabel(\"Observables\")\n", + "plt.ylabel(\"Values\")\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "e6a9ba84", + "metadata": {}, + "source": [ + "Notice that for qubits 0 and 1, the independent expectation values of both X and Z are 0, while the correlations (`XX` and `ZZ`) are 1. This is a hallmark of quantum entanglement.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "0bc582d8", + "metadata": {}, + "source": [ + "## Scale to large numbers of qubits\n", + "\n", + "In quantum computing, utility-scale work is crucial for making progress in the field. Such work requires computations to be done on a much larger scale; working with circuits that might use over 100 qubits and over 1000 gates. This example demonstrates how you can accomplish utility-scale work on IBM® QPUs by creating and analyzing a 100-qubit GHZ state. It uses the Qiskit patterns workflow and ends by measuring the expectation value $\\langle Z_0 Z_i \\rangle $ for each qubit.\n", + "\n", + "### Step 1. Map the problem\n", + "\n", + "Write a function that returns a `QuantumCircuit` that prepares an $n$-qubit GHZ state (essentially an extended Bell state), then use that function to prepare a 100-qubit GHZ state and collect the observables to be measured.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2ac02692", + "metadata": {}, + "outputs": [], + "source": [ + "def get_qc_for_n_qubit_GHZ_state(n: int) -> QuantumCircuit:\n", + " \"\"\"This function will create a qiskit.QuantumCircuit (qc)\n", + " for an n-qubit GHZ state.\n", + "\n", + " Args:\n", + " n (int): Number of qubits in the n-qubit GHZ state\n", + "\n", + " Returns:\n", + " QuantumCircuit: Quantum circuit that generate the n-qubit GHZ state,\n", + " assuming all qubits start in the 0 state\n", + " \"\"\"\n", + " if isinstance(n, int) and n >= 2:\n", + " qc = QuantumCircuit(n)\n", + " qc.h(0)\n", + " for i in range(n - 1):\n", + " qc.cx(i, i + 1)\n", + " else:\n", + " raise Exception(\"n is not a valid input\")\n", + " return qc\n", + "\n", + "\n", + "# Create a new circuit with 100 qubits in the GHZ state\n", + "n = 100\n", + "qc = get_qc_for_n_qubit_GHZ_state(n)" + ] + }, + { + "cell_type": "markdown", + "id": "5b3d0d74", + "metadata": {}, + "source": [ + "Next, map to the operators of interest. This example uses the `ZZ` operators between qubits to examine the behavior as they get farther apart. Increasingly inaccurate (corrupted) expectation values between distant qubits would reveal the level of noise present.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "863a4ec9", + "metadata": {}, + "outputs": [], + "source": [ + "# ZZII...II, ZIZI...II, ... , ZIII...IZ\n", + "operator_strings = [\n", + " \"Z\" + \"I\" * i + \"Z\" + \"I\" * (n - 2 - i) for i in range(n - 1)\n", + "]\n", + "\n", + "operators = [SparsePauliOp(operator) for operator in operator_strings]" + ] + }, + { + "cell_type": "markdown", + "id": "a0b462ce", + "metadata": {}, + "source": [ + "### Step 2. Optimize the problem for execution on quantum hardware\n", + "\n", + "The following code transforms the circuit and observables to match the backend's ISA. It requires that you have already [saved your credentials](/docs/guides/cloud-setup)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "428f05e7", + "metadata": {}, + "outputs": [], + "source": [ + "service = QiskitRuntimeService()\n", + "\n", + "backend = service.least_busy(\n", + " simulator=False, operational=True, min_num_qubits=100\n", + ")\n", + "pm = generate_preset_pass_manager(optimization_level=1, backend=backend)\n", + "\n", + "isa_circuit = pm.run(qc)\n", + "isa_operators_list = [op.apply_layout(isa_circuit.layout) for op in operators]" + ] + }, + { + "cell_type": "markdown", + "id": "2d2b5065", + "metadata": {}, + "source": [ + "### Step 3. Execute on hardware\n", + "\n", + "Submit the job and enable error suppression by using a technique to reduce errors called [dynamical decoupling](/docs/api/qiskit-ibm-runtime/options-models-dynamical-decoupling-options). The resilience level specifies how much resilience to build against errors. Higher levels generate more accurate results, at the expense of longer processing times. For further explanation of the options set in the following code, see [Error mitigation and suppression techniques](/docs/guides/error-mitigation-and-suppression-techniques).\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3aaa5025", + "metadata": {}, + "outputs": [], + "source": [ + "options = EstimatorOptions()\n", + "options.resilience_level = 1\n", + "options.dynamical_decoupling.enable = True\n", + "options.dynamical_decoupling.sequence_type = \"XY4\"\n", + "\n", + "# Create an Estimator object\n", + "estimator = Estimator(backend, options=options)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b4c3d3e7-0a0f-4023-8948-1082e225f46c", + "metadata": {}, + "outputs": [], + "source": [ + "# Submit the circuit to Estimator\n", + "job = estimator.run([(isa_circuit, isa_operators_list)])\n", + "job_id = job.job_id()\n", + "print(job_id)" + ] + }, + { + "cell_type": "markdown", + "id": "0bc64091", + "metadata": {}, + "source": [ + "### Step 4. Post-process results\n", + "\n", + "After the job completes, plot the results and notice that $\\langle Z_0 Z_i \\rangle$ decreases with increasing $i$, even though in an ideal simulation all $\\langle Z_0 Z_i \\rangle$ should be 1.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "de91ebd0", + "metadata": {}, + "outputs": [], + "source": [ + "# data\n", + "data = list(range(1, len(operators) + 1)) # Distance between the Z operators\n", + "result = job.result()[0]\n", + "values = result.data.evs # Expectation value at each Z operator.\n", + "values = [\n", + " v / values[0] for v in values\n", + "] # Normalize the expectation values to evaluate how they decay with distance.\n", + "\n", + "# plotting graph\n", + "plt.plot(data, values, marker=\"o\", label=\"100-qubit GHZ state\")\n", + "plt.xlabel(\"Distance between qubits $i$\")\n", + "plt.ylabel(r\"$\\langle Z_i Z_0 \\rangle / \\langle Z_1 Z_0 \\rangle $\")\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "0afc307c", + "metadata": {}, + "source": [ + "The previous plot shows that as the distance between qubits increases, the signal decays because of the presence of noise.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "e7c24c81", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "\n", + " * Try one of these tutorials:\n", + " * [Ground-state energy estimation of the Heisenberg chain with VQE](/docs/tutorials/spin-chain-vqe)\n", + " * Solve optimization problems using [QAOA](/docs/tutorials/quantum-approximate-optimization-algorithm)\n", + " * Train [quantum kernel](/docs/tutorials/quantum-kernel-training) models for machine learning tasks\n", + " * Find detailed installation instructions in the [Install Qiskit](/docs/guides/install-qiskit) guide.\n", + " * If you prefer not to install Qiskit locally, read about options to use Qiskit in an [online development environment](/docs/guides/online-lab-environments).\n", + " * To save multiple account credentials or to specify other account options, see detailed instructions in the [Save your login credentials](/docs/guides/save-credentials#save-your-access-credentials) guide.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "a1b8767d", + "metadata": {}, + "source": [ + "© IBM Corp., 2017-2026" + ] + } + ], + "metadata": { + "celltoolbar": "Raw Cell Format", + "hours": 0.5, + "kernelspec": { + "display_name": ".venv", + "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.14.7" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/quantum/qiskit-jupyter-app/setup.env b/quantum/qiskit-jupyter-app/setup.env new file mode 100644 index 00000000..a28cdbbc --- /dev/null +++ b/quantum/qiskit-jupyter-app/setup.env @@ -0,0 +1,59 @@ +# ============================================================ +# setup.env — Configuration template for setup.sh +# +# Copy this file to setup.env.local, fill in your values, +# and run setup.sh (it loads setup.env.local automatically): +# +# cp setup.env setup.env.local +# # edit setup.env.local with real values +# ./setup.sh +# +# DO NOT commit a copy of this file that contains real secrets. +# +# All cloud resource names (resource group, Object Storage, Code Engine +# project, application, secrets, data store) are derived by setup.sh from +# NAME_PREFIX plus a random 4-character suffix (new on every run), e.g. +# ce-qiskit-jupyterlab-3fa9 +# To reuse an existing set, export NAME_SUFFIX= before running setup.sh. +# To override one of them, export it in your shell before running setup.sh, +# for example: CE_PROJECT_NAME=my-project ./setup.sh +# ============================================================ + +# ------------------------------------------------------------ +# IBM Cloud +# ------------------------------------------------------------ + +# IBM Cloud API key (optional) — create one at: +# https://cloud.ibm.com/iam/apikeys +# If it is not set, setup.sh logs in interactively with +# 'ibmcloud login --sso' and asks you for a one-time passcode. +#IBMCLOUD_API_KEY="" + +# IBM Cloud region where all resources will be created, e.g. us-south, eu-de. +# A value exported in your shell takes precedence. +IBMCLOUD_REGION="${IBMCLOUD_REGION:-eu-de}" + +# Prefix for all resource names: lowercase letters, digits and dashes, +# starting with a letter, at most 40 characters. +# A value exported in your shell takes precedence. +NAME_PREFIX="${NAME_PREFIX:-ce-qiskit-jupyterlab}" + +# ------------------------------------------------------------ +# IBM Quantum (Qiskit Runtime Service credentials) +# ------------------------------------------------------------ + +# Your IBM Quantum API token — copy it from: +# https://quantum.cloud.ibm.com/ → Dashboard → API key +QISKIT_IBM_TOKEN="" + +# CRN of the IBM Quantum instance you want to use. +# Find it at: https://quantum.cloud.ibm.com/instances → hover over the CRN and copy. +QISKIT_IBM_INSTANCE="" + +# ------------------------------------------------------------ +# JupyterLab +# ------------------------------------------------------------ + +# Access token that protects the JupyterLab UI. +# Set this to a long, random string — e.g. output of: openssl rand -hex 32 +JUPYTER_TOKEN="" diff --git a/quantum/qiskit-jupyter-app/setup.sh b/quantum/qiskit-jupyter-app/setup.sh new file mode 100755 index 00000000..c897f7bc --- /dev/null +++ b/quantum/qiskit-jupyter-app/setup.sh @@ -0,0 +1,374 @@ +#!/usr/bin/env bash +# setup.sh — Idempotent provisioning script for Qiskit JupyterLab on Code Engine. +# +# Usage: +# ./setup.sh # sources setup.env.local automatically +# ENV_FILE=my.env ./setup.sh # use a custom env file +# NAME_PREFIX=my-lab ./setup.sh # override any variable from the shell +# NAME_SUFFIX=3fa9 ./setup.sh clean # delete the COS instance and Code Engine +# # project of that resource set (asks first; +# # add --yes to skip the confirmation) +# +# The script sources the env file with 'set -a' so every variable is +# automatically exported and visible to all subprocesses. +# +# Resource names are derived from NAME_PREFIX plus a random 4-character suffix. +# The suffix is non-deterministic: every run generates a new one and therefore +# creates a new set of resources. To reuse an existing set (e.g. to re-run the +# script), export NAME_SUFFIX= before running. The random suffix keeps +# the globally unique COS bucket name collision-free. +# Every derived name (resource group, COS, Code Engine) can be overridden by +# exporting the variable before running the script, see the section below. +# +# 'clean' removes the COS instance (including its service credentials and +# buckets) and the Code Engine project of the resource set identified by +# NAME_PREFIX and NAME_SUFFIX. The resource group itself is kept. Because the +# suffix is random, 'clean' requires NAME_SUFFIX (or explicit name overrides). + +set -euo pipefail + +# ── parse arguments ─────────────────────────────────────────────────────────── + +MODE=setup +ASSUME_YES=0 +for arg in "$@"; do + case "$arg" in + clean) MODE=clean ;; + -y|--yes) ASSUME_YES=1 ;; + *) echo "[setup] ✗ Unknown argument '$arg'. Usage: $0 [clean [--yes]]" >&2; exit 1 ;; + esac +done + +# ── load configuration ──────────────────────────────────────────────────────── + +ENV_FILE="${ENV_FILE:-setup.env.local}" +if [[ -f "$ENV_FILE" ]]; then + set -a # auto-export every variable that is set + # shellcheck source=/dev/null + source "$ENV_FILE" + set +a +else + # Env file missing — variables must already be exported in the environment. + echo "[setup] Note: '$ENV_FILE' not found; expecting variables to be pre-exported." +fi + +# ── helpers ────────────────────────────────────────────────────────────────── + +log() { echo "[setup] $*"; } +info() { echo "[setup] ✓ $*"; } +err() { echo "[setup] ✗ $*" >&2; exit 1; } + +# ce_exists +# Returns 0 if the resource exists, 1 otherwise. +ce_exists() { + ibmcloud ce "$1" get --name "$2" --output json >/dev/null 2>&1 +} + +# ── validate required variables ────────────────────────────────────────────── + +NAME_PREFIX="${NAME_PREFIX:-ce-qiskit-jupyterlab}" + +# IBMCLOUD_API_KEY is optional: when it is empty, step 1 logs in with 'ibmcloud login --sso'. +if [[ "$MODE" == clean ]]; then + required_vars=(IBMCLOUD_REGION) +else + required_vars=( + IBMCLOUD_REGION + QISKIT_IBM_TOKEN QISKIT_IBM_INSTANCE + JUPYTER_TOKEN + ) +fi + +for var in "${required_vars[@]}"; do + [[ -z "${!var:-}" ]] && err "Required variable \$$var is not set. Source setup.env.local first." +done + +# The prefix ends up in COS bucket and Code Engine names: lowercase letters, +# digits and dashes, starting with a letter, at most 40 characters. +[[ "$NAME_PREFIX" =~ ^[a-z]([a-z0-9-]{0,38}[a-z0-9])?$ ]] \ + || err "NAME_PREFIX '$NAME_PREFIX' is invalid: use lowercase letters, digits and dashes (start with a letter, max 40 characters)." + +# ── 1. IBM Cloud login ──────────────────────────────────────────────────────── + +if [[ -n "${IBMCLOUD_API_KEY:-}" ]]; then + log "Logging in to IBM Cloud with API key (region: $IBMCLOUD_REGION)…" + ibmcloud login \ + --apikey "$IBMCLOUD_API_KEY" \ + -r "$IBMCLOUD_REGION" \ + --quiet +else + log "IBMCLOUD_API_KEY is not set — logging in with SSO (region: $IBMCLOUD_REGION)…" + log "Open the URL printed below in a browser and enter the one-time passcode." + ibmcloud login --sso -r "$IBMCLOUD_REGION" +fi +info "Logged in." + +# ── 1a. Derive resource names ───────────────────────────────────────────────── + +# Random 4-character suffix (hex), different on every run unless NAME_SUFFIX is set. +if [[ -z "${NAME_SUFFIX:-}" ]]; then + if [[ "$MODE" == clean ]]; then + # A random suffix can never match existing resources, so the target must be explicit. + [[ -n "${IBMCLOUD_RESOURCE_GROUP_NAME:-}" && -n "${COS_INSTANCE_NAME:-}" && -n "${CE_PROJECT_NAME:-}" ]] \ + || err "'clean' needs NAME_SUFFIX (and NAME_PREFIX if not the default), e.g. NAME_SUFFIX=3fa9 $0 clean" + NAME_SUFFIX="n/a" + else + printf -v NAME_SUFFIX '%04x' $(( (RANDOM << 1) | (RANDOM & 1) )) # 16 random bits + fi +fi +NAME_BASE="${NAME_PREFIX}-${NAME_SUFFIX}" + +# Each name can be overridden by exporting the variable before running the script. +IBMCLOUD_RESOURCE_GROUP_NAME="${IBMCLOUD_RESOURCE_GROUP_NAME:-$NAME_BASE}" +COS_INSTANCE_NAME="${COS_INSTANCE_NAME:-${NAME_BASE}-cos}" +COS_BUCKET_NAME="${COS_BUCKET_NAME:-${NAME_BASE}-notebooks}" +COS_BUCKET_LOCATION="${COS_BUCKET_LOCATION:-$IBMCLOUD_REGION}" +COS_SERVICE_KEY_NAME="${COS_SERVICE_KEY_NAME:-${NAME_BASE}-cos-key}" +CE_PROJECT_NAME="${CE_PROJECT_NAME:-$NAME_BASE}" +CE_APP_NAME="${CE_APP_NAME:-$NAME_BASE}" +CE_HMAC_SECRET_NAME="${CE_HMAC_SECRET_NAME:-${NAME_BASE}-cos-hmac}" +CE_PDS_NAME="${CE_PDS_NAME:-${NAME_BASE}-notebooks-pds}" +CE_QISKIT_SECRET_NAME="${CE_QISKIT_SECRET_NAME:-${NAME_BASE}-qiskit}" + +log "Resource names (prefix '$NAME_PREFIX', suffix '$NAME_SUFFIX'):" +log " resource group : $IBMCLOUD_RESOURCE_GROUP_NAME" +log " COS instance : $COS_INSTANCE_NAME" +log " COS bucket : $COS_BUCKET_NAME ($COS_BUCKET_LOCATION)" +log " COS credential : $COS_SERVICE_KEY_NAME" +log " CE project : $CE_PROJECT_NAME" +log " CE application : $CE_APP_NAME" +log " CE secrets : $CE_HMAC_SECRET_NAME, $CE_QISKIT_SECRET_NAME" +log " CE data store : $CE_PDS_NAME" + +# ── clean: delete the COS instance and the Code Engine project ─────────────── + +if [[ "$MODE" == clean ]]; then + log "Clean mode — the following will be PERMANENTLY deleted:" + log " Code Engine project : $CE_PROJECT_NAME (with its apps, secrets and data stores)" + log " COS instance : $COS_INSTANCE_NAME (with its credentials and buckets)" + log " (resource group '$IBMCLOUD_RESOURCE_GROUP_NAME' is kept)" + if [[ "$ASSUME_YES" -ne 1 ]]; then + read -r -p "[setup] Continue? [y/N] " answer + [[ "$answer" =~ ^[Yy]$ ]] || err "Aborted." + fi + + ibmcloud resource group "$IBMCLOUD_RESOURCE_GROUP_NAME" --output json >/dev/null 2>&1 \ + || err "Resource group '$IBMCLOUD_RESOURCE_GROUP_NAME' does not exist — nothing to clean." + ibmcloud target -g "$IBMCLOUD_RESOURCE_GROUP_NAME" --quiet + info "Targeted resource group '$IBMCLOUD_RESOURCE_GROUP_NAME'." + + ibmcloud plugin install code-engine -f --quiet 2>/dev/null || true + + if ibmcloud ce project get --name "$CE_PROJECT_NAME" --output json >/dev/null 2>&1; then + log "Deleting Code Engine project '$CE_PROJECT_NAME'…" + ibmcloud ce project delete --name "$CE_PROJECT_NAME" --force --hard + info "Code Engine project deleted." + else + info "Code Engine project '$CE_PROJECT_NAME' not found — skipping." + fi + + if ibmcloud resource service-instance "$COS_INSTANCE_NAME" --output json >/dev/null 2>&1; then + log "Deleting COS instance '$COS_INSTANCE_NAME'…" + ibmcloud resource service-instance-delete "$COS_INSTANCE_NAME" --force --recursive + info "COS instance deleted." + else + info "COS instance '$COS_INSTANCE_NAME' not found — skipping." + fi + + info "Clean finished." + exit 0 +fi + +# ── 1b. Resource group ──────────────────────────────────────────────────────── + +log "Checking resource group '$IBMCLOUD_RESOURCE_GROUP_NAME'…" +if ibmcloud resource group "$IBMCLOUD_RESOURCE_GROUP_NAME" --output json >/dev/null 2>&1; then + info "Resource group already exists — skipping create." +else + log "Creating resource group '$IBMCLOUD_RESOURCE_GROUP_NAME'…" + ibmcloud resource group-create "$IBMCLOUD_RESOURCE_GROUP_NAME" + info "Resource group created." +fi + +# Switch the CLI context to the new resource group so all subsequent commands use it. +ibmcloud target -g "$IBMCLOUD_RESOURCE_GROUP_NAME" --quiet +info "Targeted resource group '$IBMCLOUD_RESOURCE_GROUP_NAME'." + +# ── 2. Ensure plugins are installed ────────────────────────────────────────── + +log "Ensuring ibmcloud plugins are installed…" +ibmcloud plugin install code-engine -f --quiet 2>/dev/null || true +ibmcloud plugin install cloud-object-storage -f --quiet 2>/dev/null || true +info "Plugins ready." + +# ── 3. COS service instance ─────────────────────────────────────────────────── + +log "Checking COS service instance '$COS_INSTANCE_NAME'…" +if ibmcloud resource service-instance "$COS_INSTANCE_NAME" --output json >/dev/null 2>&1; then + info "COS instance already exists — skipping create." +else + log "Creating COS service instance '$COS_INSTANCE_NAME'…" + ibmcloud resource service-instance-create \ + "$COS_INSTANCE_NAME" \ + cloud-object-storage \ + standard \ + global \ + -d premium-global-deployment-iam + info "COS instance created." +fi + +# Retrieve the COS instance CRN and configure the COS CLI plugin. +COS_INSTANCE_CRN=$(ibmcloud resource service-instance "$COS_INSTANCE_NAME" --output json \ + | jq -r '.[0].id') +ibmcloud cos config crn --crn "$COS_INSTANCE_CRN" --force +# Set the regional endpoint explicitly so the plugin never falls back to a +# stale cached endpoint (e.g. from a previous region). +ibmcloud cos config endpoint-url --url "https://s3.$COS_BUCKET_LOCATION.cloud-object-storage.appdomain.cloud" +info "COS CLI configured for instance CRN and endpoint." + +# ── 3b. COS service credential (HMAC) ───────────────────────────────────────── + +log "Checking COS service credential '$COS_SERVICE_KEY_NAME'…" +if ibmcloud resource service-key "$COS_SERVICE_KEY_NAME" --output json >/dev/null 2>&1; then + info "COS service credential already exists — reusing." +else + log "Creating COS service credential '$COS_SERVICE_KEY_NAME' with HMAC enabled…" + ibmcloud resource service-key-create \ + "$COS_SERVICE_KEY_NAME" \ + Writer \ + --instance-name "$COS_INSTANCE_NAME" \ + -p '{"HMAC":true}' + info "COS service credential created." +fi + +# Extract HMAC keys from the credential JSON — no manual copy needed. +COS_HMAC_CREDENTIALS=$(ibmcloud resource service-key "$COS_SERVICE_KEY_NAME" --output json) +COS_HMAC_ACCESS_KEY_ID=$(echo "$COS_HMAC_CREDENTIALS"|jq -r '.[0].credentials.cos_hmac_keys.access_key_id') +COS_HMAC_SECRET_ACCESS_KEY=$(echo "$COS_HMAC_CREDENTIALS"|jq -r '.[0].credentials.cos_hmac_keys.secret_access_key') +info "HMAC keys extracted from service credential." + +# ── 4. COS bucket ───────────────────────────────────────────────────────────── + +log "Checking COS bucket '$COS_BUCKET_NAME'…" +if ibmcloud cos bucket-head --bucket "$COS_BUCKET_NAME" --region "$COS_BUCKET_LOCATION" >/dev/null 2>&1; then + info "COS bucket already exists — skipping create." +else + log "Creating COS bucket '$COS_BUCKET_NAME' in '$COS_BUCKET_LOCATION'…" + ibmcloud cos bucket-create \ + --bucket "$COS_BUCKET_NAME" \ + --region "$COS_BUCKET_LOCATION" \ + --ibm-service-instance-id "$COS_INSTANCE_CRN" + info "COS bucket created." +fi + +# ── 5. Code Engine project ──────────────────────────────────────────────────── + +log "Checking Code Engine project '$CE_PROJECT_NAME'…" +if ibmcloud ce project get --name "$CE_PROJECT_NAME" --output json >/dev/null 2>&1; then + info "Code Engine project already exists — skipping create." +else + log "Creating Code Engine project '$CE_PROJECT_NAME'…" + ibmcloud ce project create --name "$CE_PROJECT_NAME" + info "Code Engine project created." +fi + +log "Selecting Code Engine project '$CE_PROJECT_NAME'…" +ibmcloud ce project select --name "$CE_PROJECT_NAME" +info "Project selected." + +# ── 6. HMAC secret ──────────────────────────────────────────────────────────── + +log "Checking HMAC secret '$CE_HMAC_SECRET_NAME'…" +if ce_exists secret "$CE_HMAC_SECRET_NAME"; then + info "HMAC secret already exists — skipping create." +else + log "Creating HMAC secret '$CE_HMAC_SECRET_NAME'…" + ibmcloud ce secret create \ + --name "$CE_HMAC_SECRET_NAME" \ + --format hmac \ + --access-key-id "$COS_HMAC_ACCESS_KEY_ID" \ + --secret-access-key "$COS_HMAC_SECRET_ACCESS_KEY" + info "HMAC secret created." +fi + +# ── 7. Persistent data store ────────────────────────────────────────────────── + +log "Checking persistent data store '$CE_PDS_NAME'…" +if ibmcloud ce persistentdatastore get --name "$CE_PDS_NAME" --output json >/dev/null 2>&1; then + info "Persistent data store already exists — skipping create." +else + log "Creating persistent data store '$CE_PDS_NAME'…" + ibmcloud ce persistentdatastore create \ + --name "$CE_PDS_NAME" \ + --cos-bucket-name "$COS_BUCKET_NAME" \ + --cos-access-secret "$CE_HMAC_SECRET_NAME" + info "Persistent data store created." +fi + +# ── 8. Qiskit credentials secret ───────────────────────────────────────────── + +# Write credentials to a temp file; always clean it up on exit. +TMPFILE=$(mktemp /tmp/qiskit-ibm-XXXXXX.json) +trap 'rm -f "$TMPFILE"' EXIT + +jq -n \ + --arg token "$QISKIT_IBM_TOKEN" \ + --arg instance "$QISKIT_IBM_INSTANCE" \ + '{token: $token, instance: $instance}' > "$TMPFILE" + +log "Checking Qiskit credentials secret '$CE_QISKIT_SECRET_NAME'…" +if ce_exists secret "$CE_QISKIT_SECRET_NAME"; then + log "Updating existing Qiskit credentials secret…" + ibmcloud ce secret update \ + --name "$CE_QISKIT_SECRET_NAME" \ + --from-file "qiskit-ibm.json=$TMPFILE" + info "Qiskit credentials secret updated." +else + log "Creating Qiskit credentials secret…" + ibmcloud ce secret create \ + --name "$CE_QISKIT_SECRET_NAME" \ + --format generic \ + --from-file "qiskit-ibm.json=$TMPFILE" + info "Qiskit credentials secret created." +fi + +# ── 9. Application create / update ─────────────────────────────────────────── + +# Flags shared between create and update. +APP_FLAGS=( + --name "$CE_APP_NAME" + --source "." + --strategy dockerfile + --size xlarge + --port 8888 + --min-scale 1 + --max-scale 1 + --scale-down-delay 600 + --cpu 1 + --memory 4G + --service-account "none" + --env "JUPYTER_TOKEN=$JUPYTER_TOKEN" +) + +# Flags only applied on create — mounts are set once at creation time and +# must not be passed on update (Code Engine preserves them across rebuilds). +CREATE_ONLY_FLAGS=( + --mount-secret "/etc/qiskit-secret=$CE_QISKIT_SECRET_NAME" + --mount-data-store "/home/jovyan/work=$CE_PDS_NAME" +) + +log "Checking application '$CE_APP_NAME'…" +if ce_exists app "$CE_APP_NAME"; then + log "Application exists — updating (triggers a new build)…" + ibmcloud ce app update "${APP_FLAGS[@]}" + info "Application updated." +else + log "Creating application '$CE_APP_NAME' (triggers a build from local source)…" + ibmcloud ce app create "${APP_FLAGS[@]}" "${CREATE_ONLY_FLAGS[@]}" + info "Application created." +fi + +# ── 10. Print URL ───────────────────────────────────────────────────────────── + +log "Waiting for application to be ready…" +ibmcloud ce app get --name "$CE_APP_NAME" --output url diff --git a/quantum/qiskit-jupyter-app/start.sh b/quantum/qiskit-jupyter-app/start.sh new file mode 100644 index 00000000..2c7c8dd7 --- /dev/null +++ b/quantum/qiskit-jupyter-app/start.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# start.sh — Container startup script for Qiskit JupyterLab on Code Engine. +# +# 1. Reads Qiskit credentials from the secret mounted at /etc/qiskit-secret/ +# (keys: token, instance) and calls QiskitRuntimeService.save_account so +# that the notebook can call QiskitRuntimeService() without arguments. +# The secret is mounted at a neutral path so Qiskit writes its own +# correctly-structured account file to $HOME/.qiskit/qiskit-ibm.json. +# 2. Starts JupyterLab on all interfaces with open origin policy. +# +# Runs as the container user defined by NB_USER (default: dev). + +set -euo pipefail + +# The CE secret holding the Qiskit credentials is mounted at /etc/qiskit-secret/. +# Each key in the secret becomes a file; we have one key: qiskit-ibm.json. +CREDS_FILE="/etc/qiskit-secret/qiskit-ibm.json" + +if [[ -f "$CREDS_FILE" ]]; then + echo "[start] Registering Qiskit IBM Runtime credentials from $CREDS_FILE…" + python3 - "$CREDS_FILE" <<'PYEOF' +import json, sys + +with open(sys.argv[1]) as f: + creds = json.load(f) + +from qiskit_ibm_runtime import QiskitRuntimeService +QiskitRuntimeService.save_account( + token=creds["token"], + instance=creds.get("instance"), + channel="ibm_quantum_platform", + overwrite=True, +) +print("[start] Qiskit account saved.") +PYEOF +else + echo "[start] Warning: $CREDS_FILE not found — skipping Qiskit account registration." +fi + +echo "[start] Starting JupyterLab…" +exec jupyter lab \ + --ip="*" \ + --NotebookApp.allow_origin="*"