Skip to Content
DocsTutorialsBuilding a VQE

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 _summary to 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 LabelCard Value
Energy-0.123456 Ha
MethodVQE
Qubits2
Operators3 (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

  1. Paste the script into the playground at /run.
  2. Click “Run as Job” and select the local backend.
  3. On the results page, you will see:
    • Summary cards showing Energy, Method, Qubits, and Operators
    • Execution timeline (Gantt chart) showing build_ansatz first, then all three measurement tasks running in parallel, then compute_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, and measure_iz — they all depend only on build_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: 3

Scaling 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

Last updated on