Adapters
Adapters provide SDK-specific integration for automatic circuit/program + result + device snapshot capture.
If you haven’t yet, read Uniform Execution Contract (UEC) to understand the ExecutionEnvelope produced by adapters.
Adapters provide SDK-specific integration for automatic circuit and result capture. Each adapter wraps your quantum backend/device/primitive to automatically log circuits, results, and device snapshots.
Installation
Install the adapter for your quantum SDK:
pip install 'devqubit[qiskit]' # IBM Qiskit (local backends, Aer)
pip install 'devqubit[qiskit-runtime]' # IBM Qiskit Runtime (cloud primitives)
pip install 'devqubit[braket]' # Amazon Braket
pip install 'devqubit[cirq]' # Google Cirq
pip install 'devqubit[pennylane]' # Xanadu PennyLane
pip install 'devqubit[cudaq]' # NVIDIA CUDA-Q
# Or install all adapters at once
pip install 'devqubit[all]'
Quick Start
All adapters follow the same pattern: wrap your executor with run.wrap():
from devqubit import track
with track(project="my-experiment") as run:
backend = run.wrap(your_backend) # Wrap once
job = backend.run(circuit) # Use normally
result = job.result() # Results auto-logged
Uniform Execution Contract (UEC)
All adapters produce a standardized ExecutionEnvelope (devqubit.envelope/1.0 schema) containing four canonical snapshots:
Snapshot |
Schema |
Description |
|---|---|---|
|
— |
Adapter and SDK version info (name, adapter, sdk_version, frontends) |
|
|
Logical and physical circuit artifacts with structural/parametric hashes |
|
|
Backend state, calibration, topology, and provider properties |
|
|
Submission metadata, transpilation info, shots, job IDs |
|
|
Normalized measurement counts, quasi-probabilities, or expectation values |
The envelope is automatically logged as devqubit.envelope.json with role envelope. This provides a complete, self-contained record of each execution that can be used for reproducibility, comparison (diff), and verification (verify_baseline).
Adapter contract:
Emit a schema-valid envelope for every execution (success or failure).
On failure, set
result.success=falsewith structuredresult.error.Store large payloads (raw results, backend properties) as artifacts via
ArtifactRef.Normalize bitstrings to canonical
cbit0_rightformat for cross-SDK comparison.
# Access envelope data after execution
run.record["execute"] # Execution metadata
run.record["device_snapshot"] # Device info summary
run.record["execution_stats"] # Aggregate statistics
Qiskit
For local backends and Aer simulators.
from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator
from devqubit import track
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
with track(project="bell-state") as run:
backend = run.wrap(AerSimulator())
transpiled = transpile(qc, backend)
job = backend.run(transpiled, shots=1000)
result = job.result()
# All artifacts captured automatically:
# - QPY binary, OpenQASM 3, circuit diagrams
# - Measurement counts
# - Backend snapshot
Captured Artifacts
Artifact |
Kind |
Role |
|---|---|---|
QPY binary |
|
|
OpenQASM 3 |
|
|
Circuit diagram |
|
|
Counts |
|
|
Full result |
|
|
Raw backend properties |
|
|
Execution envelope |
|
|
Qiskit Runtime
For IBM Quantum cloud primitives (Sampler, Estimator).
from qiskit import QuantumCircuit
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2
from devqubit import track
service = QiskitRuntimeService()
backend = service.backend("ibm_brisbane")
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.measure_all()
with track(project="runtime-experiment") as run:
sampler = run.wrap(SamplerV2(backend))
job = sampler.run([qc])
result = job.result()
Transpilation Modes
The Runtime adapter supports automatic ISA compatibility checking:
# Auto mode (default): transpile only if needed
sampler = run.wrap(SamplerV2(backend))
job = sampler.run([qc], devqubit_transpilation_mode="auto")
# Manual mode: you handle transpilation
job = sampler.run([isa_circuit], devqubit_transpilation_mode="manual")
# With custom transpilation options
job = sampler.run([qc],
devqubit_transpilation_mode="auto",
devqubit_transpilation_options={"optimization_level": 2}
)
Captured Artifacts
Artifact |
Kind |
Role |
|---|---|---|
QPY binary |
|
|
Transpiled QPY |
|
|
OpenQASM 3 |
|
|
PUB structure |
|
|
Sampler counts |
|
|
Estimator values |
|
|
Raw runtime properties |
|
|
Execution envelope |
|
|
Amazon Braket
from braket.circuits import Circuit
from braket.devices import LocalSimulator
from devqubit import track
circuit = Circuit().h(0).cnot(0, 1)
with track(project="braket-experiment") as run:
device = run.wrap(LocalSimulator())
task = device.run(circuit, shots=1000)
result = task.result()
Captured Artifacts
Artifact |
Kind |
Role |
|---|---|---|
OpenQASM 3 |
|
|
Circuit diagram |
|
|
Counts |
|
|
Raw result |
|
|
Raw device properties |
|
|
Execution envelope |
|
|
Google Cirq
import cirq
from devqubit import track
q0, q1 = cirq.LineQubit.range(2)
circuit = cirq.Circuit([
cirq.H(q0),
cirq.CNOT(q0, q1),
cirq.measure(q0, q1, key="m"),
])
with track(project="cirq-experiment") as run:
simulator = run.wrap(cirq.Simulator())
result = simulator.run(circuit, repetitions=1000)
Parameter Sweeps
import sympy
theta = sympy.Symbol("theta")
circuit = cirq.Circuit([
cirq.Ry(theta).on(q0),
cirq.measure(q0, key="m"),
])
with track(project="sweep") as run:
simulator = run.wrap(cirq.Simulator())
sweep = cirq.Linspace("theta", 0, 2 * 3.14159, 10)
results = simulator.run_sweep(circuit, sweep, repetitions=100)
Captured Artifacts
Artifact |
Kind |
Role |
|---|---|---|
Cirq JSON |
|
|
Circuit diagram |
|
|
Counts |
|
|
Raw device properties |
|
|
Execution envelope |
|
|
PennyLane
PennyLane uses in-place device patching for QNode compatibility.
import pennylane as qml
from devqubit import track
dev = qml.device("default.qubit", wires=2)
@qml.qnode(dev)
def circuit(params):
qml.RX(params[0], wires=0)
qml.CNOT(wires=[0, 1])
return qml.expval(qml.PauliZ(0))
with track(project="vqe") as run:
tracked_dev = run.wrap(dev)
# QNodes using this device are automatically tracked
for step in range(100):
result = circuit([step * 0.1])
Captured Artifacts
Artifact |
Kind |
Role |
|---|---|---|
Tape JSON |
|
|
Tape diagram |
|
|
Results |
|
|
Raw device properties |
|
|
Execution envelope |
|
|
Multi-Layer Stack
PennyLane acts as a frontend to multiple execution providers. When using external backends, the adapter captures the full stack:
# Braket backend through PennyLane
dev = qml.device("braket.aws.qubit", wires=2, device_arn="...")
# Qiskit backend through PennyLane
dev = qml.device("qiskit.remote", wires=2, backend="ibm_brisbane")
The device snapshot includes:
Frontend config: PennyLane device settings (shots, diff_method, interface)
Resolved backend: Underlying Braket/Qiskit device topology and calibration
NVIDIA CUDA-Q
import cudaq
from devqubit import track
@cudaq.kernel
def bell():
q = cudaq.qvector(2)
h(q[0])
x.ctrl(q[0], q[1])
mz(q)
with track(project="cudaq-experiment") as run:
executor = run.wrap(cudaq)
result = executor.sample(bell, shots_count=1000)
Observe (Expectation Values)
from cudaq import spin
hamiltonian = spin.z(0)
with track(project="cudaq-vqe") as run:
executor = run.wrap(cudaq)
result = executor.observe(bell, hamiltonian)
print(result.expectation())
The adapter captures the spin operator representation and type in execution_options for reproducibility.
Captured Artifacts
Artifact |
Kind |
Role |
|---|---|---|
Kernel JSON |
|
|
Kernel diagram |
|
|
MLIR (Quake) |
|
|
QIR |
|
|
Counts / expectation |
|
|
Execution envelope |
|
|
Performance Optimization
For training loops with thousands of executions, use sampling to reduce logging overhead:
with track(project="qml-training") as run:
# Default: log first execution only (fastest)
backend = run.wrap(device)
# Log every 100th execution
backend = run.wrap(device, log_every_n=100)
# Log all executions (slowest)
backend = run.wrap(device, log_every_n=-1)
# Disable new circuit detection
backend = run.wrap(device, log_new_circuits=False)
# Control stats update frequency
backend = run.wrap(device, stats_update_interval=500)
Parameters
Parameter |
Default |
Description |
|---|---|---|
|
|
|
|
|
Auto-log when circuit structure changes |
|
|
Update execution stats every N runs |
Execution Statistics
When using sampling, the tracker records aggregate statistics:
run.record["execution_stats"]
# {
# "total_executions": 10000,
# "logged_executions": 15,
# "unique_circuits": 3,
# "logged_circuits": 3,
# "last_execution_at": "2024-01-15T10:30:00Z"
# }
Common Patterns
Logging Compile Options
with track(project="test") as run:
compile_options = {
"optimization_level": 3,
"routing_method": "sabre",
"seed_transpiler": 42,
}
for key, value in compile_options.items():
run.log_param(key, value)
transpiled = transpile(qc, backend, **compile_options)
Multi-Circuit Batches
circuits = [circuit_1, circuit_2, circuit_3]
with track(project="batch") as run:
backend = run.wrap(AerSimulator())
job = backend.run(circuits, shots=1000)
result = job.result()
# All circuits and results captured automatically
Tagging Experiments
with track(project="vqe") as run:
run.set_tag("algorithm", "VQE")
run.set_tag("ansatz", "EfficientSU2")
run.set_tag("optimizer", "COBYLA")
backend = run.wrap(device)
# ... run experiment
Using Base Engine
If no adapter exists for your SDK, use the base engine directly:
from devqubit import track
with track(project="custom-sdk") as run:
run.log_param("shots", 1000)
run.set_tag("sdk", "custom")
# Log circuit as bytes
circuit_ref = run.log_bytes(
kind="custom.circuit",
data=circuit_bytes,
media_type="application/octet-stream",
role="program",
)
# Run your experiment
result = custom_sdk.run(circuit)
# Log results as JSON
run.log_json(
name="counts",
obj={"00": 500, "11": 500},
role="result",
kind="result.counts.json",
)
Creating Custom Envelopes
For full UEC compliance, create an ExecutionEnvelope manually. The envelope will be validated against devqubit.envelope/1.0 schema:
from devqubit.uec import (
ExecutionEnvelope,
ExecutionSnapshot,
DeviceSnapshot,
ProgramSnapshot,
ResultSnapshot,
ResultItem,
ProducerInfo,
)
from devqubit.utils import utc_now_iso
# Producer identifies your adapter
producer = ProducerInfo.create(
adapter="devqubit-custom",
adapter_version="0.1.0",
sdk="custom-sdk",
sdk_version="1.0.0",
frontends=["custom-sdk"],
)
# Device snapshot (devqubit.device_snapshot/1.0)
device = DeviceSnapshot(
captured_at=utc_now_iso(),
backend_name="custom_device",
backend_type="simulator",
provider="local",
)
# Program snapshot (devqubit.program_snapshot/1.0)
# For adapter runs, structural_hash and parametric_hash are required
program = ProgramSnapshot(
logical=[], # Add ProgramArtifact refs here
physical=[],
structural_hash="sha256:...",
parametric_hash="sha256:...",
num_circuits=1,
)
# Execution snapshot (devqubit.execution_snapshot/1.0)
execution = ExecutionSnapshot(
submitted_at=utc_now_iso(),
shots=1000,
)
# Result snapshot (devqubit.result_snapshot/1.0)
result = ResultSnapshot.create_success(
items=[
ResultItem(
item_index=0,
success=True,
counts={
"counts": {"00": 500, "11": 500},
"shots": 1000,
"format": {
"source_sdk": "custom-sdk",
"source_key_format": "little_endian",
"bit_order": "cbit0_right",
"transformed": False,
},
},
)
],
)
# Create envelope using factory method (auto-generates envelope_id and created_at)
envelope = ExecutionEnvelope.create(
producer=producer,
device=device,
program=program,
execution=execution,
result=result,
)
# Validate before logging (optional but recommended)
validation = envelope.validate_schema()
if not validation.ok:
print(f"Validation errors: {validation.errors}")
# Log the envelope
run.log_json(
name="execution_envelope",
obj=envelope.to_dict(),
role="envelope",
kind="devqubit.envelope.json",
)
Schema requirements for adapter runs (producer.adapter != "manual"):
programandexecutionmust existprogram.structural_hashandprogram.parametric_hashare requiredIf
program.physicalexists,executed_structural_hashandexecuted_parametric_hashare also required
Adapter API Reference
All adapters implement the same interface:
class Adapter:
name: str # Adapter identifier
def supports_executor(self, executor: Any) -> bool:
"""Check if executor is supported."""
def describe_executor(self, executor: Any) -> dict:
"""Get executor description."""
def wrap_executor(
self,
executor: Any,
tracker: Run,
*,
log_every_n: int = 0,
log_new_circuits: bool = True,
stats_update_interval: int = 1000,
**kwargs,
) -> TrackedExecutor:
"""Wrap executor with tracking."""