Skip to content

Commit a954ef5

Browse files
Fix/falqon demo issue 1726 (#1729)
**Title:** fix: correct Hc and commutator formulas, and [build_hamiltonian](cci:1://file:///e:/ARPA/OpenSource/qml/demonstrations_v2/tutorial_falqon/demo.py:190:0-207:12) coefficients in FALQON demo **Summary:** The FALQON demo's cost Hamiltonian formula, commutator formula, and [build_hamiltonian()](cci:1://file:///e:/ARPA/OpenSource/qml/demonstrations_v2/tutorial_falqon/demo.py:190:0-207:12) function all used incorrect coefficients. The root cause is a hidden 1/4 normalization applied by `pennylane.qaoa.cost.edge_driver` (used internally by `qaoa.max_clique`) that was not reflected in the demo's written math. Specific corrections: - `H_c` formula: edge coefficient `3` → `3/4`; added a note explaining the `edge_driver` normalization - `[H_d, H_c]` formula: first-sum coefficient `3` → `3/4`, second-sum `3` → `1` - `i[H_d, H_c]` formula: first-sum coefficient `6` → `3/2`, second-sum `6` → `2` - [build_hamiltonian()](cci:1://file:///e:/ARPA/OpenSource/qml/demonstrations_v2/tutorial_falqon/demo.py:190:0-207:12): edge term coefficients `6` → `1.5`, second-sum `6` → `2` - Added a `.. note::` block showing `qml.commutator` as a concise general-purpose alternative - Bumped `dateOfLastModification` in [metadata.json](cci:7://file:///e:/ARPA/OpenSource/qml/demonstrations_v2/tutorial_falqon/metadata.json:0:0-0:0) The FALQON algorithm converged correctly before and after this fix — the sign of β_k is preserved by the operator structure, so the guarantee d/dt ⟨H_c⟩ ≤ 0 holds regardless. This PR makes the written math match the actual code output. **Relevant references:** - Forum report: https://discuss.pennylane.ai/t/an-issue-in-the-falqon-demo/9290 - `edge_driver` API: https://docs.pennylane.ai/en/stable/code/api/pennylane.qaoa.cost.edge_driver.html **Possible Drawbacks:** None. The algorithm behaviour is unchanged; only the mathematical exposition is corrected. **Related GitHub Issues:** Closes #1726 --------- Co-authored-by: Catalina Albornoz <albornoz.catalina@hotmail.com>
1 parent 6116266 commit a954ef5

2 files changed

Lines changed: 61 additions & 33 deletions

File tree

demonstrations_v2/tutorial_falqon/demo.py

Lines changed: 60 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,16 @@
136136
######################################################################
137137
# We must first encode this combinatorial problem into a cost Hamiltonian :math:`H_c.` This ends up being
138138
#
139-
# .. math:: H_c = 3 \sum_{(i, j) \in E(\bar{G})} (Z_i Z_j - Z_i - Z_j) + \displaystyle\sum_{i \in V(G)} Z_i,
139+
# .. math:: H_c = \frac{3}{4} \sum_{(i, j) \in E(\bar{G})} (Z_i Z_j - Z_i - Z_j) + \displaystyle\sum_{i \in V(G)} Z_i,
140140
#
141141
# where each qubit is a node in the graph, and the states :math:`|0\rangle` and :math:`|1\rangle`
142142
# represent whether the vertex has been marked as part of the clique, as is the case for `most standard QAOA encoding
143143
# schemes <https://arxiv.org/abs/1709.03489>`__.
144144
# Note that :math:`\bar{G}` is the complement of :math:`G:` the graph formed by connecting all nodes that **do not** share
145145
# an edge in :math:`G.`
146+
# The factor of :math:`\frac{3}{4}` (rather than :math:`3`) comes from a normalization applied
147+
# internally by :func:`~pennylane.qaoa.cost.edge_driver`, which is used by
148+
# :func:`~pennylane.qaoa.cost.max_clique` to build the edge penalty terms.
146149
#
147150
# In addition to defining :math:`H_c,` we also require a driver Hamiltonian :math:`H_d` which does not commute
148151
# with :math:`H_c.` The driver Hamiltonian's role is similar to that of the mixer Hamiltonian in QAOA.
@@ -164,8 +167,8 @@
164167
# One of the main ingredients in the FALQON algorithm is the operator :math:`i [H_d, H_c].` In
165168
# the case of MaxClique, we can write down the commutator :math:`[H_d, H_c]` explicitly:
166169
#
167-
# .. math:: [H_d, H_c] = 3 \displaystyle\sum_{k \in V(G)} \displaystyle\sum_{(i, j) \in E(\bar{G})} \big( [X_k, Z_i Z_j] - [X_k, Z_i]
168-
# - [X_k, Z_j] \big) + 3 \displaystyle\sum_{i \in V(G)} \displaystyle\sum_{j \in V(G)} [X_i, Z_j].
170+
# .. math:: [H_d, H_c] = \frac{3}{4} \displaystyle\sum_{k \in V(G)} \displaystyle\sum_{(i, j) \in E(\bar{G})} \big( [X_k, Z_i Z_j] - [X_k, Z_i]
171+
# - [X_k, Z_j] \big) + \displaystyle\sum_{i \in V(G)} \displaystyle\sum_{j \in V(G)} [X_i, Z_j].
169172
#
170173
# There are two distinct commutators that we must calculate, :math:`[X_k, Z_j]` and :math:`[X_k, Z_i Z_j].`
171174
# This is straightforward as we know exactly what the
@@ -177,13 +180,14 @@
177180
# where :math:`\delta_{kj}` is the `Kronecker delta <https://en.wikipedia.org/wiki/Kronecker_delta>`__. Therefore it
178181
# follows from substitution into the above equation and multiplication by :math:`i` that:
179182
#
180-
# .. math:: i [H_d, H_c] = 6 \displaystyle\sum_{k \in V(G)} \displaystyle\sum_{(i, j) \in E(\bar{G})} \big( \delta_{ki} Y_k Z_j +
181-
# \delta_{kj} Z_{i} Y_{k} - \delta_{ki} Y_k - \delta_{kj} Y_k \big) + 6 \displaystyle\sum_{i \in V(G)} Y_{i}.
183+
# .. math:: i [H_d, H_c] = \frac{3}{2} \displaystyle\sum_{k \in V(G)} \displaystyle\sum_{(i, j) \in E(\bar{G})} \big( \delta_{ki} Y_k Z_j +
184+
# \delta_{kj} Z_{i} Y_{k} - \delta_{ki} Y_k - \delta_{kj} Y_k \big) + 2 \displaystyle\sum_{i \in V(G)} Y_{i}.
182185
#
183186
# This new operator has quite a few terms! Therefore, we write a short method which computes it for us, and returns
184187
# a :class:`~.pennylane.Hamiltonian` object. Note that this method works for any graph:
185188
#
186189

190+
187191
def build_hamiltonian(graph):
188192
H = qml.Hamiltonian([], [])
189193

@@ -195,45 +199,53 @@ def build_hamiltonian(graph):
195199
for edge in graph_c.edges:
196200
i, j = edge
197201
if k == i:
198-
H += 6 * (qml.PauliY(k) @ qml.PauliZ(j) - qml.PauliY(k))
202+
H += 1.5 * (qml.PauliY(k) @ qml.PauliZ(j) - qml.PauliY(k))
199203
if k == j:
200-
H += 6 * (qml.PauliZ(i) @ qml.PauliY(k) - qml.PauliY(k))
204+
H += 1.5 * (qml.PauliZ(i) @ qml.PauliY(k) - qml.PauliY(k))
201205
# Adds the terms in the second sum
202-
H += 6 * qml.PauliY(k)
206+
H += 2 * qml.PauliY(k)
203207

204208
return H
205209

206210

207211
print("MaxClique Commutator")
208212
print(build_hamiltonian(graph))
209213

214+
######################################################################
215+
# .. note::
216+
#
217+
# For general graphs, the commutator :math:`i[H_d, H_c]` can also be computed
218+
# directly using :func:`~pennylane.commutator`, which is more concise and avoids
219+
# the need to expand the algebra manually:
220+
#
221+
# .. code-block:: python
222+
#
223+
# cost_h, driver_h = qaoa.max_clique(graph, constrained=False)
224+
# comm_h = qml.simplify(1j * qml.commutator(driver_h, cost_h))
225+
210226
######################################################################
211227
# We can now build the FALQON algorithm. Our goal is to evolve some initial state under the Hamiltonian :math:`H,`
212228
# with our chosen :math:`\beta(t).` We first define one layer of the Trotterized time evolution, which is of
213229
# the form :math:`U_d(\beta_k) U_c.` Note that we can use the :class:`~.pennylane.templates.ApproxTimeEvolution` template:
214230

231+
215232
def falqon_layer(beta_k, cost_h, driver_h, delta_t):
216233
qml.ApproxTimeEvolution(cost_h, delta_t, 1)
217234
qml.ApproxTimeEvolution(driver_h, delta_t * beta_k, 1)
218235

236+
219237
######################################################################
220238
# We then define a method which returns a FALQON ansatz corresponding to a particular cost Hamiltonian, driver
221239
# Hamiltonian, and :math:`\Delta t.` This involves multiple repetitions of the "FALQON layer" defined above. The
222240
# initial state of our circuit is an even superposition:
223241

242+
224243
def build_maxclique_ansatz(cost_h, driver_h, delta_t):
225244
def ansatz(beta, **kwargs):
226245
layers = len(beta)
227246
for w in dev.wires:
228247
qml.Hadamard(wires=w)
229-
qml.layer(
230-
falqon_layer,
231-
layers,
232-
beta,
233-
cost_h=cost_h,
234-
driver_h=driver_h,
235-
delta_t=delta_t
236-
)
248+
qml.layer(falqon_layer, layers, beta, cost_h=cost_h, driver_h=driver_h, delta_t=delta_t)
237249

238250
return ansatz
239251

@@ -243,27 +255,36 @@ def expval_circuit(beta, measurement_h):
243255
ansatz(beta)
244256
return qml.expval(measurement_h)
245257

258+
246259
######################################################################
247260
# Finally, we implement the recursive process, where FALQON is able to determine the values
248261
# of :math:`\beta_k,` feeding back into itself as the number of layers increases. This is
249262
# straightforward using the methods defined above:
250263

264+
251265
def max_clique_falqon(graph, n, beta_1, delta_t, dev):
252-
comm_h = build_hamiltonian(graph) # Builds the commutator
253-
cost_h, driver_h = qaoa.max_clique(graph, constrained=False) # Builds H_c and H_d
254-
cost_fn = qml.QNode(expval_circuit, dev, interface="autograd") # The ansatz + measurement circuit is executable
266+
comm_h = build_hamiltonian(graph) # Builds the commutator
267+
cost_h, driver_h = qaoa.max_clique(graph, constrained=False) # Builds H_c and H_d
268+
cost_fn = qml.QNode(
269+
expval_circuit, dev
270+
) # The ansatz + measurement circuit is executable
255271

256-
beta = [beta_1] # Records each value of beta_k
257-
energies = [] # Records the value of the cost function at each step
272+
beta = [beta_1] # Records each value of beta_k
273+
energies = [] # Records the value of the cost function at each step
258274

259275
for i in range(n):
260276
# Adds a value of beta to the list and evaluates the cost function
261-
beta.append(-1 * cost_fn(beta, measurement_h=comm_h)) # this call measures the expectation of the commuter hamiltonian
262-
energy = cost_fn(beta, measurement_h=cost_h) # this call measures the expectation of the cost hamiltonian
277+
beta.append(
278+
-1 * cost_fn(beta, measurement_h=comm_h)
279+
) # this call measures the expectation of the commuter hamiltonian
280+
energy = cost_fn(
281+
beta, measurement_h=cost_h
282+
) # this call measures the expectation of the cost hamiltonian
263283
energies.append(energy)
264284

265285
return beta, energies
266286

287+
267288
######################################################################
268289
# Note that we return both the list of :math:`\beta_k` values, as well as the expectation value of the cost Hamiltonian
269290
# for each step.
@@ -277,15 +298,15 @@ def max_clique_falqon(graph, n, beta_1, delta_t, dev):
277298
beta_1 = 0.0
278299
delta_t = 0.03
279300

280-
dev = qml.device("default.qubit", wires=graph.nodes) # Creates a device for the simulation
301+
dev = qml.device("default.qubit", wires=graph.nodes) # Creates a device for the simulation
281302
res_beta, res_energies = max_clique_falqon(graph, n, beta_1, delta_t, dev)
282303

283304
######################################################################
284305
# We can then plot the expectation value of the cost Hamiltonian over the
285306
# iterations of the algorithm:
286307
#
287308

288-
plt.plot(range(n+1)[1:], res_energies)
309+
plt.plot(range(n + 1)[1:], res_energies)
289310
plt.xlabel("Iteration")
290311
plt.ylabel("Cost Function Value")
291312
plt.show()
@@ -297,18 +318,20 @@ def max_clique_falqon(graph, n, beta_1, delta_t, dev):
297318
# we can create a graph showing the probability of measuring each possible bit string.
298319
# We define the following circuit, feeding in the optimal values of :math:`\beta_k:`
299320

300-
@qml.qnode(dev, interface="autograd")
321+
322+
@qml.qnode(dev)
301323
def prob_circuit():
302324
ansatz = build_maxclique_ansatz(cost_h, driver_h, delta_t)
303325
ansatz(res_beta)
304326
return qml.probs(wires=dev.wires)
305327

328+
306329
######################################################################
307330
# Running this circuit gives us the following probability distribution:
308331
#
309332

310333
probs = prob_circuit()
311-
plt.bar(range(2**len(dev.wires)), probs)
334+
plt.bar(range(2 ** len(dev.wires)), probs)
312335
plt.xlabel("Bit string")
313336
plt.ylabel("Measurement Probability")
314337
plt.show()
@@ -320,7 +343,7 @@ def prob_circuit():
320343
#
321344

322345
graph = nx.Graph(edges)
323-
cmap = ["#00b4d9"]*3 + ["#e377c2"]*2
346+
cmap = ["#00b4d9"] * 3 + ["#e377c2"] * 2
324347
positions = nx.spring_layout(graph, seed=1)
325348
nx.draw(graph, with_labels=True, node_color=cmap, pos=positions)
326349
plt.show()
@@ -411,30 +434,33 @@ def prob_circuit():
411434
# Creates the cost and mixer Hamiltonians
412435
cost_h, mixer_h = qaoa.max_clique(new_graph, constrained=False)
413436

437+
414438
# Creates a layer of QAOA
415439
def qaoa_layer(gamma, beta):
416440
qaoa.cost_layer(gamma, cost_h)
417441
qaoa.mixer_layer(beta, mixer_h)
418442

443+
419444
# Creates the full QAOA circuit as an executable cost function
420445
def qaoa_circuit(params, **kwargs):
421446
for w in dev.wires:
422447
qml.Hadamard(wires=w)
423448
qml.layer(qaoa_layer, depth, params[0], params[1])
424449

425450

426-
@qml.qnode(dev, interface="autograd")
451+
@qml.qnode(dev)
427452
def qaoa_expval(params):
428453
qaoa_circuit(params)
429454
return qml.expval(cost_h)
430455

456+
431457
######################################################################
432458
# Now all we have to do is run FALQON for :math:`5` steps to get our initial QAOA parameters.
433459
# We set :math:`\Delta t = 0.02:`
434460

435461
delta_t = 0.02
436462

437-
res, res_energy = max_clique_falqon(new_graph, depth-1, 0.0, delta_t, dev)
463+
res, res_energy = max_clique_falqon(new_graph, depth - 1, 0.0, delta_t, dev)
438464

439465
params = np.array([[delta_t for k in res], [delta_t * k for k in res]], requires_grad=True)
440466

@@ -455,13 +481,15 @@ def qaoa_expval(params):
455481
# define a circuit which outputs the probabilities of measuring each bit string, and
456482
# create a bar graph:
457483

458-
@qml.qnode(dev, interface="autograd")
484+
485+
@qml.qnode(dev)
459486
def prob_circuit(params):
460487
qaoa_circuit(params)
461488
return qml.probs(wires=dev.wires)
462489

490+
463491
probs = prob_circuit(params)
464-
plt.bar(range(2**len(dev.wires)), probs)
492+
plt.bar(range(2 ** len(dev.wires)), probs)
465493
plt.xlabel("Bit string")
466494
plt.ylabel("Measurement Probability")
467495
plt.show()

demonstrations_v2/tutorial_falqon/metadata.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"executable_stable": true,
1212
"executable_latest": true,
1313
"dateOfPublication": "2021-05-21T00:00:00+00:00",
14-
"dateOfLastModification": "2025-09-22T15:48:14+00:00",
14+
"dateOfLastModification": "2026-03-27T00:00:00+00:00",
1515
"categories": [
1616
"Optimization"
1717
],

0 commit comments

Comments
 (0)