Covalent Quantum Electrons (QElectrons) mark a unique development in the quantum computing software stack. QElectrons facilitate scalable, concurrent, and distributed execution of circuit calculations. As an extension of Covalent's orchestration capabilities, QElectrons provide granular control over quantum resources within standard workflow Electrons, mirroring the functionality provided for classical resources. This system brings observability, caching, monitoring, and other useful features from scalable classical machine learning stacks into the quantum context.
Designed to ease the integration of quantum tasks into production-level workflows, QElectrons provide a robust framework for efficiently building, managing, and scaling quantum workflows. Whether orchestrating a large-scale quantum computation or running hybrid quantum-classical algorithms, QElectrons equip users with the tools necessary to optimize quantum tasks. This intuitive approach simplifies workflow management, letting users to concentrate on the details of their quantum algorithms, while easing the process of quantum resource management.
Before diving into the details, let’s summarize some key features.
- Flexible Hybrid Computation: By using QElectrons inside Electrons, users can easily execute quantum and classical sub-tasks on arbitrary backends. This flexibility enables efficient and effective hybrid algorithms by allowing for seamless integration of different classical compute resources; including AWS, GCP, Azure, and even on-premise systems like high-performance computing (HPC) clusters.
- Quantum Resource Clustering: Users can leverage the power of multiple quantum computers by forming Quantum Clusters across different resource providers. This lets users distribute their quantum tasks over a range of vendor machines for faster and more efficient execution.
- Custom Quantum Scheduling: Using Quantum Clusters also let users write their own scheduling or selector algorithms for QElectrons. This means users can dynamically choose which circuits go where, just in time. A user-friendly UX allows for optimal allocation of resources on a circuit-by-circuit basis.
- Asynchronous Circuit Execution: QElectrons introduces non-blocking calls to Pennylane quantum circuits. This enables the initiation of multiple circuit executions without the need to wait for results, streamlining the process and improving overall computational efficiency.
- Caching and Reproducibility: QElectrons expand Covalent’s data caching to include quantum resources. This means that results and metadata from costly QPU calls can be stored for re-use or detailed analysis at a later time. This feature not only enhances reproducibility but also optimizes resource utilization.
- UI Integration: Seamless integration with the Covalent UI brings task monitoring and data inspection to quantum resources.
- Unified Token Management: Covalent simplifies the task of token management by providing a single, user-friendly interface. This feature allows users to effortlessly maintain all their vendor specific tokens in one place.
QElectrons are accessed through the
@ct.qelectron decorator, which functions like a wrapper for Pennylane’s
QNode. For example, the following code uses a
QiskitExecutor instance to execute a QElectron on Qiskit Runtime. See the Quantum Executors section for more information.
import covalent as ct
import pennylane as qml
# Define a Qiskit Runtime executor.
qiskit_sampler = ct.executor.QiskitExecutor(
# Create a QElectron that executes on Qiskit Runtime.
@qml.qnode(qml.device("default.qubit", wires=2, shots=1024))
- QElectrons, for now, always decorate a Pennylane
Using Pennylane is not a prerequisite for all quantum workflows in Covalent. However, to utilize QElectron features such as Quantum Clusters, Pennylane is required. Traditional Qiskit and other circuits can still be used as simple classical tasks inside Electrons as demonstrated here.
- QElectrons can be used inside Electrons, but they are not workflow tasks themselves.
The minimal Covalent workflow below uses the QElectron
circuit() (declared above) inside the task
run_qiskit_experiment(). Using the
qiskit_sampler Quantum Executor, this workflow runs six random circuits concurrently on the
# Create Electrons for a simple workflow.
return random.uniform(0, 3.14159)
# Workflow task that uses the `circuit` QElectron.
results = 
# Six independent experiments.
for _ in range(6):
x = generate_rand_input()
# Dispatch workflow.
dispatch_id = ct.dispatch(workflow)()
results = ct.get_result(dispatch_id, wait=True).result
QElectrons themselves can also handle a simpler use case than one above. For example, the QElectron’s
run_later() method can be used to asynchronously evaluate multiple circuits.
# Immediately submit 6 circuits for concurrent execution.
futures = [circuit.run_later(generate_rand_input()) for _ in range(6)]
# Retrieve results as required.
results = [future.result() for future in futures]
# NOTE: Users can multiply this by using the patten inside multiple Electron.
One or more Quantum Executors can be passed to the
@ct.qelectron decorator via the
executors argument. Multiple executors are specified by using Quantum Clusters.
If no executors are specified, the thread-based local
Simulator is used by default. This means that the following cases are equivalent:
# Case 1 - default local QElectron.
# Case 2 - equivalent local QElectron.
simulator_exec = ct.executors.Simulator(parallel="thread", workers=10)
QiskitExecutor lets the QElectron send circuits to Qiskit Runtime, to be executed on IBM Quantum systems and simulators. The
QiskitExecutor also supports a local mode of operation.
# Execute on "ibmq_quito" using Qiskit Runtime's `Sampler` primitive.
qiskit_exec_quito = ct.executor.QiskitExecutor(
ibmqx_token="<token>", # can be omitted if declared in covalent config.
# Execute on "ibmq_manila" ...
qiskit_exec_manila = ct.executor.QiskitExecutor(
ibmqx_token="<token>", # can be omitted ...
# Execute locally using Qiskit's `Sampler` primitive.
qiskit_exec_local = ct.executor.QiskitExecutor(
Unlike the thread-based
QiskitExecutor relies on
asyncio for concurrency, giving us extreme scalability. Defaults for some of the settings above can also be specified using the Covalent UI.
Multiple Quantum Executors can be specified for a QElectron by using a
ct.executor.QCluster instance as the executor. A default
QCluster is implicitly created if a list of executors is passed to the
ct.qelectron. Such a cluster will utilize the
"cyclic" selector by default.
# Create a QElectron with a `QCluster` executor.
More explicitly, the previous code snippet is equivalent to the following.
# Define a `QCluster` over the "ibmq_quito" and "ibmq_manila" backends.
qiskit_cluster = ct.executors.QCluster(
# Create a QElectron with a `QCluster` executor.
When using the QElectron, the selector is applied on both normal and asynchronous calls. For example, the behaviour of the
"cyclic" selector on a list of two executors is shown below.
>>> # normal
>>> result_1 = circuit(x1) # circuit(s) submitted to "ibmq_quito"
# waiting for `result_1` ...
>>> # asynchronous
>>> future_1 = circuit.run_later(x1) # circuit(s) submitted to "ibmq_manila"
>>> future_2 = circuit.run_later(x2) # circuit(s) submitted to "ibmq_quito"
>>> result_1 = future_1.result() # waiting for `result_1` ...
Valid string values correspond to built-in
(default) a selector that cycles through the list of executors
a selector that randomly chooses an executor for each circuit
Users can also apply custom selector logic to pick from a list of executors, on a per-circuit basis. A valid callable selector must accept two positional arguments (below) and return only one executor.
the list of Quantum Executors associated with the