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?".
| Layer | What it is | Reach in with | Does NOT have |
|---|---|---|---|
PrimitiveResult | list-like, one entry per PUB; len(result) is the PUB count | result[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) | .data | get_counts(), evs |
DataBin | Estimator: evs and stds. Sampler: one BitArray per ClassicalRegister, keyed by the register NAME | .meas, db['meas'], list(db.keys()), values(), items(), shape, ndim, size | get_counts(), registers, num_bits |
BitArray | the shot data of one register | get_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.
| Member | What it gives you |
|---|---|
num_shots, num_bits | shots executed; CLASSICAL register width |
array | bit-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_bits | pools 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().
| Goal | Call |
|---|---|
| one job back, by ID | service.job(job_id) returns the RuntimeJobV2; .result() gives the stored PrimitiveResult |
| find jobs when the ID is lost | service.jobs(...), the filtered LIST call, or the Workloads page |
| keep a result on disk | json.dump(result, f, cls=RuntimeEncoder) |
read it back as a PrimitiveResult | json.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 trip | What you get |
|---|---|
| no encoder on dump | TypeError: Object of type PrimitiveResult is not JSON serializable |
RuntimeDecoder where the encoder belongs | TypeError |
plain json.load on an encoded file | nested 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_bitsfollows the CLASSICAL register, not the circuit: a 5-qubit circuit measuring three qubits reportsnum_bits == 3, whilelen(get_bitstrings())andsum(get_counts().values())both equalnum_shots. — ⚙️ proven ins7-q019bits.arrayis bit-PACKEDuint8with 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 ins7-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 ins7-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 ins7-q025- A
(4, 1)parameter array against a single observable givesevs.shape == (4,)— the trailing parameter axis is consumed, so it is not(4, 1)and not(1, 4). — ⚙️ proven ins7-q021 pub.data.stdsis the standard error the primitive ACHIEVED, not the precision you asked for and not1/sqrt(shots): on a noisy Bell/ZZ pub it read 0.0096 while both of those were 0.02. — ⚙️ proven ins7-q013
Traps:
- The missing PUB index is the classic retrieval bug:
result.data...raisesAttributeError: 'PrimitiveResult' object has no attribute 'data'— you must index the PUB first, asresult[0].data.meas.get_counts(). — ⚙️ proven ins7-q024 measis only the DEFAULT register name: a circuit measuring into a register calledreadoutneedsresult[0].data.readout.get_counts(), anddata.measraisesAttributeError. — ⚙️ proven ins7-q032bits.postselect([1], [0])returns a new BitArray of only the shots where qubit 1 read 0, at FULL width;postselecthas nonum_bits=parameter (TypeError), and chaining.slice_bits(...)afterwards silently narrows the result. — ⚙️ proven ins7-q016BitArray.concatenate_shots([a, b])pools two equal-width shot sets (200 + 200 → 400 shots, 3 bits), buta + bis aTypeErrorandfrom_counts(a.get_counts() | b.get_counts())LOSES shots, because dict union overwrites duplicate keys instead of summing them. — ⚙️ proven ins7-q018result[0].data.stdsis a silent trap when you meantevs: it exists, it has the same shape, and it plots — but on adefault_precision = 0.02sweep the standard errors are about 0.005–0.018 while the expectation values span ±0.9, so the curve collapses onto the axis. — ⚙️ proven ins7-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.
| Call | Blocks | What it does |
|---|---|---|
status() | no | returns the state string |
done(), errored(), cancelled(), running() | no | booleans built on that string; done() is literally status() == "DONE" |
in_final_state() | no | true for JOB_FINAL_STATES = ('DONE', 'CANCELLED', 'ERROR') — CANCELLED is terminal |
result() | YES | waits for a terminal state, then returns the PrimitiveResult; never partial shots |
wait_for_final_state(timeout, poll_interval) | yes | polls to a terminal state without fetching data |
cancel() | no | asks the service to drop the job; status() then reads "CANCELLED" |
job_id(), backend(), error_message(), logs(), metrics(), usage(), usage_estimation | no | bookkeeping |
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.
| Key | Meaning |
|---|---|
id, backend_name, mode | which session, on what backend, in which mode |
state | one of open, active, inactive, closed |
accepting_jobs | whether new jobs are still taken |
max_time | maximum session length in seconds, subject to plan limits |
interactive_timeout, active_timeout | idle gap allowed between jobs; how long it may stay active |
usage_time | the QPU-committed time |
started_at, activated_at, last_job_started, last_job_completed, closed_at | lifecycle 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.
| Page | What it shows |
|---|---|
| Workloads | every workload you submitted, with its Status column and job ID; cancel from the row's overflow menu or a workload's Actions dropdown |
| Instances | total plan time used and remaining |
| Analytics | job, 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()raisesRuntimeInvalidStateErrorif the job is in a state that cannot be cancelled. — 📖 runtime-job-v2 service.jobs()is the listing call:limitdefaults to 10 and it filters onbackend_name,pending,session_id,created_after/created_before(datetime objects) and more; use it when you did not record a job ID. — 📖 monitor-jobdetails()["state"]andsession.status()use different vocabularies:open/active/inactive/closedfor the field versusPending,In progress, accepting new jobs,In progress, not accepting new jobsandClosedfor the call. — 📖 session
Traps:
- Executed against the class:
status()returns a plainstr, sostatus() == JobStatus.DONEis False andstatus().nameraisesAttributeError.done()is True only forDONE, whilein_final_state()also coversCANCELLEDandERROR— and there is noFAILEDstate at all. — ⚙️ proven ins7-q034
- Index the PUB first:
result[0].data.<register>.get_counts()—PrimitiveResultitself has no.data. - Ask
list(db.keys())for the register name instead of assuming the DataBin field is calledmeas. - Read
bits.arrayas packed bytes: dtypeuint8, shape(num_shots, ceil(num_bits/8)). - Pick the post-processing method by axis:
postselectandslice_shotsdrop shots,slice_bitsdrops bits. - Save
job.job_id(), then re-hydrate a stored result withservice.job(job_id).result()after a restart. - Round-trip a result through disk with
RuntimeEncoderon dump andRuntimeDecoderon load. - Compare
job.status()against the plain string"DONE", never against aJobStatusenum 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).