Chapter 8 — Key Takeaways (Building Complex Circuits)
The ansatz-building page. The ordering hazard is the one to internalize.
Parameters
from qiskit.circuit import Parameter, ParameterVector
theta = Parameter("theta")
qc.ry(theta, 0) # the circuit is now a FUNCTION of theta
qc.ry(2 * a + 1, 0) # expressions work: one variable, several gates
thetas = ParameterVector("theta", 12) # theta[0] ... theta[11]
A parameterized circuit can be transpiled and cannot be simulated — which is the entire basis of Chapter 7's transpile-once/bind-many pattern.
★★ The ordering hazard
qc.parametersis sorted LEXICOGRAPHICALLY BY NAME, and sequence binding follows that order.
theta1 … theta12 sorts as theta1, theta10, theta11, theta12, theta2, …, theta9. Binding
[1, 2, …, 12] by sequence:
theta1 intended 1 got 1
theta2 intended 2 got 5 <-- WRONG
...
theta10 intended 10 got 2 <-- WRONG
11 of 12 gates received the wrong value.
Nothing raises. Valid circuit, right parameter count, legal angles, well-defined wrong answer.
| Fix | How |
|---|---|
ParameterVector |
elements sort by index, so theta[2] precedes theta[10] |
| dictionary binding | qc.assign_parameters(dict(zip(ordered_params, values))) |
If you will ever bind by sequence — and you will, because that is what optimizers produce — never hand-name a family of parameters.
Binding cost, 80 parameters × 200 bindings: sequence 7.4 ms, dict 28.1 ms. The fast, natural, universal way is the one that misassigns hand-named parameters.
compose vs append
compose |
append(sub.to_gate()) |
|
|---|---|---|
| Structure | flattened | one named instruction |
count_ops |
real gates | the block name |
depth |
real | 1 per block — meaningless |
.inverse(), .control(), .power() |
no | yes |
| QASM export | as gates | as a gate definition |
10 appended blocks: qc.depth() = 10 ← not the depth
transpiled depth ← THE depth
⚠️ Controlling a gate makes its global phase observable (Ch. 3 §3.7, Ch. 6 §6.6). Check
sub.global_phase before .control().
🗝️ The circuit library became FUNCTIONS in Qiskit 2.1
| Deprecated class (removed in 3.0) | Current |
|---|---|
EfficientSU2 |
efficient_su2() |
RealAmplitudes |
real_amplitudes() |
TwoLocal |
n_local() |
ZZFeatureMap |
zz_feature_map() |
QFT |
QFTGate, synth_qft_full() |
The classes were blueprint circuits — lazily rebuilt objects that were not plain
QuantumCircuits and behaved subtly differently in composition, serialization, and equality. The
functions return plain circuits.
| Construction | params (n=4) | Use for |
|---|---|---|
efficient_su2(4) |
32 | general ansatz; $R_y$ and $R_z$ |
real_amplitudes(4) |
16 | real amplitudes — molecular ground states |
n_local(...) |
configurable | custom rotation/entangling blocks |
zz_feature_map(4) |
4 | data encoding — parameters are the data, not free variables |
Barriers block optimization
x; x -> transpiles to NOTHING (depth 0)
x; barrier; x -> both survive (depth 2)
Legitimate: diagram clarity · forcing a sequence to execute (benchmarking, calibration) · constraining scheduling. Bad: a debugging habit you forget to remove — measured at +5 ISA depth on a realistic ansatz.
from qiskit.transpiler.passes import RemoveBarriers
stripped = RemoveBarriers()(qc)
Entanglement patterns
| Pattern | CNOTs | n=8 CNOTs | n=8 depth |
|---|---|---|---|
full |
$n(n-1)/2$ — quadratic | 28 | 17 |
linear |
$n-1$ | 7 | 11 |
circular |
$n$ | 8 | 12 |
pairwise |
$n-1$ | 7 | 6 |
Parameter count is identical across all of them — the entangling layer has none.
pairwise matches linear's count at ~half the depth (two parallel sub-layers). Start there. Use
full only on small circuits or all-to-all hardware — and note that its real cost is worse than
quadratic once routing inserts SWAPs at 3 CNOTs each.
★ Logical vs ISA cost
n depth | params logical ISA ratio 2q
---------------------------------------------
4 2 | 24 11 35 3.2 6
8 3 | 64 19 61 3.2 21
10 3 | 80 21 66 3.1 27
The ratio is a stable ~3× — Chapter 3 §3.8's arithmetic applied uniformly. Most of the added
depth is free rz. The number to watch is the two-qubit count: $(n-1)\times d$ for linear
entanglement, and it is what consumes the error budget.
★ The ry / ryrz surprise
rotation params ISA depth 2q pulses
ry 12 41 6 40
ryrz 24 41 6 38
Doubling the parameters costs nothing. An arbitrary single-qubit gate is 3 rz + 2 sx
regardless of complexity (Ch. 3 CS2), so ry and ry·rz merge into one and cost the same.
So the choice is entirely an optimization decision: twice the parameters, ~twice the optimizer iterations, worse barren plateaus — for states a real-amplitude ground state does not need.
"More gates means more error" is a good heuristic that fails here. Measure the cost of a design decision rather than assuming it.
Common pitfalls
- Hand-naming a parameter family and binding by sequence.
- Reading
depth()on a circuit of appended blocks. - Using the deprecated library classes from an older tutorial.
- Leaving debugging barriers in an ansatz.
- Choosing
fullentanglement by default. - Assuming more parameters means a more expensive circuit.
- Choosing an ansatz on cost alone without checking it can reach the answer.
Project piece added this chapter
vqelab/circuits.py v2 — hardware_efficient_ansatz(n, depth, entanglement, rotation),
_entangling_pairs, ansatz_report(), format_report().
Three decisions with stated reasons: ParameterVector (sequence binding is safe), $R_y$ only
(real amplitudes are what a molecular ground state needs, at half the search space), linear by
default (matches hardware connectivity; more expressiveness than the problem needs makes barren
plateaus worse).
ansatz_report() prints the four numbers you want before changing an ansatz: parameters, logical
depth, ISA depth, two-qubit gates.