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..f65ec185620
--- /dev/null
+++ b/docs/guides/function-template-aqc-trotter.ipynb
@@ -0,0 +1,704 @@
+{
+ "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 Qiskit 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",
+ "| PRE (your setup) | FUNCTION (deployed here) | POST (your analysis) |\n",
+ "| --- | --- | --- |\n",
+ "| Prepare a state, as a circuit or a product state, with an optional local kick | Trotter synthesis → AQC compression → execution on `statevector`, `fake`, or `runtime`, returning $\\langle O \\rangle(t)$ | $S(q, \\omega)$ for neutron scattering, or magnetization, transport, quench dynamics, and so on |\n",
+ "\n",
+ "The template is published in the [Qiskit Function templates repository](https://github.com/qiskit-community/qiskit-function-templates/tree/main/physics/aqc_trotter), alongside the other application templates. This notebook deploys it to your own Qiskit 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 that follow. 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. With these credentials you can instantiate the serverless client locally 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",
+ "\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). The `[extras]` syntax is honored: `qiskit-addon-aqc-tensor[quimb-jax]` is what installs `quimb` and `jax`. `cotengrust` is needed for memory efficiency during tensor network simulation. `qiskit-aer` is listed separately for the `fake` backend (local noisy simulation).\n",
+ "\n",
+ ""
+ ]
+ },
+ {
+ "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": [
+ {
+ "data": {
+ "text/plain": [
+ "QiskitFunction(aqc-dynamics-function)"
+ ]
+ },
+ "execution_count": 119,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "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 that follow."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "inputs-md",
+ "metadata": {},
+ "source": [
+ "### Inputs\n",
+ "\n",
+ "Every run is a single `fn.run(...)` call. Only the first three inputs in the table 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 description | `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`. |\n",
+ "| `return_circuits` | `False` | Return the logical AQC + Trotter circuits in the result alongside the observable series. |"
+ ]
+ },
+ {
+ "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 | Serverless account and an instance with QPU access | `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 that follow 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\": [...], # for example: [\"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; for example, 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",
+ " # present only when return_circuits=True\n",
+ " \"circuits\": [QuantumCircuit, ...], # one per evolved step; circuits[i] is at times[i + 1]\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 eight-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 single-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 later in this guide:\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 preparation, Trotter build, AQC compression |\n",
+ "| `RUNNING: WAITING_FOR_QPU` | queued on the QPU (`runtime` backend only) |\n",
+ "| `RUNNING: EXECUTING_QPU` | circuits executing (local simulators 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": 113,
+ "id": "result-code",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "observables: ['Z_0', 'Z_1', 'Z_2', 'Z_3', 'Z_4', 'Z_5', 'Z_6', 'Z_7']\n",
+ "shape: (9, 8) -> (n_times, n_observables)\n",
+ "first row (t = 0, the prepared state): [1. 1. 1. 1. 1. 1. 1. 1.]\n",
+ "last row (t = t_steps * dt): [0.1442 0.2956 0.4686 0.4877 0.4869 0.4686 0.2963 0.1441]\n",
+ "AQC fidelities: {'1': 1.0, '2': 1.0, '3': 1.0, '4': 1.0, '5': 1.0, '6': 0.9999}\n",
+ "2q depth at the final step: 210 (full Trotter) -> 79 (AQC + Trotter)\n"
+ ]
+ }
+ ],
+ "source": [
+ "import numpy as np\n",
+ "\n",
+ "result = job.result()\n",
+ "ev = np.array(result[\"expectation_values\"])\n",
+ "\n",
+ "print(\"observables:\", result[\"observable_labels\"])\n",
+ "print(\"shape:\", ev.shape, \"-> (n_times, n_observables)\")\n",
+ "print(\"first row (t = 0, the prepared state):\", np.round(ev[0], 4))\n",
+ "print(\"last row (t = t_steps * dt):\", np.round(ev[-1], 4))\n",
+ "print(\n",
+ " \"AQC fidelities:\",\n",
+ " {k: round(v, 4) for k, v in result[\"metadata\"][\"aqc_fidelities\"].items()},\n",
+ ")\n",
+ "\n",
+ "# What the compression bought: 2-qubit depth at the final time step.\n",
+ "stats = result[\"metadata\"][\"circuit_stats\"][\n",
+ " str(result[\"metadata\"][\"t_steps\"])\n",
+ "]\n",
+ "print(\n",
+ " \"2q depth at the final step:\",\n",
+ " stats[\"full_trotter\"][\"depth_2q\"],\n",
+ " \"(full Trotter) ->\",\n",
+ " stats[\"aqc_trotter\"][\"depth_2q\"],\n",
+ " \"(AQC + Trotter)\",\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. What differs from the simulator example is the chain length, the number of Trotter steps, the compression plan, the backend, and the explicit mitigation settings covered in the following section.\n",
+ "\n",
+ "### Sizing the job for the control hardware\n",
+ "\n",
+ "`estimator_options` is the input worth setting deliberately. Gate twirling builds `num_randomizations` separate randomized circuits for every PUB, and the whole job, every PUB with all of its randomizations, has to fit in the instruction memory of the QPU's classical control system. The function defaults to 1000 randomizations, so a 10-step evolution submits 11 PUBs of 1000 circuits each: roughly 11,000 circuit instances in a single job.\n",
+ "\n",
+ "Exceed what the control system holds and the job fails with [error 6073](https://ibm.biz/error_codes#6073). [Job limits](/docs/guides/job-limits) gives the thresholds and how to count against them, the main one being 26.8 million control-system instructions per qubit, applied per job rather than per PUB. Dynamical decoupling adds gates that count toward it.\n",
+ "\n",
+ "Two inputs control the size:\n",
+ "\n",
+ "- `estimator_options` sets the shot budget. Total shots is `num_randomizations * shots_per_randomization`, so you can trade randomizations against shots per randomization, keep the statistics, and still shrink the program. The following cell uses 100 randomizations at 200 shots each, which is 20,000 shots per observable and about a tenth of the circuit instances the defaults would submit. See [TwirlingOptions](/docs/api/qiskit-ibm-runtime/options-twirling-options) and [Estimator options](/docs/guides/estimator-options) for the full set of fields.\n",
+ "- `batches` splits the PUBs across that many separate runtime jobs, which is the remedy error 6073 itself suggests and why the per-job framing matters. Setting `batches=4` sends roughly three PUBs per job instead of eleven at once, and the jobs go out together in one batch so the group queues once rather than each job queueing separately.\n",
+ "\n",
+ "Remember that a supplied `estimator_options` replaces the function's defaults wholesale rather than merging into them, so dynamical decoupling and TREX are restated in the following cell to keep them switched on."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "hw-code",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "job ID (save this to reconnect later): 7229a8bf-9f83-4785-8dd4-489844abc2d9\n"
+ ]
+ }
+ ],
+ "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",
+ " \"n_steps\": 3,\n",
+ " \"ansatz_steps\": 1,\n",
+ " }, # early steps -> shallow 1-layer ansatz\n",
+ " {\n",
+ " \"n_steps\": 3,\n",
+ " \"ansatz_steps\": 2,\n",
+ " }, # later steps -> deeper 2-layer ansatz\n",
+ " ],\n",
+ " hamiltonian=H,\n",
+ " aqc_options={\"max_bond\": 32},\n",
+ " backend=\"runtime\",\n",
+ " backend_name=\"ibm_marrakesh\",\n",
+ " # The function defaults to 1000 twirling randomizations, which was too large\n",
+ " # for this device. Total shots is num_randomizations *\n",
+ " # shots_per_randomization, so this is 20,000 shots per observable.\n",
+ " estimator_options={\n",
+ " \"dynamical_decoupling\": {\"enable\": True, \"sequence_type\": \"XY4\"},\n",
+ " \"twirling\": {\n",
+ " \"enable_gates\": True,\n",
+ " \"num_randomizations\": 100,\n",
+ " \"shots_per_randomization\": 200,\n",
+ " },\n",
+ " \"resilience\": {\"measure_mitigation\": True},\n",
+ " },\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 by the preceding cell 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 following reconnect cell.\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",
+ "# from the preceding cell is already live, so skip this cell. Replace the ID that follows with your own.\n",
+ "job = serverless.get_job_by_id(\"\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 116,
+ "id": "hw-status",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "DONE\n"
+ ]
+ }
+ ],
+ "source": [
+ "# Re-run this until it reports DONE, then fetch the result in the following cell.\n",
+ "print(job.status())"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 118,
+ "id": "hw-result-code",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "backend: runtime\n",
+ "shape: (11, 10) -> (n_times, n_observables)\n",
+ "last row (t = t_steps * dt): [0.1504 0.1361 0.218 0.2144 0.2275 0.1783 0.1749 0.1599 0.0915 0.0922]\n",
+ "AQC fidelities: {'1': 1.0, '2': 1.0, '3': 1.0, '4': 1.0, '5': 0.9999, '6': 0.9999}\n",
+ "2q depth at the final step: 342 (full Trotter) -> 171 (AQC + Trotter)\n"
+ ]
+ }
+ ],
+ "source": [
+ "import numpy as np\n",
+ "\n",
+ "# Run this only once the preceding status cell reports DONE. result() blocks until\n",
+ "# the job finishes, so calling it earlier just waits.\n",
+ "result = job.result()\n",
+ "ev = np.array(result[\"expectation_values\"])\n",
+ "\n",
+ "print(\"backend:\", result[\"metadata\"][\"execution_backend\"])\n",
+ "print(\"shape:\", ev.shape, \"-> (n_times, n_observables)\")\n",
+ "print(\"last row (t = t_steps * dt):\", np.round(ev[-1], 4))\n",
+ "print(\n",
+ " \"AQC fidelities:\",\n",
+ " {k: round(v, 4) for k, v in result[\"metadata\"][\"aqc_fidelities\"].items()},\n",
+ ")\n",
+ "\n",
+ "# What the compression bought: 2-qubit depth at the final time step.\n",
+ "stats = result[\"metadata\"][\"circuit_stats\"][\n",
+ " str(result[\"metadata\"][\"t_steps\"])\n",
+ "]\n",
+ "print(\n",
+ " \"2q depth at the final step:\",\n",
+ " stats[\"full_trotter\"][\"depth_2q\"],\n",
+ " \"(full Trotter) ->\",\n",
+ " stats[\"aqc_trotter\"][\"depth_2q\"],\n",
+ " \"(AQC + Trotter)\",\n",
+ ")"
+ ]
+ },
+ {
+ "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 Template on 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/guides/qiskit-function-templates.mdx b/docs/guides/qiskit-function-templates.mdx
index 6084c510cc9..8a9d30744b2 100644
--- a/docs/guides/qiskit-function-templates.mdx
+++ b/docs/guides/qiskit-function-templates.mdx
@@ -15,11 +15,12 @@ There are two types of templates:
## Template implementations
-Qiskit Function template implementations are organized by application area. Currently included in the collection is a physics template for Hamiltonian simulation using the [AQC-Tensor Qiskit addon](https://qiskit.github.io/qiskit-addon-aqc-tensor/) and a chemistry template for electronic structure with the implicit solvent model using the [SQD Qiskit addon](/docs/addons/qiskit-addon-sqd).
-Resources to get started with these two templates are available at the following links:
+Qiskit Function template implementations are organized by application area. Currently included in the collection are two physics templates that use the [AQC-Tensor Qiskit addon](https://qiskit.github.io/qiskit-addon-aqc-tensor/) (one for Hamiltonian simulation and one for AQC + Trotter Hamiltonian dynamics) and a chemistry template for electronic structure with the implicit solvent model using the [SQD Qiskit addon](/docs/addons/qiskit-addon-sqd).
+Resources to get started with these templates are available at the following links:
- Electronic structure simulation with implicit solvent model: [template source files](https://github.com/qiskit-community/qiskit-function-templates/tree/main/chemistry/sqd_pcm) and [guide](/docs/guides/function-template-chemistry-workflow)
- Hamiltonian simulation: [template source files](https://github.com/qiskit-community/qiskit-function-templates/tree/main/physics/hamiltonian_simulation) and [guide](/docs/guides/function-template-hamiltonian-simulation)
+- AQC + Trotter Hamiltonian dynamics: [template source files](https://github.com/qiskit-community/qiskit-function-templates/tree/main/physics/aqc_trotter) and [guide](/docs/guides/function-template-aqc-trotter)
## Base templates
@@ -34,6 +35,7 @@ There are currently two templates: a [circuit function template](https://github.
- Review the guide on building a function template for [Hamiltonian simulation](/docs/guides/function-template-hamiltonian-simulation)
- Read through the guide on deploying the function template for a [chemistry workflow](/docs/guides/function-template-chemistry-workflow)
+- Deploy and run the function template for [AQC + Trotter Hamiltonian dynamics](/docs/guides/function-template-aqc-trotter)
- Check out the [Qiskit Function templates repository](https://github.com/qiskit-community/qiskit-function-templates) on GitHub.
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..c75d5621cbd
--- /dev/null
+++ b/docs/tutorials/simulate-neutron-scattering-with-a-serverless-workflow.ipynb
@@ -0,0 +1,929 @@
+{
+ "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 the following:\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 that follow. 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 exactly the retarded Green's function $G^R(j, j_c, t)$, so no conversion is needed before the Fourier transform in step 5.\n",
+ "5. Fourier transform $G^R$ into $S(q, \\omega)$.\n",
+ "\n",
+ "Problems can arise in step 3, when exact Trotter circuits for long evolutions become too deep for hardware. AQC with tensor networks 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 Template](/docs/guides/function-template-aqc-trotter) packages this whole quantum core (Trotter synthesis, AQC compression, and mitigated execution) behind one call:\n",
+ "\n",
+ "| PRE (this notebook) | FUNCTION (`aqc-dynamics-function`) | POST (this notebook) |\n",
+ "| --- | --- | --- |\n",
+ "| Ground state from DMRG plus MPS-fidelity maximization, with the neutron kick baked into the same circuit | Trotter synthesis → AQC compression → execution on `statevector`, `fake`, or `runtime`, returning $\\langle \\sigma_z^j \\rangle(t)$ per site | $S(q, \\omega)$, the dynamical structure factor |\n",
+ "\n",
+ "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. The same call also drives other 1D 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 Qiskit 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 in this tutorial 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 later: `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 Qiskit Serverless and load the deployed `aqc-dynamics-function`. Both examples in this tutorial 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 by 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 three steps into a single-layer ansatz and the next two into a deeper two-layer ansatz; the remaining five 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 Qiskit 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": "small-status",
+ "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 that follows."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "id": "small-s4-code",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "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 an IBM Quantum processor with the function's built-in error mitigation (dynamical decoupling, Pauli twirling, and twirled readout error extinction (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-compressed steps (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, but at $n = 30$.\n",
+ "\n",
+ "Expect a lower ground-state fidelity than the 10-site run: around 0.82 here against 0.98 for the smaller chain, 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 an IBM Quantum processor, so the function transpiles and executes there. The compression plan varies the ansatz depth: the first six (low-entanglement) Trotter steps compress into a shallow single-layer ansatz, the next four into a deeper two-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`). 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 IBM Quantum Compute 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* at the top of this tutorial. 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 by the preceding cell 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 following reconnect cell holds a placeholder. Replace it with your own `job_id`:\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",
+ "# from the preceding cell is already live, so skip this cell. Replace the ID that follows 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 in the following cell: QUEUED -> INITIALIZING -> RUNNING: OPTIMIZING_FOR_HARDWARE ->\n",
+ "# RUNNING: WAITING_FOR_QPU -> RUNNING: EXECUTING_QPU -> RUNNING: POST_PROCESSING\n",
+ "# -> 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 preceding status cell 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": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "n = 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\n",
+ "\n",
+ "The preceding hardware example runs a single chain length. The three spectra that follow 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 six one-layer plus four two-layer AQC-compressed steps, and `max_bond = 128`. These are recorded results, not output from the preceding cells.\n",
+ "\n",
+ "The same settings are used at all three sizes, so the spectra are directly comparable. Tuning them per chain length, with more ground-state ansatz layers or a larger `max_bond`, for example, can give better results than any shown here.\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ "\n",
+ ""
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "nextsteps",
+ "metadata": {},
+ "source": [
+ "## Next steps\n",
+ "\n",
+ "\n",
+ "\n",
+ "- Adapt this workflow to your own system: the function accepts any 1D nearest-neighbor `SparsePauliOp`, so a different chain Hamiltonian, initial state, or set of observables runs the same PRE → FUNCTION → POST pipeline. See the full input/output contract in the [AQC Dynamics Template on GitHub](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": {
+ "hours": 4,
+ "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"
+ },
+ "qpuSeconds": 1080
+ },
+ "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",