Skip to main content

Cram sheet — Section 3: Create quantum circuits

One-screen review layer for this section: short background primers per objective, then the key facts — each backed by an official source or by an executed proof from the question bank.

Construct basic quantum circuits

Only the classical bits you asked for

A circuit has exactly the classical bits you declared, and measure needs one to write into.

qc = QuantumCircuit(2, 2) # 2 qubits AND 2 clbits, registers auto-named q and c
qc.measure(0, 1) # qubit first, clbit second: qubit 0's result lands in c[1]

Building from named QuantumRegister(3, 'data') and ClassicalRegister(2, 'c') objects is purely organizational — it lets you write data[0] — and buys nothing at execution or transpile time. Auto-naming is why add_register(QuantumRegister(2, 'q')) on a QuantumCircuit(2, 2) fails on the duplicate name.

MethodMeasuresWrites into
measure(qubit, cbit)the qubits you name (ints, Bits, registers or parallel lists)the clbits you name — the mapping is yours, and the keyword is cbit, not clbit
measure_all()every qubita FRESH ClassicalRegister named meas, sized to the qubit count
measure_active()only qubits carrying operationsthe same fresh meas register

Results are little-endian: bitstrings print as c_(n−1) … c_0, with classical bit 0 on the RIGHT.

Sources: construct-circuits · measure-qubits · qiskit.circuit.QuantumCircuit

compose returns, append mutates, tensor widens

compose is functional by DEFAULT: it returns a new circuit and leaves the caller untouched, so base.compose(add_on) with the result discarded is a silent no-op.

big = base.compose(sub, qubits=[0, 2]) # keep the return value...
base.compose(sub, inplace=True) # ...or mutate in place
CallMutates the caller?ReturnsNotes
a.compose(b)only with inplace=Truea new circuitb may be narrower; qubits= maps its qubits onto a positionally; front=True prepends instead of appending
a.append(instr, qargs)yesan InstructionSet
a.tensor(b)noa widened circuitthe CALLER a takes the HIGHER-index qubits, so an X in a against an identity in b prepares |10⟩

Sources: construct-circuits · qiskit.circuit.QuantumCircuit

Must know:

  • Counts are little-endian — flipping only qubit 0 of a 3-qubit register and calling measure_all() yields 001 on every shot. — ⚙️ proven in s3-q010
  • qc.compose(sub, qubits=[0, 2]) maps sub's qubit 0 → 0 and qubit 1 → 2, so sub.cx(0, 1) becomes a CX with control 0 and target 2. — ⚙️ proven in s3-q012
  • width() is qubits + clbits (3 + 2 = 5), size() counts all operations including measures (5), and depth() is the critical path (4) — three different numbers. — ⚙️ proven in s3-q015

Traps:

  • QuantumCircuit(2) has 2 qubits and ZERO classical bits, so measure([0,1],[0,1]) raises CircuitError; naming the arguments never rescues it. — ⚙️ proven in s3-q047
  • measure_all() appends its fresh meas register even when clbits already exist — two registers, space-separated counts keys — and the barrier it inserts goes BEFORE the measurements. — ⚙️ proven in s3-q011

Construct dynamic circuits

Dynamic circuits: four context managers

A dynamic circuit is one whose later quantum operations depend, in real time, on a mid-circuit measurement result — a teleportation correction, syndrome handling. Sweeping an angle over many jobs or post-selecting shots afterwards is classical work outside the QPU, so neither needs one.

Qiskit supports exactly four such constructs, each a context manager on the circuit, and every condition is a (classical_resource, value) TUPLE.

with qc.if_test((cr[0], 1)): # one tuple, not two arguments
qc.x(1)
ConstructOpened withNotes
if_testwith qc.if_test((cr[0], 1)):the resource may be a single Clbit OR a whole ClassicalRegister
else branchwith qc.if_test((cr[0], 0)) as else_: then with else_:the else branch is the manager's return value
for_loopwith qc.for_loop(range(3)):as i is optional; the body may hold any number of gates and runs three times
switchwith qc.switch(cr[0]) as case: then with case(0):add a fallback with case(case.DEFAULT):, which may be declared last
while_loopa context manager like the othersthe fourth supported construct

For anything richer than equality with a literal, build the condition from qiskit.circuit.classical.expr.

Sources: classical-feedforward-and-control-flow · circuit_classical · qiskit.circuit.QuantumCircuit

The old conditional API is removed, not deprecated

Everything the pre-2.0 conditional API offered is gone, so there is no older spelling to fall back on.

What you might writeWhat Qiskit 2.x does
instr.c_if(cr[0], 1)AttributeErrorInstructionSet.c_if no longer exists
a condition= gate keywordthere is no such keyword
assigning to .conditionthere is no such attribute
qc.if_test(cr[0] == 1)TypeError: 'bool' object is not subscriptable, because Clbit.__eq__ returns an ordinary Python bool
if cr[0] == 1: in plain Pythonevaluates to False at BUILD time: no gate is added and nothing warns you

The last row is the dangerous one, because it is not an error at all: the circuit you get back is simply missing the gate.

Sources: classical-feedforward-and-control-flow · qiskit.circuit.QuantumCircuit

Must know:

  • Each control-flow construct is stored as a SINGLE composite instruction, so count_ops() on a conditional circuit shows if_else: 1 (or for_loop, switch_case), never the gates inside the body. — ⚙️ proven in s3-q029
  • expr.equal(cr, 3) is a valid if_test condition: it compares the whole register as an integer and lifts the bare Python 3 for you, with no expr.lift required. — ⚙️ proven in s3-q053
  • The drawer renders a for_loop as ONE boxed region labelled with the index set verbatim (For-0 range(0, 3)), spanning every wire the body touches; a statement after the block is drawn outside the box. — ⚙️ proven in s3-q058

Traps:

  • The condition is ONE tuple: if_test((cr[0], 1)) is right, if_test(cr[0], 1) — two positional arguments — is the classic wrong spelling. — ⚙️ proven in s3-q024
  • A switch value with no matching case falls through to DEFAULT: c0 = 1 against branches case(0) and case(case.DEFAULT) runs the DEFAULT body. — ⚙️ proven in s3-q052

Construct parameterized circuits

Parameters sort alphabetically; binding is functional

qc.parameters is a ParameterView sorted ALPHABETICALLY by name, not by insertion order: add gamma, then alpha, then beta, and you get ['alpha', 'beta', 'gamma']. ParameterVector elements are the one refinement — they sort by vector name and then by numeric index, so x[10] follows x[9].

t = Parameter('theta')
qc.rx(2 * t, 0) # a ParameterExpression, evaluated at bind time
bound = qc.assign_parameters({t: np.pi}) # qc keeps theta; bound has none
Binding formBehaviour
assign_parameters([0.5, 1.5])binds POSITIONALLY against that sorted view; needs one value per parameter, and a short list raises ValueError
assign_parameters({th: 1.0})binds only the parameters it names, so partial binding is legal
dict keysParameter objects or their name strings, but matched by IDENTITY: a different Parameter('th') object built elsewhere raises CircuitError
{x: [0.1, 0.2, 0.3]}binds a whole ParameterVector in one entry
inplace=Truemutates the circuit instead of returning a new one
bind_parameters(...)removed in Qiskit 2.x — AttributeError, not a deprecation warning

That sorted view is what makes a list treacherous and a dict safe.

Sources: construct-circuits · qiskit.circuit.Parameter · qiskit.circuit.ParameterVector

Must know:

  • ParameterVector('x', 3) creates the distinct parameters x[0], x[1], x[2]; it does not auto-grow, so x[3] raises IndexError. — ⚙️ proven in s3-q022
  • Reusing ONE Parameter object on two gates keeps num_parameters at 1 and binds BOTH gates at once — the weight-sharing mechanism behind tied ansätze. — ⚙️ proven in s3-q051
  • Parameters survive transpilation: transpile once into an ISA circuit, then bind as many times as you like — the bound circuit keeps its layout and needs no second transpile. — ⚙️ proven in s3-q050

Traps:

  • A list binds positionally against the sorted view: [0.5, 1.5] on a circuit with rx(theta) then rz(phi) gives phi = 0.5 and theta = 1.5 — the reverse of gate order. — ⚙️ proven in s3-q017
  • 2 * th is a ParameterExpression, not a Parameter: isinstance(2 * th, Parameter) is False, while qc.parameters still reports one free Parameter. Binding keeps the symbolic type — cast with float(...). — ⚙️ proven in s3-q054

Transpile and optimize circuits

ISA circuits and the six preset stages

Hardware runs ISA circuits only: every instruction must be in the backend's Target AND every two-qubit gate must sit on a pair connected in its CouplingMap. Both conditions, every time.

pm = generate_preset_pass_manager(optimization_level=2, backend=backend)
isa = pm.run(qc) # a single circuit in, a single circuit out
StageWhat it does
initunroll to one- and two-qubit gates
layoutchoose which physical qubits the virtual ones start on
routinginsert SWAPs so every two-qubit gate lands on a connected pair
translationrewrite every remaining gate into the basis
optimizationsimplify
schedulingtiming; the slot exists even with no scheduling method and simply runs empty

pm.stages reports exactly those six names in that order, and ROUTING — not layout — is what inserts SWAPs. Hand pm.run a LIST and you get a list back, not a circuit. A circuit wider than the device has no valid layout at all and fails in the layout stage.

Sources: transpile · transpiler-stages · transpile-with-pass-managers

Four optimization levels, one target

Levels run 0 through 3; anything else, including 4, raises ValueError. All four aim at the SAME target constraints, so they differ in effort, not in whether the output is a valid ISA circuit.

LevelLayoutOptimization added
0TrivialLayoutnone at all
1trivial or VF2 layout searchlight: one-qubit optimization plus InverseCancellation
2no trivial layoutCommutativeCancellation
3as level 2KAK resynthesis of two-qubit blocks, plus unitarity-breaking passes on measurements

Higher is not automatically better: a higher level costs compile time and is not guaranteed to give fewer two-qubit gates. The docs' own example shows identical two-qubit counts at levels 1 and 2, with only level 3 improving on them. The guide pages still call optimization_level a required positional argument, while the 2.5 signature carries a default.

Sources: set-optimization · transpile-with-pass-managers

Must know:

  • generate_preset_pass_manager defaults to optimization_level=2 in Qiskit 2.5 (a signature default, backend-independent) — level 1 was transpile()'s default back in 1.x. — ⚙️ proven in s3-q031
  • Measured routing cost: one cx(0, 4) on a 5-qubit line gives 10 CX at level 0 (trivial layout [0,1,2,3,4]) versus 1 CX at level 1 — the win is LAYOUT, not gate cancellation. — ⚙️ proven in s3-q032
  • initial_layout=[2, 3, 4] is an ORDERED virtual→physical map (virtual 0 → physical 2, 1 → 3, 2 → 4), not a set of permitted qubits. — ⚙️ proven in s3-q043
  • Passes by stage in a level-2 preset: VF2Layout/SabreLayout in layout; CheckMap/SabreSwap/VF2PostLayout in routing; BasisTranslator in translation and init; Optimize1qGatesDecomposition in optimization; ConsolidateBlocks in init. — ⚙️ proven in s3-q055
  • Level 0 schedules no optimization pass at all: nine gates in, nine out, where level 2 returns five — BOTH repeated pairs (cx-cx and z-z) cancel together, never one without the other. — ⚙️ proven in s3-q057

Traps:

  • decompose() is not a route to ISA: it rewrites h into u, which is outside the IBM basis. An h, a ccx, a non-adjacent cx and an over-wide circuit all fail the check. — ⚙️ proven in s3-q033
  • A pass manager runs with pm.run(qc); pm.transpile(qc), pm(qc) and qc.transpile(pm) do not exist. — ⚙️ proven in s3-q040
  • Without a backend object, pass basis_gates= AND coupling_map=; basis_gates alone translates but does not route, so the output is not ISA. — ⚙️ proven in s3-q035
Exam checklist
  • Declare the classical bits before you measure — QuantumCircuit(2) has none.
  • Read every counts bitstring right-to-left: classical bit 0 is the rightmost character.
  • Assign the result of compose and assign_parameters, or pass inplace=True.
  • Write each condition as one (resource, value) tuple inside a with block; c_if is gone.
  • Sort qc.parameters alphabetically in your head before binding a positional list — or bind a dict.
  • Check BOTH ISA conditions on anything you send to hardware: basis gates and coupling map.
  • Name the six preset stages in order, and say which one inserts the SWAPs.
  • Run a pass manager with pm.run(qc), at an optimization level between 0 and 3.

Every fact above is sourced: 📖 links go to official documentation, ⚙️ marks facts observed by executing code against the pinned Qiskit stack (the linked section page shows the proof evidence on its practice questions).