diff --git a/docs/api/functions/quantum-elements-orbit.mdx b/docs/api/functions/quantum-elements-orbit.mdx index 62c89c8f07dd..b8447333b212 100644 --- a/docs/api/functions/quantum-elements-orbit.mdx +++ b/docs/api/functions/quantum-elements-orbit.mdx @@ -15,7 +15,7 @@ description: "API reference for Quantum Elements Orbit, including inputs, output /> -Quantum Elements Orbit is a Qiskit Function that prepares quantum circuits for a selected IBM Quantum® backend, inserts dynamical decoupling (DD) into scheduled idle windows, and runs the resulting workload through an IBM Quantum primitive. Orbit accepts Sampler and Estimator PUBs and returns a standard [`PrimitiveResult`](/docs/api/qiskit/qiskit.primitives.PrimitiveResult) with Orbit-specific metadata attached to the top-level result and to each [Primitive Unified Bloc (PUB)](/docs/guides/primitive-input-output) result. +Quantum Elements Orbit is a Qiskit Function that prepares quantum circuits for a selected IBM Quantum® backend, inserts dynamical decoupling (DD) into scheduled idle windows, and runs the resulting workload through an IBM Quantum primitive. Workloads can run as one Quantum Compute job or be partitioned across child jobs in a server-side `qiskit-ibm-runtime` batch. Orbit accepts Sampler and Estimator PUBs and returns a standard [`PrimitiveResult`](/docs/api/qiskit/qiskit.primitives.PrimitiveResult) with Orbit-specific metadata attached to the top-level result and to each [Primitive Unified Bloc (PUB)](/docs/guides/primitive-input-output) result. If `backend_name` is omitted, Orbit selects an eligible least-busy IBM Quantum backend available to IBM Quantum Compute Service. If `options` is omitted or `None`, Orbit uses its built-in defaults: transpile and schedule circuits, insert the default DD strategy, submit to the service, and attach DD insertion metadata to the result. @@ -98,7 +98,7 @@ Function-specific options controlling Orbit execution behavior. - Default value: `None` - Valid input types: `dict` or `None` -Options control transpilation, DD insertion, Quantum Compute options, preview mode, simulator mode, backend-information export, and measurement error mitigation. +Options control transpilation, DD insertion, Quantum Compute options, `qiskit-ibm-runtime` batch execution, preview mode, simulator mode, backend-information export, and measurement error mitigation. - Unknown option keys are rejected. - Pass `None`, `{}`, or omit `options` to use all defaults. - Example: @@ -338,6 +338,7 @@ Whether Orbit saves backend calibration properties after an executed Quantum Com - Choices: `True` / `False` - When enabled, Orbit queries `service.job(job_id).properties()`, writes the serialized backend properties under `/data`, and reports the saved path in Orbit metadata. +- In batch mode, Orbit attempts one snapshot for every child job and reports them in `backendInfo.snapshots`. - Failures are reported as warnings and do not discard otherwise successful primitive results. @@ -484,7 +485,7 @@ options = { -Soft cap on the Quantum Compute job's maximum execution time. +Soft cap on each Quantum Compute primitive job's maximum execution time. - Required: No - Default value: `None` - Valid input types: `int` or `None` @@ -492,8 +493,72 @@ Soft cap on the Quantum Compute job's maximum execution time. The value is specified in seconds. - Choices: `None` or integer > 0 - When `None`, the Runtime default is used. +- In batch mode, the cap is applied to every child primitive job. +##### `batch` + + + +Server-side [`qiskit-ibm-runtime` batch](/docs/guides/run-jobs-batch) configuration. +- Required: No +- Default value: `None` +- Valid input types: `dict` or `None` + +- When omitted or `None`, Orbit submits all prepared PUBs as one Quantum Compute primitive job. +- When supplied, Orbit partitions the prepared PUBs in input order, creates the authenticated batch inside the remote Function, submits all child jobs, closes the batch, and then collects and merges the child results in the original PUB order. +- Batch mode is intended for independent workloads whose PUBs are known up front, such as parameter sweeps or curve generation. Use [session mode](/docs/guides/run-jobs-session) instead when later inputs depend on earlier results. +- Batch mode requires execution on a real IBM Quantum backend and is incompatible with `preview=True` or `simulator=True`. +- The configuration must be JSON serializable. Do not create a local `qiskit_ibm_runtime.Batch` object and pass it to `orbit.run(...)`; a caller-created batch cannot control the asynchronous Qiskit Function execution environment. +- If the PUB count is less than or equal to `max_pubs_per_job`, Orbit creates one child job and the batch provides no partition parallelism. + + + + + `max_pubs_per_job` + + + Maximum number of consecutive PUBs submitted in each child job. + - Required: Yes when `batch` is supplied + - Valid input types: `int` + - Choices: Integer > 0 + + For `N` input PUBs, Orbit creates `ceil(N / max_pubs_per_job)` child jobs. The last child job can contain fewer PUBs. + + + `max_time` + + + Optional maximum lifetime of the `qiskit-ibm-runtime` batch. + - Required: No + - Default value: `None` + - Valid input types: `int`, `str`, or `None` + + - A positive integer specifies seconds, such as `7200`. + - A duration string contains one or more positive number-and-unit pairs using `s`, `m`, `h`, or `d`, such as `"2h"` or `"1h30m"`. + - A numeric string such as `"7200"` is invalid; use the integer `7200` instead. + + + +For example, the following call partitions `ordered_pubs` into child jobs of at most 300 PUBs, all within one batch: + +```python +job = orbit.run( + primitive="sampler", + pubs=ordered_pubs, + backend_name="ibm_brisbane", + options={ + "batch": { + "max_pubs_per_job": 300, + "max_time": "2h", + } + }, +) + +result = job.result() +batch_report = result.metadata["quantum_elements_orbit"]["batch"] +``` + ##### `simulator` @@ -540,7 +605,7 @@ Duration of one DD gate or pulse. ## Outputs -The function returns a Qiskit `PrimitiveResult` containing one `PubResult` per input PUB. Orbit preserves the selected primitive's normal result data and adds Orbit metadata under `quantum_elements_orbit`. +The function returns a Qiskit `PrimitiveResult` containing one `PubResult` per input PUB. Orbit preserves the selected primitive's normal result data and adds Orbit metadata under `quantum_elements_orbit`. In batch mode, child results are merged so the returned PUB count and order match the original input PUBs, regardless of the child-job boundaries. Standard `PrimitiveResult` with Orbit metadata attached. @@ -578,6 +643,16 @@ Standard `PrimitiveResult` with Orbit metadata attached. Simulator noise mode: `"backend"` or `"ideal"`. + `executionMode` + + Present with value `"batch"` when Orbit used server-side `qiskit-ibm-runtime` batch execution. + + + `batch` + + Present in batch mode. Contains the Runtime batch ID in `id`, ordered child job IDs in `jobIds`, and ordered partition records in `partitions`. Each partition record contains `index`, `pubStart`, `pubCount`, and `jobId`. + + `primitive` Selected primitive: `"sampler"` or `"estimator"`. @@ -625,7 +700,7 @@ Standard `PrimitiveResult` with Orbit metadata attached. `backendInfo` - Backend calibration export status. Includes `enabled`, `saved`, and, when available, backend name, job ID, saved path, and warnings. + Backend calibration export status. Includes `enabled`, `saved`, and, when available, backend name, job ID, saved path, and warnings. In batch mode, `snapshots` contains one child-job export report per partition. `warnings` @@ -647,6 +722,32 @@ Standard `PrimitiveResult` with Orbit metadata attached. +### Batch output + +For a batch run, `result.metadata["quantum_elements_orbit"]["batch"]` maps each returned PUB range to the child job that executed it: + +```python +result = job.result() +orbit_report = result.metadata["quantum_elements_orbit"] +batch_report = orbit_report["batch"] + +print(orbit_report["executionMode"]) # "batch" +print(batch_report["id"]) +print(batch_report["jobIds"]) + +for partition in batch_report["partitions"]: + start = partition["pubStart"] + stop = start + partition["pubCount"] + child_results = list(result)[start:stop] + print(partition["index"], partition["jobId"], start, stop) +``` + +`job.runtime_jobs()` exposes the child job IDs through the Qiskit Function job, and `job.runtime_sessions()` exposes the batch ID. The batch metadata travels with the `PrimitiveResult`, so use it when a later analysis must map PUBs to child jobs without retaining the original Function job object. + +When `save_backend_info=True`, Orbit attempts one backend-calibration snapshot per child job and reports the collection in top-level `backendInfo.snapshots`. If MEM is enabled for Sampler PUBs, Orbit uses each child job's calibration snapshot for the PUBs in that partition before re-merging the results. + +The `RUNNING: EXECUTING_QPU` resource-usage entry reports the sum of the available child-job QPU times. Child jobs without provider QPU-time data can make that measurement partial. + ### Per-PUB Orbit metadata Each `PubResult.metadata["quantum_elements_orbit"]` contains the insertion report for that PUB. @@ -764,19 +865,21 @@ If mitigation fails, Orbit preserves the raw result and records the failure stat ## Error handling -Orbit raises structured `qiskit_serverless.ServerlessError` errors for fatal failures. Each error includes a `code`, `message`, and `details` payload. Orbit maps errors to existing IBM Quantum error-code categories when possible; validation errors use code `1221`. Orbit-specific errors use the QE reserved code range `4700` through `4709` when no existing IBM Quantum code is a better match. See the [IBM Quantum error code reference](/docs/errors) for general error-code guidance. +Orbit raises structured `qiskit_serverless.ServerlessError` errors for fatal failures. Each error includes a `code`, `message`, and `details` payload. Orbit maps errors to existing IBM Quantum error-code categories when possible; validation errors use code `1221`. Orbit-specific errors use the QE reserved code range `4700` through `4709` only when no existing IBM Quantum code is a better match. See the [IBM Quantum error code reference](/docs/errors) for general error-code guidance. Check the error `message` and `details` fields first. They identify the invalid field, backend, PUB index, or upstream Quantum Compute failure when Orbit can determine it. - - Input validation errors use code `1221`. These include invalid option types, unknown option keys, empty `pubs`, invalid `dd_strategy`, invalid `pub_options` length, `dd_qubits` with resolved `transpilation_mode` other than `"validate"`, caller-prepared circuits that are not compatible with the selected backend target, invalid `physical_layout` values, and incompatible MEM requests such as `mem=True` with `primitive="estimator"`, `preview=True`, or `simulator=True`. + - Input validation errors use code `1221`. These include invalid option types, unknown option keys, empty `pubs`, invalid `dd_strategy`, invalid `pub_options` length, `dd_qubits` with resolved `transpilation_mode` other than `"validate"`, caller-prepared circuits that are not compatible with the selected backend target, invalid `physical_layout` values, a missing or non-positive `batch.max_pubs_per_job`, an invalid `batch.max_time`, batch mode combined with preview or simulator mode, and incompatible MEM requests such as `mem=True` with `primitive="estimator"`, `preview=True`, or `simulator=True`. - Unsupported primitive errors use code `1211`. Orbit accepts only `primitive="sampler"` and `primitive="estimator"`. - Backend selection or backend capability errors use code `1007` or `1009`. These include unavailable backend names, no eligible least-busy backend, or a backend without the timing information required for DD insertion. - - DD insertion and QASM round-trip failures use code `1003`. These can occur when a circuit cannot be transpiled, scheduled, converted, or padded consistently for the selected backend and DD strategy. - - Quantum Compute submission failures use code `1245`; jobs that fail before producing a result use code `5203`. Orbit preserves an upstream Quantum Compute error code when one is exposed, with the Orbit fallback code in `details`. - - Unexpected Orbit-specific failures are reported as structured errors in the QE reserved range (`4700--4709`) when no existing IBM Quantum error code applies. + - DD insertion, post-DD basis translation, transpilation, and QASM round-trip failures use code `1003`. These can occur when a circuit cannot be transpiled, scheduled, converted, or padded consistently for the selected backend and DD strategy. + - Quantum Compute submission failures use code `1245`; jobs that fail before producing a result use code `5203`. Orbit preserves an upstream Quantum Compute error code when one is exposed, with the Orbit fallback code in `details`. For batch failures, `details` can also include `batch_id`, `failed_partition`, `failed_job_id`, `completed_job_ids`, and `remaining_job_ids`. + - Orbit pre-submit preparation failures that are not input, backend, or DD failures use QE code `4701`. The `details.stage` field identifies the boundary, such as `runtime_service` or `pre_submit_preparation`. + - Orbit result-shaping failures use QE code `4702`. These occur when Quantum Compute returns a result with the wrong PUB shape or Orbit cannot attach the required metadata; `details.stage` identifies `result_iteration`, `result_shape`, `pub_metadata`, `result_metadata`, or `batch_result_merge`. + - QE codes `4703` through `4709` are reserved for future Orbit-specific categories and are not currently assigned. Non-fatal conditions are reported as warnings instead of failing the job when Orbit can safely preserve the result. Run-level warnings appear in `metadata["quantum_elements_orbit"]["warnings"]`; PUB-level warnings appear in each PUB report. Recoverable warning events use code `1300` when the Qiskit Functions environment accepts warning events. Examples include `preview=True` taking precedence over `simulator=True`, `qiskit-ibm-runtime` DD being enabled alongside Orbit DD, Sampler ignoring `runtime_options.resilience_level`, or backend calibration export failing while the primitive result is otherwise available. -Measurement error mitigation failures are also non-fatal. If M3 mitigation cannot be applied, Orbit preserves the raw Sampler result and records `measurementErrorMitigation.status="failed"` with an error message in Orbit metadata. \ No newline at end of file +Measurement error mitigation failures are also non-fatal. If M3 mitigation cannot be applied, Orbit preserves the raw Sampler result and records `measurementErrorMitigation.status="failed"` with an error message in Orbit metadata. diff --git a/docs/guides/quantum-elements-orbit.ipynb b/docs/guides/quantum-elements-orbit.ipynb index 87c3f5747b05..a7ffe9058642 100644 --- a/docs/guides/quantum-elements-orbit.ipynb +++ b/docs/guides/quantum-elements-orbit.ipynb @@ -146,17 +146,18 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "id": "6a4e3d93", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[QiskitFunction(quantum-elements/orbit)]" + "[QiskitFunction(quantum-elements/orbit-dev),\n", + " QiskitFunction(quantum-elements/orbit)]" ] }, - "execution_count": 1, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } @@ -172,13 +173,22 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "15114161", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Orbit version: 0.3.23\n" + ] + } + ], "source": [ "# Load the function.\n", - "orbit = catalog.load(\"quantum-elements/orbit\")" + "orbit = catalog.load(\"quantum-elements/orbit\")\n", + "print(\"Orbit version:\", orbit.version)" ] }, { @@ -193,6 +203,22 @@ "Use this pattern when you want a direct raw-versus-Orbit comparison." ] }, + { + "cell_type": "markdown", + "id": "3f62f8d4", + "metadata": {}, + "source": [ + "### Scaling Bernstein-Vazirani to larger sizes\n", + "\n", + "Orbit can help preserve Bernstein-Vazirani success probability as the hidden-bitstring width grows. The representative scaling runs below compare raw execution with Orbit configurations on IBM Quantum hardware. On `ibm_miami`, raw success probability falls below 10% by a hidden-bitstring width of 9 (10 total qubits), while Orbit remains near 80–90%. On `ibm_kingston`, Orbit maintains non-trivial success probability at widths approaching 60, including approximately 15% at the largest size shown. Exact results depend on the backend calibration, circuit, and execution settings.\n", + "\n", + "![Bernstein-Vazirani success probability on ibm_miami: Orbit with and without measurement error mitigation maintains higher success probability than raw execution as hidden-bitstring width increases.](/docs/images/guides/quantum-elements-orbit/bv-fidelity-raw-orbit-mem.avif)\n", + "\n", + "![Bernstein-Vazirani success probability on ibm_kingston: box plots compare raw execution with Orbit across increasing hidden-bitstring widths, with Orbit results shown through width 60.](/docs/images/guides/quantum-elements-orbit/bv-hidden-bitstring-success-boxplot.avif)\n", + "\n", + "To preserve QPU time, this tutorial does not rerun a size sweep. Instead, the executable example below reports shot counts for one fixed hidden-bitstring width, `N = 50` (51 total qubits in this circuit), comparing raw execution with Orbit defaults in a single job." + ] + }, { "cell_type": "markdown", "id": "8708dcb1", @@ -205,7 +231,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "id": "3366889b", "metadata": {}, "outputs": [ @@ -219,7 +245,7 @@ " ('barrier', 1)])" ] }, - "execution_count": 3, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -262,7 +288,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "id": "2c6a4b83", "metadata": {}, "outputs": [], @@ -284,7 +310,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "id": "c1a340ad", "metadata": {}, "outputs": [ @@ -292,7 +318,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "c0f1b99c-8dfa-4ebb-9560-d9a37c990acc\n", + "2c427a80-4190-404c-b98a-6bbeb53fafcd\n", "QUEUED\n" ] } @@ -328,7 +354,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "id": "dbab207b", "metadata": {}, "outputs": [ @@ -336,7 +362,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "{'hidden_bitstring': '11111111111111111111111111111111111111111111111111', 'correct_counts': {'raw': 0, 'orbit': 184}, 'shots': 4096}\n" + "{'hidden_bitstring': '11111111111111111111111111111111111111111111111111', 'correct_counts': {'raw': 0, 'orbit': 370}, 'shots': 4096}\n" ] } ], @@ -395,7 +421,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "7656f768", "metadata": {}, "outputs": [ diff --git a/docs/tutorials/index.mdx b/docs/tutorials/index.mdx index 72f8008204fc..5cad6dec7fcf 100644 --- a/docs/tutorials/index.mdx +++ b/docs/tutorials/index.mdx @@ -119,7 +119,7 @@ Qiskit Functions are a collection of pre-packaged error management and applicati * [Simulate a kicked Ising model with the TEM function](/docs/tutorials/simulate-kicked-ising-tem) - * [Benchmark QFT+M process fidelity with Orbit, a Qiskit Function by Quantum Elements](/docs/tutorials/quantum-elements-orbit-qft-with-dynamic-circuits) + * [Benchmark QFT process fidelity with Orbit, a Qiskit Function by Quantum Elements](/docs/tutorials/quantum-elements-orbit-qft-with-dynamic-circuits) - Experiment with domain-specific problems with **Application functions** — with familiar inputs and outputs to classical solvers. diff --git a/docs/tutorials/quantum-elements-orbit-qft-with-dynamic-circuits.ipynb b/docs/tutorials/quantum-elements-orbit-qft-with-dynamic-circuits.ipynb index 26e309c425b2..7a824c7cec9b 100644 --- a/docs/tutorials/quantum-elements-orbit-qft-with-dynamic-circuits.ipynb +++ b/docs/tutorials/quantum-elements-orbit-qft-with-dynamic-circuits.ipynb @@ -6,13 +6,13 @@ "metadata": {}, "source": [ "---\n", - "title: Benchmark QFT+M process fidelity with Orbit, a Qiskit Function by Quantum Elements\n", - "description: Estimate QFT followed by measurement (QFT+M) process fidelity across circuit sizes; compare raw unitary, raw dynamic, and Orbit-enhanced dynamic implementations\n", + "title: Benchmark QFT process fidelity with Orbit, a Qiskit Function by Quantum Elements\n", + "description: Estimate QFT followed by measurement process fidelity across circuit sizes; compare raw unitary, raw dynamic, and Orbit-enhanced dynamic implementations\n", "---\n", "\n", "{/* cspell:ignore minexp, succ, fontsize, labelsize */}\n", "\n", - "# Benchmark QFT+M process fidelity with Orbit, a Qiskit Function by Quantum Elements" + "# Benchmark QFT process fidelity with Orbit, a Qiskit Function by Quantum Elements" ] }, { @@ -20,9 +20,9 @@ "id": "warning", "metadata": {}, "source": [ - "*Usage estimate:* 2 minutes on a Heron r3 processor. (NOTE: This is an estimate only. Your runtime might vary.) By default, this tutorial submits three Orbit function jobs into one IBM Quantum Compute Service batch mode workload, with 300 PUBs per job, for 900 PUBs and 921,600 shots in total.\n", + "*Usage estimate:* 2 minutes on a Heron r3 processor. (NOTE: This is an estimate only. Your runtime might vary.) By default, this tutorial submits one Orbit Function job using a server-side IBM Quantum Compute Service Runtime Batch with three child jobs, 300 PUBs per child job, and 921,600 shots in total.\n", "\n", - "*Warning:* Dynamic circuits are presently an experimental feature, and they are subject to limitations in Quantum Compute [[3]](#references) that could cause job failures. For example, error 6073 indicates that a job exceeded the classical-control hardware's memory limit [[4]](#references). This notebook reduces that risk by partitioning circuit sizes across three Quantum Compute jobs in one batch [[5]](#references). Each fixed-size comparison remains in one job, while large and small sizes are paired to balance the jobs' classical-control workloads." + "*Warning:* Dynamic circuits are presently an experimental feature, and they are subject to limitations in Quantum Compute [[3]](#references) that could cause job failures. For example, error 6073 indicates that a job exceeded the classical-control hardware's memory limit [[4]](#references). This notebook reduces that risk by partitioning circuit sizes across three Runtime Batch child jobs [[5]](#references). Each fixed-size comparison remains in one child job, while large and small sizes are paired to balance the jobs' classical-control workloads." ] }, { @@ -35,11 +35,10 @@ "By completing this tutorial, you will learn how to:\n", "\n", "- Prepare the product states $\\mathrm{QFT}^\\dagger|x\\rangle$ used by the sampled process-fidelity estimator in Figure 2a of Ref. [[1]](#references).\n", - "- Build equivalent unitary and dynamic implementations of quantum Fourier transform followed by measurement (QFT+M).\n", + "- Build equivalent unitary and dynamic implementations of quantum Fourier transform followed by measurement.\n", "- Select physical qubits for the dynamic circuits using current calibration and connectivity data.\n", - "- Compare raw unitary, raw dynamic, and Orbit-enhanced dynamic QFT+M process-fidelity estimates as the circuit size grows.\n", - "- Use Orbit's streamlined transpilation API with `mode=\"raw\"` and `transpilation_mode=\"validate\"`.\n", - "- Submit multiple Orbit workloads through the batch mode API while keeping every fixed-size three-strategy comparison in one job.\n", + "- Compare raw unitary, raw dynamic, and Orbit-enhanced dynamic QFT process-fidelity estimates as the circuit size grows.\n", + "- Submit one Orbit workload through the batch mode API while keeping every fixed-size three-strategy comparison in one child job.\n", "- Inspect Orbit metadata to confirm whether dynamical decoupling (DD) and measurement-error mitigation (MEM) were applied." ] }, @@ -58,7 +57,7 @@ "\\widehat{\\mathcal{F}}_{\\mathrm{proc}} = \\frac{m}{m-1}\\left(\\frac{1}{m}\\sum_{\\ell=1}^{m}\\sqrt{p_{x_\\ell}}\\right)^2 - \\frac{1}{m(m-1)}\\sum_{\\ell=1}^{m}p_{x_\\ell}.\n", "$$\n", "\n", - "The dynamic construction replaces the controlled-phase gates of unitary QFT+M with mid-circuit measurements and classically conditioned phase rotations [[1]](#references). By deferred measurement, both circuits have the same ideal output distribution. The dynamic form removes the all-to-all two-qubit-gate requirement and instead uses $O(n)$ mid-circuit measurements with feedforward and no connectivity constraint. Measurement and feedforward also leave long idle periods on qubits that have not yet been measured, making DD especially relevant.\n", + "The dynamic construction replaces the controlled-phase gates of unitary QFT+M with mid-circuit measurements and classically conditioned phase rotations [[1]](#references). By deferred measurement, both circuits have the same ideal output distribution. The unitary implementation requires all-to-all two-qubit-gates, and due to local connectivity constraints, incurs an $O(n^2)$ SWAP gate overhead. In contrast, the dynamic circuit uses only single qubit gates and $O(n)$ mid-circuit measurements that introduce long idles in the middle. Disregarding DD entirely, the basic competition is thus between $O(n^2)$ noisy unitary gates and $O(n)$ noisy mid-circuit measurements. Provided that idles during these mid-circuit measurements are low enough error, the dynamic circuit will outperform unitary implementations, and we show this can be achieved using Orbit below.\n", "\n", "**Relation to Figure 2a.** This notebook follows the paper's sampled process-fidelity protocol, but it is an Orbit-focused tutorial adaptation rather than a reproduction. For example, whereas Figure 2a used `ibm_kyiv` with 2000 shots, we use a modern device `ibm_aachen` with a smaller 1024-shot count to preserve QPU time." ] @@ -80,7 +79,7 @@ "id": "1bb67ce9", "metadata": {}, "source": [ - "![QFT process fidelity on 'ibm_aachen'](/docs/images/tutorials/quantum-elements-orbit/dynamic_qft_orbit_tutorial_aachen_notebook_3job_average.svg)" + "![QFT process fidelity on 'ibm_aachen'](/docs/images/tutorials/quantum-elements-orbit-qft-with-dynamic-circuits/dynamic_qft_orbit_tutorial_aachen_notebook_3job_average.svg)" ] }, { @@ -110,38 +109,39 @@ "source": [ "## Setup\n", "\n", - "Authenticate with [IBM Quantum® Platform](https://quantum.cloud.ibm.com), load `ibm_aachen`, and load Quantum Elements Orbit from the [Qiskit Functions Catalog](https://quantum.cloud.ibm.com/functions). The default sweep evaluates 15 circuit sizes, 20 sampled bitstrings per size, and three strategies. `NUM_BATCH_JOBS=3` partitions the sizes across three jobs in one [batch](/docs/guides/run-jobs-batch). Reduce `N_VALUES` or `M`, or increase `NUM_BATCH_JOBS`, if an individual dynamic-circuit job still reaches the backend's classical-control memory limit. Reduce `SHOTS` when the goal is to lower execution usage rather than the number or complexity of circuits." + "Authenticate with [IBM Quantum® Platform](https://quantum.cloud.ibm.com), load `ibm_aachen`, and load Quantum Elements Orbit from the [Qiskit Functions Catalog](https://quantum.cloud.ibm.com/functions).\n", + "\n", + "`N_VALUES` is the list of circuit sizes: each value is the number of logical qubits in one QFT circuit. `M` is the number of independently sampled basis labels $x$ evaluated for each size; in the fidelity estimator, this is the $m$ in the equation above. For every `(N, x)` pair, the notebook submits one PUB for each of the three strategies, so a fixed size contributes `3 * M` PUBs and `3 * M * SHOTS` circuit shots.\n", + "\n", + "The default sweep therefore evaluates 15 circuit sizes, 20 sampled bitstrings per size, and three strategies. `NUM_BATCH_PARTITIONS=3` determines the three ordered Runtime Batch partitions, and `BATCH_MAX_PUBS_PER_JOB` is passed through `orbit.run(...)` as JSON configuration. Large `N_VALUES` create more complex dynamic circuits, while larger `M` puts more PUBs in each child job; either can increase pressure on the backend's classical-control memory. If an individual dynamic-circuit child job reaches that limit, reduce `N_VALUES` or `M`, or increase `NUM_BATCH_PARTITIONS`. Reduce `SHOTS` when the goal is to lower execution usage rather than the number or complexity of circuits.\n", + "\n", + "`OPTIMIZATION_LEVEL=0` asks Qiskit's transpiler to do the required translation and routing with minimal optimization. We use it to preserve the explicitly selected dynamic-circuit layouts and make the raw comparisons easier to interpret. Levels 1–3 are valid experiments, but they can rewrite or simplify circuits and may change the selected placement; they offer little expected benefit here because the dynamic circuit has no two-qubit gates or connectivity constraint. If you change the level, rerun the benchmark and treat the results as a separate comparison." ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "id": "setup-code", "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "qiskit_runtime_service._discover_account:WARNING:2026-07-21 15:57:39,310: Loading account with the given token. A saved account will not be used.\n" - ] - }, { "data": { "text/plain": [ - "{'backend': 'ibm_aachen',\n", + "{'orbit version': '0.3.23',\n", + " 'backend': 'ibm_marrakesh',\n", " 'num_qubits': 156,\n", " 'n_values': [2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30, 35, 40],\n", " 'm': 20,\n", " 'shots': 1024,\n", - " 'num_function_jobs': 3,\n", + " 'num_function_jobs': 1,\n", + " 'num_runtime_batch_partitions': 3,\n", " 'n_groups': [[40, 2, 7, 15, 10], [35, 3, 6, 20, 9], [30, 4, 5, 25, 8]],\n", - " 'pubs_per_job': [300, 300, 300],\n", + " 'pubs_per_partition': [300, 300, 300],\n", " 'total_pubs': 900,\n", " 'total_shots': 921600}" ] }, - "execution_count": 15, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } @@ -161,27 +161,34 @@ "from qiskit.circuit import IfElseOp\n", "from qiskit.synthesis.qft import synth_qft_full\n", "from qiskit_ibm_catalog import QiskitFunctionsCatalog\n", - "from qiskit_ibm_runtime import Batch, QiskitRuntimeService\n", + "from qiskit_ibm_runtime import QiskitRuntimeService\n", "\n", - "IBM_BACKEND_NAME = \"ibm_aachen\"\n", + "IBM_BACKEND_NAME = \"ibm_marrakesh\" # Change to your preferred backend\n", "\n", + "# N_VALUES contains circuit sizes in logical qubits; M is samples per size.\n", "N_VALUES = [2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 30, 35, 40]\n", "M = 20\n", + "# Each sample is run with this many shots for each of the three strategies.\n", "SHOTS = 1024\n", - "RNG_SEED = 12345\n", - "OPTIMIZATION_LEVEL = 0\n", - "NUM_BATCH_JOBS = 3\n", + "# Transpilation options.\n", + "OPTIMIZATION_LEVEL = 0 # Level 0 preserves the explicitly selected layouts and dynamic structure.\n", + "RNG_SEED = 12345 # Reproducible transpilation seed.\n", + "# Batch partitioning options.\n", + "NUM_BATCH_PARTITIONS = 3\n", "STRATEGY_LABELS = (\"unitary/raw\", \"dynamic/raw\", \"dynamic/orbit\")\n", "\n", "\n", "def balanced_n_groups(\n", " n_values: list[int], num_jobs: int = 3\n", ") -> list[list[int]]:\n", + " \"\"\"Pair circuit sizes into balanced Runtime Batch partitions.\"\"\"\n", " values = sorted(n_values)\n", " if len(set(values)) != len(values):\n", " raise ValueError(\"N_VALUES must not contain duplicates\")\n", " if not 1 <= num_jobs <= len(values):\n", - " raise ValueError(\"NUM_BATCH_JOBS must be between 1 and len(N_VALUES)\")\n", + " raise ValueError(\n", + " \"NUM_BATCH_PARTITIONS must be between 1 and len(N_VALUES)\"\n", + " )\n", "\n", " max_group_size = (len(values) + num_jobs - 1) // num_jobs\n", " groups = [[] for _ in range(num_jobs)]\n", @@ -225,7 +232,7 @@ " return groups\n", "\n", "\n", - "N_GROUPS = balanced_n_groups(N_VALUES, NUM_BATCH_JOBS)\n", + "N_GROUPS = balanced_n_groups(N_VALUES, NUM_BATCH_PARTITIONS)\n", "\n", "service = QiskitRuntimeService(channel=\"ibm_quantum_platform\")\n", "backend = service.backend(IBM_BACKEND_NAME)\n", @@ -247,14 +254,16 @@ " )\n", "\n", "{\n", + " \"orbit version\": quantum_elements_orbit.version,\n", " \"backend\": backend.name,\n", " \"num_qubits\": backend.num_qubits,\n", " \"n_values\": N_VALUES,\n", " \"m\": M,\n", " \"shots\": SHOTS,\n", - " \"num_function_jobs\": NUM_BATCH_JOBS,\n", + " \"num_function_jobs\": 1,\n", + " \"num_runtime_batch_partitions\": NUM_BATCH_PARTITIONS,\n", " \"n_groups\": N_GROUPS,\n", - " \"pubs_per_job\": [\n", + " \"pubs_per_partition\": [\n", " len(group) * M * len(STRATEGY_LABELS) for group in N_GROUPS\n", " ],\n", " \"total_pubs\": len(N_VALUES) * M * len(STRATEGY_LABELS),\n", @@ -267,21 +276,22 @@ "id": "build-md", "metadata": {}, "source": [ - "## Build QFT+M circuits\n", + "## Build QFT circuits\n", "\n", - "For each sampled integer $x$, `bit_inv_qft` prepares the product state $\\mathrm{QFT}^\\dagger|x\\rangle$ with Hadamards followed by phase rotations. The notebook then appends either the standard unitary QFT or its semiclassical dynamic QFT+M equivalent.\n", + "For each sampled integer $x$, `bit_inv_qft` prepares the product state $\\mathrm{QFT}^\\dagger|x\\rangle$ with Hadamards followed by phase rotations. The notebook then appends either the standard unitary QFT or its semiclassical dynamic QFT equivalent.\n", "\n", "Both implementations omit the final swap network. The classical-bit display order in Qiskit therefore makes the expected measured string the reverse of the zero-padded binary representation of $x$, which is encoded by `format(x, f\"0{n}b\")[::-1]`." ] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 23, "id": "circuits-code", "metadata": {}, "outputs": [], "source": [ "def bit_inv_qft(circuit: QuantumCircuit, x: int, conv: str = \"LSB\") -> None:\n", + " \"\"\"Prepare QFT†|x⟩ as a product state on `circuit`.\"\"\"\n", " num_qubits = circuit.num_qubits\n", " circuit.h(range(num_qubits))\n", " for j in range(num_qubits):\n", @@ -294,6 +304,7 @@ "\n", "\n", "def build_unitary_qft_circuit(num_qubits: int, x: int) -> QuantumCircuit:\n", + " \"\"\"Build a unitary QFT circuit for sampled basis label `x`.\"\"\"\n", " if not 0 <= x < 2**num_qubits:\n", " raise ValueError(\n", " f\"x={x} is outside the {num_qubits}-qubit basis range\"\n", @@ -310,6 +321,7 @@ "\n", "\n", "def _warn_if_precision_loss(max_num_entanglements: int) -> None:\n", + " \"\"\"Warn when the smallest QFT rotation may lose float precision.\"\"\"\n", " if max_num_entanglements > -np.finfo(float).minexp:\n", " warnings.warn(\n", " \"precision loss in QFT.\"\n", @@ -323,6 +335,7 @@ "def synth_dynamic_qft(\n", " circuit: QuantumCircuit, *, do_swaps: bool = False\n", ") -> QuantumCircuit:\n", + " \"\"\"Append semiclassical dynamic QFT operations to `circuit`.\"\"\"\n", " num_qubits = circuit.num_qubits\n", " creg = circuit.cregs[0]\n", " _warn_if_precision_loss(num_qubits - 1)\n", @@ -343,6 +356,7 @@ "\n", "\n", "def build_dynamic_qft_circuit(num_qubits: int, x: int) -> QuantumCircuit:\n", + " \"\"\"Build a dynamic QFT circuit for sampled basis label `x`.\"\"\"\n", " if not 0 <= x < 2**num_qubits:\n", " raise ValueError(\n", " f\"x={x} is outside the {num_qubits}-qubit basis range\"\n", @@ -356,12 +370,14 @@ "\n", "\n", "def target_output_bitstring(x: int, n_qubits: int) -> str:\n", + " \"\"\"Return Qiskit's expected measured bitstring for basis label `x`.\"\"\"\n", " return format(int(x), f\"0{n_qubits}b\")[::-1]\n", "\n", "\n", "def process_fidelity_from_success_probabilities(\n", " success_probabilities: list[float],\n", ") -> float:\n", + " \"\"\"Estimate process fidelity from sampled success probabilities.\"\"\"\n", " m = len(success_probabilities)\n", " if m <= 1:\n", " raise ValueError(\n", @@ -388,7 +404,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 24, "id": "select-code", "metadata": {}, "outputs": [ @@ -397,14 +413,15 @@ "output_type": "stream", "text": [ "The top 3 qubits (according to our scoring): \n", - "[{'qubit': 0, 't1': 0.0002514242577986401, 't2': 0.00037559012475638467, 'measurement_error': 0.0028076171875, 'score': 1.0}, {'qubit': 20, 't1': 0.0002526252407383437, 't2': 0.00038493543771861573, 'measurement_error': 0.00390625, 'score': 1.0}, {'qubit': 25, 't1': 0.0002712841332005567, 't2': 0.00025793268824583597, 'measurement_error': 0.0040283203125, 'score': 1.0}]\n", + "[{'qubit': 35, 't1': 0.0004024167194138543, 't2': 0.0003811132194981602, 'measurement_error': 0.003662109375, 'score': 1.0}, {'qubit': 92, 't1': 0.0003281277199162346, 't2': 0.0002414989126012369, 'measurement_error': 0.00390625, 'score': 0.9957494563006185}, {'qubit': 5, 't1': 0.0002919284700896914, 't2': 0.0002184196315970577, 'measurement_error': 0.00537109375, 'score': 0.9776125935763066}]\n", "Worst 3 qubits (according to our scoring): \n", - "[{'qubit': 146, 't1': 7.619772882181663e-05, 't2': 0.00014166983578724752, 'measurement_error': 0.0802001953125, 'score': 0.05893378230453208}, {'qubit': 51, 't1': 0.00014200221819602618, 't2': 1.930870507157441e-06, 'measurement_error': 0.054443359375, 'score': 0.04600110909801309}, {'qubit': 35, 't1': 7.116087485472031e-05, 't2': 9.463696242928626e-05, 'measurement_error': 0.14501953125, 'score': 0.03289891864200328}]\n" + "[{'qubit': 76, 't1': 7.65424382656864e-05, 't2': 3.438638139929193e-05, 'measurement_error': 0.05859375, 'score': 0.013271219132843196}, {'qubit': 26, 't1': 7.000219049917298e-06, 't2': 7.849466281257113e-06, 'measurement_error': 0.1009521484375, 'score': 0.0}, {'qubit': 82, 't1': 7.260327905277343e-06, 't2': 6.440762177221646e-06, 'measurement_error': 0.5048828125, 'score': 0.0}]\n" ] } ], "source": [ "def value_from_property(raw):\n", + " \"\"\"Extract a numeric value from a backend property record.\"\"\"\n", " if raw is None:\n", " return None\n", " if isinstance(raw, tuple):\n", @@ -413,6 +430,7 @@ "\n", "\n", "def qubit_property_value(properties, qubit: int, *names: str) -> float | None:\n", + " \"\"\"Read the first available backend property for one qubit.\"\"\"\n", " for name in names:\n", " try:\n", " value = value_from_property(\n", @@ -426,6 +444,7 @@ "\n", "\n", "def measurement_error(properties, qubit: int) -> float | None:\n", + " \"\"\"Return readout error, deriving it from asymmetric errors if needed.\"\"\"\n", " readout = qubit_property_value(properties, qubit, \"readout_error\")\n", " if readout is not None:\n", " return readout\n", @@ -437,6 +456,7 @@ "\n", "\n", "def coupling_edges(backend) -> list[tuple[int, int]]:\n", + " \"\"\"Return directed coupling-map edges as integer pairs.\"\"\"\n", " coupling_map = getattr(backend, \"coupling_map\", None)\n", " if coupling_map is not None:\n", " try:\n", @@ -448,6 +468,7 @@ "\n", "\n", "def neighbor_map(backend) -> dict[int, set[int]]:\n", + " \"\"\"Build an undirected neighboring-qubit map from backend connectivity.\"\"\"\n", " neighbors = {qubit: set() for qubit in range(backend.num_qubits)}\n", " for a, b in coupling_edges(backend):\n", " neighbors[a].add(b)\n", @@ -458,6 +479,7 @@ "def anchored_score(\n", " value: float | None, *, good: float, bad: float, higher_is_better: bool\n", ") -> float:\n", + " \"\"\"Normalize a calibration value to a [0, 1] quality score.\"\"\"\n", " if value is None:\n", " return 0.0\n", " if higher_is_better:\n", @@ -470,6 +492,7 @@ "\n", "\n", "def qubit_metrics(backend) -> list[dict]:\n", + " \"\"\"Collect and rank calibration metrics for all backend qubits.\"\"\"\n", " properties = backend.properties()\n", " rows = []\n", " for qubit in range(backend.num_qubits):\n", @@ -500,6 +523,7 @@ "\n", "\n", "def select_dynamic_qubits(backend, n_qubits: int) -> list[int]:\n", + " \"\"\"Choose high-scoring, preferably nonadjacent qubits for dynamic circuits.\"\"\"\n", " ranked = qubit_metrics(backend)\n", " neighbors = neighbor_map(backend)\n", " selected = []\n", @@ -538,101 +562,84 @@ "\n", "For every `(N, x)` pair, the notebook transpiles the logical circuits first and creates one Sampler PUB for each strategy:\n", "\n", - "- `unitary/raw`: unitary QFT+M on the transpiler's selected layout, with routing as needed and no Orbit DD or MEM.\n", - "- `dynamic/raw`: dynamic QFT+M on the calibration-selected physical qubits, with no Orbit DD or MEM.\n", + "- `unitary/raw`: unitary QFT on the transpiler's selected layout, with routing as needed and no Orbit DD or MEM.\n", + "- `dynamic/raw`: dynamic QFT on the calibration-selected physical qubits, with no Orbit DD or MEM.\n", "- `dynamic/orbit`: the same transpiled dynamic circuit on the same physical qubits, with Orbit DD and MEM enabled.\n", "\n", "The Orbit-enhanced PUB uses `transpilation_mode=\"validate\"` because its mapping has already been chosen. Orbit validates the supplied physical circuit instead of remapping it, then applies its DD and MEM pipeline. Because only the enhanced dynamic curve requests MEM, it should not be interpreted as an isolated DD-versus-no-DD comparison.\n", "\n", - "The PUBs, per-PUB options, and result records are stored by batch-job index. For each fixed $N$, the `unitary/raw`, `dynamic/raw`, and `dynamic/orbit` PUBs are kept together in the same job. The grouping helper pairs large and small circuit sizes, alternates their order, and balances the sum of $N$ across the three jobs as a simple proxy for classical-control workload." + "The PUBs, per-PUB options, and result records are flattened in the same order. For each fixed $N$, the `unitary/raw`, `dynamic/raw`, and `dynamic/orbit` PUBs are kept together in one Runtime Batch partition. The grouping helper pairs large and small circuit sizes, alternates their order, and balances the sum of $N$ across the three partitions as a simple proxy for classical-control workload." ] }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 25, "id": "prepare-code", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'num_function_jobs': 3,\n", + "{'num_function_jobs': 1,\n", + " 'num_runtime_batch_partitions': 3,\n", " 'n_groups': {0: [40, 2, 7, 15, 10],\n", " 1: [35, 3, 6, 20, 9],\n", " 2: [30, 4, 5, 25, 8]},\n", " 'n_load_per_job': {0: 74, 1: 73, 2: 72},\n", - " 'pubs_per_job': {0: 300, 1: 300, 2: 300},\n", - " 'expected_executions_per_job': {0: 307200, 1: 307200, 2: 307200},\n", - " 'first_pub_record_by_job': {0: {'job_index': 0,\n", - " 'n_qubits': 40,\n", - " 'target_decimal': 853235401719,\n", - " 'target_bitstring': '1110111111000000110100110001010101100011',\n", - " 'label': 'unitary/raw',\n", - " 'pub_options': {'mode': 'raw'},\n", - " 'dynamic_qubits': None,\n", - " 'transpiled_depth': 4778,\n", - " 'transpiled_size': 28259},\n", - " 1: {'job_index': 1,\n", - " 'n_qubits': 35,\n", - " 'target_decimal': 26888951661,\n", - " 'target_bitstring': '10110110111010110010110101000010011',\n", - " 'label': 'unitary/raw',\n", - " 'pub_options': {'mode': 'raw'},\n", - " 'dynamic_qubits': None,\n", - " 'transpiled_depth': 3614,\n", - " 'transpiled_size': 21204},\n", - " 2: {'job_index': 2,\n", - " 'n_qubits': 30,\n", - " 'target_decimal': 620442965,\n", - " 'target_bitstring': '101010101010110011011111001001',\n", - " 'label': 'unitary/raw',\n", - " 'pub_options': {'mode': 'raw'},\n", - " 'dynamic_qubits': None,\n", - " 'transpiled_depth': 2835,\n", - " 'transpiled_size': 15078}},\n", - " 'largest_dynamic_qubit_set': [0,\n", - " 20,\n", - " 25,\n", - " 27,\n", - " 33,\n", - " 59,\n", - " 74,\n", - " 80,\n", - " 95,\n", - " 144,\n", - " 151,\n", - " 155,\n", - " 79,\n", - " 90,\n", - " 60,\n", - " 68,\n", - " 114,\n", - " 107,\n", - " 13,\n", - " 126,\n", - " 133,\n", - " 103,\n", - " 3,\n", - " 87,\n", - " 53,\n", - " 41,\n", - " 130,\n", + " 'pubs_per_partition': {0: 300, 1: 300, 2: 300},\n", + " 'expected_executions_per_partition': {0: 307200, 1: 307200, 2: 307200},\n", + " 'first_pub_record': {'partition_index': 0,\n", + " 'n_qubits': 40,\n", + " 'target_decimal': 853235401719,\n", + " 'target_bitstring': '1110111111000000110100110001010101100011',\n", + " 'label': 'unitary/raw',\n", + " 'pub_options': {'mode': 'raw'},\n", + " 'dynamic_qubits': None,\n", + " 'transpiled_depth': 4778,\n", + " 'transpiled_size': 28259},\n", + " 'largest_dynamic_qubit_set': [35,\n", + " 92,\n", " 5,\n", - " 98,\n", - " 135,\n", - " 153,\n", + " 141,\n", + " 75,\n", + " 147,\n", + " 1,\n", + " 152,\n", + " 12,\n", + " 10,\n", + " 128,\n", + " 107,\n", + " 21,\n", + " 8,\n", " 15,\n", - " 116,\n", - " 45,\n", - " 7,\n", - " 48,\n", + " 98,\n", + " 33,\n", + " 134,\n", + " 154,\n", + " 53,\n", + " 78,\n", + " 109,\n", + " 112,\n", + " 149,\n", + " 120,\n", + " 55,\n", + " 3,\n", + " 132,\n", + " 85,\n", + " 49,\n", + " 115,\n", + " 101,\n", + " 95,\n", + " 64,\n", " 136,\n", - " 11,\n", - " 147,\n", - " 77]}" + " 71,\n", + " 90,\n", + " 56,\n", + " 18,\n", + " 44]}" ] }, - "execution_count": 31, + "execution_count": 25, "metadata": {}, "output_type": "execute_result" } @@ -644,9 +651,9 @@ " \"dynamic/orbit\": {\"mode\": \"orbit\", \"transpilation_mode\": \"validate\"},\n", "}\n", "rng = np.random.default_rng(RNG_SEED)\n", - "pubs_by_job = [[] for _ in N_GROUPS]\n", - "pub_options_by_job = [[] for _ in N_GROUPS]\n", - "pub_records_by_job = [[] for _ in N_GROUPS]\n", + "pubs = []\n", + "pub_options = []\n", + "pub_records = []\n", "layout_summary = {}\n", "\n", "target_decimals_by_n = {\n", @@ -654,7 +661,7 @@ " for n_qubits in N_VALUES\n", "}\n", "\n", - "for job_index, n_group in enumerate(N_GROUPS):\n", + "for partition_index, n_group in enumerate(N_GROUPS):\n", " for n_qubits in n_group:\n", " dynamic_qubits = select_dynamic_qubits(backend, n_qubits)\n", " layout_summary[str(n_qubits)] = {\"dynamic_qubits\": dynamic_qubits}\n", @@ -686,11 +693,11 @@ " for label in STRATEGY_LABELS:\n", " circuit = circuits_by_label[label]\n", " options = dict(strategy_options[label])\n", - " pubs_by_job[job_index].append((circuit, None, SHOTS))\n", - " pub_options_by_job[job_index].append(options)\n", - " pub_records_by_job[job_index].append(\n", + " pubs.append((circuit, None, SHOTS))\n", + " pub_options.append(options)\n", + " pub_records.append(\n", " {\n", - " \"job_index\": job_index,\n", + " \"partition_index\": partition_index,\n", " \"n_qubits\": n_qubits,\n", " \"target_decimal\": x,\n", " \"target_bitstring\": target_bitstring,\n", @@ -706,25 +713,28 @@ " }\n", " )\n", "\n", + "BATCH_MAX_PUBS_PER_JOB = max(\n", + " len(group) * M * len(STRATEGY_LABELS) for group in N_GROUPS\n", + ")\n", + "\n", "{\n", - " \"num_function_jobs\": len(N_GROUPS),\n", + " \"num_function_jobs\": 1,\n", + " \"num_runtime_batch_partitions\": len(N_GROUPS),\n", " \"n_groups\": {\n", " job_index: group for job_index, group in enumerate(N_GROUPS)\n", " },\n", " \"n_load_per_job\": {\n", " job_index: sum(group) for job_index, group in enumerate(N_GROUPS)\n", " },\n", - " \"pubs_per_job\": {\n", - " job_index: len(pubs) for job_index, pubs in enumerate(pubs_by_job)\n", + " \"pubs_per_partition\": {\n", + " partition_index: len(group) * M * len(STRATEGY_LABELS)\n", + " for partition_index, group in enumerate(N_GROUPS)\n", " },\n", - " \"expected_executions_per_job\": {\n", - " job_index: len(pubs) * SHOTS\n", - " for job_index, pubs in enumerate(pubs_by_job)\n", - " },\n", - " \"first_pub_record_by_job\": {\n", - " job_index: records[0]\n", - " for job_index, records in enumerate(pub_records_by_job)\n", + " \"expected_executions_per_partition\": {\n", + " partition_index: len(group) * M * len(STRATEGY_LABELS) * SHOTS\n", + " for partition_index, group in enumerate(N_GROUPS)\n", " },\n", + " \"first_pub_record\": pub_records[0],\n", " \"largest_dynamic_qubit_set\": layout_summary[str(max(N_VALUES))][\n", " \"dynamic_qubits\"\n", " ],\n", @@ -738,75 +748,55 @@ "source": [ "## Run the benchmark\n", "\n", - "Create one [batch](/docs/guides/run-jobs-batch), then submit three Orbit function jobs into it.\n", + "Pass the JSON-serializable Batch configuration through `orbit.run(...)`. Orbit creates the authenticated Runtime Batch inside the Function, submits the ordered PUB partitions, closes the Batch after submission, and returns one merged result to the caller. A caller-created local `Batch` cannot control an asynchronous Qiskit Function job.\n", "\n", - "The jobs are partitioned by qubit-count groups so that all three strategies for a fixed $N$ can be compared That is, all 60 PUBs for a fixed $N$ — 20 sampled inputs times the three strategies — therefore execute in the same job and can be thus be compared as fairly as possible (otherwise, if run in different jobs, the device could drift while in queue). The default groups combine large and small circuits and contain 300 PUBs each, reducing the chance that one job accumulates all of the largest dynamic programs while preserving within-job comparisons." + "The ordered PUB list is partitioned by qubit-count groups so that all three strategies for a fixed $N$ can be compared fairly: all 60 PUBs for a fixed $N$ — 20 sampled inputs times the three strategies — execute in the same Runtime Batch child job. The default groups combine large and small circuits and contain 300 PUBs each, reducing the chance that one child job accumulates all of the largest dynamic programs while preserving within-job comparisons." ] }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 26, "id": "run-code", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'runtime_batch_id': '80120e36-436d-46c7-96c9-597ec86060c1',\n", - " 'jobs': {0: {'backend': 'ibm_aachen',\n", - " 'function_job_id': '2b6b05ac-0136-40f0-94bf-ade5658f5f4f',\n", - " 'status': 'QUEUED',\n", - " 'n_values': [40, 2, 7, 15, 10],\n", - " 'num_pubs': 300},\n", - " 1: {'backend': 'ibm_aachen',\n", - " 'function_job_id': '4f046fd4-80e4-460b-87c7-e7252691f764',\n", - " 'status': 'QUEUED',\n", - " 'n_values': [35, 3, 6, 20, 9],\n", - " 'num_pubs': 300},\n", - " 2: {'backend': 'ibm_aachen',\n", - " 'function_job_id': '6d70d64d-fa38-4ca2-9cbd-ffda5d8c99be',\n", - " 'status': 'QUEUED',\n", - " 'n_values': [30, 4, 5, 25, 8],\n", - " 'num_pubs': 300}}}" + "{'backend': 'ibm_marrakesh',\n", + " 'function_job_id': '4b953302-afc3-4889-9ecf-e63d78b4e252',\n", + " 'status': 'QUEUED',\n", + " 'num_pubs': 900,\n", + " 'max_pubs_per_runtime_job': 300,\n", + " 'n_groups': [[40, 2, 7, 15, 10], [35, 3, 6, 20, 9], [30, 4, 5, 25, 8]]}" ] }, - "execution_count": 32, + "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "runtime_batch = Batch(backend=backend)\n", - "jobs = []\n", - "try:\n", - " for job_index, pubs in enumerate(pubs_by_job):\n", - " jobs.append(\n", - " quantum_elements_orbit.run(\n", - " primitive=\"sampler\",\n", - " pubs=pubs,\n", - " backend_name=backend.name,\n", - " options={\n", - " \"pub_options\": pub_options_by_job[job_index],\n", - " \"save_backend_info\": True,\n", - " },\n", - " )\n", - " )\n", - "except Exception:\n", - " runtime_batch.close()\n", - " raise\n", + "job = quantum_elements_orbit.run(\n", + " primitive=\"sampler\",\n", + " pubs=pubs,\n", + " backend_name=backend.name,\n", + " options={\n", + " \"pub_options\": pub_options,\n", + " \"batch\": {\n", + " \"max_pubs_per_job\": BATCH_MAX_PUBS_PER_JOB,\n", + " \"max_time\": \"2h\",\n", + " },\n", + " \"save_backend_info\": True,\n", + " },\n", + ")\n", "\n", "{\n", - " \"runtime_batch_id\": runtime_batch.session_id,\n", - " \"jobs\": {\n", - " job_index: {\n", - " \"backend\": backend.name,\n", - " \"function_job_id\": job.job_id,\n", - " \"status\": job.status(),\n", - " \"n_values\": N_GROUPS[job_index],\n", - " \"num_pubs\": len(pubs_by_job[job_index]),\n", - " }\n", - " for job_index, job in enumerate(jobs)\n", - " },\n", + " \"backend\": backend.name,\n", + " \"function_job_id\": job.job_id,\n", + " \"status\": job.status(),\n", + " \"num_pubs\": len(pubs),\n", + " \"max_pubs_per_runtime_job\": BATCH_MAX_PUBS_PER_JOB,\n", + " \"n_groups\": N_GROUPS,\n", "}" ] }, @@ -817,116 +807,130 @@ "source": [ "## Retrieve results and compute process fidelity\n", "\n", - "Retrieve and validate each qubit-group result independently, then merge the three job streams through their job-indexed records. The batch is kept open while all function results are requested and is closed in a `finally` block after every job has been attempted. For each PUB, $p_x$ is the probability assigned to the expected bitstring. `extract_counts` reads the counts returned to the caller; for `dynamic/orbit`, these are the MEM-adjusted counts when mitigation succeeds. `extract_raw_counts` also recovers the corresponding unmitigated counts recorded in Orbit metadata. The code groups the 20 values of $p_x$ for each `(N, label)` pair and applies the estimator introduced above.\n", + "Retrieve the single merged result returned by the Orbit Function. Orbit records the Runtime Batch ID, child Runtime job IDs, and PUB ranges in the result metadata; the PUB order is preserved across child jobs. For each PUB, $p_x$ is the probability assigned to the expected bitstring. `extract_counts` reads the counts returned to the caller; for `dynamic/orbit`, these are the MEM-adjusted counts when mitigation succeeds. `extract_raw_counts` also recovers the corresponding unmitigated counts recorded in Orbit metadata. The code groups the 20 values of $p_x$ for each `(N, label)` pair and applies the estimator introduced above.\n", "\n", "The plotted `process_fidelity` dictionary therefore uses raw counts for `unitary/raw` and `dynamic/raw`, but MEM-adjusted counts for `dynamic/orbit`. The parallel `raw_process_fidelity` dictionary retains an unmitigated calculation for every strategy and is useful when separating the effect of MEM from the rest of the Orbit pipeline. MEM corrects the returned output histogram; it cannot retroactively change a mid-circuit measurement result that was already used by real-time feedforward." ] }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 27, "id": "results-code", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'runtime_batch_id': '80120e36-436d-46c7-96c9-597ec86060c1',\n", - " 'function_job_ids': {0: '2b6b05ac-0136-40f0-94bf-ade5658f5f4f',\n", - " 1: '4f046fd4-80e4-460b-87c7-e7252691f764',\n", - " 2: '6d70d64d-fa38-4ca2-9cbd-ffda5d8c99be'},\n", - " 'n_groups': {0: [40, 2, 7, 15, 10],\n", - " 1: [35, 3, 6, 20, 9],\n", - " 2: [30, 4, 5, 25, 8]},\n", - " 'process_fidelity': {'2': {'dynamic/orbit': 0.9870551835473073,\n", - " 'dynamic/raw': 0.9912537998030566,\n", - " 'unitary/raw': 0.9884650767434809},\n", - " '3': {'dynamic/orbit': 0.9662998634131841,\n", - " 'dynamic/raw': 0.9699631603283018,\n", - " 'unitary/raw': 0.9388637172865901},\n", - " '4': {'dynamic/orbit': 0.9271266520750502,\n", - " 'dynamic/raw': 0.7334377020091254,\n", - " 'unitary/raw': 0.9010122207121433},\n", - " '5': {'dynamic/orbit': 0.8883501513887149,\n", - " 'dynamic/raw': 0.6577660260669806,\n", - " 'unitary/raw': 0.7806443417987445},\n", - " '6': {'dynamic/orbit': 0.8524225652033044,\n", - " 'dynamic/raw': 0.4444025126308521,\n", - " 'unitary/raw': 0.7167426842521228},\n", - " '7': {'dynamic/orbit': 0.832962085697061,\n", - " 'dynamic/raw': 0.2253787798698553,\n", - " 'unitary/raw': 0.5746335601063436},\n", - " '8': {'dynamic/orbit': 0.7881895956180588,\n", - " 'dynamic/raw': 0.16909516699831612,\n", - " 'unitary/raw': 0.5408263851227074},\n", - " '9': {'dynamic/orbit': 0.7422635627368794,\n", - " 'dynamic/raw': 0.0242474245097341,\n", - " 'unitary/raw': 0.4855953298367578},\n", - " '10': {'dynamic/orbit': 0.7002274273149545,\n", - " 'dynamic/raw': 0.033718865729016285,\n", - " 'unitary/raw': 0.3607634828181049},\n", - " '15': {'dynamic/orbit': 0.4694995355699914,\n", - " 'dynamic/raw': 7.70970394736842e-05,\n", - " 'unitary/raw': 0.054582117352985286},\n", - " '20': {'dynamic/orbit': 0.24118032284867608,\n", - " 'dynamic/raw': 4.235164736271502e-22,\n", - " 'unitary/raw': 0.0},\n", - " '25': {'dynamic/orbit': 0.027122712989729438,\n", + "{'function_job_id': '4b953302-afc3-4889-9ecf-e63d78b4e252',\n", + " 'runtime_batch_id': '096da6a9-cbea-4940-bd6f-3bdebfad436e',\n", + " 'runtime_job_ids': ['dabhtfh6e67c73a1dspg',\n", + " 'dabhtkp6e67c73a1dt0g',\n", + " 'dabhtple36ac739ff7ag'],\n", + " 'runtime_batch_partitions': [{'index': 0,\n", + " 'pubStart': 0,\n", + " 'pubCount': 300,\n", + " 'jobId': 'dabhtfh6e67c73a1dspg'},\n", + " {'index': 1,\n", + " 'pubStart': 300,\n", + " 'pubCount': 300,\n", + " 'jobId': 'dabhtkp6e67c73a1dt0g'},\n", + " {'index': 2,\n", + " 'pubStart': 600,\n", + " 'pubCount': 300,\n", + " 'jobId': 'dabhtple36ac739ff7ag'}],\n", + " 'n_groups': [[40, 2, 7, 15, 10], [35, 3, 6, 20, 9], [30, 4, 5, 25, 8]],\n", + " 'process_fidelity': {'2': {'dynamic/orbit': 0.96795267841156,\n", + " 'dynamic/raw': 0.982106196462969,\n", + " 'unitary/raw': 0.9813114213091467},\n", + " '3': {'dynamic/orbit': 0.9260299657995851,\n", + " 'dynamic/raw': 0.9561294394338111,\n", + " 'unitary/raw': 0.9500873287318629},\n", + " '4': {'dynamic/orbit': 0.8830473125154273,\n", + " 'dynamic/raw': 0.8338948172317725,\n", + " 'unitary/raw': 0.8723284938083211},\n", + " '5': {'dynamic/orbit': 0.8345263398914793,\n", + " 'dynamic/raw': 0.7202370952637628,\n", + " 'unitary/raw': 0.7886522480980407},\n", + " '6': {'dynamic/orbit': 0.7555427381229081,\n", + " 'dynamic/raw': 0.5132161092086612,\n", + " 'unitary/raw': 0.7212899564100468},\n", + " '7': {'dynamic/orbit': 0.7050351887334156,\n", + " 'dynamic/raw': 0.3785120988393675,\n", + " 'unitary/raw': 0.6045265978953258},\n", + " '8': {'dynamic/orbit': 0.6646106162043616,\n", + " 'dynamic/raw': 0.16869967802023164,\n", + " 'unitary/raw': 0.5043677114922691},\n", + " '9': {'dynamic/orbit': 0.5230999959600392,\n", + " 'dynamic/raw': 0.03629305030182246,\n", + " 'unitary/raw': 0.4292104907804543},\n", + " '10': {'dynamic/orbit': 0.5578720776016518,\n", + " 'dynamic/raw': 0.041261905412736485,\n", + " 'unitary/raw': 0.3509234542669733},\n", + " '15': {'dynamic/orbit': 0.2580377878661623,\n", + " 'dynamic/raw': 3.083881578947369e-05,\n", + " 'unitary/raw': 0.09345722346055228},\n", + " '20': {'dynamic/orbit': 0.05567530378670723,\n", + " 'dynamic/raw': 0.0,\n", + " 'unitary/raw': 0.00010793585526315788},\n", + " '25': {'dynamic/orbit': 0.003482218097213064,\n", " 'dynamic/raw': 0.0,\n", " 'unitary/raw': 0.0},\n", - " '30': {'dynamic/orbit': 0.0003581886014704875,\n", + " '30': {'dynamic/orbit': 7.268778589499868e-06,\n", " 'dynamic/raw': 0.0,\n", " 'unitary/raw': 0.0},\n", " '35': {'dynamic/orbit': 0.0, 'dynamic/raw': 0.0, 'unitary/raw': 0.0},\n", " '40': {'dynamic/orbit': 0.0, 'dynamic/raw': 0.0, 'unitary/raw': 0.0}},\n", - " 'mean_success_probability': {'2': {'dynamic/orbit': 0.987060546875,\n", - " 'dynamic/raw': 0.991259765625,\n", - " 'unitary/raw': 0.9884765625},\n", - " '3': {'dynamic/orbit': 0.96630859375,\n", - " 'dynamic/raw': 0.969970703125,\n", - " 'unitary/raw': 0.939013671875},\n", - " '4': {'dynamic/orbit': 0.9271484375,\n", - " 'dynamic/raw': 0.7337890625,\n", - " 'unitary/raw': 0.901318359375},\n", - " '5': {'dynamic/orbit': 0.88837890625,\n", - " 'dynamic/raw': 0.657861328125,\n", - " 'unitary/raw': 0.78115234375},\n", - " '6': {'dynamic/orbit': 0.85244140625,\n", - " 'dynamic/raw': 0.44453125,\n", - " 'unitary/raw': 0.71728515625},\n", - " '7': {'dynamic/orbit': 0.8330078125,\n", - " 'dynamic/raw': 0.22568359375,\n", - " 'unitary/raw': 0.575390625},\n", - " '8': {'dynamic/orbit': 0.788232421875,\n", - " 'dynamic/raw': 0.169189453125,\n", - " 'unitary/raw': 0.541796875},\n", - " '9': {'dynamic/orbit': 0.742333984375,\n", - " 'dynamic/raw': 0.0244140625,\n", - " 'unitary/raw': 0.487353515625},\n", - " '10': {'dynamic/orbit': 0.70029296875,\n", - " 'dynamic/raw': 0.033935546875,\n", - " 'unitary/raw': 0.363037109375},\n", - " '15': {'dynamic/orbit': 0.4697265625,\n", - " 'dynamic/raw': 0.00029296875,\n", - " 'unitary/raw': 0.055615234375},\n", - " '20': {'dynamic/orbit': 0.241357421875,\n", - " 'dynamic/raw': 4.8828125e-05,\n", + " 'mean_success_probability': {'2': {'dynamic/orbit': 0.96796875,\n", + " 'dynamic/raw': 0.98212890625,\n", + " 'unitary/raw': 0.98134765625},\n", + " '3': {'dynamic/orbit': 0.92607421875,\n", + " 'dynamic/raw': 0.95615234375,\n", + " 'unitary/raw': 0.950146484375},\n", + " '4': {'dynamic/orbit': 0.88310546875,\n", + " 'dynamic/raw': 0.833935546875,\n", + " 'unitary/raw': 0.872705078125},\n", + " '5': {'dynamic/orbit': 0.8345703125,\n", + " 'dynamic/raw': 0.7203125,\n", + " 'unitary/raw': 0.78896484375},\n", + " '6': {'dynamic/orbit': 0.7556640625,\n", + " 'dynamic/raw': 0.51337890625,\n", + " 'unitary/raw': 0.721826171875},\n", + " '7': {'dynamic/orbit': 0.70517578125,\n", + " 'dynamic/raw': 0.3787109375,\n", + " 'unitary/raw': 0.605419921875},\n", + " '8': {'dynamic/orbit': 0.66484375,\n", + " 'dynamic/raw': 0.16884765625,\n", + " 'unitary/raw': 0.50576171875},\n", + " '9': {'dynamic/orbit': 0.5232421875,\n", + " 'dynamic/raw': 0.0365234375,\n", + " 'unitary/raw': 0.43203125},\n", + " '10': {'dynamic/orbit': 0.5580078125,\n", + " 'dynamic/raw': 0.04150390625,\n", + " 'unitary/raw': 0.353466796875},\n", + " '15': {'dynamic/orbit': 0.258349609375,\n", + " 'dynamic/raw': 0.0001953125,\n", + " 'unitary/raw': 0.09453125},\n", + " '20': {'dynamic/orbit': 0.057568359375,\n", + " 'dynamic/raw': 0.0,\n", + " 'unitary/raw': 0.000341796875},\n", + " '25': {'dynamic/orbit': 0.006396484375,\n", + " 'dynamic/raw': 0.0,\n", " 'unitary/raw': 0.0},\n", - " '25': {'dynamic/orbit': 0.041015625, 'dynamic/raw': 0.0, 'unitary/raw': 0.0},\n", - " '30': {'dynamic/orbit': 0.0013671875,\n", + " '30': {'dynamic/orbit': 0.000146484375,\n", " 'dynamic/raw': 0.0,\n", " 'unitary/raw': 0.0},\n", " '35': {'dynamic/orbit': 0.0, 'dynamic/raw': 0.0, 'unitary/raw': 0.0},\n", " '40': {'dynamic/orbit': 0.0, 'dynamic/raw': 0.0, 'unitary/raw': 0.0}}}" ] }, - "execution_count": 35, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def extract_counts(pub_result) -> dict[str, int]:\n", + " \"\"\"Extract the first nonempty classical-register counts mapping.\"\"\"\n", " data = getattr(pub_result, \"data\", None)\n", " if data is None:\n", " raise TypeError(\"pub_result.data is missing\")\n", @@ -947,6 +951,7 @@ "\n", "\n", "def extract_raw_counts(pub_result) -> dict[str, int]:\n", + " \"\"\"Extract unmitigated counts from Orbit metadata when available.\"\"\"\n", " orbit_metadata = pub_result.metadata.get(\"quantum_elements_orbit\", {})\n", " mem_report = orbit_metadata.get(\"measurementErrorMitigation\", {})\n", " return mem_report.get(\"rawCounts\") or extract_counts(pub_result)\n", @@ -955,6 +960,7 @@ "def probability_for_bitstring(\n", " counts: dict[str, int], bitstring: str, n_qubits: int\n", ") -> float:\n", + " \"\"\"Return observed probability of an expected bitstring.\"\"\"\n", " total = sum(counts.values())\n", " if total <= 0:\n", " return 0.0\n", @@ -965,81 +971,84 @@ " return float(normalized.get(bitstring, 0) / total)\n", "\n", "\n", - "results_by_job = {}\n", - "job_failures = []\n", "try:\n", - " for job_index, job in enumerate(jobs):\n", - " try:\n", - " job_result = job.result()\n", - " except Exception as exc:\n", - " job_logs = getattr(job, \"logs\", lambda: \"\")()\n", - " if job_logs:\n", - " print(f\"Logs for job {job_index} ({job.job_id}):\\n{job_logs}\")\n", - " job_failures.append(\n", - " f\"job {job_index} ({job.job_id}) failed: {type(exc).__name__}: {exc}\"\n", - " )\n", - " continue\n", + " job_result = job.result()\n", + "except Exception as exc:\n", + " job_logs = getattr(job, \"logs\", lambda: \"\")()\n", + " if job_logs:\n", + " print(f\"Logs for Function job {job.job_id}:\\n{job_logs}\")\n", + " raise RuntimeError(\n", + " f\"Batched Orbit Function job {job.job_id} failed: \"\n", + " f\"{type(exc).__name__}: {exc}\"\n", + " ) from exc\n", "\n", - " expected_results = len(pub_records_by_job[job_index])\n", - " if len(job_result) != expected_results:\n", - " job_failures.append(\n", - " f\"job {job_index} ({job.job_id}) returned {len(job_result)} PUB results; \"\n", - " f\"expected {expected_results}\"\n", - " )\n", - " continue\n", - " results_by_job[job_index] = job_result\n", - "finally:\n", - " runtime_batch.close()\n", + "if len(job_result) != len(pub_records):\n", + " raise RuntimeError(\n", + " f\"Function job {job.job_id} returned {len(job_result)} PUB results; \"\n", + " f\"expected {len(pub_records)}\"\n", + " )\n", "\n", - "if job_failures:\n", + "batch_report = job_result.metadata.get(\"quantum_elements_orbit\", {}).get(\n", + " \"batch\", {}\n", + ")\n", + "batch_partitions = batch_report.get(\"partitions\", [])\n", + "partition_by_pub = {}\n", + "for partition in batch_partitions:\n", + " start = int(partition[\"pubStart\"])\n", + " stop = start + int(partition[\"pubCount\"])\n", + " for pub_index in range(start, stop):\n", + " partition_by_pub[pub_index] = partition\n", + "\n", + "if len(partition_by_pub) != len(pub_records):\n", " raise RuntimeError(\n", - " \"One or more batched Orbit jobs failed:\\n\" + \"\\n\".join(job_failures)\n", + " \"Orbit Batch metadata does not cover every returned PUB.\"\n", " )\n", "\n", "grouped_success = defaultdict(list)\n", "grouped_raw_success = defaultdict(list)\n", "pub_summaries = []\n", "\n", - "for job_index, job_result in sorted(results_by_job.items()):\n", - " records = pub_records_by_job[job_index]\n", - " for record, pub_result in zip(records, job_result, strict=True):\n", - " label = record[\"label\"]\n", - " n_qubits = record[\"n_qubits\"]\n", - " counts = extract_counts(pub_result)\n", - " raw_counts = extract_raw_counts(pub_result)\n", - " success = probability_for_bitstring(\n", - " counts, record[\"target_bitstring\"], n_qubits\n", - " )\n", - " raw_success = probability_for_bitstring(\n", - " raw_counts, record[\"target_bitstring\"], n_qubits\n", - " )\n", - " key = (n_qubits, label)\n", - " grouped_success[key].append(success)\n", - " grouped_raw_success[key].append(raw_success)\n", - "\n", - " orbit_report = pub_result.metadata.get(\"quantum_elements_orbit\", {})\n", - " mem_report = orbit_report.get(\"measurementErrorMitigation\", {})\n", - " pub_summaries.append(\n", - " {\n", - " **record,\n", - " \"function_job_id\": jobs[job_index].job_id,\n", - " \"runtime_batch_id\": runtime_batch.session_id,\n", - " \"success_probability\": success,\n", - " \"raw_success_probability\": raw_success,\n", - " \"orbit_mode\": orbit_report.get(\"mode\"),\n", - " \"transpilation_mode\": orbit_report.get(\"transpilationMode\"),\n", - " \"physical_layout\": orbit_report.get(\"physicalLayout\"),\n", - " \"dd_status\": orbit_report.get(\"status\", \"not_applied\"),\n", - " \"num_sequences_added\": orbit_report.get(\n", - " \"numSequencesAdded\", 0\n", - " ),\n", - " \"num_gaps_filled\": orbit_report.get(\"numGapsFilled\", 0),\n", - " \"dynamic_dd_seq\": orbit_report.get(\"dynamicDdSeq\"),\n", - " \"mem_status\": mem_report.get(\"status\", \"not_requested\"),\n", - " \"warnings\": orbit_report.get(\"warnings\", [])\n", - " + mem_report.get(\"warnings\", []),\n", - " }\n", - " )\n", + "for pub_index, (record, pub_result) in enumerate(\n", + " zip(pub_records, job_result, strict=True)\n", + "):\n", + " partition = partition_by_pub[pub_index]\n", + " label = record[\"label\"]\n", + " n_qubits = record[\"n_qubits\"]\n", + " counts = extract_counts(pub_result)\n", + " raw_counts = extract_raw_counts(pub_result)\n", + " success = probability_for_bitstring(\n", + " counts, record[\"target_bitstring\"], n_qubits\n", + " )\n", + " raw_success = probability_for_bitstring(\n", + " raw_counts, record[\"target_bitstring\"], n_qubits\n", + " )\n", + " key = (n_qubits, label)\n", + " grouped_success[key].append(success)\n", + " grouped_raw_success[key].append(raw_success)\n", + "\n", + " orbit_report = pub_result.metadata.get(\"quantum_elements_orbit\", {})\n", + " mem_report = orbit_report.get(\"measurementErrorMitigation\", {})\n", + " pub_summaries.append(\n", + " {\n", + " **record,\n", + " \"function_job_id\": job.job_id,\n", + " \"runtime_batch_id\": batch_report.get(\"id\"),\n", + " \"runtime_job_id\": partition.get(\"jobId\"),\n", + " \"runtime_batch_partition\": partition.get(\"index\"),\n", + " \"success_probability\": success,\n", + " \"raw_success_probability\": raw_success,\n", + " \"orbit_mode\": orbit_report.get(\"mode\"),\n", + " \"transpilation_mode\": orbit_report.get(\"transpilationMode\"),\n", + " \"physical_layout\": orbit_report.get(\"physicalLayout\"),\n", + " \"dd_status\": orbit_report.get(\"status\", \"not_applied\"),\n", + " \"num_sequences_added\": orbit_report.get(\"numSequencesAdded\", 0),\n", + " \"num_gaps_filled\": orbit_report.get(\"numGapsFilled\", 0),\n", + " \"dynamic_dd_seq\": orbit_report.get(\"dynamicDdSeq\"),\n", + " \"mem_status\": mem_report.get(\"status\", \"not_requested\"),\n", + " \"warnings\": orbit_report.get(\"warnings\", [])\n", + " + mem_report.get(\"warnings\", []),\n", + " }\n", + " )\n", "\n", "process_fidelity = defaultdict(dict)\n", "raw_process_fidelity = defaultdict(dict)\n", @@ -1066,13 +1075,11 @@ "raw_mean_success_probability = dict(raw_mean_success_probability)\n", "\n", "{\n", - " \"runtime_batch_id\": runtime_batch.session_id,\n", - " \"function_job_ids\": {\n", - " job_index: job.job_id for job_index, job in enumerate(jobs)\n", - " },\n", - " \"n_groups\": {\n", - " job_index: group for job_index, group in enumerate(N_GROUPS)\n", - " },\n", + " \"function_job_id\": job.job_id,\n", + " \"runtime_batch_id\": batch_report.get(\"id\"),\n", + " \"runtime_job_ids\": batch_report.get(\"jobIds\", []),\n", + " \"runtime_batch_partitions\": batch_partitions,\n", + " \"n_groups\": N_GROUPS,\n", " \"process_fidelity\": process_fidelity,\n", " \"mean_success_probability\": mean_success_probability,\n", "}" @@ -1090,7 +1097,7 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 28, "id": "metadata-code", "metadata": {}, "outputs": [ @@ -1149,7 +1156,7 @@ " 'MEM was applied unconditionally to the returned output bitstring without inferring whether each bit came from a terminal measurement or a mid-circuit measurement. This is meaningful for bits intended as circuit outputs, but Orbit does not retroactively or in real time change conditional branches that used unmitigated measurement results.']}]}" ] }, - "execution_count": 36, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } @@ -1190,14 +1197,14 @@ "source": [ "## Plot process-fidelity curves\n", "\n", - "The plot shows the sampled QFT+M process-fidelity point estimate versus qubit count for the three strategies. `dynamic/raw` and `dynamic/orbit` share a physical layout at each size; `unitary/raw` uses the transpiler's layout and routing.\n", + "The plot shows the sampled QFT process-fidelity point estimate versus qubit count for the three strategies. `dynamic/raw` and `dynamic/orbit` share a physical layout at each size; `unitary/raw` uses the transpiler's layout and routing.\n", "\n", "Unlike Figure 2a, this plot does not show a unitary-with-DD curve or uncertainty bands, and its raw curves are not readout-mitigated. It is best read as a Figure-2a-style scaling comparison for this Orbit workflow, not as a direct reproduction of the published curves." ] }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 29, "id": "1972ceb9", "metadata": {}, "outputs": [], @@ -1205,21 +1212,19 @@ "from datetime import datetime\n", "from zoneinfo import ZoneInfo\n", "\n", - "closed_at = runtime_batch.details()[\"closed_at\"] # \"2026-07-22T00:08:54.89Z\"\n", - "closed_dt = datetime.fromisoformat(closed_at.replace(\"Z\", \"+00:00\"))\n", - "closed_local = closed_dt.astimezone(ZoneInfo(\"America/Los_Angeles\"))" + "finished_local = datetime.now(ZoneInfo(\"America/Los_Angeles\"))" ] }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 30, "id": "plot-code", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "\"Output" + "\"Output" ] }, "metadata": {}, @@ -1234,9 +1239,9 @@ " \"unitary/raw\": \"#6e6e6e\",\n", "}\n", "pretty_labels = {\n", - " \"dynamic/orbit\": \"Dynamic QFT+M with Orbit\",\n", - " \"dynamic/raw\": \"Dynamic QFT+M\",\n", - " \"unitary/raw\": \"Unitary QFT+M\",\n", + " \"dynamic/orbit\": \"Dynamic QFT with Orbit\",\n", + " \"dynamic/raw\": \"Dynamic QFT\",\n", + " \"unitary/raw\": \"Unitary QFT\",\n", "}\n", "\n", "series = []\n", @@ -1271,7 +1276,7 @@ "\n", "ax.set_xlabel(\"N qubits\")\n", "ax.set_ylabel(\"Process fidelity\")\n", - "finished_time_for_title = globals().get(\"finished_local\", closed_local)\n", + "finished_time_for_title = finished_local\n", "ax.set_title(\n", " f\"Dynamic QFT Orbit results on {IBM_BACKEND_NAME}\\n\"\n", " f\"Job finished {finished_time_for_title:%Y-%m-%d %H:%M %Z}\"\n", @@ -1323,11 +1328,12 @@ "## Next steps\n", "\n", "- See the [Orbit guide](/docs/guides/quantum-elements-orbit) and [API reference](/docs/api/functions/quantum-elements-orbit) documentation.\n", - "- Try a different backend, an alternative layout, or experiment with alternative orbit-enabled dynamical decoupling sequence by altering the `dd_strategy` option. Keep in mind that because of the experimental nature of dynamic circuits, you must be careful of possible job failure modes (see [[3]](#references) and [[4]](#references)). If you encounter negative stretch values [[3]](#references), try a smaller (fewer pulse) DD sequence. If you encounter [[4]](#references), increase `NUM_BATCH_JOBS`, reduce `M`, or reduce the largest values in `N_VALUES`." + "- Try a different backend, an alternative layout, or experiment with alternative orbit-enabled dynamical decoupling sequence by altering the `dd_strategy` option. Keep in mind that because of the experimental nature of dynamic circuits, you must be careful of possible job failure modes (see [[3]](#references) and [[4]](#references)). If you encounter negative stretch values [[3]](#references), try a smaller (fewer pulse) DD sequence. If you encounter [[4]](#references), increase `NUM_BATCH_PARTITIONS`, reduce `M`, or reduce the largest values in `N_VALUES`, all of which are defined in the Setup section near the top." ] } ], "metadata": { + "hours": 1, "kernelspec": { "display_name": "Python 3", "language": "python", @@ -1345,7 +1351,6 @@ "pygments_lexer": "ipython3", "version": "3" }, - "hours": 1, "qpuSeconds": 120 }, "nbformat": 4, diff --git a/public/docs/images/guides/quantum-elements-orbit/bv-fidelity-raw-orbit-mem.avif b/public/docs/images/guides/quantum-elements-orbit/bv-fidelity-raw-orbit-mem.avif new file mode 100644 index 000000000000..184bdef87a0e Binary files /dev/null and b/public/docs/images/guides/quantum-elements-orbit/bv-fidelity-raw-orbit-mem.avif differ diff --git a/public/docs/images/guides/quantum-elements-orbit/bv-hidden-bitstring-success-boxplot.avif b/public/docs/images/guides/quantum-elements-orbit/bv-hidden-bitstring-success-boxplot.avif new file mode 100644 index 000000000000..b2a238fd8119 Binary files /dev/null and b/public/docs/images/guides/quantum-elements-orbit/bv-hidden-bitstring-success-boxplot.avif differ diff --git a/public/docs/images/guides/quantum-elements-orbit/extracted-outputs/7656f768-0.avif b/public/docs/images/guides/quantum-elements-orbit/extracted-outputs/7656f768-0.avif index e7d4e8d332f0..fc11dd6a9823 100644 Binary files a/public/docs/images/guides/quantum-elements-orbit/extracted-outputs/7656f768-0.avif and b/public/docs/images/guides/quantum-elements-orbit/extracted-outputs/7656f768-0.avif differ diff --git a/public/docs/images/tutorials/quantum-elements-orbit/dynamic_qft_orbit_tutorial_aachen_notebook_3job_average.svg b/public/docs/images/tutorials/quantum-elements-orbit-qft-with-dynamic-circuits/dynamic_qft_orbit_tutorial_aachen_notebook_3job_average.svg similarity index 100% rename from public/docs/images/tutorials/quantum-elements-orbit/dynamic_qft_orbit_tutorial_aachen_notebook_3job_average.svg rename to public/docs/images/tutorials/quantum-elements-orbit-qft-with-dynamic-circuits/dynamic_qft_orbit_tutorial_aachen_notebook_3job_average.svg diff --git a/public/docs/images/tutorials/quantum-elements-orbit-qft-with-dynamic-circuits/extracted-outputs/plot-code-0.avif b/public/docs/images/tutorials/quantum-elements-orbit-qft-with-dynamic-circuits/extracted-outputs/plot-code-0.avif new file mode 100644 index 000000000000..2c6d8cc67d65 Binary files /dev/null and b/public/docs/images/tutorials/quantum-elements-orbit-qft-with-dynamic-circuits/extracted-outputs/plot-code-0.avif differ diff --git a/public/docs/images/tutorials/quantum-elements-orbit/extracted-outputs/plot-code-0.avif b/public/docs/images/tutorials/quantum-elements-orbit/extracted-outputs/plot-code-0.avif deleted file mode 100644 index 89f21118e6f3..000000000000 Binary files a/public/docs/images/tutorials/quantum-elements-orbit/extracted-outputs/plot-code-0.avif and /dev/null differ