Skip to content
Kartikey Purohit
← Course overview

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:

  1. Morning (25 min + 30 min grading): official Sample Test, closed book, timed.
  2. Midday (90 min): finish §7 — job lifecycle, result anatomy, counts analysis (this lesson).
  3. 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 list

job.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 alpha

Estimator 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 precision is exactly this target standard error; default precision on the service is 0.015625 = 1/√4096 (verified in returned metadata['target_precision']).
  • Requesting precision p costs 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 distributions

What 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

  1. Conditions: closed book, no REPL, timer at 25 minutes. Answer everything — no blanks (no negative marking on the real exam).
  2. 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.
  3. 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
  1. 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)

  1. Tally wrong answers from the sample test and your my-notes.md logs from Days 1–7 by section.
  2. 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.
  3. 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.