Skip to main contentIBM Quantum Documentation Preview

Migrate to the Qiskit Runtime V2 primitives

Warning

The original primitives (referred to as the V1 primitives), V1 Sampler and V1 Estimator, have been deprecated in qiskit-ibm-runtime 0.23. Their support will be 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 in the Qiskit 1.0 feature changes page. See StatevectorSampler and 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 and 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 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 and SamplerV2 API reference for full details.


Major changes

Import

For backward compatibility, you must explicity import the V2 primitives. Specifying import <primitive>V2 as <primitive> is not required, but makes it easier to transition code to V2.

Warning

After the V1 primitives are no longer supported, import <primitive> will import the V2 version of the specified primitive.

from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit_ibm_runtime import SamplerV2 as 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: (<circuit>, <parameter values>, <shots>), where <parameter values>and <shots> are optional.
  • Estimator V2 PUB format: (<circuit>, <observables>, <parameter values>, <precision>), where <parameter values>and <precision> are optional. Numpy broadcasting rules(opens in a new tab) 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():

# 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():

# 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 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 <circuit_name>.cregs. For example, qc.cregs.

    # Define a quantum circuit with 2 qubits
    circuit = QuantumCircuit(2)
    circuit.h(0)
    circuit.cx(0, 1)
    circuit.measure_all()
    circuit.draw()
            ┌───┐      ░ ┌─┐
       q_0: ┤ H ├──■───░─┤M├───
            └───┘┌─┴─┐ ░ └╥┘┌─┐
       q_1: ─────┤ X ├─░──╫─┤M├
                 └───┘ ░  ║ └╥┘
    meas: 2/══════════════╩══╩═
                          0  1

Estimator examples (input and output)

# 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

Sampler examples (input and output)

  # 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()

Example that uses different output registers

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: {pub_result.data.alpha.get_counts()}")
print(f" >> Counts for the beta output register: {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 for the list of available options.

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))
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))

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 1Level 2
    Measurement twirlingMeasurement twirling
    Readout error mitigationReadout error mitigation
    ZNE
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
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: {sampler.options.dynamical_decoupling.sequence_type}")

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 for details.

job = estimator.run(...)
 
# check if a job is still running
print(f"Job {job.job_id()} is still running: {job.status() == "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 Estimator class (for example estimator = Estimator(backend, options={“dynamical_decoupling”: {“enable”: True}})), or set them after initialization:

    estimator = Estimator(backend)
    estimator.options.dynamical_decoupling.enable = True
  3. Review all the supported 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.

  5. 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(opens in a new tab) for more details.

  6. You can optionally specify the precision you want for that specific PUB.

  7. Update the estimator 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.

  8. 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:

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.

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}")

Run multiple experiments in a single job

Use Estimator to determine the expectation values of multiple circuit-observable pairs.

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}")

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.

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}")

Use sessions and advanced options

Explore sessions and advanced options to optimize circuit performance on QPUs.

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(service=service, 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: {another_result[0].data.evs}")
    print(f" > More Metadata: {another_result[0].metadata}")

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 Sampler class (for example sampler = Sampler(backend, options={“default_shots”: 1024})), or set them after initialization:
    sampler = Sampler(backend)
    sampler.options.default_shots = 1024
  3. Review all the supported 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 the sampler 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:
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()}")
Note

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 <circuit_name>.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.

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()

Run multiple experiments in a single job

Use Sampler to determine the counts or quasi-probability distributions of multiple circuits in one job.

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()}")

Run parameterized circuits

Run several experiments in a single job, leveraging parameter values to increase circuit reusability.

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: {pub_result.data.meas.get_counts()}")

Use sessions and advanced options

Explore sessions and advanced options to optimize circuit performance on QPUs.

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(service=service, 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()}")