Quantum Optimization: Max-Cut with QAOA, VQE, and FALQONยถ
In this tutorial, we will explore three different quantum optimization algorithms to solve the Max-Cut problem using the Ket language:
Algorithm |
Approach |
Description |
|---|---|---|
QAOA |
Hybrid Variational |
Ansatz inspired by adiabatic evolution, scales well with layers (\(p\)) |
VQE |
Hybrid Variational |
Hardware-efficient ansatz with automatic gradients |
FALQON |
Deterministic Feedback |
No classical iterative optimizer required |
๐ก Prerequisite: Basic knowledge of quantum computing
๐ง Environment Setupยถ
Before we begin, letโs install the necessary dependencies.
Weโll use ket-lang as our core quantum programming library.
from functools import partial
from scipy.optimize import minimize
import networkx as nx
import plotly.express as px
import plotly.graph_objects as go
import plotly.io as pio
pio.renderers.default = "notebook_connected"
# Import all elements from the Ket API
from ket import *
from ket import ket_version
ket_version()
['Ket v0.10.1',
'libket v0.7.1 [rustc 1.97.0 (2d8144b78 2026-07-07) x86_64-unknown-linux-gnu]',
'kbw v0.5.1 [rustc 1.97.0 (2d8144b78 2026-07-07) x86_64-unknown-linux-gnu]']
โ๏ธ Part 1: The Max-Cut Problemยถ
What is Max-Cut?ยถ
Max-Cut is a classic problem in graph theory. Given a graph \(G = (V, \mathcal{E})\), the goal is:
Partition the set of vertices \(V\) into two disjoint subsets \(S\) and \(\bar{S}\) such that we maximize the number of edges crossing the partition (i.e., edges with one endpoint in \(S\) and the other in \(\bar{S}\)).
Formally, we want to maximize:
Why is it hard?ยถ
The number of possible partitions grows exponentially with the number of vertices: for \(n\) vertices, there are \(2^{n-1}\) distinct partitions. Checking all of them is impractical even for moderately sized graphs.
Why is it interesting for quantum computing?ยถ
Max-Cut is a paradigmatic problem for quantum optimization algorithms because:
It maps directly to a quantum Hamiltonian (Pauli operators)
It serves as a benchmark for NISQ (Noisy Intermediate-Scale Quantum) devices
It has real-world applications in VLSI circuit cutting, statistical physics, and machine learning
Intuitive Exampleยถ
Imagine a social network where edges represent conflicts between people. We want to divide the group into two teams such that the maximum number of conflicts are placed between the teams (rather than inside them).
Available Instancesยถ
For this tutorial, weโve prepared six graphs of increasing complexity. You can swap the instance at any time to see how the algorithms behave!
# ================================================================
# GRAPH INSTANCES
# ================================================================
graphs = {
# number of nodes: edges
# Instance 1: Simple Ring (4 nodes)
# Trivial solution: alternating colors. Optimal = 4 cut edges.
4: [(0, 1), (1, 2), (2, 3), (3, 0)],
# Instance 2: Butterfly (5 nodes), geometric frustration
# The triangle (0,1,2) prevents a perfect coloring (odd cycle).
5: [(0, 1), (0, 2), (1, 2), (2, 3), (2, 4), (3, 4)],
# Instance 3: Triangular Prism (6 nodes), 3-regular graph
# Two triangles connected by lateral edges.
6: [(0, 1), (1, 2), (2, 0), (3, 4), (4, 5), (5, 3), (0, 3), (1, 4), (2, 5)],
# Instance 4: Cube (8 nodes), hypercube topology
# 3-regular graph with 12 edges. Optimal = 8 cut edges (bipartite!).
8: [(0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (0, 4), (1, 5), (2, 6), (3, 7)],
# Instance 5: Petersen Graph (10 nodes), classic benchmark
# Famous for being 3-regular, highly symmetric and internally connected.
10: [(0, 1), (1, 2), (2, 3), (3, 4), (4, 0), (0, 5), (1, 6), (2, 7), (3, 8), (4, 9), (5, 7), (7, 9), (9, 6), (6, 8), (8, 5)],
# Instance 6: 3x4 Grid (12 nodes), lattice topology
# Simulates square grid interactions, common in superconducting quantum hardware.
12: [(0, 1), (1, 2), (2, 3), (4, 5), (5, 6), (6, 7), (8, 9), (9, 10), (10, 11), (0, 4), (4, 8), (1, 5), (5, 9), (2, 6), (6, 10), (3, 7), (7, 11)],
}
Graph and Cut Visualizationยถ
The plot_maxcut function below serves two purposes:
No result (
result=None): shows the original graph with all nodes in the same color.With result (
result=<integer>): interprets the integer as an \(n\)-bit binary string, colors the nodes in set \(S\) ๐ต blue and those in set \(\bar{S}\) ๐ด red, and highlights the cut edges with dashed red lines.
Binary representation: An \(n\)-qubit state measured as integer
rmaps qubit \(i\) to bit \(i\) of the binary representation ofr. Bit0โ vertex in blue group; bit1โ red group.
def plot_maxcut(n: int, result: int | None = None):
"""
Plots the graph with n vertices, highlighting the cut if 'result' is provided.
Parameters
----------
n : number of vertices (selects the graph from the `graphs` dictionary)
result : n-bit integer representing the partition (optional)
- bit 0 โ blue group (set S)
- bit 1 โ red group (set Sฬ)
"""
G = nx.Graph()
G.add_edges_from(graphs[n])
pos = nx.spring_layout(G, seed=42)
# โโ Node colors โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
node_colors = ["#00dfff"] * n # default blue (no result)
bin_str = None
if result is not None:
bin_str = bin(result)[2:].zfill(n)
node_colors = ["#EF553B" if bit == "1" else "#00dfff" for bit in bin_str]
# โโ Edge classification โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
uncut_x, uncut_y = [], []
cut_x, cut_y = [], []
for u, v in G.edges():
x0, y0 = pos[u]
x1, y1 = pos[v]
# An edge is "cut" if the two nodes have different bits
if bin_str is not None and bin_str[u] != bin_str[v]:
cut_x.extend([x0, x1, None])
cut_y.extend([y0, y1, None])
else:
uncut_x.extend([x0, x1, None])
uncut_y.extend([y0, y1, None])
# โโ Plotly traces construction โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
traces = []
if uncut_x: # uncut edges: solid gray
traces.append(
go.Scatter(
x=uncut_x,
y=uncut_y,
line=dict(width=3, color="#888"),
hoverinfo="none",
mode="lines",
)
)
if cut_x: # cut edges: dashed red
traces.append(
go.Scatter(
x=cut_x,
y=cut_y,
line=dict(width=3, color="#EF553B", dash="dot"),
hoverinfo="none",
mode="lines",
name="Cut edge",
)
)
# โโ Node trace โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
traces.append(
go.Scatter(
x=[pos[v][0] for v in G.nodes()],
y=[pos[v][1] for v in G.nodes()],
mode="markers+text",
text=list(G.nodes()),
textposition="middle center",
textfont=dict(color="white", size=16),
marker=dict(size=45, color=node_colors, line_width=2),
)
)
# โโ Cut count (if result exists) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
cut_count = ""
if bin_str is not None:
n_cut = sum(1 for (u, v) in G.edges() if bin_str[u] != bin_str[v])
cut_count = f", {n_cut} edges cut out of {len(graphs[n])}"
title = (
("Max-Cut Result" + cut_count)
if result is not None
else f"Graph with {n} vertices"
)
fig = go.Figure(
data=traces,
layout=go.Layout(
title=title,
title_x=0.5,
showlegend=False,
xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
width=500,
height=500,
),
)
fig.show()
# Initial graph visualization (no result)
plot_maxcut(4)
๐งฎ Part 2: Quantum Mapping โ Hamiltoniansยถ
The first step to solving Max-Cut quantumly is to translate the problem into the language of quantum mechanics. Instead of maximizing the number of cut edges, we want to minimize the energy of a physical system described by an operator called the Hamiltonian.
Variable Encodingยถ
Each vertex \(v_i\) is mapped to a qubit \(i\). The state of each qubit (0 or 1) indicates which group the vertex belongs to. Measuring the system at the end of the circuit reveals the partition found.
๐ข Cost Hamiltonian (\(H_C\))ยถ
The Cost Hamiltonian encodes the Max-Cut objective function using the Pauli-Z operator (which has eigenvalues \(+1\) and \(-1\) corresponding to qubits in state \(|0\rangle\) and \(|1\rangle\)).
For each edge \((a, b) \in \mathcal{E}\), the product \(Z_a Z_b\) equals:
\(+1\) if the vertices are in the same group (edge not cut)
\(-1\) if the vertices are in different groups (edge cut)
Therefore, the Hamiltonian:
evaluates to \(-1\) for each cut edge and \(0\) for each uncut edge. Minimizing \(H_C\) is equivalent to maximizing the number of cut edges!
plot_maxcut(4, 0b1011)
๐ก Mixer Hamiltonian (\(H_M\))ยถ
To allow the algorithm to explore the state space and avoid getting stuck in a local minimum, we need a second Hamiltonian that does not commute with \(H_C\). The standard choice is the sum of Pauli-X operators (quantum NOT gate) on all qubits:
The non-commutativity \([H_C, H_M] \neq 0\) ensures that the alternating evolution of the two Hamiltonians creates quantum interference, efficiently exploring the solution space.
Implementation in Ketยถ
In Ket, Hamiltonians are constructed inside a {func}\~ket.gates.obs`block, which creates a{class}`~ket.expv.Hamiltonian`` object โ an algebraic representation of the operator that can be
used both to calculate expected values and to generate time-evolution unitaries \(e^{-i t H}\).
def cost_h(edges: list[tuple[int, int]], q: Quant) -> Hamiltonian:
"""
Builds the Max-Cut Cost Hamiltonian:
H_C = -1/2 * ฮฃ_{(a,b) โ E} (1 - Z_aยทZ_b)
The `with obs()` block tells Ket we are building an observable
(not executing gates in the circuit). The result is a Hamiltonian
object that can be used in `evolve()` or `exp_value()`.
Parameters
----------
edges : list of graph edges, e.g. [(0,1), (1,2), ...]
q : qubit register allocated in the current process
"""
with obs():
# For each edge (a,b), (1 - Z_a*Z_b) equals 2 if the edge is cut, 0 otherwise.
# Summing and dividing by -2 gives -1 per cut edge (minimization โ maximization).
Hc = sum(1 - Z(q[a]) * Z(q[b]) for a, b in edges)
return -Hc / 2
def mixer_h(nodes: Quant) -> Hamiltonian:
"""
Builds the Mixer Hamiltonian:
H_M = ฮฃ_q X_q
The X operator (Pauli-X = NOT gate) creates transitions between states |0โฉ and |1โฉ,
allowing the system to explore different vertex colorings.
Its non-commutativity with Z is fundamental: [X, Z] = 2iY โ 0.
Parameters
----------
nodes : list/Quant of qubits to be connected by the mixer
"""
with obs():
Hm = sum(X(q) for q in nodes)
return Hm
1๏ธโฃ QAOA โ Quantum Approximate Optimization Algorithmยถ
How QAOA Worksยถ
QAOA was proposed by Farhi, Goldstone, and Gutmann in 2014 as a hybrid quantum-classical method for combinatorial optimization problems.
The algorithm prepares an initial state and applies \(p\) layers of alternating unitary evolutions:
Cost Evolution: \(e^{-i \gamma H_C}\) (adds phases according to the cost function)
Mixer Evolution: \(e^{-i \beta H_M}\) (creates transitions between states)
The parameters \(\gamma\) and \(\beta\) are optimized by a classical algorithm (like COBYLA) to minimize the expected value \(\langle H_C \rangle\).
def qaoa_ansatz(edges, qubits, gamma, beta):
"""
Builds the QAOA variational circuit with p layers.
Structure:
1. Initial state: uniform superposition with H on all qubits
2. For each pair (ฮณ_k, ฮฒ_k):
a. Cost Hamiltonian evolution: e^{-i ฮณ_k H_C}
b. Mixer Hamiltonian evolution: e^{-i ฮฒ_k H_M}
The `evolve(t * H)` function in Ket directly applies the matrix exponential
e^{-i t H} to the current circuit state, without manually decomposing
into primitive gates!
Parameters
----------
edges : list of graph edges
qubits : qubit register of the process
gamma : list of p angles ฮณ (cost evolution)
beta : list of p angles ฮฒ (mixer evolution)
"""
# Step 1: prepare uniform superposition |+โฉ^โn
H(qubits)
# Step 2: apply p alternating layers of cost and mixer
for g, b in zip(gamma, beta):
evolve(g * cost_h(edges, qubits)) # e^{-i ฮณ H_C}
evolve(b * mixer_h(qubits)) # e^{-i ฮฒ H_M}
QAOA Circuit Visualizationยถ
Ketโs qulib.draw function allows us to visualize the quantum circuit that will be executed.
Letโs see what a 1-layer (\(p=1\)) QAOA circuit looks like for the selected graph:
# QAOA circuit visualization for p=1 (one layer)
# Using parameters ฮณ=0.7, ฮฒ=0.3 as an illustrative example
n = 6
qulib.draw(lambda q: qaoa_ansatz(graphs[n], q, gamma=[0.7], beta=[0.3]), n, fold=-1)
Objective Function and Classical-Quantum Loopยถ
The qaoa_objective function is called repeatedly by the COBYLA optimizer.
In each call, it:
Creates a new Ket Process (qubit allocation in the simulator).
Executes the ansatz with the current parameters.
Calculates and returns the expected value \(\langle H_C \rangle\).
def qaoa_objective(edges, n, parameters, final=False):
"""
Hybrid classical-quantum objective function for QAOA.
This function is called in two contexts:
- During optimization (final=False): returns โจH_Cโฉ for the optimizer to minimize.
- After convergence (final=True): returns samples of the optimized state.
Parameters
----------
edges : graph edges
n : number of vertices (= number of qubits)
parameters : 1D vector with [ฮณ_1,...,ฮณ_p, ฮฒ_1,...,ฮฒ_p] concatenated
final : if True, returns samples instead of the expected value
"""
# Split parameters into two vectors: gamma and beta
p = len(parameters) // 2
gamma = parameters[:p]
beta = parameters[p:]
process = Process(num_qubits=n, simulator="dense", execution="batch")
qubits = process.alloc(n)
qaoa_ansatz(edges, qubits, gamma, beta)
if final:
return sample(qubits)
return exp_value(cost_h(edges, qubits)).get()
Executing QAOAยถ
Now we run the full optimization loop. SciPy (COBYLA) will call qaoa_objective
repeatedly, adjusting parameters until it converges to a local minimum of \(\langle H_C \rangle\).
# โโ Configuration โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
p = 2 # number of QAOA layers
dt = 0.5 # initial parameter scale
# Initialization inspired by adiabatic annealing:
# ฮณ increasing: small at the start, larger at the end โ cost gains importance gradually
# ฮฒ decreasing: large at the start, smaller at the end โ mixer dominates early on
gamma_init = [i / p * dt for i in range(1, p + 1)] # [dt/p, 2dt/p, ..., dt]
beta_init = [(1 - i / p) * dt for i in range(1, p + 1)] # [dt(1-1/p), ..., 0]
print("Initial parameters:")
print(f" ฮณ (cost, increasing): {gamma_init}")
print(f" ฮฒ (mixer, decreasing): {beta_init}")
# Concatenate [ฮณ_1,...,ฮณ_p, ฮฒ_1,...,ฮฒ_p] into a single vector for SciPy
initial_params_qaoa = gamma_init + beta_init
# โโ Optimization โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
res_qaoa = minimize(
partial(qaoa_objective, graphs[n], n), # function for COBYLA to minimize
initial_params_qaoa,
method="COBYLA",
)
print(f"\nQAOA optimization completed in {res_qaoa.nfev} objective function evaluations.")
print(f"Final energy โจH_Cโฉ = {res_qaoa.fun:.4f} (lower = more cut edges)")
print(
f"Optimal parameters: ฮณ = {res_qaoa.x[:p].round(4)}, ฮฒ = {res_qaoa.x[p:].round(4)}"
)
# โโ Final result โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# Use the optimal parameters to sample the final quantum state
samples_qaoa = qaoa_objective(graphs[n], n, res_qaoa.x, final=True)
# Histogram colored by energy: darker bars = states with higher cost (better cuts)
samples_qaoa.histogram("bin", hamiltonian=partial(cost_h, graphs[n])).show()
# Visualize the cut found in the graph
best_state_qaoa = samples_qaoa.most_frequent_state()
print(
f"\nMost probable state: {bin(best_state_qaoa)[2:].zfill(n)} (integer: {best_state_qaoa})"
)
plot_maxcut(n, best_state_qaoa)
Initial parameters:
ฮณ (cost, increasing): [0.25, 0.5]
ฮฒ (mixer, decreasing): [0.25, 0.0]
QAOA optimization completed in 99 objective function evaluations.
Final energy โจH_Cโฉ = -6.3919 (lower = more cut edges)
Optimal parameters: ฮณ = [0.6747 1.5856], ฮฒ = [ 1.3373 -0.2674]
Most probable state: 011100 (integer: 28)
2๏ธโฃ VQE โ Variational Quantum Eigensolverยถ
How VQE Worksยถ
VQE was originally proposed by Peruzzo et al. (2014) to calculate molecular energies, but it applies to any problem that can be mapped to a Hamiltonian.
Unlike QAOA, which uses Hamiltonians that encode the problem directly into the circuitโs gates, VQE uses a Hardware-Efficient Ansatz (HEA). This ansatz consists of generic parametrized rotation gates (like \(R_Y\)) and fixed entangling gates (like \(CZ\)), which are easy to implement on real quantum hardware.
VQE relies heavily on the classical optimizer to find the right angles that minimize the energy.
def vqe_ansatz(qubits, parameters):
"""
Hardware-Efficient Ansatz (HEA) for VQE.
Structure of each layer:
1. RY rotations(ฮธ_i) on each qubit โ free parameterization
2. CZ gates between neighboring qubits โ local entanglement
The iterator `p` steps through the parameters sequentially.
When parameters are exhausted (next returns False), the function returns.
This lets the number of layers be controlled simply by the length of the parameter vector.
Parameters
----------
qubits : qubit register of the current process
parameters : vector of angles ฮธ (can be a list of floats or Ket Param objects)
"""
p = iter(parameters)
while True:
# โโ Individual rotation layer โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
for q in qubits:
angle = next(p, False)
if angle is False: # parameters exhausted: circuit complete
return
RY(angle, q) # rotation around the Y axis: Ry(ฮธ) = e^{-iฮธY/2}
# โโ Entanglement layer โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# CZ between consecutive qubits creates quantum correlations between neighboring vertices
for i in range(len(qubits) - 1):
CZ(qubits[i], qubits[i + 1])
def vqe_objective(edges, n, parameters, final=False):
"""
VQE objective function with support for automatic gradients.
Unlike QAOA, here we enable `gradient=True` in the Process so that
Ket automatically computes โโจH_Cโฉ/โฮธ_i for each parameter.
With gradients available, we can use SciPy's L-BFGS-B optimizer,
which converges much faster than COBYLA on smooth cost surfaces.
Parameters
----------
edges : graph edges
n : number of qubits
parameters : vector of angles ฮธ
final : if True, returns samples; if False, returns (energy, gradient)
"""
process = Process(
num_qubits=n,
simulator="dense",
execution="batch",
gradient=not final, # gradients only during optimization (not in the final call)
)
if not final:
# Register parameters as differentiable variables of the process.
# This allows Ket to track their derivatives automatically.
parameters = process.param(*parameters)
qubits = process.alloc(n)
vqe_ansatz(qubits, parameters)
if final:
return sample(qubits)
# Expected value of the energy
result = exp_value(cost_h(edges, qubits)).get()
# Extract gradients: โโจH_Cโฉ/โฮธ_i for each parameter.
# Ket computes this automatically using the Parameter Shift Rule.
grad = [p.grad for p in parameters]
return result, grad
VQE Circuit Visualizationยถ
Letโs visualize the VQE circuit before running the optimization:
# VQE circuit visualization (one layer of n parameters)
qulib.draw(lambda q: vqe_ansatz(q, [0.1] * n), n, fold=-1)
Executing VQEยถ
VQE uses SciPyโs L-BFGS-B optimizer, which is a second-order quasi-Newton method. It uses the gradients calculated by Ket to converge much faster than COBYLA.
Tip: The number of parameters controls the expressive power of the ansatz.
With num_layers * n parameters, we have num_layers layers of RY + CZ rotations.
# โโ Configuration โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
num_layers = 2 # number of RY + CZ layers
num_params = num_layers * n # total number of parameters ฮธ
# Initialization with small random perturbations to break symmetry
import random
random.seed(42)
initial_params_vqe = [random.uniform(0.0, 0.3) for _ in range(num_params)]
print(f"VQE with {num_layers} layers ร {n} qubits = {num_params} parameters")
# โโ Optimization โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
res_vqe = minimize(
partial(vqe_objective, graphs[n], n),
initial_params_vqe,
method="L-BFGS-B", # uses gradients provided by Ket
jac=True, # indicates the function returns (value, gradient)
)
print(f"\nVQE optimization completed in {res_vqe.nfev} objective function evaluations.")
print(f"Final energy โจH_Cโฉ = {res_vqe.fun:.4f}")
# โโ Final result โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
samples_vqe = vqe_objective(graphs[n], n, res_vqe.x, final=True)
samples_vqe.histogram("bin", hamiltonian=partial(cost_h, graphs[n])).show()
best_state_vqe = samples_vqe.most_frequent_state()
print(
f"\nMost probable state: {bin(best_state_vqe)[2:].zfill(n)} (integer: {best_state_vqe})"
)
plot_maxcut(n, best_state_vqe)
VQE with 2 layers ร 6 qubits = 12 parameters
VQE optimization completed in 30 objective function evaluations.
Final energy โจH_Cโฉ = -7.0000
Most probable state: 101010 (integer: 42)
3๏ธโฃ FALQON โ Feedback-based ALgorithm for Quantum OptimizatioNยถ
How FALQON Worksยถ
FALQON was proposed by Magann et al. (2021) as an alternative to QAOA that completely eliminates the classical iterative optimizer. Instead of adjusting parameters via minimization, FALQON uses a feedback control law based on Lyapunov theory:
If we choose the parameter \(\beta_k\) such that the energy never increases from one layer to the next, we guarantee monotonic convergence to a minimum.

The Lyapunov Feedback Lawยถ
The central theorem of FALQON establishes that if we define:
where \(\langle \cdot \rangle_k\) is the expected value in the state after the \(k\)-th layer, then the energy must decrease (or remain the same) at each step:
The term \(i[H_M, H_C] = i(H_M H_C - H_C H_M)\) is called the commutator Hamiltonian and measures the โflowโ of energy between the two Hamiltonians.
Advantage and Limitationยถ
โ Advantage: Guaranteed monotonic convergence, no risk of barren plateaus or bad local minima. โ ๏ธ Limitation: May require more layers than QAOA to achieve the same solution quality.
execution="live" Mode in Ketยถ
FALQON requires measuring the system between layers (to calculate \(\beta_k\)) and continuing
to apply gates to the same state. This is possible in Ket using execution="live", which keeps
the quantum state active in the simulator and allows interleaved reading/writing.
This is an exclusive Ket feature that greatly facilitates FALQON implementation!
def falqon_layer(edges, qubits, beta, delta_t):
"""
Applies one FALQON layer to the current state.
Each layer consists of:
1. Cost evolution: e^{-i ฮt H_C}
2. Mixer evolution: e^{-i ฮฒยทฮt H_M}
The parameter ฮฒ is determined by the feedback from the previous state,
while ฮt is a fixed time step (analogous to a numerical integration step).
Parameters
----------
edges : graph edges
qubits : process qubits (state preserved between calls with execution='live')
beta : feedback parameter computed in the previous iteration
delta_t : time step (controls the magnitude of the evolution per layer)
"""
evolve(delta_t * cost_h(edges, qubits)) # e^{-i ฮt H_C}
evolve(beta * delta_t * mixer_h(qubits)) # e^{-i ฮฒยทฮt H_M}
def beta_h(edges, qubits):
"""
Builds the commutator Hamiltonian for computing the feedback parameter ฮฒ.
The Lyapunov law of FALQON determines:
ฮฒ_k = -โจi[H_M, H_C]โฉ_k = -โจi(H_MยทH_C - H_CยทH_M)โฉ_k
The expected value of this Hamiltonian in the current state gives exactly
the ฮฒ that guarantees the energy will not increase in the next layer.
Note: The @ operator in Ket represents the product of Hamiltonians (composition).
"""
Hm = mixer_h(qubits)
Hc = cost_h(edges, qubits)
# Commutator: [H_M, H_C] = H_MยทH_C - H_CยทH_M
# Multiplied by i and negated, we get ฮฒ that guarantees energy decrease
A = 1j * (Hm @ Hc - Hc @ Hm)
return -A
FALQON Circuit Visualizationยถ
Letโs see what a FALQON layer looks like with example parameters:
# FALQON layer visualization with ฮฒ=0.1, ฮt=0.2
delta_t = 0.01 # time step, small value for smooth evolution
qulib.draw(partial(falqon_layer, graphs[n], beta=0.1, delta_t=0.2), n, fold=-1)
Executing FALQONยถ
The FALQON loop is simple and deterministic: at each iteration, we apply a layer and calculate the next \(\beta\) from the current state.
Difference from QAOA/VQE: There is no call to SciPy! The entire process is quantum-deterministic.
# โโ Configuration โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
num_layers = 64 # number of layers (more layers = better approximation)
delta_t = 0.01 # time step (smaller = smoother evolution, but more layers needed)
# History for convergence visualization
beta_list = [0.0] # ฮฒโ = 0 (no mixer evolution in the first layer)
cost_list = [] # energy โจH_Cโฉ over the layers
# โโ Process with execution='live' โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# This mode keeps the quantum state active between calls, allowing
# measurements and new gates to be interleaved on the same state.
process = Process(
num_qubits=n,
simulator="dense",
execution="live", # persistent state between operations!
)
# Initial state: uniform superposition |+โฉ^โn
qubits = H(process.alloc(n))
print(f"FALQON: {num_layers} layers, ฮt = {delta_t}")
print("Running...")
FALQON: 64 layers, ฮt = 0.01
Running...
# โโ Feedback loop โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
for k in range(num_layers):
# Step 1: apply the k-th layer with ฮฒ computed in the previous step
falqon_layer(graphs[n], qubits, beta_list[-1], delta_t)
# Step 2: FEEDBACK โ measure the commutator to get the next ฮฒ
# exp_value() queries the current state WITHOUT collapsing the quantum state
beta_k = exp_value(beta_h(graphs[n], qubits)).get()
beta_list.append(beta_k)
# Step 3: record the current energy for monitoring
cost_k = exp_value(cost_h(graphs[n], qubits)).get()
cost_list.append(cost_k)
print(f"Initial energy: {cost_list[0]:.4f}")
print(f"Final energy: {cost_list[-1]:.4f}")
print(f"Energy reduction: {cost_list[0] - cost_list[-1]:.4f}")
Initial energy: -4.5000
Final energy: -5.7050
Energy reduction: 1.2050
# โโ Final result โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
samples_falqon = sample(qubits)
samples_falqon.histogram("bin", hamiltonian=partial(cost_h, graphs[n])).show()
best_state_falqon = samples_falqon.most_frequent_state()
print(
f"\nMost probable state: {bin(best_state_falqon)[2:].zfill(n)} (integer: {best_state_falqon})"
)
plot_maxcut(n, best_state_falqon)
Most probable state: 110001 (integer: 49)
# โโ FALQON convergence charts โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
layers = list(range(1, len(cost_list) + 1))
# Energy trajectory (must be monotonically decreasing!)
fig_cost = px.line(
x=layers,
y=cost_list,
labels={"x": "Layer k", "y": "โจH_Cโฉ (Energy)"},
title="FALQON: Energy Trajectory over Layers<br><sup>Monotonic convergence guaranteed by Lyapunov's law</sup>",
markers=True,
color_discrete_sequence=["#636EFA"],
)
fig_cost.add_hline(
y=min(cost_list),
line_dash="dash",
line_color="red",
annotation_text=f"Minimum energy: {min(cost_list):.4f}",
)
fig_cost.show()
# Feedback parameter ฮฒ evolution over layers
fig_beta = px.line(
x=layers,
y=beta_list[1:], # discard initial ฮฒโ = 0
labels={"x": "Layer k", "y": "ฮฒ_k (feedback parameter)"},
title="FALQON: Feedback Parameter ฮฒ over Layers<br><sup>Computed from the commutator i[H_M, H_C]</sup>",
markers=True,
color_discrete_sequence=["#EF553B"],
)
fig_beta.add_hline(y=0, line_dash="dot", line_color="gray")
fig_beta.show()
Note that the energy is monotonically decreasing, a theoretical guarantee of FALQON!
๐ Conclusions and Next Stepsยถ
What we learnedยถ
In this tutorial, we implemented three quantum optimization algorithms using the Ket language:
Algorithm |
Strength |
When to use |
|---|---|---|
QAOA |
Physically motivated ansatz, good scaling with \(p\) |
When problem structure is well known |
VQE |
Automatic gradients, fast convergence |
When using real hardware with limited connectivity |
FALQON |
Guaranteed monotonic convergence, no classical optimizer |
When avoiding barren plateaus is critical |
Ket Features Usedยถ
with obs(): Building Hamiltonians as quantum observablesevolve(t * H): Exact time evolution \(e^{-i t H}\) without manual decompositionexp_value(H).get(): Calculating the expected value \(\langle H \rangle\)sample(q): Sampling the final quantum stateprocess.param(*ฮธ): Differentiable parameters for automatic gradientsexecution="batch": Efficient batch submission to the simulatorexecution="live": Persistent state between operations (essential for FALQON)