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.parameters is 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 full entanglement 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 v2hardware_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.