diff --git a/docs/guides/_toc.json b/docs/guides/_toc.json index 5e275d4e9488..6af06a452e12 100644 --- a/docs/guides/_toc.json +++ b/docs/guides/_toc.json @@ -898,8 +898,8 @@ "url": "/docs/guides/pulse-migration" }, { - "title": "Migrate to the Qiskit Runtime V2 primitives", - "url": "/docs/guides/v2-primitives" + "title": "Migrate from Sampler to Executor", + "url": "/docs/guides/migrating-from-sampler-to-executor" }, { "title": "Migrate to local simulators", diff --git a/docs/guides/migrating-from-sampler-to-executor.mdx b/docs/guides/migrating-from-sampler-to-executor.mdx new file mode 100644 index 000000000000..853472f5059c --- /dev/null +++ b/docs/guides/migrating-from-sampler-to-executor.mdx @@ -0,0 +1,472 @@ +--- +title: Migrate from Sampler to Executor primitive +description: Migrate from using the Sampler V2 to Executor primitive in IBM Compute Service + +--- + +{/* cspell:ignore expvals */} + +# Migrating from Sampler to Executor + +A guide for moving quantum sampling workloads from the Qiskit Runtime **Sampler** +primitive to the **Executor** primitive. + +> **Beta notice.** The Executor primitive is part of the +> [directed execution model](https://quantum.cloud.ibm.com/docs/en/guides/directed-execution-model), +> which is currently in **beta** and may change. Test it and file feedback on the +> [Samplomatic](https://github.com/Qiskit/samplomatic/issues) or +> [qiskit-ibm-runtime](https://github.com/Qiskit/qiskit-ibm-runtime/issues) GitHub repos. + +--- + +## Should you migrate? + +Sampler and Executor both sample the output registers of quantum circuits, but they +target different users: + +- **Sampler** is a high-level abstraction. It has built-in error suppression (dynamical decoupling, twirling), makes implicit decisions for you, and + is designed so algorithm developers can focus on innovation rather than data + conversion. +- **Executor** is part of the directed execution model. It has **no built-in error + suppression or mitigation**. Instead, you capture your design intent on the client + side (via circuit annotations and a *samplex*), and the costly generation of circuit + variants is shifted to the server side. Executor makes **no implicit decisions** — + it follows your directives exactly, giving full control and transparency. + +Beyond control and transparency, Executor and Samplomatic also expose **additional +capabilities that Sampler does not offer**, including: + +- **More twirling groups.** Samplomatic lets you choose which twirling group to apply + per box, rather than being limited to the single strategy Sampler applies for you. It also supports additional twirling groups beyond Pauli. + For example, `qiskit-ibm-runtime` 0.48.0 added support for the `"local_c1"` twirling + group. +- **Kerneled *and* classified measurements together.** Setting + `QuantumProgram.meas_level = "both"` (added in `qiskit-ibm-runtime` 0.48.0) requests + that *both* classified and kerneled measurements be present in the results, instead + of picking a single measurement type per job. +- **Twirling for circuits with fractional gates.** Executor can apply twirling to + circuits that contain fractional gates — something Sampler does not support. +- **Fine-grained, composable error mitigation** — for example, choosing exactly which + circuit layers to mitigate and adjusting the noise rates injected into the circuit. + +This list is **not comprehensive** — it highlights a few representative capabilities. +Moreover, going forward, **new capabilities are expected to be released to Executor +first** and may or may not be ported back to Sampler. If you rely on access to the +latest features, Executor is the more future-proof choice. + +**Migrate to Executor if** you are a quantum information scientist running +utility-scale experiments who needs fine-grained, reproducible control over +techniques like Pauli twirling, noise-model learning and injection, and basis +changes — or who needs one of the additional capabilities above. + +**Stay on Sampler if** you want a simple, high-level interface and are happy to let +the primitive manage error suppression/mitigation for you. + +--- + +## Package requirements + +Executor and the directed execution model require an additional package, **Samplomatic**: + +```bash +pip install qiskit qiskit-ibm-runtime samplomatic + +# For visualization support: +# pip install samplomatic[vis] +``` + +> **Version note:** `qiskit-ibm-runtime` 0.48.0 is recommended for Executor, as it adds +> the `meas_level = "both"` option and the `"local_c1"` twirling group, and requires +> `qiskit >= 2.3.0` and `samplomatic >= 0.18.0`. + +> **Note:** Unlike Sampler (`SamplerV2`), the base Qiskit package does not yet provide +> a base class for the Executor primitive. + +--- + +## Conceptual mapping + +| Concept | Sampler | Executor | +| ---------------------- | ----------------------------------------- | --------------------------------------------------------------- | +| Import | `from qiskit_ibm_runtime import SamplerV2`| `from qiskit_ibm_runtime import Executor` | +| Input | List of **PUBs** (tuples) | A **`QuantumProgram`** of `QuantumProgramItem` objects | +| Circuit + params | `(circuit, params, shots)` tuple | `program.append_circuit_item(circuit, circuit_arguments=...)` | +| Twirling | `TwirlingOptions` | Explicit via annotated boxes + **samplex** (`append_samplex_item`) | +| Run call | `sampler.run([pub, ...])` | `executor.run(program)` | +| Result type | `PrimitiveResult` of `SamplerPubResult` | `QuantumProgramResult` (iterable) | +| Accessing data | `result[0].data.` (`BitArray`) | `result[0][""]` (`np.ndarray`) | +| Noise management | Built-in options | You compose it (annotations, samplex, `NoiseLearnerV3`) | + +--- + +## Side-by-side: a basic sampling job + +### Before — Sampler + +```python +import numpy as np +from qiskit.circuit import QuantumCircuit +from qiskit.transpiler import generate_preset_pass_manager +from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler + +# 1. Account + backend +service = QiskitRuntimeService() +backend = service.least_busy(operational=True, simulator=False) + +# 2. Circuit +circuit = QuantumCircuit(2) +circuit.h(0) +circuit.h(1) +circuit.cz(0, 1) +circuit.h(1) +circuit.measure_all() + +# 3. Transpile to ISA +pm = generate_preset_pass_manager(optimization_level=1, backend=backend) +isa_circuit = pm.run(circuit) + +# 4. Run — shots are passed to run() +sampler = Sampler(mode=backend) +job = sampler.run([(isa_circuit,)], shots=25) +result = job.result() + +# 5. Access results: a BitArray keyed by register name +counts = result[0].data.meas.get_counts() +``` + +### After — Executor + +```python +import numpy as np +from qiskit.circuit import QuantumCircuit +from qiskit.transpiler import generate_preset_pass_manager +from qiskit_ibm_runtime import QiskitRuntimeService, Executor +from qiskit_ibm_runtime.quantum_program import QuantumProgram + +# 1. Account + backend (unchanged) +service = QiskitRuntimeService() +backend = service.least_busy(operational=True, simulator=False) + +# 2. Circuit (unchanged) +circuit = QuantumCircuit(2) +circuit.h(0) +circuit.h(1) +circuit.cz(0, 1) +circuit.h(1) +circuit.measure_all() + +# 3. Transpile to ISA (unchanged) +pm = generate_preset_pass_manager(optimization_level=1, backend=backend) +isa_circuit = pm.run(circuit) + +# 4. Build a QuantumProgram — shots are on the program +program = QuantumProgram(shots=25) +program.append_circuit_item(isa_circuit) + +# 5. Run +executor = Executor(mode=backend) +job = executor.run(program) +result = job.result() + +# 6. Access results: a plain np.ndarray keyed by register name +# shape = (shots, register_bits) +meas = result[0]["meas"] +``` + +Key differences to notice: + +1. **No PUBs.** You build a `QuantumProgram` and append items to it instead of passing + a list of tuples. +2. **Shots move to the program.** `QuantumProgram(shots=...)` applies to every item in + the program. To use different shot counts, submit **separate programs (separate + jobs)**. +3. **Results are NumPy arrays, not `BitArray`s.** You index by register name string + (`result[0]["meas"]`) and get an `np.ndarray` back — no need to remember the + `.data.` attribute path. + +--- + +## Choosing between `append_circuit_item` and `append_samplex_item` + +A `QuantumProgram` accepts two kinds of items, and which one you use depends on whether +you want the item's circuit to be **randomized at runtime**: + +- **`append_circuit_item`** — appends a `CircuitItem`: a circuit and (optionally) its + parameter values. It is executed **as-is, without any randomization**. Use this when + you just want to sample a circuit, exactly as Sampler would with a PUB that has no + twirling — for example, a plain sampling job, or when you have already baked in any + variants you want yourself. +- **`append_samplex_item`** — appends a `SamplexItem`: a *template circuit* plus a + *samplex* that generates randomized parameter sets on the server side. Use this + whenever you want the circuit's content randomized — the primary case being + **twirling** (gate or measurement) or **noise injection**. This is what replaces + Sampler's built-in twirling. + +A single `QuantumProgram` can mix both item types; each appended item is executed as an +independent task and produces its own entry in the results. As a rule of thumb: reach +for `append_circuit_item` when the answer to "does this circuit need randomization?" is +no, and `append_samplex_item` when it is yes. + +The next two sections show each in turn: parameterized circuits use +`append_circuit_item`, and migrating twirling uses `append_samplex_item`. + +--- + +## Migrating parameterized circuits + +With Sampler, parameter values are the second element of the PUB tuple. With Executor, +pass them as `circuit_arguments` to `append_circuit_item`. + +### Before — Sampler + +```python +params = np.random.rand(10, circuit.num_parameters) # 10 parameter sets +job = sampler.run([(isa_circuit, params)]) +result = job.result() +``` + +### After — Executor + +```python +program = QuantumProgram(shots=1024) +program.append_circuit_item( + isa_circuit, + circuit_arguments=np.random.rand(10, circuit.num_parameters), # 10 sets +) +job = executor.run(program) +result = job.result() + +# CircuitItem result shape: (parameter_sets, shots, register_bits) -> (10, 1024, 2) +result_0 = result[0]["meas"] +``` + +--- + +## Migrating built-in twirling to explicit annotations + +This is the most significant change. Sampler applies twirling for you via options. +Executor requires you to declare that intent explicitly using **annotated boxes** and +a **samplex** (from Samplomatic). + +### Before — Sampler (twirling via options) + +```python +sampler = Sampler(mode=backend) +sampler.options.twirling.enable_gates = True +sampler.options.twirling.enable_measure = True +job = sampler.run([(isa_circuit, params)]) +``` + +### After — Executor (twirling via boxes + samplex) + +```python +from samplomatic import build +from samplomatic.transpiler import generate_boxing_pass_manager + +# 1. Group gates and measurements into annotated boxes with twirling annotations +boxes_pm = generate_boxing_pass_manager( + enable_gates=True, # gate twirling + enable_measures=True, # measurement twirling +) +boxed_circuit = boxes_pm.run(isa_circuit) + +# 2. Build the (template circuit, samplex) pair. +# The template circuit's single-qubit gates are replaced by parameterized gates; +# the samplex encodes how to generate the randomized parameters at runtime. +template_circuit, samplex = build(boxed_circuit) + +# 3. Append as a samplex item, specifying the number of randomizations +program = QuantumProgram(shots=1024) +program.append_samplex_item( + template_circuit, + samplex=samplex, + samplex_arguments={ + "parameter_values": np.random.rand(10, 2), # original circuit params + }, + shape=(28, 10), # 28 randomizations x 10 parameter sets +) + +# 4. Run +job = executor.run(program) +result = job.result() +``` + +Because the template circuit and samplex are built **on the client side**, you can +inspect and sample them locally to verify the output *before* sending anything to +hardware. + +### Sampling the template circuit locally + +You can draw randomizations from the samplex yourself and bind them to the template +circuit, so you can confirm the samplex is producing the parameter values you expect. +The parameter values returned by `samplex.sample` are directly compatible with the +template circuit's parameters. + +```python +# Check which inputs the samplex requires (for the twirling example above, +# this is just the original circuit's parameter values). +print(samplex.inputs()) + +# Bind the required inputs, then draw a few randomizations locally. +inputs = samplex.inputs().bind( + parameter_values=np.random.rand(2), # one set of the original circuit's params +) +outputs = samplex.sample(inputs, num_randomizations=3) + +# Assign one randomization's parameter values to the template circuit and inspect it. +bound_template = template_circuit.assign_parameters(outputs["parameter_values"][0]) +bound_template.draw("mpl", idle_wires=False) +``` + +To go further, you can verify each randomization is *logically equivalent* to the +original circuit — for example by converting both to `Operator`s and comparing (after +accounting for the `outputs["measurement_flips."]` corrections that undo +measurement twirling), or by comparing expectation values from a local +`StatevectorSampler`/`StatevectorEstimator` run. See the Samplomatic +[Samplex inputs and outputs](https://qiskit.github.io/samplomatic/guides/samplex_io.html) +guide for a complete walkthrough. + +--- + +## Handling twirled results (bit-flip corrections) + +When you apply measurement twirling through a `SamplexItem`, Executor returns the raw +(twirled) measurements **plus** the bit-flip corrections needed to undo the twirling. +You apply them yourself — nothing is corrected implicitly. + +```python +# SamplexItem result shape: (randomizations, parameter_sets, shots, register_bits) +result_1 = result[1]["meas"] # e.g. (28, 10, 1024, 2) + +# Bit-flip corrections to undo measurement twirling +flips_1 = result[1]["measurement_flips.meas"] # e.g. (28, 10, 1, 2) + +# Undo the twirling via classical XOR (broadcasts over the shots axis) +unflipped_result_1 = result_1 ^ flips_1 +``` + +There is no equivalent step in Sampler — it undoes twirling for you internally. + +--- + +## Result access cheat sheet + +| Task | Sampler | Executor | +| ----------------------------- | ------------------------------------------------ | ------------------------------------------- | +| Get register data | `result[0].data.meas` | `result[0]["meas"]` | +| Data type | `BitArray` | `np.ndarray` | +| Counts dictionary | `result[0].data.meas.get_counts()` | Post-process the array yourself | +| Multiple registers | `result[0].data.` per register | `result[0][""]` per register | +| CircuitItem array shape | n/a | `(parameter_sets, shots, register_bits)` | +| SamplexItem array shape | n/a | `(randomizations, parameter_sets, shots, register_bits)` | +| Undo measurement twirling | Automatic | `result[i]["measurement_flips."]` + XOR | + +> Sampler's `BitArray` offers helpers (`get_counts`, `slice_bits`, `slice_shots`, +> `expectation_values`, post-selection masks). With Executor you receive raw NumPy +> arrays and perform this post-processing with standard NumPy operations. + +--- + +## Options: what changes + +Executor options are a smaller, lower-level set than Sampler's, because +error-mitigation choices now live in your annotations/samplex rather than in options. + +There is also a **structural difference in where settings live**. With Sampler, +everything — including choices that affect result post-processing — is configured on +the primitive's options or in the PUB. With Executor, choices that affect how the job's +results are shaped and post-processed belong on the **`QuantumProgram`**, not on +`ExecutorOptions`. The two clearest examples are: + +- **`shots`** → `QuantumProgram(shots=...)` (Sampler: per-PUB or `run(shots=...)`) +- **`meas_level`** → `QuantumProgram.meas_level` (Sampler: an execution option) + +`ExecutorOptions`, by contrast, holds only lower-level execution and environment +settings that don't change the structure of the returned data. + +```python +from qiskit_ibm_runtime import Executor, ExecutorOptions + +options = ExecutorOptions( + environment={"log_level": "INFO"}, + execution={"init_qubits": True}, +) +# or mutate after construction: +options = ExecutorOptions() +options.environment.log_level = "INFO" +options.execution.init_qubits = True + +executor = Executor(mode=backend, options=options) +``` + +`ExecutorOptions` (see the +[`ExecutorOptions` API reference](https://quantum.cloud.ibm.com/docs/en/api/qiskit-ibm-runtime/options-models-executor-options)) +has just three top-level groups: + +- **`environment`** ([`EnvironmentOptions`](https://quantum.cloud.ibm.com/docs/en/api/qiskit-ibm-runtime/options-models-environment-options)) — + `log_level`, `job_tags`, `private`, `max_execution_time`, `image` +- **`execution`** ([`ExecutionOptions`](https://quantum.cloud.ibm.com/docs/en/api/qiskit-ibm-runtime/options-models-execution-options)) — + `init_qubits`, `rep_delay`, `scheduler_timing`, `stretch_values` (the last two are + experimental and populate per-item metadata in the results) +- **`experimental`** — a free-form `dict` of experimental options passed to the executor + +Notably **absent** compared to Sampler options: there are no `twirling`, +`dynamical_decoupling`, or resilience/error-mitigation option groups — those intents +are expressed through the directed execution model instead. Executor's `execution` +group is also leaner than Sampler's — there is **no `meas_type`** option here, for +example. + +--- + +## Migration checklist + +1. **Install Samplomatic** (`pip install samplomatic`) alongside `qiskit` and + `qiskit-ibm-runtime`. +2. **Swap the import**: `SamplerV2` → `Executor` (plus `QuantumProgram`). +3. **Replace PUB tuples** with a `QuantumProgram` and `append_circuit_item` / + `append_samplex_item` calls. +4. **Move shots** from the PUB to `QuantumProgram(shots=...)`; split jobs if you need + different shot counts. +5. **Move parameter values** from the PUB into `circuit_arguments` (circuit items) or + `samplex_arguments` (samplex items). +6. **Re-express error suppression/mitigation**: replace `twirling` / resilience options + with annotated boxes (`generate_boxing_pass_manager`) and a samplex (`build`). Use + `NoiseLearnerV3` where you previously relied on built-in noise learning. +7. **Update result parsing**: `result[i].data.` (`BitArray`) → + `result[i][""]` (`np.ndarray`); rewrite `get_counts`-based post-processing as + NumPy operations. +8. **Undo twirling explicitly** using the `measurement_flips.` corrections and an + XOR. + +--- + +## Current limitations and caveats + +Because Executor and the directed execution model are in beta, a few things are worth +knowing before you commit to a migration: + +- **No simulator support yet.** Unlike Sampler — which has an `AerSampler` + implementation in [`qiskit-aer`](https://github.com/Qiskit/qiskit-aer) for local + simulation — there is currently **no simulator backend for Executor**. Simulator + support is expected to arrive soon. In the meantime, you can still inspect and sample + the template circuit locally (see + [Sampling the template circuit locally](#sampling-the-template-circuit-locally)) to + validate your workflow before submitting to hardware. +- **This guide covers Sampler only, not Estimator.** Migrating from **Estimator** to + Executor is considerably more involved than migrating from Sampler, because Estimator + computes expectation values rather than returning raw samples — reproducing that with + Executor requires additional post-processing. Utility functions to help with the + Estimator → Executor migration are still **under development**, so this guide + intentionally focuses on the Sampler workflow. + +--- + +## References + +- [Executor quickstart](https://quantum.cloud.ibm.com/docs/en/guides/get-started-with-executor) +- [Executor inputs and outputs](https://quantum.cloud.ibm.com/docs/en/guides/executor-input-output) +- [Executor options](https://quantum.cloud.ibm.com/docs/en/guides/executor-options) +- [Directed execution model](https://quantum.cloud.ibm.com/docs/en/guides/directed-execution-model) +- [Sampler quickstart](https://quantum.cloud.ibm.com/docs/en/guides/get-started-with-sampler) +- [Sampler inputs and outputs](https://quantum.cloud.ibm.com/docs/en/guides/sampler-input-output) +- [Samplomatic documentation](https://qiskit.github.io/samplomatic) diff --git a/docs/guides/qiskit-runtime-primitives.mdx b/docs/guides/qiskit-runtime-primitives.mdx index ea380fb16852..df91edf9d67b 100644 --- a/docs/guides/qiskit-runtime-primitives.mdx +++ b/docs/guides/qiskit-runtime-primitives.mdx @@ -8,6 +8,7 @@ description: Introduction to primitives in Qiskit Runtime, and an explanation of Primitives were created to simplify the most common tasks for quantum computers: namely, sampling quantum states and calculating expectation values. The first Qiskit Runtime primitives ([`EstimatorV2`](/docs/api/qiskit-ibm-runtime/estimator-v2) and [`SamplerV2`](/docs/api/qiskit-ibm-runtime/sampler-v2)) are implementations of the Qiskit [primitives base classes](/docs/guides/primitives). They provide a more sophisticated implementation (for example, by including error mitigation) as a cloud-based service and are used to access IBM Quantum® hardware. The newest Qiskit Runtime primitive, [Executor](/docs/api/qiskit-ibm-runtime/executor) (which is in beta), provides a lower-level interface that gives more visibility and control without sacrificing performance. +`Executor` also expose **additional capabilities that Sampler does not offer**, such as additional twirling groups and twirling for circuits with fractional gates. Refer to [Migrating from Sampler to Executor](/docs/guides/migrating-from-sampler-to-executor) for more information on whether to migrate to `Executor`. -# Migrate to the Qiskit Runtime V2 primitives - - -The original primitives (referred to as the V1 primitives), [V1 Sampler](/docs/api/qiskit-ibm-runtime/0.26/sampler-v1) and [V1 Estimator](/docs/api/qiskit-ibm-runtime/0.26/estimator-v1), have been deprecated in `qiskit-ibm-runtime` 0.23. -Their support was removed on 15 August 2024. - - -With the deprecation of the V1 primitives, all code should be migrated to use the V2 interfaces. This guide describes what changed in the Qiskit Runtime V2 primitives (available with qiskit-ibm-runtime 0.21.0) and why, describes each new primitive in detail, and gives examples to help you migrate code from using the legacy primitives to the V2 primitives. The examples in the guide all use the Qiskit Runtime primitives, but in general, the same changes apply to the other primitive implementations. The functions unique to Qiskit Runtime such as error mitigation remain unique to Qiskit Runtime. - -For information about the changes to the Qiskit reference primitives (now called *statevector* primitives), see [the qiskit.primitives section](qiskit-1.0-features#qiskit.primitives) in the Qiskit 1.0 feature changes page. See [StatevectorSampler](/docs/api/qiskit/qiskit.primitives.StatevectorSampler) and [StatevectorEstimator](/docs/api/qiskit/qiskit.primitives.StatevectorEstimator) for V2 primitive reference implementations. - -## Overview - -Version 2 of the primitives is introduced with a new base class for both Sampler and Estimator ([BaseSamplerV2](/docs/api/qiskit/qiskit.primitives.BaseSamplerV2) and [BaseEstimatorV2](/docs/api/qiskit/qiskit.primitives.BaseEstimatorV2)), along with new types for their inputs and outputs. - -The new interface lets you specify a single circuit and multiple observables (if using Estimator) and parameter value sets for that circuit, so that sweeps over parameter value sets and observables can be efficiently specified. Previously, you had to specify the same circuit multiple times to match the size of the data to be combined. Also, while you can still use `resilience_level` (if using Estimator) as the simple knob, V2 primitives give you the flexibility to turn on or off individual error mitigation / suppression methods to customize them for your needs. - -To reduce the total job execution time, V2 primitives only accept circuits and observables that use instructions supported by the target QPU (quantum processing unit). Such circuits and observables are referred to as instruction set architecture (ISA) circuits and observables. V2 primitives do not perform layout, routing, and translation operations. See the [transpilation documentation](/docs/guides/transpile) for instructions to transform circuits. - -Sampler V2 is simplified to focus on its core task of sampling the output register from execution of quantum circuits. It returns the samples, whose type is defined by the program, without weights. The output data is also separated by the output register names defined by the program. This change enables future support for circuits with classical control flow. - -See the [EstimatorV2 API reference](/docs/api/qiskit-ibm-runtime/estimator-v2) and [SamplerV2 API reference](/docs/api/qiskit-ibm-runtime/sampler-v2) for full details. - -## Major changes - -### Import - -For backward compatibility, you must explicity import the V2 primitives. Specifying `import V2 as ` is not required, but makes it easier to transition code to V2. - - -After the V1 primitives are no longer supported, `import ` will import the V2 version of the specified primitive. - - - - - ```python -from qiskit_ibm_runtime import EstimatorV2 as Estimator -``` - - - - ```python -from qiskit_ibm_runtime import Estimator -``` - - - - - - ```python -from qiskit_ibm_runtime import SamplerV2 as Sampler -``` - - - - ```python -from qiskit_ibm_runtime import Sampler -``` - - - - -### Input and output - -#### Input - -Both `SamplerV2` and `EstimatorV2` take one or more *primitive unified blocs* (PUBs) as the input. Each PUB is a tuple that contains **one** circuit and the data broadcasted to that circuit, which can be multiple observables and parameters. Each PUB returns a result. - -* Sampler V2 PUB format: (``, ``, ``), where ``and `` are optional. -* Estimator V2 PUB format: (``, ``, ``, ``), where ``and `` are optional. - Numpy [broadcasting rules](https://numpy.org/doc/stable/user/basics.broadcasting.html) are used when combining observables and parameter values. - -Additionally, the following changes have been made: - -* Estimator V2 has gained a `precision` argument in the `run()` method that specifies the targeted precision of the expectation value estimates. -* Sampler V2 has the `shots` argument in its `run()` method. - -##### Examples - -Estimator V2 example that uses precision in `run()`: - -```python -# Estimate expectation values for two PUBs, both with 0.05 precision. -estimator.run([(circuit1, obs_array1), - (circuit2, obs_array_2)], precision=0.05) -``` - -Sampler V2 example that uses shots in `run()`: - -```python -# Sample two circuits at 128 shots each. -sampler.run([circuit1, circuit2], shots=128) - -# Sample two circuits at different amounts of shots. -# The "None"s are necessary as placeholders -# for the lack of parameter values in this example. -sampler.run([ - (circuit1, None, 123), - (circuit2, None, 456), -]) -``` - -#### Output - -The output is now in the [`PubResult`](/docs/api/qiskit/qiskit.primitives.PubResult) format. A `PubResult` is the data and metadata resulting from a single PUB’s execution. - -* Estimator V2 continues to return expectation values. -* The `data` portion of a Estimator V2 PubResult contains both expectation values and standard errors (`stds`). V1 returned variance in metadata. -* Sampler V2 returns per-shot measurements in the form of **bitstrings**, - instead of the quasi-probability distributions from the V1 - interface. The bitstrings show the measurement outcomes, preserving the shot - order in which they were measured. -* Sampler V2 has convenience methods like `get_counts()` to help with migration. -* The Sampler V2 result objects organize data in terms of their **input circuits' classical register names**, for - compatibility with dynamic circuits. By default, the classical register name is `meas`, as shown in the following example. When defining your circuit, if you create one or more classical registers with a non-default name, use that name to get the results. You can find the classical register name by running `.cregs`. For example, `qc.cregs`. - - ```python - # Define a quantum circuit with 2 qubits - circuit = QuantumCircuit(2) - circuit.h(0) - circuit.cx(0, 1) - circuit.measure_all() - circuit.draw() - ``` - ```text - ┌───┐ ░ ┌─┐ - q_0: ┤ H ├──■───░─┤M├─── - └───┘┌─┴─┐ ░ └╥┘┌─┐ - q_1: ─────┤ X ├─░──╫─┤M├ - └───┘ ░ ║ └╥┘ - meas: 2/══════════════╩══╩═ - 0 1 - ``` - -#### Estimator examples (input and output) - -{/*Verified by Elena.*/} - - - - ```python - # Estimator V1: Execute 1 circuit with 4 observables - job = estimator_v1.run([circuit] * 4, [obs1, obs2, obs3, obs4]) - evs = job.result().values - - # Estimator V2: Execute 1 circuit with 4 observables - job = estimator_v2.run([(circuit, [obs1, obs2, obs3, obs4])]) - evs = job.result()[0].data.evs -``` - - - - ```python - # Estimator V1: Execute 1 circuit with 4 observables and 2 parameter sets - job = estimator_v1.run([circuit] * 8, [obs1, obs2, obs3, obs4] * 2, - [vals1, vals2] * 4) - evs = job.result().values - - # Estimator V2: Execute 1 circuit with 4 observables and 2 parameter sets - - job = estimator_v2.run([(circuit, [[obs1], [obs2], [obs3], [obs4]], - [[vals1], [vals2]])]) - evs = job.result()[0].data.evs -``` - - - - ```python - # Estimator V1: Cannot execute 2 circuits with different observables - - # Estimator V2: Execute 2 circuits with 2 different observables. There are - # two PUBs because each PUB can have only one circuit. - job = estimator_v2.run([(circuit1, obs1), (circuit2, obs2)]) - evs1 = job.result()[0].data.evs # result for pub 1 (circuit 1) - evs2 = job.result()[1].data.evs # result for pub 2 (circuit 2) -``` - - - - -#### Sampler examples (input and output) - -{/*Verified by Elena.*/} - - - - ```python - # Sampler V1: Execute 1 circuit with 3 parameter sets - job = sampler_v1.run([circuit] * 3, [vals1, vals2, vals3]) - dists = job.result().quasi_dists - - # Sampler V2: Executing 1 circuit with 3 parameter sets - job = sampler_v2.run([(circuit, [vals1, vals2, vals3])]) - counts = job.result()[0].data.meas.get_counts() -``` - - - - ```python - # Sampler V1: Execute 2 circuits with 1 parameter set - job = sampler_v1.run([circuit1, circuit2], [vals1] * 2) - dists = job.result().quasi_dists - - # Sampler V2: Execute 2 circuits with 1 parameter set - job = sampler_v2.run([(circuit1, vals1), (circuit2, vals1)]) - counts1 = job.result()[0].data.meas.get_counts() # result for pub 1 (circuit 1) - counts2 = job.result()[1].data.meas.get_counts() # result for pub 2 (circuit 2) -``` - - - - The V1 output format was a dictionary of bitstrings (as an int) as the key and quasi-probabilities as the value for each circuit. The V2 format has the same key (but as a string) and counts as the value. To convert the V2 format to V1, divide the counts by the number of shots, where the number of shots selected is described in the [Sampler options](/docs/guides/sampler-options#shots) guide. - - ```python - v2_result = sampler_v2_job.result() - v1_format = [] - for pub_result in v2_result: - counts = pub_result.data.meas.get_counts() - v1_format.append( {int(key, 2): val/shots for key, val in counts.items()} ) -``` - - - -Example that uses different output registers - -```python -from qiskit import ClassicalRegister, QuantumRegister, QuantumCircuit -from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler - -alpha = ClassicalRegister(5, "alpha") -beta = ClassicalRegister(7, "beta") -qreg = QuantumRegister(12) - -circuit = QuantumCircuit(qreg, alpha, beta) -circuit.h(0) -circuit.measure(qreg[:5], alpha) -circuit.measure(qreg[5:], beta) - -service = QiskitRuntimeService() -backend = service.least_busy(operational=True, - simulator=False, min_num_qubits=12) -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) -isa_circuit = pm.run(circuit) - -sampler = Sampler(backend) -job = sampler.run([isa_circuit]) -result = job.result() -# Get results for the first (and only) PUB -pub_result = result[0] -print(f" >> Counts for the alpha output register: " -f"{pub_result.data.alpha.get_counts()}") -print(f" >> Counts for the beta output register: " -f"{pub_result.data.beta.get_counts()}") -``` - - -### Options - -Options are specified differently in the V2 primitives in these ways: - -- `SamplerV2` and `EstimatorV2` now have separate options classes. You can see the available options and update option values during or after primitive initialization. -- Instead of the `set_options()` method, V2 primitive options have the `update()` method that applies changes to the `options` attribute. -- If you do not specify a value for an option, it is given a special value of `Unset` and the server defaults are used. -- For V2 primitives, the `options` attribute is the `dataclass` Python type. You can use the built-in `asdict` method to convert it to a dictionary. - -See the [API reference](/docs/api/qiskit-ibm-runtime/options) for the list of available options. - -{/*Verified EstimatorV2. 2/23 */} - - - - ```python -from dataclasses import asdict -from qiskit_ibm_runtime import QiskitRuntimeService -from qiskit_ibm_runtime import EstimatorV2 as Estimator - -service = QiskitRuntimeService() -backend = service.least_busy(operational=True, simulator=False) - -# Setting options during primitive initialization -estimator = Estimator(backend, options={"resilience_level": 2}) - -# Setting options after primitive initialization -# This uses auto complete. -estimator.options.default_shots = 4000 -# This does bulk update. -estimator.options.update(default_shots=4000, resilience_level=2) - -# Print the dictionary format. -# Server defaults are used for unset options. -print(asdict(estimator.options)) -``` - - - - ```python -from qiskit_ibm_runtime import QiskitRuntimeService, Sampler, Options - -service = QiskitRuntimeService() -backend = service.least_busy(operational=True, simulator=False) - -# Setting options during primitive initialization -options = Options() -# This uses auto complete. -options.resilience_level = 2 -estimator = Estimator(backend=backend, options=options) - -# Setting options after primitive initialization. -# This does bulk update. -estimator.set_options(shots=4000) -``` - - - - - - ```python -from dataclasses import asdict -from qiskit_ibm_runtime import QiskitRuntimeService -from qiskit_ibm_runtime import SamplerV2 as Sampler - -service = QiskitRuntimeService() -backend = service.least_busy(operational=True, simulator=False) - -# Setting options during primitive initialization -sampler = Sampler(backend, options={"default_shots": 4096}) - -# Setting options after primitive initialization -# This uses auto complete. -sampler.options.dynamical_decoupling.enable = True -# Turn on gate twirling. Requires qiskit_ibm_runtime 0.23.0 or later. -sampler.options.twirling.enable_gates = True - -# This does bulk update. -# The value for default_shots is overridden -# if you specify shots with run() or in the PUB. -sampler.options.update(default_shots=1024, - dynamical_decoupling={"sequence_type": "XpXm"}) - -# Print the dictionary format. -# Server defaults are used for unset options. -print(asdict(sampler.options)) -``` - - - - ```python -from qiskit_ibm_runtime import QiskitRuntimeService, Sampler, Options - -service = QiskitRuntimeService() -backend = service.least_busy(operational=True, simulator=False) - -# Setting options during primitive initialization -options = Options() -# This uses auto complete. -options.resilience_level = 2 -sampler = Sampler(backend=backend, options=options) - -# Setting options after primitive initialization. -# This does bulk update. -sampler.set_options(shots=2000) - ``` - - - - -### Error mitigation and suppression - -* Because Sampler V2 returns samples without postprocessing, it does not support resilience levels. -* Sampler V2 does not support `optimization_level`. -* Estimator V2 will drop support for `optimization_level` on or around 30 September 2024. -* Estimator V2 does not support resilience level 3. This is because resilience level 3 in V1 Estimator uses Probabilistic Error Cancellation (PEC), which is proven to give unbiased results at the cost of exponential processing time. Level 3 was removed to draw attention to that tradeoff. You can, however, still use PEC as the error mitigation method by specifying the `pec_mitigation` option. -* Estimator V2 supports `resilience_level` 0-2, as described in the following table. These options are more advanced than their V1 counterparts. You can also explicitly turn on / off individual error mitigation / suppression methods. - - | Level 1 | Level 2 | - |---------------------------|---------------------------| - | Measurement twirling | Measurement twirling | - | Readout error mitigation | Readout error mitigation | - | | ZNE | - -{/*Verified EstimatorV2. 2/23 */} - - - - ```python -from dataclasses import asdict -from qiskit_ibm_runtime import QiskitRuntimeService -from qiskit_ibm_runtime import EstimatorV2 as Estimator - -service = QiskitRuntimeService() -backend = service.least_busy(operational=True, simulator=False) - -# Setting options during primitive initialization -estimator = Estimator(backend) - -# Set resilience_level to 0 -estimator.options.resilience_level = 0 - -# Turn on measurement error mitigation -estimator.options.resilience.measure_mitigation = True -``` - - - - ```python -from qiskit_ibm_runtime import Estimator, Options - -estimator = Estimator(backend, options=options) - -options = Options() - -options.resilience_level = 2 -``` - - - - - - - - ```python -from qiskit_ibm_runtime import SamplerV2 as Sampler - -sampler = Sampler(backend) -# Turn on dynamical decoupling with sequence XpXm. -sampler.options.dynamical_decoupling.enable = True -sampler.options.dynamical_decoupling.sequence_type = "XpXm" - -print(f">> dynamical decoupling sequence to use: " -f"{sampler.options.dynamical_decoupling.sequence_type}") -``` - - - - - ```python -from qiskit_ibm_runtime import Sampler, Options - -sampler = Sampler(backend, options=options) - -options = Options() - -options.resilience_level = 2 -``` - - - - -### Transpilation - -V2 primitives support only circuits that adhere to the Instruction Set Architecture (ISA) of a particular backend. Because the primitives do not perform layout, routing, and translation operations, the corresponding transpilation options from V1 are not supported. - - -### Job status - -The V2 primitives have a new `RuntimeJobV2` class, which inherits from `BasePrimitiveJob`. The `status()` method of this new class returns a string instead of a JobStatus enum from Qiskit. See the [RuntimeJobV2 API reference](/docs/api/qiskit-ibm-runtime/runtime-job-v2) for details. - - - -```python -job = estimator.run(...) - -# check if a job is still running -print(f"Job {job.job_id()} is still running: {job.status() == "RUNNING"}") -``` - - - -```python -from qiskit.providers.jobstatus import JobStatus - -job = estimator.run(...) - -#check if a job is still running -print(f"Job {job.job_id()} is still running: {job.status() is JobStatus.RUNNING}") -``` - - - -## Steps to migrate to Estimator V2 - -1. Replace `from qiskit_ibm_runtime import Estimator` with `from qiskit_ibm_runtime import EstimatorV2 as Estimator`. -2. Remove any `from qiskit_ibm_runtime import Options` statements, since the `Options` class is not used by V2 primitives. You can instead pass options as a dictionary when initializing the `EstimatorV2` class (for example `estimator = Estimator(backend, options={“dynamical_decoupling”: {“enable”: True}})`), or set them after initialization: - ```python - estimator = Estimator(backend) - estimator.options.dynamical_decoupling.enable = True - ``` - -3. Review all the [supported options](/docs/api/qiskit-ibm-runtime/options) and make updates accordingly. -4. Group each circuit you want to run with the observables and parameter values you want to apply to the circuit in a tuple (a PUB). For example, use `(circuit1, observable1, parameter_set1)` if you want to run `circuit1` with `observable1` and `parameter_set1`. - - 1. You might need to reshape your arrays of observables or parameter sets if you want to apply their outer product. For example, an array of observables of shape (4, 1) and an array of parameter sets of shape (1, 6) will give you a result of (4, 6) expectation values. See the [Numpy broadcasting rules](https://numpy.org/doc/stable/user/basics.broadcasting.html) for more details. - 2. You can optionally specify the precision you want for that specific PUB. -5. Update Estimator's `run()` method to pass in the list of PUBs. For example, `run([(circuit1, observable1, parameter_set1)])`. - You can optionally specify a `precision` here, which would apply to all PUBs. -6. Estimator V2 job results are grouped by PUBs. You can see the expectation value and standard error for each PUB by indexing to it. For example: - ```python - pub_result = job.result()[0] - print(f">>> Expectation values: {pub_result.data.evs}") - print(f">>> Standard errors: {pub_result.data.stds}") - ``` - -## Estimator full examples - -### Run a single experiment - -Use Estimator to determine the expectation value of a single circuit-observable pair. - -{/*Verified EstimatorV2. 2/23 */} - - - - ```python -import numpy as np -from qiskit.circuit.library import IQP -from qiskit.quantum_info import SparsePauliOp, random_hermitian -from qiskit_ibm_runtime import EstimatorV2 as Estimator, QiskitRuntimeService - -service = QiskitRuntimeService() -backend = service.least_busy(operational=True, - simulator=False, min_num_qubits=127) -estimator = Estimator(backend) - -n_qubits = 127 - -mat = np.real(random_hermitian(n_qubits, seed=1234)) -circuit = IQP(mat) -observable = SparsePauliOp("Z" * n_qubits) - -pm = generate_preset_pass_manager(optimization_level=1, backend=backend) -isa_circuit = pm.run(circuit) -isa_observable = observable.apply_layout(isa_circuit.layout) - -job = estimator.run([(isa_circuit, isa_observable)]) -result = job.result() - -print(f" > Expectation value: {result[0].data.evs}") -print(f" > Metadata: {result[0].metadata}") -``` - - - - ```python -import numpy as np -from qiskit.circuit.library import IQP -from qiskit.quantum_info import SparsePauliOp, random_hermitian -from qiskit_ibm_runtime import QiskitRuntimeService, Estimator - -service = QiskitRuntimeService() - -backend = service.least_busy(operational=True, - simulator=False, min_num_qubits=127) - -n_qubits = 127 - -mat = np.real(random_hermitian(n_qubits, seed=1234)) -circuit = IQP(mat) -observable = SparsePauliOp("Z" * n_qubits) - -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) -isa_circuit = pm.run(circuit) -isa_observable = observable.apply_layout(isa_circuit.layout) - -estimator = Estimator(backend) -job = estimator.run(isa_circuit, isa_observable) -result = job.result() - -print(f" > Observable: {observable.paulis}") -print(f" > Expectation value: {result.values}") -print(f" > Metadata: {result.metadata}") -``` - - - -### Run multiple experiments in a single job - -Use Estimator to determine the expectation values of multiple circuit-observable pairs. - -{/*Verified EstimatorV2. 2/23 */} - - - -```python -service = QiskitRuntimeService() -backend = service.least_busy(operational=True, simulator=False) - -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) - -n_qubits = 3 -rng = np.random.default_rng() -mats = [np.real(random_hermitian(n_qubits, seed=rng)) for _ in range(3)] -circuits = [IQP(mat) for mat in mats] -observables = [ - SparsePauliOp("X" * n_qubits), - SparsePauliOp("Y" * n_qubits), - SparsePauliOp("Z" * n_qubits), -] - -isa_circuits = pm.run(circuits) -isa_observables = [ob.apply_layout(isa_circuits[0].layout) for ob in observables] - - -estimator = Estimator(backend) -job = estimator.run([(isa_circuits[0], isa_observables[0]),(isa_circuits[1], - isa_observables[1]),(isa_circuits[2], isa_observables[2])]) -job_result = job.result() -for idx in range(len(job_result)): - pub_result = job_result[idx] - print(f">>> Expectation values for PUB {idx}: {pub_result.data.evs}") - print(f">>> Standard errors for PUB {idx}: {pub_result.data.stds}") -``` - - - -```python -import numpy as np -from qiskit.circuit.library import IQP -from qiskit.quantum_info import SparsePauliOp, random_hermitian -from qiskit_ibm_runtime import QiskitRuntimeService, Estimator - -service = QiskitRuntimeService() - -backend = service.least_busy(operational=True, - simulator=False, min_num_qubits=127) - -n_qubits = 127 - -rng = np.random.default_rng() -mats = [np.real(random_hermitian(n_qubits, seed=rng)) for _ in range(3)] -circuits = [IQP(mat) for mat in mats] -observables = [ - SparsePauliOp("X" * n_qubits), - SparsePauliOp("Y" * n_qubits), - SparsePauliOp("Z" * n_qubits), -] - -# Get ISA circuits - -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) -isa_circuits = pm.run(circuits) -isa_observables = [ob.apply_layout(isa_circuits[0].layout) for ob in observables] - -estimator = Estimator(backend) -job = estimator.run(isa_circuits, isa_observables) -result = job.result() - -print(f" > Expectation values: {result.values}") -``` - - - -### Run parameterized circuits - -Use Estimator to run multiple experiments in a single job, leveraging parameter values to increase circuit reusability. In the following example, notice that steps 1 and 2 are the same for V1 and V2. - - - - ```python -import numpy as np - -from qiskit.circuit import QuantumCircuit, Parameter -from qiskit.quantum_info import SparsePauliOp -from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit_ibm_runtime import QiskitRuntimeService - -# Step 1: Map classical inputs to a quantum problem - -theta = Parameter("θ") - -chsh_circuit = QuantumCircuit(2) -chsh_circuit.h(0) -chsh_circuit.cx(0, 1) -chsh_circuit.ry(theta, 0) - -number_of_phases = 21 -phases = np.linspace(0, 2 * np.pi, number_of_phases) -individual_phases = [[ph] for ph in phases] - -ZZ = SparsePauliOp.from_list([("ZZ", 1)]) -ZX = SparsePauliOp.from_list([("ZX", 1)]) -XZ = SparsePauliOp.from_list([("XZ", 1)]) -XX = SparsePauliOp.from_list([("XX", 1)]) -ops = [ZZ, ZX, XZ, XX] - -# Step 2: Optimize problem for quantum execution. - -service = QiskitRuntimeService() -backend = service.least_busy(operational=True, simulator=False) - -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) -chsh_isa_circuit = pm.run(chsh_circuit) -isa_observables = [operator.apply_layout(chsh_isa_circuit.layout) - for operator in ops] - -from qiskit_ibm_runtime import EstimatorV2 as Estimator - -# Step 3: Execute using Qiskit primitives. - -# Reshape observable array for broadcasting -reshaped_ops = np.fromiter(isa_observables, dtype=object) -reshaped_ops = reshaped_ops.reshape((4, 1)) - -estimator = Estimator(backend, options={"default_shots": int(1e4)}) -job = estimator.run([(chsh_isa_circuit, reshaped_ops, individual_phases)]) -# Get results for the first (and only) PUB -pub_result = job.result()[0] -print(f">>> Expectation values: {pub_result.data.evs}") -print(f">>> Standard errors: {pub_result.data.stds}") -print(f">>> Metadata: {pub_result.metadata}") -``` - - - - ```python -import numpy as np - -from qiskit.circuit import QuantumCircuit, Parameter -from qiskit.quantum_info import SparsePauliOp -from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit_ibm_runtime import QiskitRuntimeService - -# Step 1: Map classical inputs to a quantum problem - -theta = Parameter("θ") - -chsh_circuit = QuantumCircuit(2) -chsh_circuit.h(0) -chsh_circuit.cx(0, 1) -chsh_circuit.ry(theta, 0) - -number_of_phases = 21 -phases = np.linspace(0, 2 * np.pi, number_of_phases) -individual_phases = [[ph] for ph in phases] - -ZZ = SparsePauliOp.from_list([("ZZ", 1)]) -ZX = SparsePauliOp.from_list([("ZX", 1)]) -XZ = SparsePauliOp.from_list([("XZ", 1)]) -XX = SparsePauliOp.from_list([("XX", 1)]) -ops = [ZZ, ZX, XZ, XX] - -# Step 2: Optimize problem for quantum execution. - -service = QiskitRuntimeService() -backend = service.least_busy(operational=True, simulator=False) - -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) -chsh_isa_circuit = pm.run(chsh_circuit) -isa_observables = [operator.apply_layout(chsh_isa_circuit.layout) - for operator in ops] - -from qiskit_ibm_runtime import Estimator - -# Step 3: Execute using Qiskit Primitives. -num_ops = len(isa_observables) - -batch_circuits = [chsh_isa_circuit] * number_of_phases * num_ops -batch_ops = [op for op in isa_observables for _ in individual_phases] -batch_phases = individual_phases * num_ops - -estimator = Estimator(backend, options={"shots": int(1e4)}) -job = estimator.run(batch_circuits, batch_ops, batch_phases) -expvals = job.result().values -``` - - - -### Use sessions and advanced options - -Explore sessions and advanced options to optimize circuit performance on QPUs. - - - -The following code block will return an error for users on the Open Plan because it uses sessions. Workloads on the Open Plan can run only in [job mode](/docs/guides/execution-modes#job-mode) or [batch mode](/docs/guides/execution-modes#batch-mode). - - - -{/*Verified EstimatorV2. 2/23 */} - - - -```python -import numpy as np -from qiskit.circuit.library import IQP -from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit.quantum_info import SparsePauliOp, random_hermitian -from qiskit_ibm_runtime import ( - QiskitRuntimeService, Session, EstimatorV2 as Estimator -) - -n_qubits = 127 - -rng = np.random.default_rng(1234) -mat = np.real(random_hermitian(n_qubits, seed=rng)) -circuit = IQP(mat) -mat = np.real(random_hermitian(n_qubits, seed=rng)) -another_circuit = IQP(mat) -observable = SparsePauliOp("X" * n_qubits) -another_observable = SparsePauliOp("Y" * n_qubits) - -pm = generate_preset_pass_manager(optimization_level=1, backend=backend) -isa_circuit = pm.run(circuit) -another_isa_circuit = pm.run(another_circuit) -isa_observable = observable.apply_layout(isa_circuit.layout) -another_isa_observable = another_observable.apply_layout( - another_isa_circuit.layout -) -service = QiskitRuntimeService() - -backend = service.least_busy(operational=True, - simulator=False, min_num_qubits=127) - -with Session(backend=backend) as session: - estimator = Estimator() - - estimator.options.resilience_level = 1 - - job = estimator.run([(isa_circuit, isa_observable)]) - another_job = estimator.run([( - another_isa_circuit, another_isa_observable - )]) - result = job.result() - another_result = another_job.result() - - # first job - print(f" > Expectation value: {result[0].data.evs}") - print(f" > Metadata: {result[0].metadata}") - - # second job - print(f" > Another Expectation value: " - f"{another_result[0].data.evs}") - print(f" > More Metadata: {another_result[0].metadata}") -``` - - - -```python -import numpy as np -from qiskit.circuit.library import IQP -from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit.quantum_info import SparsePauliOp, random_hermitian -from qiskit_ibm_runtime import QiskitRuntimeService, Session, Estimator, Options - -n_qubits = 127 - -rng = np.random.default_rng(1234) -mat = np.real(random_hermitian(n_qubits, seed=rng)) -circuit = IQP(mat) -mat = np.real(random_hermitian(n_qubits, seed=rng)) -another_circuit = IQP(mat) -observable = SparsePauliOp("X" * n_qubits) -another_observable = SparsePauliOp("Y" * n_qubits) - -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) -isa_circuit = pm.run(circuit) -another_isa_circuit = pm.run(another_circuit) -isa_observable = observable.apply_layout(isa_circuit.layout) -another_isa_observable = another_observable.apply_layout(another_isa_circuit.layout) - -options = Options() -options.optimization_level = 2 -options.resilience_level = 2 - -service = QiskitRuntimeService() - -backend = service.least_busy(operational=True, - simulator=False, min_num_qubits=127) - -with Session(backend=backend) as session: - estimator = Estimator(options=options) - job = estimator.run(isa_circuit, isa_observable) - another_job = estimator.run(another_isa_circuit, another_isa_observable) - result = job.result() - another_result = another_job.result() - -# first job -print(f" > Expectation values job 1: {result.values}") - -# second job -print(f" > Expectation values job 2: {another_result.values}") -``` - - - -## Steps to migrate to Sampler V2 - -1. Replace `from qiskit_ibm_runtime import Sampler` with `from qiskit_ibm_runtime import SamplerV2 as Sampler`. -2. Remove any `from qiskit_ibm_runtime import Options` statements, since the `Options` class is not used by V2 primitives. You can instead pass options as a dictionary when initializing the `SamplerV2` class (for example `sampler = Sampler(backend, options={“default_shots”: 1024})`), or set them after initialization: - ```python - sampler = Sampler(backend) - sampler.options.default_shots = 1024 - ``` -3. Review all the [supported options](/docs/api/qiskit-ibm-runtime/options) and make updates accordingly. -4. Group each circuit you want to run with the observables and parameter values you want to apply to the circuit in a tuple (a PUB). For example, use `(circuit1, parameter_set1)` if you want to run `circuit1` with `parameter_set1`. - You can optionally specify the shots you want for that specific PUB. -5. Update Sampler's `run()` method to pass in the list of PUBs. For example, `run([(circuit1, parameter_set1)])`. - You can optionally specify `shots` here, which would apply to all PUBs. -6. Sampler V2 job results are grouped by PUBs. You can see the output data for each PUB by indexing to it. While Sampler V2 returns unweighted samples, the result class has a convenience method to get counts instead. For example: - ```python - pub_result = job.result()[0] - print(f">>> Counts: {pub_result.data.meas.get_counts()}") - print(f">>> Per-shot measurement: {pub_result.data.meas.get_counts()}") - ``` - - You need the classical register name to get the results. By default, it is named `meas` when you use `measure_all()`. When defining your circuit, if you create one or more classical registers with a non-default name, use that name to get the results. You can find the classical register name by running `.cregs`. For example, `qc.cregs`. - - -## Sampler full examples - -### Run a single experiment - -Use Sampler to determine the counts or quasi-probability distribution of a single circuit. - -{/*Verified 2/28 */} - - -```python -import numpy as np -from qiskit.circuit.library import IQP -from qiskit.quantum_info import random_hermitian -from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler -from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager - -service = QiskitRuntimeService() - -backend = service.least_busy(operational=True, - simulator=False, min_num_qubits=127) - -n_qubits = 127 - -mat = np.real(random_hermitian(n_qubits, seed=1234)) -circuit = IQP(mat) -circuit.measure_all() - -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) -isa_circuit = pm.run(circuit) - -sampler = Sampler(backend) -job = sampler.run([isa_circuit]) -result = job.result() -``` - - - - ```python -import numpy as np -from qiskit.circuit.library import IQP -from qiskit.quantum_info import random_hermitian -from qiskit_ibm_runtime import QiskitRuntimeService, Sampler - -service = QiskitRuntimeService() - -backend = service.least_busy(operational=True, - simulator=False, min_num_qubits=127) - -n_qubits = 127 - -mat = np.real(random_hermitian(n_qubits, seed=1234)) -circuit = IQP(mat) -circuit.measure_all() - -sampler = Sampler(backend) -job = sampler.run(circuit) -result = job.result() - -print(f" > Quasi-probability distribution: {result.quasi_dists}") -print(f" > Metadata: {result.metadata}") - ``` - - - -### Run multiple experiments in a single job - -Use Sampler to determine the counts or quasi-probability distributions of multiple circuits in one job. - -{/*Verified 2/28 */} - - - -```python -import numpy as np -from qiskit.circuit.library import IQP -from qiskit.quantum_info import random_hermitian -from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler - -service = QiskitRuntimeService() - -backend = service.least_busy(operational=True, - simulator=False, min_num_qubits=127) - -n_qubits = 127 - -rng = np.random.default_rng() -mats = [np.real(random_hermitian(n_qubits, seed=rng)) for _ in range(3)] -circuits = [IQP(mat) for mat in mats] -for circuit in circuits: - circuit.measure_all() - -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) -isa_circuits = pm.run(circuits) - -sampler = Sampler(backend) -job = sampler.run(isa_circuits) -result = job.result() - -for idx, pub_result in enumerate(result): - print(f" > Counts for pub {idx}: {pub_result.data.meas.get_counts()}") -``` - - - -```python -import numpy as np -from qiskit.circuit.library import IQP -from qiskit.quantum_info import random_hermitian -from qiskit_ibm_runtime import QiskitRuntimeService, Sampler - -service = QiskitRuntimeService() - -backend = service.least_busy(operational=True, - simulator=False, min_num_qubits=127) - -n_qubits = 127 - -rng = np.random.default_rng() -mats = [np.real(random_hermitian(n_qubits, seed=rng)) for _ in range(3)] -circuits = [IQP(mat) for mat in mats] -for circuit in circuits: - circuit.measure_all() - -sampler = Sampler(backend) -job = sampler.run(circuits) -result = job.result() - -print(f" > Quasi-probability distribution: {result.quasi_dists}") -``` - - - -### Run parameterized circuits - -Run several experiments in a single job, leveraging parameter values to increase circuit reusability. - -{/*Verified 2/28 */} - - - -```python -import numpy as np -from qiskit.circuit.library import RealAmplitudes -from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit_ibm_runtime import QiskitRuntimeService - -# Step 1: Map classical inputs to a quantum problem -num_qubits = 127 -circuit = RealAmplitudes(num_qubits=num_qubits, reps=2) -circuit.measure_all() - -# Define three sets of parameters for the circuit -rng = np.random.default_rng(1234) -parameter_values = [ - rng.uniform(-np.pi, np.pi, size=circuit.num_parameters) for _ in range(3) -] - -# Step 2: Optimize problem for quantum execution. - -service = QiskitRuntimeService() -backend = service.least_busy(operational=True, - simulator=False, min_num_qubits=num_qubits) - -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) -isa_circuit = pm.run(circuit) - -# Step 3: Execute using Qiskit primitives. - -from qiskit_ibm_runtime import SamplerV2 as Sampler - -sampler = Sampler(backend) -job = sampler.run([(isa_circuit, parameter_values)]) -result = job.result() -# Get results for the first (and only) PUB -pub_result = result[0] -# Get counts from the classical register "meas". -print(f" >> Counts for the meas output register: " - f"{pub_result.data.meas.get_counts()}") -``` - - - -```python -import numpy as np -from qiskit.circuit.library import RealAmplitudes -from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager -from qiskit_ibm_runtime import QiskitRuntimeService - -# Step 1: Map classical inputs to a quantum problem -num_qubits = 5 -circuit = RealAmplitudes(num_qubits=num_qubits, reps=2) -circuit.measure_all() - -# Define three sets of parameters for the circuit -rng = np.random.default_rng(1234) -parameter_values = [ - rng.uniform(-np.pi, np.pi, size=circuit.num_parameters) for _ in range(3) -] - -# Step 2: Optimize problem for quantum execution. - -service = QiskitRuntimeService() -backend = service.least_busy(operational=True, - simulator=False, min_num_qubits=num_qubits) - -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) -isa_circuit = pm.run(circuit) - -# Step 3: Execute using Qiskit primitives. - -from qiskit_ibm_runtime import Sampler - -sampler = Sampler(backend) -job = sampler.run([isa_circuit] * 3, parameter_values) -result = job.result() - -print(f" > Quasi-probability distribution: {result.quasi_dists}") -print(f" > Metadata: {result.metadata}") -``` - - - -### Use sessions and advanced options - -Explore sessions and advanced options to optimize circuit performance on QPUs. - - - -The following code block will return an error for users on the Open Plan because it uses sessions. Workloads on the Open Plan can run only in [job mode](/docs/guides/execution-modes#job-mode) or [batch mode](/docs/guides/execution-modes#batch-mode). - - - -{/*Verified 2/28 */} - - - -```python -import numpy as np -from qiskit.circuit.library import IQP -from qiskit.quantum_info import random_hermitian -from qiskit_ibm_runtime import ( - QiskitRuntimeService, SamplerV2 as Sampler, Session - ) - -n_qubits = 127 - -rng = np.random.default_rng(1234) -mat = np.real(random_hermitian(n_qubits, seed=rng)) -circuit = IQP(mat) -circuit.measure_all() -mat = np.real(random_hermitian(n_qubits, seed=rng)) -another_circuit = IQP(mat) -another_circuit.measure_all() - -pm = generate_preset_pass_manager(backend=backend, optimization_level=1) -isa_circuit = pm.run(circuit) -another_isa_circuit = pm.run(another_circuit) - -service = QiskitRuntimeService() - -# Turn on dynamical decoupling with sequence XpXm. -sampler.options.dynamical_decoupling.enable = True -sampler.options.dynamical_decoupling.sequence_type = "XpXm" - -backend = service.least_busy(operational=True, - simulator=False, min_num_qubits=127) - -with Session(backend=backend) as session: - sampler = Sampler() - job = sampler.run([isa_circuit]) - another_job = sampler.run([another_isa_circuit]) - result = job.result() - another_result = another_job.result() - -# first job -print(f" > Counts for job 1: {result[0].data.meas.get_counts()}") - -# second job -print(f" > Counts for job 2: {another_result[0].data.meas.get_counts()}") -``` - - - -```python -import numpy as np -from qiskit.circuit.library import IQP -from qiskit.quantum_info import random_hermitian -from qiskit_ibm_runtime import QiskitRuntimeService, Sampler, Session, Options - -n_qubits = 127 - -rng = np.random.default_rng(1234) -mat = np.real(random_hermitian(n_qubits, seed=rng)) -circuit = IQP(mat) -circuit.measure_all() -mat = np.real(random_hermitian(n_qubits, seed=rng)) -another_circuit = IQP(mat) -another_circuit.measure_all() - -options = Options() -options.optimization_level = 2 -options.resilience_level = 0 - -service = QiskitRuntimeService() - -backend = service.least_busy(operational=True, - simulator=False, min_num_qubits=127) - -with Session(backend=backend) as session: - sampler = Sampler(options=options) - job = sampler.run(circuit) - another_job = sampler.run(another_circuit) - result = job.result() - another_result = another_job.result() - -# first job -print(f" > Quasi-probability distribution job 1: {result.quasi_dists}") - -# second job -print(f" > Quasi-probability distribution job 2: {another_result.quasi_dists}") -``` - - - -## Next steps - - - - Learn more about setting options in the [Sampler options](/docs/guides/sampler-options), [Estimator options](/docs/guides/estimator-options), and [Executor options](/docs/guides/executor-options) guides. - - Learn more details about [Primitive inputs and outputs](/docs/guides/primitive-input-output#pubs). - - Experiment with the [CHSH inequality](/docs/tutorials/chsh-inequality) tutorial. -