Building a VQE Optimization
This tutorial walks you through building a Variational Quantum Eigensolver (VQE) workflow that uses parallel task execution to measure multiple Pauli operators simultaneously. You will learn how Marqov automatically detects independent tasks and runs them in parallel.
By the end, you will have:
- Built a 5-task VQE workflow with 3 parallel measurement tasks
- Understood how execution levels and the dependency graph work
- Used
_summaryto produce dashboard cards for your results
Prerequisites
- Completed the Your First Quantum Script tutorial
- Basic understanding of VQE (variational parameters, expectation values, Hamiltonians)
What We Are Building
A VQE workflow that evaluates the energy of a parameterized ansatz circuit. The workflow has 5 tasks arranged across 3 execution levels:
Level 0: build_ansatz (1 task, runs first)
Level 1: measure_zz, measure_zi, measure_iz (3 tasks, run in parallel)
Level 2: compute_energy (1 task, runs after all measurements complete)The 3 measurement tasks at Level 1 are independent of each other — they all depend only on build_ansatz. Marqov detects this and runs them concurrently, reducing total execution time.
Step 1: Define the Ansatz Builder
The first task builds a parameterized circuit based on a rotation angle theta:
from marqov import task, workflow
import math
@task
def build_ansatz(theta):
"""Build a parameterized circuit (Level 0)."""
return {
"gate_sequence": [
{"gate": "ry", "qubit": 0, "angle": theta},
{"gate": "cx", "qubit": [0, 1]},
{"gate": "ry", "qubit": 1, "angle": theta * 0.5},
],
"n_qubits": 2,
"theta": theta,
}This task returns a dictionary describing the circuit. It runs at Level 0 because it has no dependencies on other tasks.
Note that @task is used without parentheses here. When you don’t need to configure retries or timeouts, you can use the bare decorator. For tasks that call external APIs, you would use @task(retries=3, timeout=3600).
Step 2: Define the Measurement Tasks
Next, define three measurement tasks. Each measures a different Pauli operator:
@task
def measure_zz(circuit):
"""Measure ZZ expectation value (Level 1 - parallel)."""
theta = circuit["theta"]
return {"operator": "ZZ", "expectation": math.cos(theta) * 0.5}
@task
def measure_zi(circuit):
"""Measure ZI expectation value (Level 1 - parallel)."""
theta = circuit["theta"]
return {"operator": "ZI", "expectation": math.sin(theta) * 0.3}
@task
def measure_iz(circuit):
"""Measure IZ expectation value (Level 1 - parallel)."""
theta = circuit["theta"]
return {"operator": "IZ", "expectation": -math.cos(theta) * 0.2}All three tasks take circuit as input — the output of build_ansatz. None of them depend on each other. Marqov automatically detects this and assigns them all to Level 1, meaning they will execute in parallel.
In a real VQE, these tasks would run actual quantum circuits with basis rotations for each Pauli term. Here we use analytic formulas for simplicity, but the workflow structure is identical.
Step 3: Define the Energy Computation
The final task combines the expectation values into a total energy:
@task
def compute_energy(zz, zi, iz):
"""Combine expectation values into energy (Level 2)."""
energy = zz["expectation"] + zi["expectation"] + iz["expectation"]
return {
"energy": round(energy, 6),
"components": {
zz["operator"]: zz["expectation"],
zi["operator"]: zi["expectation"],
iz["operator"]: iz["expectation"],
},
"_summary": {
"Energy": f"{energy:.6f} Ha",
"Method": "VQE",
"Qubits": "2",
"Operators": "3 (ZZ, ZI, IZ)",
},
}This task depends on all three measurement tasks, so Marqov places it at Level 2. It cannot run until all measurements complete.
The _summary Key
The _summary dictionary is a special convention. When the workflow result contains a _summary key, the execution dashboard extracts it and renders each key-value pair as a card at the top of the results page:
| Card Label | Card Value |
|---|---|
| Energy | -0.123456 Ha |
| Method | VQE |
| Qubits | 2 |
| Operators | 3 (ZZ, ZI, IZ) |
This gives you a quick visual summary without digging into the raw JSON.
Step 4: Compose the Workflow
Now tie everything together with @workflow:
@workflow
def vqe_energy_eval(theta=0.7, **kwargs):
circuit = build_ansatz(theta)
zz = measure_zz(circuit)
zi = measure_zi(circuit)
iz = measure_iz(circuit)
return compute_energy(zz, zi, iz)When vqe_energy_eval(theta=0.7) is called, Marqov traces the function and builds this dependency graph:
build_ansatz
|
+---> measure_zz ---+
| |
+---> measure_zi ---+---> compute_energy
| |
+---> measure_iz ---+The three measurement calls are independent (they share the same input circuit but do not depend on each other’s outputs), so they are grouped into a single execution level and run concurrently.
Complete Script
Here is the full script ready to paste into the playground:
from marqov import task, workflow
import math
@task
def build_ansatz(theta):
"""Build a parameterized circuit (Level 0 - single task)."""
return {
"gate_sequence": [
{"gate": "ry", "qubit": 0, "angle": theta},
{"gate": "cx", "qubit": [0, 1]},
{"gate": "ry", "qubit": 1, "angle": theta * 0.5},
],
"n_qubits": 2,
"theta": theta,
}
@task
def measure_zz(circuit):
"""Measure ZZ expectation value (Level 1 - parallel)."""
theta = circuit["theta"]
return {"operator": "ZZ", "expectation": math.cos(theta) * 0.5}
@task
def measure_zi(circuit):
"""Measure ZI expectation value (Level 1 - parallel)."""
theta = circuit["theta"]
return {"operator": "ZI", "expectation": math.sin(theta) * 0.3}
@task
def measure_iz(circuit):
"""Measure IZ expectation value (Level 1 - parallel)."""
theta = circuit["theta"]
return {"operator": "IZ", "expectation": -math.cos(theta) * 0.2}
@task
def compute_energy(zz, zi, iz):
"""Combine expectation values into energy (Level 2 - single task)."""
energy = zz["expectation"] + zi["expectation"] + iz["expectation"]
return {
"energy": round(energy, 6),
"components": {
zz["operator"]: zz["expectation"],
zi["operator"]: zi["expectation"],
iz["operator"]: iz["expectation"],
},
"_summary": {
"Energy": f"{energy:.6f} Ha",
"Method": "VQE",
"Qubits": "2",
"Operators": "3 (ZZ, ZI, IZ)",
},
}
@workflow
def vqe_energy_eval(theta=0.7, **kwargs):
circuit = build_ansatz(theta)
zz = measure_zz(circuit)
zi = measure_zi(circuit)
iz = measure_iz(circuit)
return compute_energy(zz, zi, iz)Step 5: Submit and View Results
- Paste the script into the playground at
/run. - Click “Run as Job” and select the local backend.
- On the results page, you will see:
- Summary cards showing Energy, Method, Qubits, and Operators
- Execution timeline (Gantt chart) showing
build_ansatzfirst, then all three measurement tasks running in parallel, thencompute_energy - Task table with 3 execution levels and 5 total tasks
How Execution Levels Work
Marqov determines execution levels using a topological sort of the dependency graph:
- Level 0: Tasks with no dependencies. In our workflow, that is
build_ansatz. - Level 1: Tasks whose dependencies are all in Level 0. That is
measure_zz,measure_zi, andmeasure_iz— they all depend only onbuild_ansatz. - Level 2: Tasks whose dependencies include Level 1 tasks. That is
compute_energy, which depends on all three measurement tasks.
Tasks within the same level run in parallel. Levels execute in strict order — Level 1 cannot start until all Level 0 tasks complete.
The dashboard’s execution overview line shows this:
5 tasks | 3 execution levels | Max parallelism: 3Scaling to a Real VQE
The real VQE H2 benchmark in the Marqov repository uses the same pattern with 5 Pauli terms (ZI, IZ, ZZ, XX, YY) instead of 3. Each measurement task runs a quantum circuit on AWS Braket SV1 with basis rotations for the target Pauli operator. The structure is identical:
@workflow(name="VQE-H2-SV1")
def vqe_step(theta, executor_config):
circuit = build_ansatz(theta)
circuit_dict = circuit.to_dict()
# 5 independent measurements -- run in parallel
zi = measure_pauli(circuit_dict, "ZI", executor_config)
iz = measure_pauli(circuit_dict, "IZ", executor_config)
zz = measure_pauli(circuit_dict, "ZZ", executor_config)
xx = measure_pauli(circuit_dict, "XX", executor_config)
yy = measure_pauli(circuit_dict, "YY", executor_config)
return compute_energy(zi, iz, zz, xx, yy)With 5 parallel measurements on SV1, this achieves roughly 43% speedup compared to sequential execution.
Next Steps
- Parallel Task Execution — deep dive into how Marqov builds and executes the dependency graph
- Running on Real Hardware — run your VQE on cloud simulators and QPUs
- Viewing Results — explore every section of the execution dashboard