Skip to main content

Cram sheet — Section 7: Retrieve and analyze the results of 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.

Retrieve previous experiment results (session/Qiskit Runtime)

Result anatomy: PrimitiveResult → PubResult → DataBin → BitArray

Every V2 job returns the same container chain, so most retrieval questions are really "at which layer does this attribute live?".

LayerWhat it isReach in withDoes NOT have
PrimitiveResultlist-like, one entry per PUB; len(result) is the PUB countresult[0], result[1].data, .get_counts(), .quasi_dists, .results
PubResult (Sampler: SamplerPubResult)one PUB's output: .data plus .metadata (shots or target_precision, circuit_metadata).dataget_counts(), evs
DataBinEstimator: evs and stds. Sampler: one BitArray per ClassicalRegister, keyed by the register NAME.meas, db['meas'], list(db.keys()), values(), items(), shape, ndim, sizeget_counts(), registers, num_bits
BitArraythe shot data of one registerget_counts(), get_bitstrings(), get_int_counts().name — the name exists only as the DataBin key
counts = result[0].data.meas.get_counts() # Sampler
evs = result[0].data.evs # Estimator
plot_histogram([result[0].data.meas.get_counts(),
result[1].data.meas.get_counts()], legend=["a", "b"])

evs and stds are real float64 arrays whose shape follows the PUB's broadcast shape, and stds reports the standard error actually achieved. plot_histogram needs one counts dict per legend entry.

The Sampler DataBin holds one BitArray per ClassicalRegister, so splitting one register into alpha and beta gives data.alpha and data.beta rather than a single flat array.

A BitArray stores the sampled bitstrings as BYTES, shots on the left axis. Its surface is small enough to memorize, and the docs recommend post-processing on the array rather than on slow dictionaries.

MemberWhat it gives you
num_shots, num_bitsshots executed; CLASSICAL register width
arraybit-PACKED uint8, shape (num_shots, ceil(num_bits/8))
get_counts()bitstring to integer tally, summing to num_shots
get_bitstrings()the per-shot list, length num_shots
get_int_counts()the same tallies keyed by the bitstring read as a binary INTEGER
postselect(indices, selection)only the shots whose listed bits match
slice_bits(indices), slice_shots(indices)fewer bits, all shots; all bits, fewer shots — valid bit indices run 0..num_bits-1
BitArray.concatenate_shots([a, b]), concatenate_bitspools shot sets of equal width; widens them instead
expectation_values(observable)diagonal expectation values straight from the shots

Sources: primitive-input-output · qiskit.primitives.DataBin · qiskit.primitives.PrimitiveResult · qiskit.primitives.BitArray

Getting results back after the kernel dies

Results do not live in your Python process. IBM Quantum automatically stores the results of EVERY job server-side, so dropping the job variable or restarting the kernel cancels nothing and loses nothing. The one handle worth keeping is the ID string from job.job_id().

GoalCall
one job back, by IDservice.job(job_id) returns the RuntimeJobV2; .result() gives the stored PrimitiveResult
find jobs when the ID is lostservice.jobs(...), the filtered LIST call, or the Workloads page
keep a result on diskjson.dump(result, f, cls=RuntimeEncoder)
read it back as a PrimitiveResultjson.load(f, cls=RuntimeDecoder)

Note the singular. Wrong spellings to recognise instantly: service.result(...), service.get_result(...), service.retrieve_job(...) (the V1 provider idiom), and calling QiskitRuntimeService.job(...) on the class rather than on an instance. Resubmitting the circuit is not retrieval: Runtime does not deduplicate, so an identical PUB becomes a new job with a new ID and freshly sampled shots.

Mistake on the disk round tripWhat you get
no encoder on dumpTypeError: Object of type PrimitiveResult is not JSON serializable
RuntimeDecoder where the encoder belongsTypeError
plain json.load on an encoded filenested dicts that look fine, then KeyError: 0 when you index the pub
json.dumps(result, f, cls=RuntimeEncoder)positional-argument TypeError: dumps/loads are the STRING variants, and everything after the first argument is keyword-only
result.to_json()no such method

Sources: save-jobs · monitor-job

Must know:

  • num_bits follows the CLASSICAL register, not the circuit: a 5-qubit circuit measuring three qubits reports num_bits == 3, while len(get_bitstrings()) and sum(get_counts().values()) both equal num_shots. — ⚙️ proven in s7-q019
  • bits.array is bit-PACKED uint8 with shape (num_shots, ceil(num_bits/8)): a 10-qubit circuit sampled 1024 times gives (1024, 2) — not (1024, 10), (1024, 8) or the transpose. — ⚙️ proven in s7-q012
  • Counts keys are little-endian bitstrings: X on qubit 0 of a 3-qubit circuit sampled 500 times gives {'001': 500} — integer tallies, not {'100': 500} and not fractions. — ⚙️ proven in s7-q014
  • bits.expectation_values('ZZ') computes a diagonal expectation value straight from the shots — an ideal Bell sample returns the 0-d scalar 1.0, one value per Pauli string, with no Estimator involved. — ⚙️ proven in s7-q025
  • A (4, 1) parameter array against a single observable gives evs.shape == (4,) — the trailing parameter axis is consumed, so it is not (4, 1) and not (1, 4). — ⚙️ proven in s7-q021
  • pub.data.stds is the standard error the primitive ACHIEVED, not the precision you asked for and not 1/sqrt(shots): on a noisy Bell/ZZ pub it read 0.0096 while both of those were 0.02. — ⚙️ proven in s7-q013

Traps:

  • The missing PUB index is the classic retrieval bug: result.data... raises AttributeError: 'PrimitiveResult' object has no attribute 'data' — you must index the PUB first, as result[0].data.meas.get_counts(). — ⚙️ proven in s7-q024
  • meas is only the DEFAULT register name: a circuit measuring into a register called readout needs result[0].data.readout.get_counts(), and data.meas raises AttributeError. — ⚙️ proven in s7-q032
  • bits.postselect([1], [0]) returns a new BitArray of only the shots where qubit 1 read 0, at FULL width; postselect has no num_bits= parameter (TypeError), and chaining .slice_bits(...) afterwards silently narrows the result. — ⚙️ proven in s7-q016
  • BitArray.concatenate_shots([a, b]) pools two equal-width shot sets (200 + 200 → 400 shots, 3 bits), but a + b is a TypeError and from_counts(a.get_counts() | b.get_counts()) LOSES shots, because dict union overwrites duplicate keys instead of summing them. — ⚙️ proven in s7-q018
  • result[0].data.stds is a silent trap when you meant evs: it exists, it has the same shape, and it plots — but on a default_precision = 0.02 sweep the standard errors are about 0.005–0.018 while the expectation values span ±0.9, so the curve collapses onto the axis. — ⚙️ proven in s7-q038

Monitor jobs

The job handle: one string, several predicates, one blocking call

sampler.run(...) and estimator.run(...) return a RuntimeJobV2 immediately — the job itself executes server-side. Everything you do afterwards is either a cheap non-blocking query or the one blocking call.

job.status() returns a PLAIN STRING, typed in the API reference as Literal['INITIALIZING', 'QUEUED', 'RUNNING', 'CANCELLED', 'DONE', 'ERROR'], which is why official examples write if j.status() == "DONE". It is not a JobStatus enum member, and it carries no queue information — queue position is a metrics() or Workloads concern.

CallBlocksWhat it does
status()noreturns the state string
done(), errored(), cancelled(), running()nobooleans built on that string; done() is literally status() == "DONE"
in_final_state()notrue for JOB_FINAL_STATES = ('DONE', 'CANCELLED', 'ERROR') — CANCELLED is terminal
result()YESwaits for a terminal state, then returns the PrimitiveResult; never partial shots
wait_for_final_state(timeout, poll_interval)yespolls to a terminal state without fetching data
cancel()noasks the service to drop the job; status() then reads "CANCELLED"
job_id(), backend(), error_message(), logs(), metrics(), usage(), usage_estimationnobookkeeping

Cancelling is a state change, not a deletion, and a cancelled job hands back no partial data. There is no job.stop() and no QiskitRuntimeService.cancel(): the service FINDS jobs while the job object owns the lifecycle. Deleting your local variable leaves the job queued and still consuming your instance's time. One removal to recognise — qiskit.tools.job_monitor disappeared with Qiskit 1.0, so modern monitoring is status() in a loop, wait_for_final_state(), or the Workloads page.

Sources: runtime-job-v2 · monitor-job

Watching a session, and the platform-side view

session.details() returns a DICTIONARY describing the SESSION, not its jobs.

KeyMeaning
id, backend_name, modewhich session, on what backend, in which mode
stateone of open, active, inactive, closed
accepting_jobswhether new jobs are still taken
max_timemaximum session length in seconds, subject to plan limits
interactive_timeout, active_timeoutidle gap allowed between jobs; how long it may stay active
usage_timethe QPU-committed time
started_at, activated_at, last_job_started, last_job_completed, closed_atlifecycle timestamps

Nothing in there is per-job: a queue position belongs to a job and results to job.result(). In local testing mode — a Session built on a fake backend — details() returns None, and session_id is None too.

The two lifecycle verbs are examined on their difference. close() stops the session ACCEPTING new jobs while queued and running jobs finish, and the session terminates once nothing is pending; cancel() cancels all PENDING jobs in the session. The rest of the surface is small and shared with Batch: backend(), status(), usage(), from_id(session_id, service) and the service/session_id attributes. Neither class exposes a public run — you run through a primitive constructed with mode=session.

Several official answers point at the browser instead.

PageWhat it shows
Workloadsevery workload you submitted, with its Status column and job ID; cancel from the row's overflow menu or a workload's Actions dropdown
Instancestotal plan time used and remaining
Analyticsjob, batch and session counts, for accounts you own or manage

Sources: session · monitor-job

Must know:

  • Cancellation is a job-handle operation: job.cancel() in Qiskit, or the Cancel action on the Workloads page; cancel() raises RuntimeInvalidStateError if the job is in a state that cannot be cancelled. — 📖 runtime-job-v2
  • service.jobs() is the listing call: limit defaults to 10 and it filters on backend_name, pending, session_id, created_after/created_before (datetime objects) and more; use it when you did not record a job ID. — 📖 monitor-job
  • details()["state"] and session.status() use different vocabularies: open/active/inactive/closed for the field versus Pending, In progress, accepting new jobs, In progress, not accepting new jobs and Closed for the call. — 📖 session

Traps:

  • Executed against the class: status() returns a plain str, so status() == JobStatus.DONE is False and status().name raises AttributeError. done() is True only for DONE, while in_final_state() also covers CANCELLED and ERROR — and there is no FAILED state at all. — ⚙️ proven in s7-q034
Exam checklist
  • Index the PUB first: result[0].data.<register>.get_counts()PrimitiveResult itself has no .data.
  • Ask list(db.keys()) for the register name instead of assuming the DataBin field is called meas.
  • Read bits.array as packed bytes: dtype uint8, shape (num_shots, ceil(num_bits/8)).
  • Pick the post-processing method by axis: postselect and slice_shots drop shots, slice_bits drops bits.
  • Save job.job_id(), then re-hydrate a stored result with service.job(job_id).result() after a restart.
  • Round-trip a result through disk with RuntimeEncoder on dump and RuntimeDecoder on load.
  • Compare job.status() against the plain string "DONE", never against a JobStatus enum member.
  • Separate the session verbs: close() stops new jobs being accepted, cancel() kills the pending ones.

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).