Day 3 — Quantum Operations
Lesson
C1000-179 exam sprint · Qiskit 2.5.1 · every snippet below was executed and outputs are real. Section 2 is ~11 of 68 questions. Most of them hinge on one convention: little-endian.
0. THE Rule of this Section: Little-Endian
Qiskit orders qubits little-endian everywhere:
- In a Pauli string, the rightmost character acts on qubit 0.
- In a bitstring / statevector label, the rightmost bit is qubit 0.
- In a statevector index, the integer value of the bitstring (q_{n-1}...q1 q0) is the index.
Pauli('IZ') → I on qubit 1, Z on QUBIT 0 (rightmost = q0!)
Pauli('XZ') → X on qubit 1, Z on qubit 0; matrix = np.kron(X, Z)
label '10' → q1 = 1, q0 = 0 → statevector index 2
⚠️ COMMON EXAM TRAP #1 (the big one)
Pauli('IZ')does NOT act Z on qubit 1. Read Pauli strings right to left: position 0 from the right = qubit 0. IBM writes distractors that assume big-endian. When in doubt on the exam, mentally rewrite:'IZ'= Z₀,'ZI'= Z₁.
Verified:
import numpy as np
from qiskit.quantum_info import Pauli
np.diag(Pauli('IZ').to_matrix()).real # [ 1. -1. 1. -1.] = kron(I, Z) → Z on q0
np.diag(Pauli('ZI').to_matrix()).real # [ 1. 1. -1. -1.] = kron(Z, I) → Z on q1to_matrix() of a multi-qubit Pauli string 'AB' is np.kron(A, B) — the leftmost
letter is the highest qubit and comes first in the Kronecker product:
X = np.array([[0,1],[1,0]]); Z = np.diag([1,-1])
np.allclose(Pauli('XZ').to_matrix(), np.kron(X, Z)) # True
np.allclose(Pauli('XZ').to_matrix(), np.kron(Z, X)) # False1. Matrix Reference Table (memorize cold)
| Gate | Matrix | Notes |
|---|---|---|
| I | [[1, 0], [0, 1]] |
identity |
| X | [[0, 1], [1, 0]] |
bit flip; X|0⟩=|1⟩ |
| Y | [[0, -i], [i, 0]] |
Y = iXZ; Y|0⟩ = i|1⟩ |
| Z | [[1, 0], [0, -1]] |
phase flip; Z|+⟩=|−⟩ |
| H | (1/√2)[[1, 1], [1, -1]] |
H|0⟩=|+⟩, H|1⟩=|−⟩; H = H† |
| S | [[1, 0], [0, i]] |
S = √Z; S² = Z; S|+⟩ = |i⟩ |
| T | [[1, 0], [0, e^{iπ/4}]] = [[1,0],[0,(1+i)/√2]] |
T = √S; T² = S; T⁴ = Z |
Also: sdg = diag(1, −i), tdg = diag(1, e^{−iπ/4}). All verified via Operator(qc).data.
Ladder to remember: T² = S, S² = Z, (so T⁴ = Z), Z² = I, H² = I.
2. The Pauli Class
2.1 Construction from strings, with phase prefixes
Valid prefixes: '', '-', 'i', '-i' (the group phase (−i)^k).
from qiskit.quantum_info import Pauli
Pauli('IZ') # IZ (2 qubits, Z on qubit 0)
Pauli('-iXY') # -iXY (Y on q0, X on q1, overall phase -i)
Pauli('-iXY').phase # 1
Pauli('-Z').phase # 2
Pauli('iZ').phase # 3
Pauli('X').phase # 0⚠️ TRAP:
.phasestores k where prefix = (−i)^k:''→0,'-i'→1,'-'→2,'i'→3. It is not "0,1,2,3 = +1,i,−1,−i".
to_matrix() includes the phase: Pauli('iZ').to_matrix() = [[1j, 0], [0, -1j]].
.to_label() round-trips: Pauli('-iXY').to_label() → '-iXY'.
2.2 Pauli products — compose vs dot
A.dot(B)= matrix product A·B (B applied first).A.compose(B)= matrix product B·A (A applied first — circuit order!).
X, Y = Pauli('X'), Pauli('Y')
X.dot(Y) # iZ (X·Y = iZ)
X.compose(Y) # -iZ (Y·X = -iZ)Cyclic products (verified): X@Y = iZ, Y@Z = iX, Z@X = iY; reversed order flips the sign
(X@Z = -iY). Operator shortcuts: @ = dot, & = compose, ^ = tensor.
2.3 tensor vs expand, adjoint
a, b = Pauli('X'), Pauli('Z')
a.tensor(b) # XZ — a goes to the HIGHER qubits (left of string)
a.expand(b) # ZX — reversed: b goes to the higher qubits
Pauli('-iXY').adjoint() # iXY (phase conjugated; Paulis are Hermitian up to phase)2.4 Commutation
Two Pauli strings commute iff they anticommute on an even number of positions.
Pauli('X').commutes(Pauli('Z')) # False (single position, anticommute)
Pauli('XX').commutes(Pauli('ZZ')) # True (two anticommuting positions → even)
Pauli('XI').commutes(Pauli('ZI')) # False (one anticommuting position: qubit 1)
Pauli('XI').commutes(Pauli('IZ')) # True (act on different qubits)
Pauli('X').anticommutes(Pauli('Z')) # True3. SparsePauliOp — Observables / Hamiltonians
3.1 Construction
from qiskit.quantum_info import SparsePauliOp
op = SparsePauliOp.from_list([("XZ", 1.0), ("IY", 0.5)])
# SparsePauliOp(['XZ', 'IY'], coeffs=[1. +0.j, 0.5+0.j])
op.coeffs # array([1. +0.j, 0.5+0.j]) ← ALWAYS complex128
op.paulis # PauliList(['XZ', 'IY'])
op.num_qubits # 2
SparsePauliOp("XYZ") # single term, default coeff 1+0j⚠️ TRAP:
coeffsare always complex (complex128), even if you pass floats/ints. An answer choice showingcoeffs=[1., 0.5](real dtype) is wrong; it prints1.+0.j.
from_sparse_list gives (pauli chars, qubit indices, coeff) — indices map chars to qubits:
op2 = SparsePauliOp.from_sparse_list(
[("ZX", [1, 4], 2.0), # Z on qubit 1, X on qubit 4
("YY", [0, 3], -1.0)], # Y on qubits 0 and 3
num_qubits=5)
# SparsePauliOp(['XIIZI', 'IYIIY'], coeffs=[ 2.+0.j, -1.+0.j])Check that: 'XIIZI' read right-to-left = q0:I, q1:Z, q2:I, q3:I, q4:X. Little-endian again.
3.2 simplify(), arithmetic, adjoint
op3 = SparsePauliOp.from_list([("XX", 1.0), ("XX", 1.0), ("ZZ", 0.0)])
op3.simplify()
# SparsePauliOp(['XX'], coeffs=[2.+0.j]) — merges duplicates, drops zero coeffs
SparsePauliOp.from_list([("Z", 1.0), ("Z", -1.0)]).simplify()
# SparsePauliOp(['I'], coeffs=[0.+0.j]) — full cancellation leaves a zero identity term
SparsePauliOp(["XI"]) + SparsePauliOp(["IX"]) # ['XI', 'IX'], coeffs=[1.+0.j, 1.+0.j]
2j * SparsePauliOp(["Z"]) # ['Z'], coeffs=[0.+2.j]
((1+2j) * SparsePauliOp(["Z"])).adjoint() # ['Z'], coeffs=[1.-2.j] (conjugates coeffs)3.3 compose / tensor / expand (same conventions as Pauli)
A = SparsePauliOp(["X"]); B = SparsePauliOp(["Z"])
A.tensor(B) # ['XZ'] — A on higher qubit
A.expand(B) # ['ZX'] — B on higher qubit
A.compose(B) # ['Y'], coeffs=[0.+1.j] (= Z·X = iY : compose applies A first)Typical Estimator Hamiltonian: SparsePauliOp.from_list([("ZZ", 1.0), ("XI", 0.5), ("IX", 0.5)]).
4. Statevector
4.1 Construction
from qiskit.quantum_info import Statevector
from qiskit import QuantumCircuit
Statevector.from_label('01').data.real # [0. 1. 0. 0.] → index 1 (q1=0, q0=1)
Statevector.from_label('10').data.real # [0. 0. 1. 0.] → index 2 (q1=1, q0=0)Labels beyond 0/1: '+', '-' (X eigenstates), 'r', 'l' (Y eigenstates):
'r' = (|0⟩ + i|1⟩)/√2, 'l' = (|0⟩ − i|1⟩)/√2.
qc = QuantumCircuit(2)
qc.h(0); qc.cx(0, 1) # Bell state
bell = Statevector.from_instruction(qc)
bell.data.real # [0.7071 0. 0. 0.7071]⚠️ TRAP — statevector index ordering: the amplitude at index k belongs to basis state |bin(k)⟩ read little-endian.
qc.x(0)on a 2-qubit circuit gives[0, 1, 0, 0](index 1 = '01' = q0 flipped), not[0, 0, 1, 0].qc.x(1)gives[0, 0, 1, 0](index 2 = '10').
4.2 probabilities_dict / sample_counts
bell.probabilities_dict() # {'00': 0.5, '11': 0.5} (keys are little-endian bitstrings)
bell.seed(42)
bell.sample_counts(1000) # {'00': 503, '11': 497} (random unless seeded)probabilities() returns the array form: for qc.h(1) on 2 qubits → [0.5, 0. , 0.5, 0. ]
(indices 0 and 2 = '00' and '10' — superposition on qubit 1).
4.3 evolve
sv = Statevector.from_label('0')
sv.evolve(qc_with_x).data.real # [0. 1.] — accepts Operator, Gate, or QuantumCircuit
sv.evolve(Pauli('Z')) # also accepts Pauli directly4.4 expectation_value
Statevector.from_label('0').expectation_value(Pauli('Z')) # 1.0
Statevector.from_label('+').expectation_value(Pauli('X')) # ~1.0
bell.expectation_value(SparsePauliOp(['ZZ'])) # ~1+0j
bell.expectation_value(SparsePauliOp(['ZI'])) # 0j (single-qubit ⟨Z⟩ vanishes!)
bell.expectation_value(SparsePauliOp(['XX'])) # ~1+0jLittle-endian in expectations too — on |01⟩ (q0 = 1):
sv01 = Statevector.from_label('01')
sv01.expectation_value(Pauli('IZ')) # -1.0 (Z on q0, which is |1⟩)
sv01.expectation_value(Pauli('ZI')) # +1.0 (Z on q1, which is |0⟩)Also useful: sv.inner(other) (⟨sv|other⟩ = 0.7071… for ⟨0|+⟩), and
sv.equiv(other) — True if equal up to global phase.
5. Operator
from qiskit.quantum_info import Operator
qc = QuantumCircuit(1); qc.h(0)
Operator(qc).data # [[0.7071, 0.7071], [0.7071, -0.7071]]
Operator(qc).is_unitary() # True
Operator(np.array([[1, 1], [0, 1]])).is_unitary() # FalseOperator(circuit)builds the full unitary; a circuit drawn left→right multiplies matrices right→left: circuith(0); z(0)has unitary Z·H.A.compose(B)= B·A ("A then B", circuit order);A.dot(B)= A·B.A == B→ exact equality.A.equiv(B)→ equal up to global phase.Operator(qc).power(2),.adjoint(),.tensor(),.expand()all exist.
qs = QuantumCircuit(1); qs.s(0)
Operator(qs).power(2) == Operator(Pauli('Z')) # True (S² = Z)6. Fidelity, DensityMatrix, partial_trace
from qiskit.quantum_info import state_fidelity, partial_trace, DensityMatrix
state_fidelity(Statevector.from_label('0'), Statevector.from_label('+')) # 0.5
state_fidelity(bell, bell) # ~1.0
rho0 = partial_trace(bell, [1]) # trace OUT qubit 1, keep qubit 0
rho0.data.real # [[0.5, 0. ], [0. , 0.5]] = I/2, maximally mixed
rho0.purity() # ~0.5 (pure state would give 1.0)A reduced single qubit of a Bell pair is maximally mixed — that is entanglement: no single-qubit state can describe half of an entangled pair.
7. Gate Effects, Phases, Identities
7.1 Action on basis / Bloch states
| State | Bloch axis | X | Z | H | S |
|---|---|---|---|---|---|
| |0⟩ | +Z | |1⟩ | |0⟩ | |+⟩ | |0⟩ |
| |1⟩ | −Z | |0⟩ | −|1⟩ | |−⟩ | i|1⟩ |
| |+⟩ | +X | |+⟩ | |−⟩ | |0⟩ | |i⟩ (= 'r') |
| |−⟩ | −X | −|−⟩ | |+⟩ | |1⟩ | |l⟩ (= (|0⟩−i|1⟩)/√2) |
Verified: S|+⟩ = [0.7071, 0.7071i] which is Statevector.from_label('r');
T|+⟩ = [0.7071, 0.5+0.5i] (45° around Z).
7.2 Global vs relative phase
- Global phase (e^{iφ} on the whole state) is unobservable:
Statevector([1,0]).equiv(Statevector([1j,0]))→ True. - Relative phase (between amplitudes) is physical: Z turns |+⟩ into |−⟩ — different state,
yet Z-basis probabilities unchanged (
{'0': 0.5, '1': 0.5}both). You need an X-basis measurement (apply H first) to see it. - Z, S, T all leave computational-basis probabilities of any state untouched (diagonal matrices).
7.3 Entanglement recipe
h(0) then cx(0, 1) on |00⟩ → (|00⟩ + |11⟩)/√2. Signatures on the exam:
counts only 00/11, ⟨ZZ⟩ = ⟨XX⟩ = +1, ⟨ZI⟩ = ⟨IZ⟩ = 0, reduced states = I/2.
7.4 Identities (all verified with Operator(...).equiv)
- HZH = X and HXH = Z (H swaps the X and Z axes)
- HYH = −Y; S² = Z; T² = S; H² = X² = Y² = Z² = I
- CX(a,b) matrix for control=q0, target=q1 (little-endian!):
[[1,0,0,0],[0,0,0,1],[0,0,1,0],[0,1,0,0]]— it maps index 1 (|01⟩) ↔ index 3 (|11⟩).
7.5 Rotations: R_P(θ) = exp(−iθP/2)
theta = 0.7
np.allclose(Operator(qc_rz_07).data,
expm(-1j*theta/2 * Pauli('Z').to_matrix())) # Truerx(π)=[[0,-i],[-i,0]]= −iX →equiv(X)is True,==is False.rz(π)=diag(−i, i)= −iZ ≡ Z up to global phase (equivTrue, exact False).ry(π/2)|0⟩= |+⟩ =[0.7071, 0.7071](Y rotations keep real amplitudes).- Period is 4π (R_P(2π) = −I), another classic distractor.
⚠️ TRAP:
Operator(rz(π)) == Operator(z)is False;.equiv()is True. If the exam asks "identical" vs "equivalent up to global phase", rz(π) is the latter.
8. Coding Labs — predict, then run
Do these with /home/kartikey_purohit/development/ibm-qiskit/.venv/bin/python.
Lab A — "The H sandwich": prove HZH = X and HXH = Z numerically
Predict first: which of the two comparisons (== vs equiv) will be True?
Then build h(0); z(0); h(0) and h(0); x(0); h(0) circuits, wrap in Operator,
and compare with Operator(Pauli('X')) / Operator(Pauli('Z')) using both == and
.equiv(). Bonus: show HYH = −Y (hint: compare against Pauli('-Y')).
Lab B — Bell autopsy: half of an entangled pair is pure noise
Build the Bell state with from_instruction. Predict: ⟨ZZ⟩, ⟨XX⟩, ⟨ZI⟩, ⟨IZ⟩, and what
partial_trace(bell, [1]) looks like. Then compute all five, plus purity() of the
reduced state and state_fidelity between the reduced state and DensityMatrix.from_label('0').
Explain why a Bloch vector of length 0 means "entangled" here.
Lab C — Phase detective: rz(π) vs Z, and the S gate's hidden work
- Predict the 4 booleans:
Operator(rz_pi) == Operator(Z),.equiv(), and the same pair forrx(π)vs X. Run and check. - Take |+⟩, apply S, print
probabilities_dict()(predict: changed or not?), then verify the result equalsStatevector.from_label('r')with.equiv(). - Make the phase visible with a basis change. Predict the Z-basis probabilities of H·Z|+⟩, H·S|+⟩, and H·T|+⟩ (each: apply the phase gate to |+⟩, then H, then look). One is deterministic, one is still 50/50, one is ~85/15 — which is which, and why? (Hint: H reads out the X axis; S parks the state on the Y axis.)
9. Thirty-Second Cram Card
- Pauli string: rightmost char = qubit 0.
Pauli('IZ')= Z₀. to_matrix('AB')= kron(A, B). Phase prefix k in.phase: (−i)^k.compose= "then" (B·A);dot= matrix product (A·B);tensorputs self on high qubits.- SparsePauliOp coeffs: complex128 always;
simplify()merges/drops terms. - Statevector index k ↔ bitstring bin(k), rightmost bit = q0.
- ⟨Bell|ZZ|Bell⟩ = ⟨XX⟩ = 1, single-qubit ⟨Z⟩ = 0, reduced state = I/2.
- HZH = X, HXH = Z, S² = Z, T² = S, rz(π) ≡ Z up to global phase.
- Paulis commute iff anticommuting positions are even in number: XX vs ZZ → commute.