Skip to main content

Overview

Every QuantumDevice has a set of runtime options that control the behavior of the device.run() call. These options govern which steps in the job submission pipeline are enabled, and can be updated using device.set_options(). The four default options correspond to the pipeline steps that precede job submission:
OptionDefaultTypeDescription
transpileTrueboolConvert the program to the device’s target program type
transformTrueboolApply device-specific passes (e.g. gate decomposition, topology mapping)
validateValidationLevel.RAISEValidationLevel or intVerify the program satisfies device constraints
prepareTrueboolSerialize the program to the submission IR format
device.set_options(transform=False)
device.set_options(validate=False)
Each option corresponds to a step in the apply_runtime_profile pipeline invoked by device.run():
run_input ─> transpile ─> transform ─> validate ─> prepare ─> submit
Disabling an option skips that step entirely. For example, setting transform=False means the program will not undergo any device-specific gate set transformations before validation.

Transform

The QuantumDevice.transform step applies device-specific transformations to the program. This is where gate decompositions, topology mapping, and other hardware-aware compilation passes are performed. The base class implementation is a no-op — each provider overrides transform() with its own logic. Setting transform=False skips this step:
device.set_options(transform=False)

IonQ

The IonQDevice.transform method decomposes the input OpenQASM program into the IonQ native gate set. It loads the program, applies gate mappings (single-qubit, two-qubit, and three-qubit), and falls back to pyqasm.unroll() if the initial transformation fails:
from qbraid.runtime import QbraidProvider

provider = QbraidProvider()

device = provider.get_device("ionq:ionq:sim:simulator")

# By default, transform=True, so the circuit is automatically
# decomposed into the IonQ native gate set (gpi, gpi2, ms, zz, etc.)
job = device.run(qasm_program, shots=100)
To skip the IonQ gate decomposition and submit the program as-is:
device.set_options(transform=False)

job = device.run(qasm_program, shots=100)

IBM

The QiskitBackend.transform method runs the program through a Qiskit PassManager to produce an ISA (Instruction Set Architecture) circuit compatible with the target backend. By default, it uses generate_preset_pass_manager(backend=...), but you can supply your own:
from qiskit.transpiler import PassManager

device.set_options(pass_manager=my_custom_pass_manager)
See the Provider-Specific Options section for more details on the pass_manager option.

AWS

The BraketDevice.transform method applies provider-aware transformations. For IonQ devices accessed through Amazon Braket, it routes the circuit through pytket for an expanded gate set, then applies Braket-specific transformations. For other Braket-supported devices, it applies standard device transformations through the load_program interface.

Validate

The validate step verifies that the run input satisfies all criteria for submission to the target device. This includes checks such as ensuring the number of qubits in the circuit does not exceed the device’s capacity, and that the program conforms to the device’s ProgramSpec. Validation behavior is controlled by the ValidationLevel enum:
LevelValueBehavior
ValidationLevel.NONE0No validation is performed
ValidationLevel.WARN1Warnings are issued if validation fails
ValidationLevel.RAISE2Exceptions are raised if validation fails (default)

Validation in action

By default, validation is set to RAISE. If a program violates device constraints, a ProgramValidationError is raised:
from qbraid.runtime import QbraidProvider

provider = QbraidProvider()

device = provider.get_device("aws:rigetti:qpu:ankaa-3")

job = device.run(program, shots=100)
# ProgramValidationError: Number of qubits in the circuit (100) exceeds the device's capacity (84).

Disabling validation

To skip validation entirely, set the validation level to NONE. You can use either the enum or pass False as a shorthand:
from qbraid.runtime import ValidationLevel

# Using the enum
device.set_options(validate=ValidationLevel.NONE)

# Or using the bool shorthand
device.set_options(validate=False)

job = device.run(program, shots=100)

Downgrading to warnings

To receive warnings instead of errors, set the validation level to WARN:
device.set_options(validate=ValidationLevel.WARN)

job = device.run(program, shots=100)
# UserWarning: Number of qubits in the circuit (100) exceeds the device's capacity (84).

Re-enabling validation

To restore the default strict validation behavior:
# Using the enum
device.set_options(validate=ValidationLevel.RAISE)

# Or using the bool shorthand
device.set_options(validate=True)

Prepare

The prepare step serializes the quantum program into the intermediate representation (IR) required for submission to the target device’s API. This might produce a JSON payload, an OpenQASM string, bytecode, or any other format expected by the provider. The base class implementation delegates to ProgramSpec.serialize(), which is defined per-provider. For example, Pasqal Pulser sequences are serialized into a JSON-based abstract representation, and OpenQASM programs may be wrapped into a Program object with format and data fields. Setting prepare=False skips this step, leaving the program in its current in-memory representation:
device.set_options(prepare=False)
This can be useful during debugging or when you want to inspect the transformed program before it gets serialized:
# Disable prepare and submit to inspect the post-transform program
device.set_options(prepare=False)

# apply_runtime_profile runs transpile, transform, and validate
# but skips serialization
processed = device.apply_runtime_profile(program)
print(type(processed))  # Still a circuit object, not serialized IR

Provider-Specific Options

Providers can extend the default options with their own fields. These additional options are merged into the device’s configuration at initialization and follow the same set_options interface.

IBM pass_manager

The QiskitBackend adds a pass_manager option that accepts a Qiskit PassManager instance (or None). When set, the custom pass manager is used during the transform step instead of the default preset pass manager:
from qiskit.transpiler import PassManager
from qiskit.transpiler.passes import Optimize1qGates, CXCancellation

# Create a custom pass manager
custom_pm = PassManager([Optimize1qGates(), CXCancellation()])

device.set_options(pass_manager=custom_pm)

job = device.run(circuit, shots=100)
Reset to the default behavior by setting pass_manager back to None:
device.set_options(pass_manager=None)
The pass_manager option includes a built-in validator that only accepts None or a PassManager instance. Attempting to set it to any other type raises a ValueError.

Advanced: RuntimeOptions

Under the hood, device options are managed by the RuntimeOptions class, a dataclass-like container with dictionary-style access, dynamic field support, and custom validators. While device.set_options() is the primary interface for users, understanding RuntimeOptions can be useful when writing a new provider.

Initializing with custom fields

from qbraid.runtime.options import RuntimeOptions

options = RuntimeOptions(transpile=True, custom_field=42)

options["custom_field"]
# 42

Adding dynamic fields

Fields can be added at any time using attribute or dictionary syntax:
options["new_field"] = "hello"
options.another_field = 99

print(options)
# RuntimeOptions(transpile=True, custom_field=42, new_field='hello', another_field=99)

Removing dynamic fields

Dynamic (non-default) fields can be deleted. Default fields cannot be removed:
del options["new_field"]       # OK - dynamic field
del options["another_field"]   # OK - dynamic field
del options["transpile"]       # KeyError: Cannot delete default field 'transpile'.

Setting validators

Validators are callables that return True for valid values. Once set, every update to the field is checked against the validator:
options = RuntimeOptions(threshold=0.5)
options.set_validator("threshold", lambda x: isinstance(x, float) and 0 <= x <= 1)

options.threshold = 0.8   # OK
options.threshold = 1.5   # ValueError: Value '1.5' is not valid for field 'threshold'.
options.threshold = "abc" # ValueError
If a validator already exists and the current value does not satisfy the new validator, a ValueError is raised. You must update or delete the field first before replacing the validator:
options = RuntimeOptions(count=5)
options.set_validator("count", lambda x: x > 0)

# This will raise because the current value 5 does not satisfy the new validator
options.set_validator("count", lambda x: x > 10)
# ValueError: Existing value '5' for field 'count' is not valid for the new validator.

Merging options

Providers use merge() to combine custom options with the device defaults. The override_validators parameter controls whether validators from the merged options replace existing ones:
defaults = RuntimeOptions(transform=True, validate=2)
custom = RuntimeOptions(validate=0)

defaults.merge(custom, override_validators=False)

defaults["validate"]
# 0
This is what happens internally when you pass options to a QuantumDevice constructor, as shown in the IBM pass_manager example.