Day 8 — Official Sample Gaps
Lesson
Date: Fri Aug 1, 2026 · Sections: §7 leftovers (results/jobs, 10%) + mixed review of trap topics
Environment: source .venv/bin/activate (qiskit 2.5.1, qiskit-ibm-runtime 0.48.0, qiskit-aer 0.17.2)
Today has three blocks:
- Morning (25 min + 30 min grading): official Sample Test, closed book, timed.
- Midday (90 min): finish §7 — job lifecycle, result anatomy, counts analysis (this lesson).
- Afternoon/evening: triage weak areas + Sample Paper 01.
Part A — §7 Completion Material
A.1 Job lifecycle and status (§7.1)
A primitive run() call is asynchronous — it returns a job immediately; results come later.
RuntimeJobV2 status values are plain strings (verified in runtime 0.48.0 source):
JobStatus = Literal["INITIALIZING", "QUEUED", "RUNNING", "CANCELLED", "DONE", "ERROR"]
JOB_FINAL_STATES = ("DONE", "CANCELLED", "ERROR")Lifecycle: INITIALIZING → QUEUED → RUNNING → DONE (or CANCELLED / ERROR).
| Method | Returns |
|---|---|
job.job_id() |
string ID — save it to retrieve the job later |
job.status() |
one of the six strings above (V2 job = string, not enum) |
job.done() |
True iff status == "DONE" |
job.in_final_state() |
True for DONE / CANCELLED / ERROR |
job.errored() / job.cancelled() |
booleans for those two final states |
job.cancel() |
request cancellation |
job.result() |
blocks until done, then returns PrimitiveResult |
job.metrics() |
dict of timestamps, QPU usage, executions (cloud jobs) |
job.usage() |
QPU time consumed, in seconds (cloud jobs) |
Note: the local StatevectorSampler returns a PrimitiveJob whose status() is a JobStatus
enum (JobStatus.RUNNING, JobStatus.DONE) — the string statuses above are the Runtime V2 job
model, which is what the exam asks about.
Retrieving jobs later (new Python session, days later — results are stored server-side):
service = QiskitRuntimeService()
job = service.job("d1a2b3c4...") # single job by ID
result = job.result()
jobs = service.jobs(limit=10, pending=False, backend_name="ibm_brisbane") # filtered listjob.metrics() includes queue/run timestamps and usage (QPU seconds); job.usage() is the
shortcut for just the QPU time — that is what burns your open-plan minutes.
A.2 PrimitiveResult anatomy (§7.2)
One PUB in → one PubResult out. PrimitiveResult is an iterable of PubResults, indexed by
PUB position. All of the following was run and verified with StatevectorSampler(seed=42):
from qiskit import QuantumCircuit
from qiskit.primitives import StatevectorSampler
qc = QuantumCircuit(2)
qc.h(0); qc.cx(0, 1)
qc.measure_all() # creates creg named "meas"
job = StatevectorSampler(seed=42).run([qc], shots=1000)
result = job.result() # PrimitiveResult
pub = result[0] # SamplerPubResult (PUB #0)
pub.data # DataBin — fields named after cregs: ['meas']
ba = pub.data.meas # BitArray
ba.num_shots # 1000
ba.num_bits # 2
ba.get_counts() # {'11': 497, '00': 503}
ba.get_bitstrings()[:5] # ['11', '00', '11', '11', '00']
pub.metadata # {'shots': 1000, 'circuit_metadata': {}}
result.metadata # {'version': 2} (job-level metadata)The hierarchy to memorize:
PrimitiveResult # iterable; result[i] per PUB; .metadata (job-level)
└── PubResult / SamplerPubResult
├── .data # DataBin
│ ├── Sampler: .<creg_name> → BitArray (.get_counts/.get_bitstrings/.num_shots/.num_bits)
│ └── Estimator: .evs, .stds (numpy arrays)
└── .metadata # per-PUB (shots, target_precision, ...)
Multiple classical registers → one BitArray per register, join_data() merges (verified):
# creg "alpha" ← q0 (after X), creg "beta" ← q1
r = job.result()[0]
r.data.alpha.get_counts() # {'1': 100}
merged = r.join_data() # BitArray, num_bits=2
merged.get_counts() # {'01': 100} ← beta joins to the LEFT of alphaEstimator side (verified with StatevectorEstimator, Bell circuit, obs ZZ + XX):
r = est.run([(qc_no_meas, obs)]).result()
r[0].data.evs # 2.0 (⟨ZZ⟩=1 and ⟨XX⟩=1 for the Bell state, coeffs 1.0 each)
r[0].data.stds # 0.0 on ideal simulator; nonzero on hardware/fake backends
r[0].metadata # {'target_precision': ..., 'shots': ..., ...}On a fake backend (FakeManilaV2, opt level 1, obs = SparsePauliOp('ZZ') mapped with
apply_layout), the same flow gave evs ≈ 0.88, stds ≈ 0.0075 (stochastic — no seed; rerun and
it moves a little) — noisy hardware pulls ⟨ZZ⟩ below the ideal 1.0, and stds quantifies the
statistical uncertainty. The per-PUB metadata came back as
{'target_precision': 0.015625, 'shots': 4096, 'circuit_metadata': {}} (verified).
One more §7.2 name to recognize: ensemble_standard_error — an extra DataBin field that
cloud Estimator results can carry alongside evs/stds (error of the twirled ensemble).
Awareness only: verified locally that StatevectorEstimator and fake-backend EstimatorV2
DataBins contain exactly ['evs', 'stds'] — the extra field appears on real service results.
A.3 Analysis: counts → probabilities → expectation values (§7.3)
Probabilities = counts normalized by total shots:
counts = {'11': 497, '00': 503}
shots = sum(counts.values())
probs = {b: n / shots for b, n in counts.items()} # {'11': 0.497, '00': 0.503}Manual ⟨Z⟩ from single-qubit counts — Z eigenvalues: |0⟩ → +1, |1⟩ → −1:
expZ = (counts.get('0', 0) - counts.get('1', 0)) / shots
# H|0⟩ measured 4096 times gave {'1': 2030, '0': 2066} → ⟨Z⟩ ≈ 0.0088 ≈ 0 ✓Multi-qubit ⟨Z…Z⟩ = parity: eigenvalue is (−1)^(number of 1s in the bitstring):
zz = sum(((-1) ** b.count('1')) * n for b, n in counts.items()) / shots
# Bell counts {'11': 497, '00': 503} → ⟨ZZ⟩ = 1.0 (both bitstrings have even parity)Standard error vs shots: statistical error ∝ 1/√shots.
- 4× the shots → ½ the error. 100× the shots → 1/10 the error.
- Estimator's
precisionis exactly this target standard error; default precision on the service is0.015625 = 1/√4096(verified in returnedmetadata['target_precision']). - Requesting precision
pcosts shots ∝ 1/p² — halving the error quadruples the QPU time.
A.4 Simulator vs hardware comparison (§7.3)
Ideal simulators give the exact distribution (± sampling noise); hardware adds readout errors, decoherence, and gate errors → spurious bitstrings and damped expectation values. Lab-verified:
from qiskit_aer import AerSimulator
from qiskit.quantum_info import hellinger_fidelity
noisy_sim = AerSimulator.from_backend(FakeManilaV2()) # noise model from backend snapshot
ideal_sim = AerSimulator()
c_ideal = ideal_sim.run(qc, shots=4096).result().get_counts()
c_noisy = noisy_sim.run(isa_qc, shots=4096).result().get_counts()
# ideal: {'11': 2098, '00': 1998} ← only Bell outcomes
# noisy: {'00': 1908, '01': 134, '10': 125, '11': 1929} ← leakage into 01/10
hellinger_fidelity(c_ideal, c_noisy) # 0.9367 — closeness of distributionsWhat to say on the exam: simulator shows only the theoretically allowed outcomes; hardware shows a spread over nearby bitstrings; mitigation (TREX/ZNE) recovers expectation values in post-processing but cannot recover per-shot bitstrings.
A.5 Rapid-fire self-check (60 seconds each, out loud, before the question bank)
Cover the right column. Every one of these is a one-liner the exam loves.
| Prompt | Answer |
|---|---|
job.status() on RuntimeJobV2 returns…? |
A plain string ("QUEUED", "DONE", …), not an enum |
| The three final job states? | DONE, CANCELLED, ERROR |
| Retrieve yesterday's job? | service.job(job_id) → .result() |
| QPU seconds consumed? | job.usage() (or inside job.metrics()) |
measure_all() → data attribute? |
result[0].data.meas |
QuantumCircuit(2, 2) → data attribute? |
result[0].data.c |
| Two cregs, one merged BitArray? | result[0].join_data() |
| Counts → probabilities? | divide each count by total shots |
| ⟨Z⟩ from 1-qubit counts? | (n0 − n1) / shots |
| ⟨ZZ⟩ from 2-qubit counts? | parity: Σ (−1)^(#1s) · n / shots |
| 4× shots does what to error? | halves it (error ∝ 1/√shots) |
precision=0.005 vs 0.01 costs…? |
4× the shots (shots ∝ 1/p²) |
| Estimator DataBin fields? | evs, stds (+ ensemble_standard_error on cloud) |
| Hardware counts vs ideal counts? | spurious bitstrings appear; mitigation fixes ⟨O⟩, never per-shot data |
If more than 3 of these made you hesitate, redo Labs 1–2 before touching the question bank.
Part B — Official Sample Test: Grading Protocol
File: resource-artifactory/official/C1000-179_SAM_SampleTestQiskitv2.pdf
- Conditions: closed book, no REPL, timer at 25 minutes. Answer everything — no blanks (no negative marking on the real exam).
- Grade immediately with the answer key at the end of the PDF.
- Known dispute — Q16: the official key's reasoning assumes twirling is exclusive to one resilience level, but per Qiskit docs issue #4298 twirling is also applied at resilience level 2 (level 2 = level 1's TREX/twirling plus ZNE). If you picked the answer consistent with "twirling happens at level 2 too," count yourself correct and move on.
- Score interpretation (this is your single best predictor):
| Score | Meaning | Action |
|---|---|---|
| ≥ 85% | On track | Normal Day 8: this lesson + Paper 01 tonight |
| 70–84% | Passing zone, thin margin | Do the triage below on every wrong answer before Paper 01 |
| < 70% | Red alert | Rebalance: today + Day 9 morning become question-bank redo for the 2 worst sections; push Papers 02–03 to Day 9 afternoon |
- For every wrong answer, write three lines in
my-notes.md:- Syllabus section it maps to (§1–§8).
- Why you missed it: (a) never knew, (b) knew but confused two similar APIs, (c) endianness/ trap, (d) misread the question.
- The one sentence that would have gotten it right.
Triage protocol for weak areas (afternoon)
- Tally wrong answers from the sample test and your
my-notes.mdlogs from Days 1–7 by section. - Pick the 3 worst topics. For each (≈30 min per topic):
- Re-read only that lesson section (not the whole day).
- Re-type its lab code from scratch in the venv — typing, not reading, is what sticks.
- Redo the corresponding question-bank questions you got wrong.
- Weight by exam value: a weak §1 (18%) outranks a weak §8 (6%) — break ties toward Sections 1–5.
Part C — Labs (run everything in the venv)
Lab 1 — result spelunking (15 min). Build a 3-qubit GHZ with two named cregs (2 bits + 1 bit),
measure across both, run StatevectorSampler, and: list result[0].data fields, get counts per
register, join_data() and confirm which register lands on the left, convert joined counts to
probabilities.
Lab 2 — manual expectation (15 min). Prepare ry(0.8) on one qubit, measure, 8192 shots.
Compute ⟨Z⟩ from counts and compare to cos(0.8) ≈ 0.6967. Then rerun with 512 shots ×10 times
and watch the spread — that spread is the 1/√shots story.
Lab 3 — noisy vs ideal (15 min). Reproduce §A.4 with a 3-qubit GHZ. Compute
hellinger_fidelity. Which spurious bitstrings appear most, and why (single bit-flips)?
Evening: Sample Paper 01, half-length, 45 min, closed book. Grade, log, sleep.