Runtime Options
Settings applied at the device level that control the behavior of a device.run call
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:
| Option | Default | Type | Description |
|---|---|---|---|
transpile | True | bool | Convert the program to the device’s target program type |
transform | True | bool | Apply device-specific passes (e.g. gate decomposition, topology mapping) |
validate | ValidationLevel.RAISE | ValidationLevel or int | Verify the program satisfies device constraints |
prepare | True | bool | Serialize 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 ─> submitDisabling 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.
API Reference:
qbraid.runtime.RuntimeOptions ↗
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.
Quobly
The QbraidDevice.transform method is a no-op for qbraid:quobly:sim:alloy-forge. Programs reach
Quobly’s Alloy Forge emulator exactly as written, and decomposition into the native
RX / RY / RZ / RZZ gate set happens on Quobly’s side after submission. An h/cx circuit is
therefore submitted as h/cx.
One consequence is worth knowing before you submit: because no coupling map is published for this device, neither the transform nor the validate step routes a two-qubit gate between non-adjacent qubits on its linear array. Such a gate still runs and still returns results under the indices you wrote, but satisfying the connectivity costs extra native two-qubit gates, which the Pioneer noise model charges for. See Quobly Alloy Forge.
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:
| Level | Value | Behavior |
|---|---|---|
ValidationLevel.NONE | 0 | No validation is performed |
ValidationLevel.WARN | 1 | Warnings are issued if validation fails |
ValidationLevel.RAISE | 2 | Exceptions are raised if validation fails (default) |
API Reference:
qbraid.runtime.ValidationLevel ↗
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:cepheus-1-108q")
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 IRProvider-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"]
# 42Adding 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" # ValueErrorIf 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"]
# 0This is what happens internally when you pass options to a QuantumDevice constructor, as
shown in the IBM pass_manager example.
Thanks for your feedback.

