diff --git a/docs/guides/_toc.json b/docs/guides/_toc.json index c544199ef51..40392520d78 100644 --- a/docs/guides/_toc.json +++ b/docs/guides/_toc.json @@ -825,6 +825,10 @@ { "title": "Template for Hamiltonian simulation", "url": "/docs/guides/function-template-hamiltonian-simulation" + }, + { + "title": "Template for AQC + Trotter Hamiltonian dynamics", + "url": "/docs/guides/function-template-aqc-trotter" } ] }, diff --git a/docs/guides/function-template-aqc-trotter.ipynb b/docs/guides/function-template-aqc-trotter.ipynb new file mode 100644 index 00000000000..61060fca6d7 --- /dev/null +++ b/docs/guides/function-template-aqc-trotter.ipynb @@ -0,0 +1,591 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "frontmatter", + "metadata": {}, + "source": [ + "---\n", + "title: Deploy and run a Qiskit Function template for AQC + Trotter Hamiltonian dynamics\n", + "description: Deploy the AQC + Trotter Hamiltonian dynamics function template to IBM Quantum Serverless, then run it on a simulator and on a QPU.\n", + "---\n", + "\n", + "{/* cspell:ignore Trotter Trotterization quimb cotengra cotengrust Suzuki fidelities isa */}\n", + "\n", + "# Deploy and run a Qiskit Function template for AQC + Trotter Hamiltonian dynamics" + ] + }, + { + "cell_type": "markdown", + "id": "version-info", + "metadata": { + "tags": [ + "version-info" + ] + }, + "source": [] + }, + { + "cell_type": "markdown", + "id": "overview", + "metadata": {}, + "source": [ + "## Overview\n", + "\n", + "This is an experiment-agnostic Qiskit Function template for Hamiltonian dynamics. Given a 1D nearest-neighbor Pauli Hamiltonian, a prepared initial state (optional), and a set of observables, it runs Trotter time-evolution, approximate quantum compilation (AQC) circuit compression, and mitigated execution, then returns each observable's time series. Swap the setup (PRE) and the analysis (POST) and the same core drives a different experiment:\n", + "\n", + "```\n", + " PRE (your setup) FUNCTION (deployed here) POST (your analysis)\n", + " prepare a state -> Trotter -> AQC compress -> execute -> S(q, w) (neutron)\n", + " (circuit / product) (statevector / fake / runtime) magnetization, transport,\n", + " + optional local kick -> (t) quench dynamics, ...\n", + "```\n", + "\n", + "The template is published in the Qiskit Function templates repository, alongside the other application templates. See: [AQC Dynamics Template](https://github.com/qiskit-community/qiskit-function-templates/tree/main/physics/aqc_trotter). This notebook deploys it to your own IBM Quantum® Serverless account. Run it once, and any notebook can then call the function with `serverless.load(\"aqc-dynamics-function\")`.\n", + "\n", + "For a worked scientific example, see [Simulate neutron scattering with an AQC + Trotter dynamics Serverless workflow](/docs/tutorials/simulate-neutron-scattering-with-a-serverless-workflow), which calls this function to compute the dynamical structure factor of KCuF$_3$. This notebook covers deployment and the input contract instead." + ] + }, + { + "cell_type": "markdown", + "id": "requirements", + "metadata": {}, + "source": [ + "## Requirements\n", + "\n", + "Before starting, be sure you have the following in this notebook's kernel environment:\n", + "\n", + "- Qiskit SDK v2.0 or later (`pip install qiskit`).\n", + "- The Qiskit IBM Catalog client (`pip install qiskit-ibm-catalog`), which deploys and runs workloads on Qiskit Serverless.\n", + "\n", + "The function's own scientific dependencies (`qiskit-addon-aqc-tensor`, `cotengrust`, `qiskit-aer`) do not need to be installed locally." + ] + }, + { + "cell_type": "markdown", + "id": "source-files", + "metadata": {}, + "source": [ + "## Get the template source files\n", + "\n", + "The function is a small Python package that Qiskit Serverless runs in the cloud, so its source has to exist as local files that are uploaded at deploy time. The package is published in the Qiskit Function templates repository.\n", + "\n", + "Download **[`source_files`](https://download-directory.github.io/?url=https%3A%2F%2Fgithub.com%2Fqiskit-community%2Fqiskit-function-templates%2Ftree%2Fmain%2Fphysics%2Faqc_trotter%2Fsource_files)**\n", + "\n", + "The download is a single zip, named after the full path of the directory in the repository:\n", + "\n", + "`qiskit-community qiskit-function-templates main physics aqc_trotter source_files.zip`\n", + "\n", + "1. Unzip it into the directory that holds this notebook.\n", + "2. Rename the extracted folder from that long name to `source_files`.\n", + "\n", + "Your working directory then looks like this:\n", + "\n", + "```\n", + "your-working-directory/\n", + "├── function-template-aqc-trotter.ipynb <- this notebook\n", + "└── source_files/ <- the renamed folder\n", + " ├── __init__.py\n", + " ├── program.py\n", + " └── source/\n", + " ├── __init__.py\n", + " ├── _serverless.py\n", + " ├── app_function.py\n", + " ├── aqc.py\n", + " ├── build.py\n", + " ├── execute.py\n", + " └── hamiltonian.py\n", + "```\n", + "\n", + "The name has to be exactly `source_files`, because that is the `working_dir` Step 3 uploads.\n", + "\n", + "`program.py` is the entry point the gateway invokes. Everything under `source/` is the implementation, split by stage: Hamiltonian and Trotter synthesis, AQC compression, and execution. None of it needs editing to run the examples below. Step 3 uploads the whole directory, so repeat that step whenever you change a file." + ] + }, + { + "cell_type": "markdown", + "id": "auth-md", + "metadata": {}, + "source": [ + "## 1. Authentication\n", + "\n", + "Use `qiskit-ibm-catalog` to authenticate to `QiskitServerless` with your API key (token) and CRN (instance), which you can find on the [IBM Quantum Platform](https://quantum.cloud.ibm.com) dashboard. This will allow you to locally instantiate the serverless client to upload or run the selected function:\n", + "\n", + "```python\n", + "from qiskit_ibm_catalog import QiskitServerless\n", + "serverless = QiskitServerless(channel=\"ibm_quantum_platform\", token=\"MY_TOKEN\", instance=\"MY_CRN\")\n", + "```\n", + "\n", + "You can optionally use `save_account()` to save your credentials in your local environment (see the [Set up your IBM Cloud account](/docs/guides/cloud-setup#cloud-save) guide). Note that this writes your credentials to the same file as [`QiskitRuntimeService.save_account()`](/docs/api/qiskit-ibm-runtime/qiskit-runtime-service#save_account):\n", + "\n", + "```python\n", + "QiskitServerless.save_account(channel=\"ibm_quantum_platform\", token=\"MY_TOKEN\", instance=\"MY_CRN\")\n", + "```\n", + "\n", + "If the account is saved, there is no need to provide the token to authenticate:" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "id": "auth-code", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_ibm_catalog import QiskitServerless\n", + "\n", + "# Authenticate to the remote cluster\n", + "# In this case, loading a saved account\n", + "serverless = QiskitServerless()\n", + "\n", + "# REPLACE WITH YOUR OWN CREDENTIALS or SAVED ACCOUNT\n", + "# serverless = QiskitServerless(channel=\"ibm_quantum_platform\", token=\"MY_TOKEN\", instance=\"MY_CRN\")" + ] + }, + { + "cell_type": "markdown", + "id": "deps-md", + "metadata": {}, + "source": [ + "## 2. Declare dependencies\n", + "\n", + "Packages the function needs on top of the managed base serverless image.\n", + "\n", + "> The gateway only installs names on its allowlist ([`requirements-dynamic-dependencies.txt`](https://github.com/Qiskit/qiskit-serverless/blob/main/docker-images/requirements-dynamic-dependencies.txt)), matched by package name and pinned to the allowed version with `==`. Anything else must arrive transitively (as a dependency of an allowlisted package). `[extras]` *are* honored — `qiskit-addon-aqc-tensor[quimb-jax]` is what drags `quimb` / `jax` in here. `cotengrust` is needed for memory efficiency during tensor network simulation. `qiskit-aer` is listed separately for the `fake` backend (local noisy simulation)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "deps-code", + "metadata": {}, + "outputs": [], + "source": [ + "DEPENDENCIES = [\n", + " \"qiskit-addon-aqc-tensor[quimb-jax]==0.3.1\",\n", + " \"qiskit-aer==0.17.2\",\n", + " \"cotengrust==0.2.0\",\n", + "]" + ] + }, + { + "cell_type": "markdown", + "id": "upload-md", + "metadata": {}, + "source": [ + "## 3. Define and upload the function" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "upload-code", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit_ibm_catalog import QiskitFunction\n", + "\n", + "fn = QiskitFunction(\n", + " title=\"aqc-dynamics-function\",\n", + " entrypoint=\"program.py\",\n", + " working_dir=\"source_files/\",\n", + " dependencies=DEPENDENCIES,\n", + ")\n", + "serverless.upload(fn)" + ] + }, + { + "cell_type": "markdown", + "id": "verify-md", + "metadata": {}, + "source": [ + "## 4. Verify it registered" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "verify-code", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "QiskitFunction(aqc-dynamics-function)" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "next(p for p in serverless.list() if p.title == \"aqc-dynamics-function\")" + ] + }, + { + "cell_type": "markdown", + "id": "reference-md", + "metadata": {}, + "source": [ + "## Function reference\n", + "\n", + "This is a brief introduction. Every field is documented in full in the [AQC Dynamics Template README](https://github.com/qiskit-community/qiskit-function-templates/blob/main/physics/aqc_trotter/README.md): the complete inputs table with its validation rules, the output fields, the execution backends, and further worked examples. What follows is the short version, enough to read the examples below." + ] + }, + { + "cell_type": "markdown", + "id": "inputs-md", + "metadata": {}, + "source": [ + "### Inputs\n", + "\n", + "Every run is a single `fn.run(...)` call. Only the first three inputs below are required: `hamiltonian`, `t_steps`, and `aqc_segments`. Everything after them is optional and falls back to the default shown, so a minimal call is three arguments and the rest of the table is the functionality you can opt into. The Hamiltonian's `num_qubits` sets the chain length, so there is no separate size input.\n", + "\n", + "| Input | Default | Description |\n", + "|---|---|---|\n", + "| `hamiltonian` | required | 1D nearest-neighbor Pauli Hamiltonian as a `SparsePauliOp`. Strings are Pauli operators, so there is no implicit factor of one half. |\n", + "| `t_steps` | required | Total Trotter steps. Evolves to `T = t_steps * dt` and reports every observable at each `t_k = k * dt`. |\n", + "| `aqc_segments` | required | Compression plan: a list of `{\"n_steps\": k, \"ansatz_steps\": m}`. `sum(n_steps)` steps are compressed; the rest run as plain Trotter. |\n", + "| `dt` | `0.2` | Physical time advanced by one Trotter step. |\n", + "| `initial_state` | `\\|0...0>` | A prepared `QuantumCircuit` to evolve. Bake any local kick into this circuit. |\n", + "| `observables` | per-site `Z` | Anything `EstimatorV2` accepts as its `observables` argument. One observable per output column. |\n", + "| `trotter_options` | 2nd-order Suzuki | `{\"method\": ..., \"synthesis_settings\": {...}}`. `reps` and `time` are owned by the function. |\n", + "| `aqc_options` | see below | `max_bond` (`32`), `cutoff` (`1e-8`), `autodiff_backend` (`\"jax\"`), `fidelity_target` (`None`), `optimizer_settings` (L-BFGS-B, `jac=True`, `maxiter=300`). |\n", + "| `estimator_options` | DD, twirling, TREX | `EstimatorV2.options`, passed through as-is. A supplied dictionary replaces the defaults wholesale rather than merging into them. |\n", + "| `transpiler_options` | `{\"optimization_level\": 3}` | `generate_preset_pass_manager` keyword arguments. `backend` and `target` are rejected, since the execution path owns them. |\n", + "| `backend` | `\"runtime\"` | `\"statevector\"`, `\"fake\"`, or `\"runtime\"`. |\n", + "| `backend_name` | least busy | IBM backend name for `runtime`, or a named fake backend. |\n", + "| `batches` | `1` | Split the circuits across N runtime jobs. One batch submits a single job and creates no session. |\n", + "| `parallel_sim` | `False` | Fan the local simulator paths across all available cores with Ray. No effect on `runtime`. |" + ] + }, + { + "cell_type": "markdown", + "id": "backends-md", + "metadata": {}, + "source": [ + "### Execution backends\n", + "\n", + "All three paths share the same code and the same mitigation settings. They differ only in where the circuits run.\n", + "\n", + "| `backend` | What it is | Credentials | Notes |\n", + "|---|---|---|---|\n", + "| `\"statevector\"` | Exact `StatevectorEstimator` | Serverless account only | The exact reference path. No QPU time. |\n", + "| `\"fake\"` | Noisy local simulation on a Qiskit fake backend | Serverless account only | A faithful rehearsal of the mitigated `runtime` path. Needs `qiskit-aer`. Defaults to the 127-qubit `fake_sherbrooke`. |\n", + "| `\"runtime\"` (default) | The mitigated `EstimatorV2` against a real QPU | Yes | `backend_name` optional; omitting it selects the least busy device. |\n", + "\n", + "Both simulator paths still call the deployed function, so they need a saved Serverless account even though they use no QPU time. The two examples below run the same workload on `statevector` first, then on `runtime`." + ] + }, + { + "cell_type": "markdown", + "id": "output-md", + "metadata": {}, + "source": [ + "### Output\n", + "\n", + "`job.result()` returns a plain dictionary:\n", + "\n", + "```python\n", + "{\n", + " \"times\": [...], # length t_steps + 1, t_k = k * dt (t=0 is the prepared state)\n", + " \"expectation_values\": [[...]], # shape (n_times, n_observables)\n", + " \"observable_labels\": [...], # e.g. [\"Z_0\", \"ZZ_0_1\"]\n", + " \"metadata\": {\n", + " \"n\", \"t_steps\", \"dt\", \"tier\",\n", + " \"aqc_compressed_steps\": 5, # total compressed steps (= sum of segment n_steps)\n", + " \"aqc_segments\": [ # per segment: the plan plus its own results\n", + " {\"n_steps\": 3, \"ansatz_steps\": 1, \"steps\": [1, 2, 3], \"n_params\": 133,\n", + " \"fidelities\": {1: ..., 2: ..., 3: ...}},\n", + " {\"n_steps\": 2, \"ansatz_steps\": 2, \"steps\": [4, 5], \"n_params\": 245,\n", + " \"fidelities\": {4: ..., 5: ...}},\n", + " ],\n", + " \"execution_backend\",\n", + " \"aqc_fidelities\": {1: ..., 2: ...}, # flat per-step fidelity, all compressed steps\n", + " \"circuit_stats\": { # per-step 2q depth and gate count, full Trotter vs AQC\n", + " 1: {\"full_trotter\": {\"depth_2q\": ..., \"num_2q_gates\": ...},\n", + " \"aqc_trotter\": {\"depth_2q\": ..., \"num_2q_gates\": ...}},\n", + " 2: {...},\n", + " },\n", + " \"warnings\": [...], # non-fatal notices, e.g. a cotengrust fallback\n", + " \"resource_usage\": { # per stage; QPU_TIME is the charged QPU time\n", + " \"RUNNING: OPTIMIZING_FOR_HARDWARE\": {\"CPU_TIME\": ...},\n", + " \"RUNNING: WAITING_FOR_QPU\": {\"CPU_TIME\": ...},\n", + " \"RUNNING: EXECUTING_QPU\": {\"QPU_TIME\": ...},\n", + " },\n", + " },\n", + "}\n", + "```\n", + "\n", + "`aqc_fidelities` and `circuit_stats` are the two to read first: together they tell you whether the compression stayed faithful and whether it actually saved depth. On `runtime`, `resource_usage` reports the queue wait separately from the QPU time you are charged for. A rejected input fails fast as a structured `ServerlessError` (code `4615`)." + ] + }, + { + "cell_type": "markdown", + "id": "sim-md", + "metadata": {}, + "source": [ + "## Simulator example\n", + "\n", + "Run the function on the exact `statevector` backend first. It spends no QPU time and validates the deployment end to end. The model here is an 8-qubit transverse-field Ising chain, and `observables` is omitted so the function measures the default per-site $Z$.\n", + "\n", + "The compression plan is the input worth understanding. Each segment `{\"n_steps\": k, \"ansatz_steps\": m}` compresses `k` consecutive Trotter steps into an ansatz built from an `m`-step Trotter target, and any steps beyond `sum(n_steps)` run as plain Trotter. Early, low-entanglement steps compress well into a shallow 1-layer ansatz; later, more-entangled steps need a deeper one." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "sim-code", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "job id: ee1f3793-e995-427d-81d1-5924549beb38\n" + ] + } + ], + "source": [ + "from qiskit.quantum_info import SparsePauliOp\n", + "\n", + "fn = serverless.load(\"aqc-dynamics-function\")\n", + "\n", + "n = 8\n", + "H = SparsePauliOp.from_sparse_list(\n", + " [(\"ZZ\", [i, i + 1], 1.0) for i in range(n - 1)]\n", + " + [(\"X\", [i], 0.8) for i in range(n)],\n", + " num_qubits=n,\n", + ")\n", + "\n", + "job = fn.run(\n", + " t_steps=8,\n", + " aqc_segments=[\n", + " {\n", + " \"n_steps\": 4,\n", + " \"ansatz_steps\": 1,\n", + " }, # early steps -> shallow 1-layer ansatz\n", + " {\n", + " \"n_steps\": 2,\n", + " \"ansatz_steps\": 2,\n", + " }, # later steps -> deeper 2-layer ansatz\n", + " ],\n", + " hamiltonian=H,\n", + " aqc_options={\"max_bond\": 32},\n", + " backend=\"statevector\",\n", + ")\n", + "print(\"job id:\", job.job_id)" + ] + }, + { + "cell_type": "markdown", + "id": "follow-md", + "metadata": {}, + "source": [ + "### Follow the run and read the result\n", + "\n", + "`status()` reports both the coarse job lifecycle and the per-stage sub-status the function publishes as it runs. The same stages apply to the hardware run below:\n", + "\n", + "`QUEUED -> INITIALIZING -> RUNNING: OPTIMIZING_FOR_HARDWARE -> RUNNING: WAITING_FOR_QPU -> RUNNING: EXECUTING_QPU -> RUNNING: POST_PROCESSING -> DONE`\n", + "\n", + "| `status()` value | Stage |\n", + "|---|---|\n", + "| `RUNNING: OPTIMIZING_FOR_HARDWARE` | state prep, Trotter build, AQC compression |\n", + "| `RUNNING: WAITING_FOR_QPU` | queued on the QPU (`runtime` backend only) |\n", + "| `RUNNING: EXECUTING_QPU` | circuits executing (local sims mark this directly) |\n", + "| `RUNNING: POST_PROCESSING` | assembling the result dictionary |\n", + "\n", + "Terminal states are `DONE`, `ERROR`, and `CANCELED`. This `statevector` run has no QPU queue, so it skips `RUNNING: WAITING_FOR_QPU`. Use `job.logs()` at any point to see the per-stage logs, including the AQC fidelity reached at each step." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "status-code", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DONE\n" + ] + } + ], + "source": [ + "print(job.status()) # re-run until this reports DONE" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "result-code", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "observable labels: ['Z_0', 'Z_1', 'Z_2', 'Z_3', 'Z_4', 'Z_5', 'Z_6', 'Z_7']\n", + "times: [0.0, 0.2, 0.4, 0.6000000000000001, 0.8, 1.0, 1.2000000000000002, 1.4000000000000001, 1.6]\n", + "AQC fidelities: {'1': 1.0, '2': 1.0, '3': 1.0, '4': 1.0, '5': 1.0, '6': 0.9999}\n" + ] + } + ], + "source": [ + "result = job.result()\n", + "print(\"observable labels:\", result[\"observable_labels\"])\n", + "print(\"times:\", result[\"times\"])\n", + "print(\n", + " \"AQC fidelities:\",\n", + " {k: round(v, 4) for k, v in result[\"metadata\"][\"aqc_fidelities\"].items()},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "hw-md", + "metadata": {}, + "source": [ + "## Hardware example\n", + "\n", + "A function call with `backend=\"runtime\"` transpiles and executes on a real IBM Quantum processor, with the function's built-in error mitigation: dynamical decoupling (XY4), gate twirling, and twirled readout error extinction (TREX). `backend_name` selects the device; omit it and the function takes the least busy one.\n", + "\n", + "Nothing about the science code changes. Only the chain length, the number of Trotter steps, and the backend differ from the simulator example." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "hw-code", + "metadata": {}, + "outputs": [], + "source": [ + "from qiskit.quantum_info import SparsePauliOp\n", + "\n", + "fn = serverless.load(\"aqc-dynamics-function\")\n", + "\n", + "n = 10\n", + "H = SparsePauliOp.from_sparse_list(\n", + " [(\"ZZ\", [i, i + 1], 1.0) for i in range(n - 1)]\n", + " + [(\"X\", [i], 0.8) for i in range(n)],\n", + " num_qubits=n,\n", + ")\n", + "\n", + "job = fn.run(\n", + " t_steps=10,\n", + " aqc_segments=[\n", + " {\"n_steps\": 3, \"ansatz_steps\": 1},\n", + " {\"n_steps\": 3, \"ansatz_steps\": 2},\n", + " ],\n", + " hamiltonian=H,\n", + " backend=\"runtime\",\n", + ")\n", + "print(\"job id (save this to reconnect later):\", job.job_id)" + ] + }, + { + "cell_type": "markdown", + "id": "hw-reconnect-md", + "metadata": {}, + "source": [ + "\n", + "\n", + "A hardware run is not quick, and most of the time is classical rather than on the QPU. The AQC compression runs inside the function before anything reaches the QPU, and the QPU queue is on top of that. You do not need to keep this notebook or kernel open while it runs.\n", + "\n", + "Copy the job id printed above and save it. The next three cells let you pick the run back up later:\n", + "\n", + "1. Reconnect, only needed in a new kernel session: re-run the [Authentication](#1-authentication) cell to recreate `serverless`, then rebuild the `job` handle from the id you saved. Skip this cell if you are still in the session where you submitted, because the handle is already live.\n", + "2. Check status: re-run until it reports `DONE`.\n", + "3. Fetch the result: run only once the status is `DONE`.\n", + "\n", + "Paste your saved id over the placeholder in the reconnect cell below.\n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "hw-reconnect", + "metadata": {}, + "outputs": [], + "source": [ + "# Reconnect to a previously submitted job by its id. Only needed in a NEW kernel\n", + "# session; if you are still in the session where you submitted, the `job` handle\n", + "# above is already live, so skip this cell. Replace the id below with your own.\n", + "job = serverless.get_job_by_id(\"\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "hw-status", + "metadata": {}, + "outputs": [], + "source": [ + "# Re-run this until it reports DONE, then fetch the result below.\n", + "print(job.status())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "hw-result-code", + "metadata": {}, + "outputs": [], + "source": [ + "# Run this only once the status cell above reports DONE. result() blocks until\n", + "# the job finishes, so calling it earlier just waits.\n", + "result = job.result()\n", + "print(\"observable labels:\", result[\"observable_labels\"])\n", + "print(\"times:\", result[\"times\"])\n", + "print(\n", + " \"AQC fidelities:\",\n", + " {k: round(v, 4) for k, v in result[\"metadata\"][\"aqc_fidelities\"].items()},\n", + ")\n", + "print(\"resource usage:\", result[\"metadata\"][\"resource_usage\"])" + ] + }, + { + "cell_type": "markdown", + "id": "nextsteps", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "\n", + "\n", + "- Work through [Simulate neutron scattering with an AQC + Trotter dynamics Serverless workflow](/docs/tutorials/simulate-neutron-scattering-with-a-serverless-workflow), the companion example that calls this deployed function to compute the dynamical structure factor of KCuF$_3$.\n", + "- Read the [AQC Dynamics Function Template Github](https://github.com/qiskit-community/qiskit-function-templates/blob/main/physics/aqc_trotter/) for the complete input and output contract, further examples, and citation details.\n", + "- Browse the [Qiskit Function templates repository](https://github.com/qiskit-community/qiskit-function-templates/tree/main/physics/aqc_trotter) for other application templates built the same way.\n", + "- Read the [Qiskit Serverless guide](/docs/guides/serverless) for managing deployed functions.\n", + "- Go deeper on the AQC compression stage with the [Qiskit addon: AQC-Tensor](https://qiskit.github.io/qiskit-addon-aqc-tensor/) documentation.\n", + "\n", + "" + ] + } + ], + "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": 5 +} diff --git a/docs/tutorials/_toc.json b/docs/tutorials/_toc.json index 4e7370c233d..be43d1d77fc 100644 --- a/docs/tutorials/_toc.json +++ b/docs/tutorials/_toc.json @@ -61,6 +61,10 @@ "title": "Simulate neutron scattering in quantum materials with quantum circuits", "url": "/docs/tutorials/simulate-neutron-scattering" }, + { + "title": "Simulate neutron scattering with an AQC + Trotter dynamics Serverless workflow", + "url": "/docs/tutorials/simulate-neutron-scattering-with-a-serverless-workflow" + }, { "title": "Krylov quantum diagonalization of lattice Hamiltonians", "url": "/docs/tutorials/krylov-quantum-diagonalization" diff --git a/docs/tutorials/index.mdx b/docs/tutorials/index.mdx index b3792b5ddbf..b9e9b052187 100644 --- a/docs/tutorials/index.mdx +++ b/docs/tutorials/index.mdx @@ -51,6 +51,8 @@ These tutorials focus on estimating physically meaningful quantities, such as en * [Simulate neutron scattering in quantum materials with quantum circuits](/docs/tutorials/simulate-neutron-scattering) +* [Simulate neutron scattering with an AQC + Trotter dynamics Serverless workflow](/docs/tutorials/simulate-neutron-scattering-with-a-serverless-workflow) + * [Krylov quantum diagonalization of lattice Hamiltonians](/docs/tutorials/krylov-quantum-diagonalization) * [Nishimori phase transition](/docs/tutorials/nishimori-phase-transition) diff --git a/docs/tutorials/simulate-neutron-scattering-with-a-serverless-workflow.ipynb b/docs/tutorials/simulate-neutron-scattering-with-a-serverless-workflow.ipynb new file mode 100644 index 00000000000..8f637cef2bf --- /dev/null +++ b/docs/tutorials/simulate-neutron-scattering-with-a-serverless-workflow.ipynb @@ -0,0 +1,939 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "frontmatter", + "metadata": {}, + "source": [ + "---\n", + "title: Simulate neutron scattering with an AQC + Trotter dynamics Serverless workflow\n", + "description: Compute the dynamical structure factor S(q, w) of the quantum magnet KCuF3 by running a Trotter workflow with AQC compression as a deployed function template.\n", + "---\n", + "\n", + "{/* cspell:ignore Trotter Trotterization spinon spinons KCuF quimb DMRG magnon antiferromagnetic Suzuki fidelities isa COBYQA Gjjc qpoints viridis fontsize vmax vmin */}\n", + "\n", + "# Simulate neutron scattering with an AQC + Trotter dynamics Serverless workflow\n", + "*Usage estimate: 18 minutes on a Heron r3 processor (NOTE: This is an estimate only. Your runtime might vary.)*" + ] + }, + { + "cell_type": "markdown", + "id": "learning-outcomes", + "metadata": {}, + "source": [ + "## Learning outcomes\n", + "\n", + "After completing this tutorial, you can expect to understand:\n", + "\n", + "- How an inelastic neutron-scattering spectrum maps to the dynamical structure factor $S(q, \\omega)$ of a 1D quantum magnet.\n", + "- How to prepare the KCuF$_3$ (isotropic Heisenberg) ground state with the density matrix renormalization group (DMRG) and matrix product state (MPS) fidelity maximization.\n", + "- How to run Trotter time-evolution, approximate quantum compilation (AQC) circuit compression, and mitigated execution as a single function call.\n", + "- How to post-process the per-site $\\langle \\sigma_z \\rangle(t)$ time series into $S(q, \\omega)$ and identify the two-spinon continuum." + ] + }, + { + "cell_type": "markdown", + "id": "prerequisites", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "- Familiarity with [Qiskit Patterns](/docs/guides/intro-to-patterns), [`SparsePauliOp`](/docs/api/qiskit/qiskit.quantum_info.SparsePauliOp), and [Trotter time-evolution](/learning/courses/utility-scale-quantum-computing/quantum-simulation).\n", + "- Basic exposure to tensor-network methods (DMRG and MPS) is helpful but not required, as is familiarity with the [`qiskit-addon-aqc-tensor`](https://github.com/Qiskit/qiskit-addon-aqc-tensor) library that the function uses to compress Trotter circuits." + ] + }, + { + "cell_type": "markdown", + "id": "background", + "metadata": {}, + "source": [ + "## Background\n", + "\n", + "Inelastic neutron scattering measures the dynamical structure factor $S(q, \\omega)$, the space-and-time Fourier transform of the spin-spin correlation function, so reproducing $S(q, \\omega)$ from a microscopic spin model is a direct, falsifiable test of a quantum simulation. This tutorial studies KCuF$_3$, a spin-$\\frac{1}{2}$ antiferromagnetic Heisenberg chain whose excitations are not single spin flips but pairs of fractionalized spinons: instead of a sharp magnon dispersion, $S(q, \\omega)$ shows a broad *two-spinon continuum*, bounded below by $\\tfrac{\\pi}{2}|\\sin q|$ and above by $\\pi|\\sin(q/2)|$. Those are the dashed curves on the plots below. The physics in full, and the comparison against measured neutron data, are covered in the [original tutorial](/docs/tutorials/simulate-neutron-scattering) and in Lee et al., [arXiv:2603.15608](https://arxiv.org/abs/2603.15608).\n", + "\n", + "The quantum workflow mirrors the scattering experiment:\n", + "\n", + "1. Prepare the chain's ground state $|\\psi_0\\rangle$.\n", + "2. Kick it with a local perturbation at the center site, a $\\pi/2$ $Z$-rotation, mimicking the momentum and energy transfer from the neutron.\n", + "3. Time-evolve under the Heisenberg Hamiltonian, $e^{-iHt}$, with a Trotter product formula.\n", + "4. Measure the per-site magnetization $\\langle \\sigma_z^j \\rangle(t)$; as a function of site $j$ and time $t$ this *is* the retarded Green's function $G^R(j, j_c, t)$.\n", + "5. Fourier transform $G^R$ into $S(q, \\omega)$.\n", + "\n", + "The bottleneck is step 3: exact Trotter circuits for long evolutions become too deep for hardware. Approximate quantum compilation with tensor networks (AQC) addresses this by compressing a block of Trotter steps into a fixed, shallow parameterized ansatz whose state fidelity to the exact evolution is maximized classically with an MPS simulator ([arXiv:2301.08609](https://arxiv.org/abs/2301.08609)). The [AQC Dynamics Function](/docs/guides/function-template-aqc-trotter) packages this whole quantum core (Trotter synthesis, AQC compression, and mitigated execution) behind one call:\n", + "\n", + "```\n", + " PRE (this notebook) FUNCTION (aqc-dynamics-function) POST (this notebook)\n", + " ground state (DMRG + MPS -> Trotter -> AQC compress -> execute -> S(q, w): the dynamical\n", + " fidelity max) + neutron kick (statevector / fake / runtime) structure factor\n", + " -> (t) per site\n", + "```\n", + "\n", + "So the experiment-specific work stays here in the notebook: ground-state preparation (PRE) and the $S(q, \\omega)$ post-processing (POST). The two quantum-heavy steps, compression and execution, run inside the function.\n", + "\n", + "This tutorial is a companion to [Simulate neutron scattering in quantum materials with quantum circuits](/docs/tutorials/simulate-neutron-scattering), which builds the same experiment inline: the same KCuF$_3$ model, ground-state preparation, neutron kick, and post-processing, with the Trotter synthesis, AQC compression, and mitigated execution written out step by step. Read that tutorial to learn how AQC compression works. Read this one to run the same experiment through a deployed function template: the quantum core becomes a single function call, and the multi-hour AQC compression runs inside the Serverless worker instead of on your machine, so you do not need an HPC system or an open kernel while it runs. Because the function is Hamiltonian-agnostic, the same call also drives other dynamics experiments." + ] + }, + { + "cell_type": "markdown", + "id": "requirements", + "metadata": {}, + "source": [ + "## Requirements\n", + "\n", + "Before starting this tutorial, be sure you have the following:\n", + "\n", + "- The function deployed to your IBM Quantum® Serverless account. Run the companion function template first: [Deploy and run the AQC + Trotter dynamics function template](/docs/guides/function-template-aqc-trotter). That guide walks through getting the source files and uploading the function to your account. This tutorial only calls the deployed function.\n", + "\n", + "- IBM Quantum credentials saved for `QiskitServerless` (see the function template). Both examples below call the deployed function, so both need them.\n", + "\n", + "- Qiskit SDK v2.0 or later (`pip install qiskit`).\n", + "\n", + "- The Qiskit IBM Catalog client (`pip install qiskit-ibm-catalog`).\n", + "\n", + "- NumPy, SciPy, and Matplotlib (`pip install numpy scipy matplotlib`). SciPy 1.14 or later is needed for the COBYQA optimizer used in ground-state preparation.\n", + "\n", + "- The AQC tensor-network stack, because the ground-state preparation in Step 1 runs locally in this notebook: `pip install 'qiskit-addon-aqc-tensor[quimb-jax]==0.3.1'`\n", + "\n", + "\n", + "The first call to a newly deployed function waits while the Serverless worker installs its dependencies, so expect extra latency on that run." + ] + }, + { + "cell_type": "markdown", + "id": "setup-md", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "Import the libraries and define the experiment-specific helpers used below: `build_gs_ansatz` (the Hamiltonian variational ansatz, or HVA, for ground-state preparation), `prepare_ground_state` (DMRG plus MPS-fidelity maximization), and `get_spectrum`, `plot_green`, and `plot_spectrum` (the $S(q, \\omega)$ post-processing). These are adapted from the [original neutron-scattering tutorial](/docs/tutorials/simulate-neutron-scattering)." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "setup-imports", + "metadata": {}, + "outputs": [], + "source": [ + "from functools import partial\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import scipy.optimize\n", + "\n", + "import quimb.tensor as qtn\n", + "from qiskit import QuantumCircuit\n", + "from qiskit.quantum_info import SparsePauliOp\n", + "from qiskit_addon_aqc_tensor.simulation import tensornetwork_from_circuit\n", + "from qiskit_addon_aqc_tensor.simulation.quimb import QuimbSimulator\n", + "from qiskit_ibm_catalog import QiskitServerless" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "setup-helpers", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Setup complete - helpers defined.\n" + ] + } + ], + "source": [ + "# Dynamical structure factor via discrete Fourier transform\n", + "\n", + "\n", + "def get_spectrum(n, Gjjc, dt, time_steps, q_steps, w_steps):\n", + " \"\"\"Compute the dynamical structure factor from the retarded Green's function.\n", + "\n", + " Uses the center-site approximation and a discrete Fourier transform.\n", + " \"\"\"\n", + " green = Gjjc / 4 # sigma -> S=1/2\n", + " omega_max = np.pi / dt\n", + " qpoints = np.arange(0, 2 * np.pi, 2 * np.pi / q_steps)\n", + " omegas = np.arange(0, omega_max, omega_max / w_steps)\n", + " green_map = np.zeros((omegas.shape[0], qpoints.shape[0]))\n", + " center = n // 2 - 1\n", + " for iw, w in enumerate(omegas):\n", + " exponent = np.exp(1j * w * dt * np.arange(1, time_steps + 1))\n", + " S_w = np.dot(green.T, exponent) * dt\n", + " for iq, q in enumerate(qpoints):\n", + " q_matrix = np.exp(-1j * q * np.arange(-center, center + 2, 1))\n", + " green_map[iw, iq] = np.imag(np.dot(S_w, q_matrix))\n", + " return green_map\n", + "\n", + "\n", + "# Plotting helpers\n", + "\n", + "\n", + "def plot_spectrum(\n", + " dsf,\n", + " dt,\n", + " q_steps,\n", + " w_steps,\n", + " lower_bound=False,\n", + " upper_bound=False,\n", + " title=None,\n", + "):\n", + " \"\"\"Heat-map of the dynamical structure factor.\"\"\"\n", + " omega_max = np.pi / dt\n", + " qpoints = np.arange(0, 2 * np.pi, 2 * np.pi / q_steps)\n", + " omegas = np.arange(0, omega_max, omega_max / w_steps)\n", + " x, y = np.meshgrid(qpoints, omegas)\n", + " fig, ax = plt.subplots(figsize=(8, 5))\n", + " c = ax.pcolormesh(x, y, dsf / np.max(dsf), cmap=\"viridis\", shading=\"auto\")\n", + " fig.colorbar(c, ax=ax, label=\"Normalized intensity\")\n", + " if lower_bound:\n", + " ax.plot(\n", + " qpoints,\n", + " np.pi * np.abs(np.sin(qpoints)) / 2,\n", + " \"--\",\n", + " color=\"white\",\n", + " lw=1.5,\n", + " label=\"Lower bound\",\n", + " )\n", + " if upper_bound:\n", + " ax.plot(\n", + " qpoints,\n", + " np.pi * np.abs(np.sin(qpoints / 2)),\n", + " \"--\",\n", + " color=\"red\",\n", + " lw=1.5,\n", + " label=\"Upper bound\",\n", + " )\n", + " ax.set_ylim(0, 3.6)\n", + " ax.set_xlim(0, 2 * np.pi - 2 * np.pi / q_steps)\n", + " ax.set_xlabel(r\"$q$\", fontsize=16)\n", + " ax.set_ylabel(r\"$\\tilde{\\omega} = \\omega / J$\", fontsize=16)\n", + " ax.set_xticks([0, np.pi / 2, np.pi, 3 * np.pi / 2, 2 * np.pi])\n", + " ax.set_xticklabels([\"0\", r\"$\\pi/2$\", r\"$\\pi$\", r\"$3\\pi/2$\", r\"$2\\pi$\"])\n", + " if lower_bound or upper_bound:\n", + " ax.legend(loc=\"upper right\", fontsize=11)\n", + " if title:\n", + " ax.set_title(title, fontsize=14)\n", + " plt.tight_layout()\n", + " plt.show()\n", + "\n", + "\n", + "def plot_green(n, Gjjc, time_steps, dt, title=None):\n", + " \"\"\"Heat-map of the retarded Green's function in real space and time.\"\"\"\n", + " fig, ax = plt.subplots(figsize=(8, 6))\n", + " t_axis = np.arange(1, time_steps + 1) * dt\n", + " site_axis = np.arange(n)\n", + " x, y = np.meshgrid(t_axis, site_axis)\n", + " c = ax.pcolormesh(\n", + " x,\n", + " y,\n", + " np.real(Gjjc).T,\n", + " cmap=\"RdBu\",\n", + " vmax=0.5,\n", + " vmin=-0.5,\n", + " shading=\"auto\",\n", + " )\n", + " fig.colorbar(c, ax=ax, label=r\"Re $G^R(j, j_c, t)$\")\n", + " ax.set_xlabel(r\"Time ($t / J^{-1}$)\", fontsize=16)\n", + " ax.set_ylabel(\"Site index $j$\", fontsize=16)\n", + " if title:\n", + " ax.set_title(title, fontsize=14)\n", + " plt.tight_layout()\n", + " plt.show()\n", + "\n", + "\n", + "# Variational ground-state ansatz (HVA)\n", + "\n", + "\n", + "def _apply_xxz_pair_gate(qc, q0, q1, theta):\n", + " \"\"\"Apply the parameterized XXZ-type two-qubit gate used in the HVA.\"\"\"\n", + " qc.cx(q0, q1)\n", + " qc.rz(theta, q1)\n", + " qc.h(q0)\n", + " qc.rz(theta + np.pi / 2, q0)\n", + " qc.cx(q0, q1)\n", + " qc.rz(-theta, q1)\n", + " qc.h(q1)\n", + " qc.cx(q1, q0)\n", + " qc.rz(np.pi / 2, q1)\n", + " qc.rz(-np.pi / 2, q0)\n", + " qc.h(q1)\n", + " qc.h(q0)\n", + "\n", + "\n", + "def build_gs_ansatz(n, params, layers):\n", + " \"\"\"Build the Hamiltonian variational ansatz (HVA) circuit for\n", + " ground-state preparation of the 1D Heisenberg model.\n", + "\n", + " Starts from a product of singlet pairs and applies alternating\n", + " odd/even layers of parameterized XXZ gates. For layer r,\n", + " params[2 * r] is the odd-layer (inter-pair) angle and\n", + " params[2 * r + 1] is the even-layer (intra-pair) angle.\n", + " \"\"\"\n", + " qc = QuantumCircuit(n)\n", + " # Initial singlet product state\n", + " for i in range(n // 2):\n", + " qc.x(2 * i)\n", + " qc.x(2 * i + 1)\n", + " qc.h(2 * i + 1)\n", + " qc.cx(2 * i + 1, 2 * i)\n", + " # Variational layers\n", + " for r in range(layers):\n", + " for i in range(1, (n + 1) // 2): # odd layer\n", + " _apply_xxz_pair_gate(qc, 2 * i - 1, 2 * i, params[2 * r])\n", + " for i in range(n // 2): # even layer\n", + " _apply_xxz_pair_gate(qc, 2 * i, 2 * i + 1, params[2 * r + 1])\n", + " return qc\n", + "\n", + "\n", + "def prepare_ground_state(n, gs_layers=5, max_bond=128, cutoff=1e-8):\n", + " \"\"\"Prepare the KCuF3 (isotropic Heisenberg) ground state as a QuantumCircuit.\n", + "\n", + " Runs DMRG (quimb MPO + DMRG2) to get the chain's ground state, then optimizes\n", + " the HVA angles to maximize the MPS overlap ||^2. No exact\n", + " diagonalization, so it scales to larger n.\n", + " \"\"\"\n", + " J = Jz = 1.0\n", + " builder = qtn.SpinHam1D(S=1 / 2)\n", + " builder += J * 0.5, \"+\", \"-\"\n", + " builder += J * 0.5, \"-\", \"+\"\n", + " builder += Jz, \"Z\", \"Z\"\n", + " H_mpo = builder.build_mpo(L=n)\n", + " dmrg = qtn.DMRG2(H_mpo)\n", + " dmrg.solve(tol=1e-8, verbosity=0)\n", + "\n", + " gs_sim = QuimbSimulator(\n", + " quimb_circuit_factory=partial(\n", + " qtn.CircuitMPS, gate_opts=dict(cutoff=cutoff, max_bond=max_bond)\n", + " ),\n", + " autodiff_backend=\"jax\",\n", + " )\n", + "\n", + " def gs_infidelity(params):\n", + " psi = tensornetwork_from_circuit(\n", + " build_gs_ansatz(n, params, gs_layers), gs_sim\n", + " ).psi\n", + " return 1 - abs(psi.H @ dmrg.state) ** 2\n", + "\n", + " # Seed and optimizer match the original tutorial. Each layer starts at\n", + " # [0, pi/2]: an odd-layer angle of 0 makes the inter-pair gate the identity,\n", + " # and an even-layer angle of pi/2 makes the intra-pair gate a SWAP (since\n", + " # 0.5 * (XX + YY + ZZ) = SWAP - I/2). That puts the seed at the singlet-pair\n", + " # product limit, which is already a decent approximation to the Heisenberg\n", + " # ground state, so the optimizer only has to refine it. The small jitter\n", + " # (fixed RNG seed, so runs are reproducible) breaks the exact symmetry\n", + " # between layers; COBYQA then runs for up to 100 iterations.\n", + " rng = np.random.default_rng(12345)\n", + " x0 = np.tile([0.0, np.pi / 2], gs_layers) + rng.normal(\n", + " scale=0.1, size=2 * gs_layers\n", + " )\n", + " result_gs = scipy.optimize.minimize(\n", + " gs_infidelity, x0, method=\"COBYQA\", options={\"maxiter\": 100}\n", + " )\n", + " print(f\"DMRG ground-state energy: {dmrg.energy:.6f}\")\n", + " print(f\"GS fidelity: {1 - result_gs.fun:.4f}\")\n", + " return build_gs_ansatz(n, result_gs.x, gs_layers)\n", + "\n", + "\n", + "print(\"Setup complete - helpers defined.\")" + ] + }, + { + "cell_type": "markdown", + "id": "setup-load-md", + "metadata": {}, + "source": [ + "### Load the function template\n", + "\n", + "Connect to IBM Quantum Serverless and load the deployed `aqc-dynamics-function`. Both examples below call the same `fn` handle, so the function is loaded once, here." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "setup-connect", + "metadata": {}, + "outputs": [], + "source": [ + "# Credentials are read from the account saved once via QiskitServerless.save_account(...)\n", + "serverless = QiskitServerless()\n", + "fn = serverless.load(\"aqc-dynamics-function\")" + ] + }, + { + "cell_type": "markdown", + "id": "small-md", + "metadata": {}, + "source": [ + "## Small-scale simulator example\n", + "\n", + "We first run the full workflow on a small 10-site chain using the exact `statevector` backend. This validates the PRE → FUNCTION → POST pipeline before spending any QPU time." + ] + }, + { + "cell_type": "markdown", + "id": "small-s1-md", + "metadata": {}, + "source": [ + "### Step 1: Map classical inputs to a quantum problem\n", + "\n", + "Build the KCuF$_3$ Hamiltonian as a `SparsePauliOp` (isotropic Heisenberg: $XX + YY + ZZ$ at coupling $\\tfrac14$ on each nearest-neighbor bond; the strings are Pauli operators, so $\\tfrac14$ gives the spin-$\\frac{1}{2}$ coupling). Prepare the ground state with DMRG plus MPS-fidelity maximization, then bake in the neutron kick: a $\\pi/2$ $Z$-rotation at the center site. The prepared circuit is what we hand to the function as `initial_state`. We leave `observables` at its default (per-site $Z$), which is exactly the $\\langle \\sigma_z^j \\rangle(t)$ readout the neutron workflow needs." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "small-s1-code", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DMRG ground-state energy: -4.258035\n", + "GS fidelity: 0.9841\n", + "Prepared 10-qubit ground state with the neutron kick at site 4.\n" + ] + } + ], + "source": [ + "n = 10\n", + "dt = 0.6 # physical time per Trotter step (also the omega-axis unit in POST)\n", + "time_steps = 10\n", + "center = n // 2 - 1\n", + "\n", + "# MPS-simulator settings, shared by the ground-state prep here and the AQC\n", + "# compression inside the function (matches the original tutorial).\n", + "mps_max_bond = 32\n", + "mps_cutoff = 1e-8\n", + "\n", + "# 1D isotropic Heisenberg (KCuF3) Hamiltonian on n qubits\n", + "H = SparsePauliOp.from_sparse_list(\n", + " [(p, [i, i + 1], 0.25) for i in range(n - 1) for p in (\"XX\", \"YY\", \"ZZ\")],\n", + " num_qubits=n,\n", + ")\n", + "\n", + "# Ground state (DMRG + fidelity max) + neutron kick baked into the same circuit\n", + "gs_circuit = prepare_ground_state(\n", + " n, gs_layers=3, max_bond=mps_max_bond, cutoff=mps_cutoff\n", + ")\n", + "gs_circuit.rz(\n", + " np.pi / 2, center\n", + ") # exp(-i (pi/2)/2 Z_center): the neutron perturbation\n", + "print(\n", + " f\"Prepared {n}-qubit ground state with the neutron kick at site {center}.\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "small-s3-md", + "metadata": {}, + "source": [ + "### Steps 2 and 3: Compress and execute with the function template\n", + "\n", + "In a hand-written workflow these are two separate stages: optimize the circuits for hardware (Step 2) and execute them (Step 3). The function template collapses both into one call. It performs Trotter synthesis, AQC compression, and hardware transpilation, then runs the circuits (here on the exact simulator, later with built-in error mitigation on hardware). The two tuning parameters are `aqc_segments` (the compression plan) and `aqc_options` (the MPS and optimizer settings). Each segment `{\"n_steps\": k, \"ansatz_steps\": m}` compresses `k` consecutive Trotter steps into an ansatz built from an `m`-step Trotter target, and any steps beyond `sum(n_steps)` run as plain Trotter. Early, low-entanglement steps compress well into a shallow (`ansatz_steps=1`) ansatz, so here we compress the first 3 steps into a 1-layer ansatz and the next 2 into a deeper 2-layer ansatz; the remaining 5 of the 10 Trotter steps run as plain Trotter. For `aqc_options` we mirror the original tutorial: MPS bond dimension `max_bond=32`, `cutoff=1e-8`, and an L-BFGS-B optimizer capped at 100 iterations.\n", + "\n", + "Call the function loaded in Setup. `backend=\"statevector\"` runs the exact reference path: no QPU time, with the circuits running on an exact statevector simulator inside the serverless worker (a saved Serverless account is still needed to call it). The `initial_state` carries the prepared ground state (including the kick); `observables` is omitted so the function measures the default per-site $Z$." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "small-s3-run", + "metadata": {}, + "outputs": [], + "source": [ + "job = fn.run(\n", + " t_steps=time_steps,\n", + " aqc_segments=[\n", + " {\n", + " \"n_steps\": 3,\n", + " \"ansatz_steps\": 1,\n", + " }, # early steps -> shallow 1-layer ansatz\n", + " {\n", + " \"n_steps\": 2,\n", + " \"ansatz_steps\": 2,\n", + " }, # later steps -> deeper 2-layer ansatz\n", + " ],\n", + " aqc_options={\n", + " \"max_bond\": mps_max_bond, # MPS bond dimension for AQC compression\n", + " \"cutoff\": mps_cutoff,\n", + " \"optimizer_settings\": {\n", + " \"method\": \"L-BFGS-B\",\n", + " \"jac\": True,\n", + " \"options\": {\"maxiter\": 100},\n", + " },\n", + " },\n", + " dt=dt,\n", + " hamiltonian=H,\n", + " initial_state=gs_circuit, # prepared ground state including the neutron kick\n", + " # observables omitted -> default per-site Z (the neutron sigma_z readout)\n", + " backend=\"statevector\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "bf76d298", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DONE\n" + ] + } + ], + "source": [ + "print(job.status()) # rerun this cell until status says DONE" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "small-s3-result", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AQC fidelities: {'1': 1.0, '2': 0.9999, '3': 0.9992, '4': 0.9998, '5': 0.9995}\n", + "Green's function shape: (10, 10)\n" + ] + } + ], + "source": [ + "# The per-site (t) the function returns is the retarded Green's function\n", + "# G(j, j_c, t). The workflow samples t = 1..time_steps, so drop the t = 0 row (the\n", + "# prepared+kicked state before any evolution) before post-processing.\n", + "result = job.result()\n", + "print(\n", + " \"AQC fidelities:\",\n", + " {k: round(v, 4) for k, v in result[\"metadata\"][\"aqc_fidelities\"].items()},\n", + ")\n", + "\n", + "ev = np.array(result[\"expectation_values\"])\n", + "Gjjc = ev[1:] # shape (time_steps, n)\n", + "print(\"Green's function shape:\", Gjjc.shape)" + ] + }, + { + "cell_type": "markdown", + "id": "small-s4-md", + "metadata": {}, + "source": [ + "### Step 4: Post-process and return result in desired classical format\n", + "\n", + "Fourier-transform the Green's function into $S(q, \\omega)$, mirror-symmetrize, and clip negatives: the standard neutron post-processing. Mirroring is exact because $S(q, \\omega) = S(-q, \\omega)$ for this model, and the negative values that survive are artifacts of Fourier-transforming a finite, discretely sampled time series, so they are clipped to zero. On this small exact run the two-spinon continuum is only coarsely resolved, but the machinery is identical to the hardware run below." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "small-s4-code", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "q_res, w_res = 100, 100\n", + "spectrum = get_spectrum(n, Gjjc, dt, time_steps, q_res, w_res)\n", + "spectrum = -(spectrum + spectrum[:, ::-1]) / 2 # mirror symmetry\n", + "spectrum = np.clip(spectrum, a_min=0, a_max=None) # clip negatives\n", + "\n", + "plot_green(\n", + " n,\n", + " Gjjc,\n", + " time_steps,\n", + " dt,\n", + " title=f\"Retarded Green's function - {n} qubits (AQC, statevector)\",\n", + ")\n", + "plot_spectrum(\n", + " spectrum,\n", + " dt,\n", + " q_res,\n", + " w_res,\n", + " lower_bound=True,\n", + " upper_bound=True,\n", + " title=f\"Dynamical structure factor - {n} qubits (AQC, statevector)\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "large-md", + "metadata": {}, + "source": [ + "## Large-scale hardware example\n", + "\n", + "The same workflow scales up without changing any of the science code: a 30-site chain, twice the Trotter depth (20 steps), a compression plan that varies the ansatz depth (a deeper ansatz for the later, more-entangled steps), and execution on a real IBM Quantum processor with the function's built-in error mitigation (dynamical decoupling, Pauli twirling, and twirled readout error extinction, or TREX). We walk through the same four steps as the simulator example, reusing the `fn` handle from Setup.\n", + "\n", + "| | Small scale | Large scale |\n", + "| -------------------------------- | ------------- | ------------------------------------- |\n", + "| Qubits | 10 | 30 |\n", + "| Trotter steps | 10 | 20 |\n", + "| AQC segments (1-layer + 2-layer) | 3 + 2 = 5 | 6 + 4 = 10 |\n", + "| Ground-state ansatz layers | 3 | 5 |\n", + "| MPS max bond dimension | 32 | 128 |\n", + "| Backend | `statevector` | QPU with DD, Pauli twirling, and TREX |" + ] + }, + { + "cell_type": "markdown", + "id": "large-s1-md", + "metadata": {}, + "source": [ + "### Step 1: Map classical inputs to a quantum problem\n", + "\n", + "Build the same KCuF$_3$ Heisenberg `SparsePauliOp` and prepare the ground state, now with a deeper `gs_layers=5` ansatz for the longer chain, then bake in the $\\pi/2$ $Z$ neutron kick at the center site. This is identical to the small-scale mapping, just at $n = 30$.\n", + "\n", + "Expect a lower ground-state fidelity than the 10-site run: around 0.82 here against 0.98 above, because five HVA layers cannot fully capture a 30-site ground state. That is expected rather than a failure, and the original tutorial accepts roughly 0.65 at 50 sites for the same reason. Raising `gs_layers` or the COBYQA iteration cap improves it, at extra classical cost." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "large-code", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DMRG ground-state energy: -13.111355\n", + "GS fidelity: 0.8201\n", + "Prepared 30-qubit ground state with the neutron kick at site 14.\n" + ] + } + ], + "source": [ + "n = 30\n", + "dt = 0.6\n", + "time_steps = 20\n", + "center = n // 2 - 1\n", + "\n", + "# Same MPS settings as the original large-scale run: a larger bond for the\n", + "# longer, more-entangled chain (shared by GS prep and AQC compression).\n", + "mps_max_bond = 128\n", + "mps_cutoff = 1e-8\n", + "\n", + "# Same KCuF3 Hamiltonian and ground-state prep, on a larger chain\n", + "H = SparsePauliOp.from_sparse_list(\n", + " [(p, [i, i + 1], 0.25) for i in range(n - 1) for p in (\"XX\", \"YY\", \"ZZ\")],\n", + " num_qubits=n,\n", + ")\n", + "gs_circuit = prepare_ground_state(\n", + " n, gs_layers=5, max_bond=mps_max_bond, cutoff=mps_cutoff\n", + ")\n", + "gs_circuit.rz(np.pi / 2, center) # neutron kick at the center site\n", + "print(\n", + " f\"Prepared {n}-qubit ground state with the neutron kick at site {center}.\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "large-s3-md", + "metadata": {}, + "source": [ + "### Steps 2 and 3: Compress and execute with the function template\n", + "\n", + "The same single call as the simulator example, now with `backend_name` pointing at a real IBM Quantum processor, so the function transpiles and executes there. The compression plan varies the ansatz depth: the first 6 (low-entanglement) Trotter steps compress into a shallow 1-layer ansatz, the next 4 into a deeper 2-layer ansatz, and the remaining 10 of the 20 steps run as plain Trotter. `aqc_options` raises the MPS bond dimension to `max_bond=128` for the longer, more-entangled chain (matching the original), keeping the same L-BFGS-B optimizer capped at 100 iterations. The `estimator_options` turn on the built-in error mitigation: dynamical decoupling (XY4), gate twirling, and TREX measurement mitigation. The function's defaults already match the original tutorial for all of these except the TREX learning budget (`measure_noise_learning`), which is the only genuine difference. The whole block is still written out because a caller-supplied `estimator_options` replaces the function's defaults wholesale instead of merging into them, so omitting a key would fall back to the Qiskit Runtime default rather than the function's." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "large-s3-run", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "job id (save this to reconnect later): 43ed8d07-6d7d-4f33-b70a-7f31b765b310\n" + ] + } + ], + "source": [ + "# Steps 2 + 3: the function compresses (varied ansatz) and executes on hardware.\n", + "job = fn.run(\n", + " t_steps=time_steps,\n", + " aqc_segments=[\n", + " {\n", + " \"n_steps\": 6,\n", + " \"ansatz_steps\": 1,\n", + " }, # early steps -> shallow 1-layer ansatz\n", + " {\n", + " \"n_steps\": 4,\n", + " \"ansatz_steps\": 2,\n", + " }, # later steps -> deeper 2-layer ansatz\n", + " ],\n", + " aqc_options={\n", + " \"max_bond\": mps_max_bond, # 128 for the longer chain\n", + " \"cutoff\": mps_cutoff,\n", + " \"optimizer_settings\": {\n", + " \"method\": \"L-BFGS-B\",\n", + " \"jac\": True,\n", + " \"options\": {\"maxiter\": 100},\n", + " },\n", + " },\n", + " dt=dt,\n", + " hamiltonian=H,\n", + " initial_state=gs_circuit,\n", + " backend_name=\"ibm_pittsburgh\",\n", + " # Mitigation settings from the original tutorial. Only the two\n", + " # measure_noise_learning values differ from the function's defaults; the rest\n", + " # restates them, because a caller-supplied estimator_options dict replaces the\n", + " # function's defaults wholesale rather than merging into them.\n", + " estimator_options={\n", + " \"environment\": {\"job_tags\": [\"TUT-SNS\"]},\n", + " \"dynamical_decoupling\": {\"enable\": True, \"sequence_type\": \"XY4\"},\n", + " \"twirling\": {\n", + " \"enable_gates\": True,\n", + " \"num_randomizations\": 1000,\n", + " \"shots_per_randomization\": 128,\n", + " },\n", + " \"resilience\": {\n", + " \"measure_mitigation\": True,\n", + " \"measure_noise_learning\": {\n", + " \"num_randomizations\": 32,\n", + " \"shots_per_randomization\": 100,\n", + " },\n", + " },\n", + " },\n", + ")\n", + "print(\"job id (save this to reconnect later):\", job.job_id)" + ] + }, + { + "cell_type": "markdown", + "id": "large-reconnect-md", + "metadata": {}, + "source": [ + "\n", + "\n", + "The large-scale run is not quick, and most of the time is classical rather than on the QPU. The AQC compression runs inside the function before anything reaches the QPU: at 30 sites with `max_bond=128` that took close to four hours in our run, against the roughly 18 minutes of QPU time quoted in the *Usage estimate* above. Queue wait is on top of both. You do not need to keep this notebook or kernel open while it runs.\n", + "\n", + "Copy the job id printed above and save it. The next three cells let you pick the run back up later:\n", + "\n", + "1. Reconnect, only needed in a new kernel session: re-run the [Setup](#setup) cells to recreate `serverless`, then rebuild the `job` handle from the id you saved. Skip this cell if you are still in the session where you submitted, because the handle is already live.\n", + "2. Check status: re-run until it reports `DONE`.\n", + "3. Fetch the result: run only once the status is `DONE`.\n", + "\n", + "The reconnect cell below carries the job id from our own run. Paste yours over there:\n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "large-reconnect", + "metadata": {}, + "outputs": [], + "source": [ + "# Reconnect to a previously submitted job by its id. Only needed in a NEW kernel\n", + "# session; if you are still in the session where you submitted, the `job` handle\n", + "# above is already live, so skip this cell. Replace the id below with your own.\n", + "job = serverless.get_job_by_id(\"\")" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "large-status", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DONE\n" + ] + } + ], + "source": [ + "# Check where the job is. Re-run this until it reports DONE before fetching the\n", + "# result below: OPTIMIZING_FOR_HARDWARE -> WAITING_FOR_QPU -> EXECUTING_QPU ->\n", + "# POST_PROCESSING -> DONE.\n", + "print(job.status())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "large-s3-result", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AQC fidelities: {'1': 1.0, '2': 0.9994, '3': 0.9944, '4': 0.9853, '5': 0.9747, '6': 0.959, '7': 0.9495, '8': 0.9542, '9': 0.9533, '10': 0.9451}\n" + ] + } + ], + "source": [ + "# Run this only once the status cell above reports DONE. result() blocks until\n", + "# the job finishes, so calling it earlier just waits (possibly for hours).\n", + "result = job.result()\n", + "print(\n", + " \"AQC fidelities:\",\n", + " {k: round(v, 4) for k, v in result[\"metadata\"][\"aqc_fidelities\"].items()},\n", + ")\n", + "\n", + "ev = np.array(result[\"expectation_values\"])\n", + "Gjjc = ev[1:] # drop the t = 0 row -> shape (time_steps, n)" + ] + }, + { + "cell_type": "markdown", + "id": "large-s4-md", + "metadata": {}, + "source": [ + "### Step 4: Post-process and return result in desired classical format\n", + "\n", + "Identical post-processing to the simulator run: Fourier-transform the Green's function into $S(q, \\omega)$, mirror-symmetrize, and clip negatives. With the longer chain and evolution the two-spinon continuum is far better resolved. It should fill the band between the dashed bounds, brightest near $q = \\pi$." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "large-result", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "\"Output" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "n = job.result()[\"metadata\"][\"n\"]\n", + "q_res, w_res = 100, 100\n", + "spectrum = get_spectrum(n, Gjjc, dt, time_steps, q_res, w_res)\n", + "spectrum = -(spectrum + spectrum[:, ::-1]) / 2 # mirror symmetry\n", + "spectrum = np.clip(spectrum, a_min=0, a_max=None) # clip negatives\n", + "\n", + "plot_green(\n", + " n,\n", + " Gjjc,\n", + " time_steps,\n", + " dt,\n", + " title=f\"Retarded Green's function - {n} qubits (AQC, hardware)\",\n", + ")\n", + "plot_spectrum(\n", + " spectrum,\n", + " dt,\n", + " q_res,\n", + " w_res,\n", + " lower_bound=True,\n", + " upper_bound=True,\n", + " title=f\"Dynamical structure factor - {n} qubits (AQC, hardware)\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "appendix-md", + "metadata": {}, + "source": [ + "## Appendix: How the workflow scales\n", + "\n", + "The hardware example above runs a single chain length. The three spectra below come from earlier hardware runs of this same workflow on `ibm_pittsburgh` at 10, 20, and 30 sites, with every other input held fixed: 20 Trotter steps at `dt = 0.6`, the compression plan of 6 one-layer plus 4 two-layer segments, and `max_bond = 128`. These are recorded results, not output from the cells above.\n", + "\n", + "![Dynamical structure factor at 10 sites, a single sharp bright peak at q = pi near the lower bound](/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/appendix-dsf-10.avif \"10 qubits\")\n", + "\n", + "![Dynamical structure factor at 20 sites, spectral weight filling the band between the two dashed two-spinon bounds](/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/appendix-dsf-20.avif \"20 qubits\")\n", + "\n", + "![Dynamical structure factor at 30 sites, the continuum resolved more finely with fainter contrast and some weight outside the bounds](/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/appendix-dsf-30.avif \"30 qubits\")\n", + "\n", + "All three recover the two-spinon continuum, brightest at $q = \\pi$ and bounded by the dashed curves, so the physics holds at every size. What changes with chain length is a tradeoff rather than a straight improvement. Momentum resolution sharpens as $\\Delta q = 2\\pi / n$, so 30 sites map the shape of the continuum far more finely than 10 can. Signal quality moves the other way: longer chains mean deeper circuits, so noise accumulates, contrast fades, and spurious weight leaks outside the bounds.\n", + "\n", + "The two halves of the workflow scale differently in cost as well:\n", + "\n", + "| Qubits | Classical (build + AQC) | QPU usage |\n", + "|---|---|---|\n", + "| 10 | 4m 3s | 14m 21s |\n", + "| 20 | 24m 52s | 15m 58s |\n", + "| 30 | 230m 57s (about 3h 51m) | 17m 39s |\n", + "\n", + "Queue time is not counted in either column. The classical stage climbs steeply, roughly 6 times from 10 to 20 qubits and another 9 times to 30, dominated by the AQC fidelity optimization at `max_bond = 128`. QPU usage grows only about 1.2 times across the same range, because the circuit count and shot budget follow `t_steps` and the twirling settings rather than the qubit count." + ] + }, + { + "cell_type": "markdown", + "id": "nextsteps", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "\n", + "\n", + "- Adapt this workflow to your own system: the function is Hamiltonian-agnostic, so a different `SparsePauliOp`, initial state, or set of observables runs the same PRE → FUNCTION → POST pipeline. See the full input/output contract in the [AQC Dynamics Template](https://github.com/qiskit-community/qiskit-function-templates/tree/main/physics/aqc_trotter).\n", + "- Read the paper this benchmark comes from: Lee et al., [*Benchmarking quantum simulation with neutron-scattering experiments*](https://arxiv.org/abs/2603.15608) (arXiv:2603.15608).\n", + "- Compare with the [original \"Simulate neutron scattering\" tutorial](/docs/tutorials/simulate-neutron-scattering), the inline workflow this one ports onto a deployed function template.\n", + "- Go deeper on the [error mitigation and suppression techniques](/docs/guides/error-mitigation-and-suppression-techniques) applied on the hardware run: dynamical decoupling, Pauli twirling, and TREX.\n", + "\n", + "" + ] + } + ], + "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": 5 +} diff --git a/public/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/appendix-dsf-10.avif b/public/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/appendix-dsf-10.avif new file mode 100644 index 00000000000..66e03e792b3 Binary files /dev/null and b/public/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/appendix-dsf-10.avif differ diff --git a/public/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/appendix-dsf-20.avif b/public/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/appendix-dsf-20.avif new file mode 100644 index 00000000000..aa13514f37a Binary files /dev/null and b/public/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/appendix-dsf-20.avif differ diff --git a/public/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/appendix-dsf-30.avif b/public/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/appendix-dsf-30.avif new file mode 100644 index 00000000000..555f6fb2042 Binary files /dev/null and b/public/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/appendix-dsf-30.avif differ diff --git a/public/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/extracted-outputs/large-result-0.avif b/public/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/extracted-outputs/large-result-0.avif new file mode 100644 index 00000000000..ab48d3cd3b1 Binary files /dev/null and b/public/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/extracted-outputs/large-result-0.avif differ diff --git a/public/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/extracted-outputs/large-result-1.avif b/public/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/extracted-outputs/large-result-1.avif new file mode 100644 index 00000000000..600133f28ca Binary files /dev/null and b/public/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/extracted-outputs/large-result-1.avif differ diff --git a/public/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/extracted-outputs/small-s4-code-0.avif b/public/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/extracted-outputs/small-s4-code-0.avif new file mode 100644 index 00000000000..0221680ba53 Binary files /dev/null and b/public/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/extracted-outputs/small-s4-code-0.avif differ diff --git a/public/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/extracted-outputs/small-s4-code-1.avif b/public/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/extracted-outputs/small-s4-code-1.avif new file mode 100644 index 00000000000..56548bc6df1 Binary files /dev/null and b/public/docs/images/tutorials/simulate-neutron-scattering-with-a-serverless-workflow/extracted-outputs/small-s4-code-1.avif differ diff --git a/qiskit_bot.yaml b/qiskit_bot.yaml index 83c1ee2bc42..097ef95f7ff 100644 --- a/qiskit_bot.yaml +++ b/qiskit_bot.yaml @@ -413,6 +413,8 @@ notifications: - "@jenglick" - "@garrison" - "`@beckykd`" + "docs/guides/function-template-aqc-trotter": + - "@pdd23001" "docs/guides/operator-class": - "`@mtreinish`" "docs/guides/online-lab-environments": @@ -831,6 +833,8 @@ notifications: "docs/tutorials/simulate-neutron-scattering": - "`@nathanearnestnoble`" - "@kevinsung" + "docs/tutorials/simulate-neutron-scattering-with-a-serverless-workflow": + - "@pdd23001" "docs/tutorials/readout-error-mitigation-sampler": - "`@nathanearnestnoble`" - "`@jlapeyre`" diff --git a/scripts/config/notebook-testing.toml b/scripts/config/notebook-testing.toml index 7a278f3adac..f6e5b24e06d 100644 --- a/scripts/config/notebook-testing.toml +++ b/scripts/config/notebook-testing.toml @@ -145,6 +145,10 @@ notebooks = [ "docs/guides/function-template-hamiltonian-simulation.ipynb", "docs/guides/function-template-chemistry-workflow.ipynb", + # Deploys a Qiskit Function to Serverless and runs it, so it needs saved + # credentials and a deployed function. Not runnable in CI. + "docs/guides/function-template-aqc-trotter.ipynb", + # Only works in runtime 0.41.1 "docs/guides/monitor-job.ipynb", @@ -222,6 +226,7 @@ notebooks = [ "docs/tutorials/compilation-methods-for-hamiltonian-simulation-circuits.ipynb", "docs/tutorials/solve-market-split-problem-with-iskay-quantum-optimizer.ipynb", "docs/tutorials/simulate-neutron-scattering.ipynb", + "docs/tutorials/simulate-neutron-scattering-with-a-serverless-workflow.ipynb", # Don't test any learning notebooks "learning/courses/quantum-computing-in-practice/introduction.ipynb",