Case Study 1: The Optimizer That Could Not Converge
"The circuit was valid. The parameters were all bound. Every angle was a legal number. The answer was still wrong, and nothing raised."
Executive Summary
A team's VQE will not converge. The optimizer wanders, the energy plateaus far above the known answer, and restarts from different initial points land in different places. Every diagnosis they try points at the physics: the ansatz must be insufficiently expressive, or the landscape must have barren plateaus, or the noise must be too high.
The cause is a naming convention. Their parameters are called theta1 through theta16, the
optimizer hands back a NumPy array, and assign_parameters binds by position against a list sorted
lexicographically — so theta10 comes before theta2, and fifteen of sixteen angles land in the
wrong gate.
This case study reproduces the failure, shows why each of their four diagnoses was reasonable and wrong, and derives the general defense. It is short because the bug is small; it is here because that smallness is exactly what makes it survive review.
Skills applied: parameters and binding (§8.1, §8.2); ansatz construction (§8.8); expressiveness diagnostics (Ch. 4 checkpoint); the discipline of checking the pipeline before the physics (Ch. 7 Case Study 1).
The Setup
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
def ansatz(n, depth):
"""A hardware-efficient ansatz. Looks fine."""
params = [Parameter(f"theta{i}") for i in range(1, n * (depth + 1) + 1)]
qc = QuantumCircuit(n)
k = 0
for layer in range(depth + 1):
for q in range(n):
qc.ry(params[k], q)
k += 1
if layer < depth:
for q in range(n - 1):
qc.cx(q, q + 1)
return qc
Four qubits, depth three, sixteen parameters. The construction is correct: the right gates in the right places, one parameter each, used in creation order.
The optimizer loop is also correct:
from scipy.optimize import minimize
def energy(values):
bound = ansatz_circuit.assign_parameters(values) # <-- the bug lives here
return float(estimator.run([(bound, hamiltonian)]).result()[0].data.evs)
result = minimize(energy, x0=initial_values, method="COBYLA")
values is a NumPy array of sixteen numbers. assign_parameters accepts it. Nothing raises.
The Bug
params = [Parameter(f"theta{i}") for i in range(1, 17)]
qc = QuantumCircuit(1)
for p in params:
qc.rz(p, 0)
print("created:", [p.name for p in params][:6], "...")
print("sorted :", [p.name for p in qc.parameters][:6], "...")
created: ['theta1', 'theta2', 'theta3', 'theta4', 'theta5', 'theta6'] ...
sorted : ['theta1', 'theta10', 'theta11', 'theta12', 'theta13', 'theta14'] ...
Sequence binding follows the sorted order. Binding [1, 2, …, 16] gives:
gate uses you intended it got
----------------------------------
theta1 1 1
theta2 2 8 <-- WRONG
theta3 3 9 <-- WRONG
...
theta10 10 2 <-- WRONG
...
Fifteen of sixteen wrong. The circuit is entirely valid; the angles are simply in the wrong gates.
Why Each Diagnosis Was Reasonable — and Wrong
The team tried four things, in a sensible order.
"The ansatz is not expressive enough." They increased depth from 3 to 6. Convergence got
worse, which they read as evidence of barren plateaus. In fact more parameters means a longer
sorted list and a more scrambled permutation — the symptom worsening with depth was a clue pointing
directly at the cause, and it was read as pointing away from it.
"It is a barren plateau." They measured gradient variance and found it small. It was small, because the effective objective function is the true one composed with a fixed permutation of its inputs — a function with the same landscape statistics but a scrambled parameterization. Barren plateaus were present in the sense the measurement detects, and were not the cause.
"The noise is too high." They ran on the noiseless simulator. It still failed, which should have been decisive and was instead read as "the ansatz really is the problem." A failure that survives the removal of noise is not a noise failure, and that inference is worth making explicitly every time.
"The optimizer is wrong for this landscape." They switched COBYLA to SPSA to L-BFGS-B. All three failed, differently. Three optimizers failing on the same problem is weak evidence about optimizers and strong evidence about the problem.
🐛 Debug This — The check that would have found it in thirty seconds.
```python
Bind a KNOWN, DISTINGUISHABLE pattern and read it back.
probe = list(range(1, ansatz_circuit.num_parameters + 1)) bound = ansatz_circuit.assign_parameters(probe) got = [float(i.operation.params[0]) for i in bound.data if i.operation.name in ("ry", "rz", "rx")] assert got == [float(v) for v in probe], f"binding is scrambled: {got}" ```
Bind
[1, 2, 3, …]— values chosen so that any permutation is immediately visible — and check that gate $k$ received value $k$.This test can never fail once you use a
ParameterVector, which is precisely the argument for writing it: a test that cannot fail costs nothing to keep, and this one would have saved several days.The general principle: when a numerical pipeline misbehaves, verify that your inputs arrive where you think they arrive before you theorize about the numerics. It is the cheapest check available and it is almost never the first one tried.
The Fix
from qiskit.circuit import ParameterVector
def ansatz(n, depth):
thetas = ParameterVector("theta", n * (depth + 1)) # theta[0] ... theta[k]
qc = QuantumCircuit(n)
k = 0
for layer in range(depth + 1):
for q in range(n):
qc.ry(thetas[k], q)
k += 1
if layer < depth:
for q in range(n - 1):
qc.cx(q, q + 1)
return qc
One line changed. Qiskit sorts ParameterVector elements by index, so theta[2] precedes
theta[10] and sequence binding is exact.
The VQE converged on the next run.
Why This Survives Review
Three properties, and they generalize to a whole class of bug.
It is invisible in the source. Nothing about Parameter(f"theta{i}") looks wrong. It is the
obvious way to write it, it reads correctly, and the error is in an interaction between two pieces of
code that are individually fine.
It has no failure signature. No exception, no warning, no NaN, no out-of-range value. The
circuit is valid and the state it prepares is a perfectly good quantum state. The only symptom is
"the answer is wrong," which is the least diagnostic symptom there is.
Its symptoms mimic a well-known real problem. Poor convergence, worsening with depth, small gradients — that is the textbook description of barren plateaus, a genuine and much-discussed phenomenon. The bug wore the costume of the field's most famous difficulty, and that costume is what made four competent diagnoses all point the wrong way.
🔬 Honest Assessment — How much published work does this affect?
Unknowable, and worth thinking about anyway.
The bug requires hand-named parameters and sequence binding, and it silently produces a valid circuit with scrambled angles. Its effect is to make a variational algorithm underperform — which is, unfortunately, the expected outcome of most near-term variational experiments. A result that underperforms gets attributed to noise, to ansatz expressiveness, or to barren plateaus, all of which are real. A bug whose symptom is "it did not work as well as hoped" is well camouflaged in a field where most things do not.
This is a specific instance of a general problem in quantum computing: the absence of ground truth. In most of computing, a wrong answer is detectably wrong. Here, for anything large enough to be interesting, there is nothing to check against. Chapter 27's testing strategy is largely an attempt to build checks that do not require knowing the answer, and this case study is the cheapest example of one: you do not need to know the right energy to know that your parameters arrived in the wrong gates.
Lessons
- Use
ParameterVectorfor any family of parameters. One line, and the hazard is structurally impossible. - If you must hand-name, bind by dictionary.
dict(zip(ordered_params, values))is immune. - Test that your inputs arrive where you think they do. Bind
[1, 2, 3, …]and read it back. The test can never fail, which is why it is worth keeping. - A failure that survives removing noise is not a noise failure. Make that inference explicitly.
- Three optimizers failing is evidence about the problem, not the optimizers.
- A symptom that worsens with problem size is a clue about mechanism. Here it pointed straight at the permutation and was read as pointing at barren plateaus.
- Check the pipeline before the physics. It is cheaper, and it is almost never done first.
- Beware bugs that wear the costume of a known difficulty. Barren plateaus are real; that is exactly what made them such an effective disguise.
Questions
-
Reproduce the failure: build the hand-named ansatz, bind
[1, …, 16], and report how many gates received the wrong value. Then fix it and confirm. -
The symptom worsened with depth. Quantify it: for $n = 4$ and depth 1 through 6, compute the fraction of parameters that land in the wrong gate. Is it monotonic? Explain the pattern.
-
Write the probe test from the 🐛 Debug This callout as a
pytesttest. Why is a test that can never fail still worth having in a suite? (Chapter 27 §27.6 has a general answer.) -
Would the bug appear if the parameters were named
theta01…theta16with zero padding? Verify. Is zero-padding a satisfactory fix? What breaks it? -
Construct a case where the scrambling is harmless — where the wrong assignment still reaches the correct minimum. What property of the ansatz makes this possible, and why does it not rescue the general case?
-
The team switched optimizers three times. Design the experiment they should have run instead: what single test would have distinguished "the ansatz cannot represent the answer" from "the pipeline is broken"?
-
Hardest. The 🔬 Honest Assessment argues that this bug is camouflaged by the field's low expectations. Propose a verification practice — something a group could adopt as policy — that would catch this class of error without requiring knowledge of the correct answer. What would it cost, and what would it fail to catch?