Skill Tracer
Skills › Scientific Agent Skills › qiskit › testing.md

testing.md

426 lines · skills/qiskit/references/testing.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# Testing, Reproducibility, and Troubleshooting
2
3Quantum workflows combine deterministic program transformations, stochastic sampling, changing hardware, and classical post-processing. Test each layer separately.
4

Testing, Reproducibility, and Troubleshooting

Quantum workflows combine deterministic program transformations, stochastic sampling, changing hardware, and classical post-processing. Test each layer separately.

Raw lines 1 to 4
5## Testing Pyramid
6
71. **Pure classical tests**: bit ordering, objective functions, post-processing.
82. **Circuit semantic tests**: small statevectors, operators, and measurement bases.
93. **Primitive contract tests**: PUB shapes, result registers, metadata handling.
104. **Transpilation tests**: target compatibility, layout propagation, structural budgets.
115. **Noisy simulation tests**: robustness under a recorded model.
126. **Bounded hardware smoke tests**: smallest useful QPU job.
14Do not use QPU jobs as unit tests.

Testing Pyramid

  1. Pure classical tests: bit ordering, objective functions, post-processing.
  2. Circuit semantic tests: small statevectors, operators, and measurement bases.
  3. Primitive contract tests: PUB shapes, result registers, metadata handling.
  4. Transpilation tests: target compatibility, layout propagation, structural budgets.
  5. Noisy simulation tests: robustness under a recorded model.
  6. Bounded hardware smoke tests: smallest useful QPU job.

Do not use QPU jobs as unit tests.

Raw lines 5 to 15
16## Run the Bundled Smoke Test
18```bash
19python scripts/check_environment.py
20python scripts/run_local_primitives.py --shots 256 --seed 43
21```
23Machine-readable output:
25```bash
26python scripts/check_environment.py --json
27python scripts/run_local_primitives.py --json
28```

Run the Bundled Smoke Test

python scripts/check_environment.py
python scripts/run_local_primitives.py --shots 256 --seed 43

Machine-readable output:

python scripts/check_environment.py --json
python scripts/run_local_primitives.py --json
Raw lines 16 to 29
Runs code python scripts/check_environment.py :19
Runs code python scripts/run_local_primitives.py :20
30## Deterministic Seeds
32Use separate explicit seeds:
34```python
35seed_transpiler = 43
36seed_simulator = 44
38pass_manager = generate_preset_pass_manager(
39 backend=backend,
40 optimization_level=1,
41 seed_transpiler=seed_transpiler,
44sampler = StatevectorSampler(seed=seed_simulator)
45```
47Record both. A simulator seed does not make real hardware deterministic. A transpiler seed does not freeze calibration changes or behavior across package versions.

Deterministic Seeds

Use separate explicit seeds:

seed_transpiler = 43
seed_simulator = 44

pass_manager = generate_preset_pass_manager(
    backend=backend,
    optimization_level=1,
    seed_transpiler=seed_transpiler,
)

sampler = StatevectorSampler(seed=seed_simulator)

Record both. A simulator seed does not make real hardware deterministic. A transpiler seed does not freeze calibration changes or behavior across package versions.

Raw lines 30 to 48
49## Test Circuit Semantics
51For small unitary circuits:
53```python
54from qiskit.quantum_info import Operator, Statevector
56reference_state = Statevector.from_instruction(reference_circuit)
57candidate_state = Statevector.from_instruction(candidate_circuit)
58assert reference_state.equiv(candidate_state)
60reference_operator = Operator(reference_circuit)
61candidate_operator = Operator(candidate_circuit)
62assert reference_operator.equiv(candidate_operator)
63```
65`equiv()` accounts for global phase. Only construct dense operators for small circuits; memory grows as \(4^n\).
67Remove final measurements before statevector comparison:
69```python
70unitary_part = measured_circuit.remove_final_measurements(
71 inplace=False
73```
75Circuits with resets, measurements, or classical control need behavior-specific tests rather than unitary equivalence.

Test Circuit Semantics

For small unitary circuits:

from qiskit.quantum_info import Operator, Statevector

reference_state = Statevector.from_instruction(reference_circuit)
candidate_state = Statevector.from_instruction(candidate_circuit)
assert reference_state.equiv(candidate_state)

reference_operator = Operator(reference_circuit)
candidate_operator = Operator(candidate_circuit)
assert reference_operator.equiv(candidate_operator)

equiv() accounts for global phase. Only construct dense operators for small circuits; memory grows as \(4^n\).

Remove final measurements before statevector comparison:

unitary_part = measured_circuit.remove_final_measurements(
    inplace=False
)

Circuits with resets, measurements, or classical control need behavior-specific tests rather than unitary equivalence.

Raw lines 49 to 76
77## Test Bit Ordering Explicitly
79```python
80def qiskit_bitstring_to_values(bitstring):
81 return [
82 int(bit)
83 for bit in reversed(bitstring.replace(" ", ""))
84 ]
86assert qiskit_bitstring_to_values("10") == [0, 1]
87```
89Also test Pauli-label mapping:
91```python
92from qiskit.quantum_info import SparsePauliOp
94operator = SparsePauliOp.from_list([("ZI", 1.0)])
95assert operator.num_qubits == 2
96# "ZI" means Z on qubit 1 and I on qubit 0.
97```
99This is especially important for optimization variables and graph vertices. The reversal above decodes classical bits; qubit interpretation additionally requires the measurement map. Use asymmetric states and reversed measurement operands, plus unequal named registers, to detect errors that Bell/GHZ symmetry hides.

Test Bit Ordering Explicitly

def qiskit_bitstring_to_values(bitstring):
    return [
        int(bit)
        for bit in reversed(bitstring.replace(" ", ""))
    ]

assert qiskit_bitstring_to_values("10") == [0, 1]

Also test Pauli-label mapping:

from qiskit.quantum_info import SparsePauliOp

operator = SparsePauliOp.from_list([("ZI", 1.0)])
assert operator.num_qubits == 2
# "ZI" means Z on qubit 1 and I on qubit 0.

This is especially important for optimization variables and graph vertices. The reversal above decodes classical bits; qubit interpretation additionally requires the measurement map. Use asymmetric states and reversed measurement operands, plus unequal named registers, to detect errors that Bell/GHZ symmetry hides.

Raw lines 77 to 100
101## Test Parameter Order and Shapes
103```python
104parameter_order = list(circuit.parameters)
105assert parameter_values.shape[-1] == len(parameter_order)
107stored_names = [parameter.name for parameter in parameter_order]
108assert stored_names == expected_parameter_names
109```
111For every nontrivial PUB broadcast, assert the result shape:
113```python
114pub_result = estimator.run(
115 [(circuit, observables, parameter_values)]
116).result()[0]
118assert pub_result.data.evs.shape == expected_shape
119```
121Do not rely on a visual reading of nested lists.

Test Parameter Order and Shapes

parameter_order = list(circuit.parameters)
assert parameter_values.shape[-1] == len(parameter_order)

stored_names = [parameter.name for parameter in parameter_order]
assert stored_names == expected_parameter_names

For every nontrivial PUB broadcast, assert the result shape:

pub_result = estimator.run(
    [(circuit, observables, parameter_values)]
).result()[0]

assert pub_result.data.evs.shape == expected_shape

Do not rely on a visual reading of nested lists.

Raw lines 101 to 122
123## Test Sampled Results Statistically
125Never assert an exact count split from finite shots:
127```python
128counts = pub_result.data.meas.get_counts()
129shots = sum(counts.values())
131p_zero = counts.get("0", 0) / shots
132assert abs(p_zero - 0.5) < 0.1
133```
135Choose tolerance from the expected binomial uncertainty and desired failure probability, not an arbitrary constant copied into every test.
137For exact probability assertions, use `Statevector.probabilities_dict()` on a small unitary circuit:
139```python
140from qiskit.quantum_info import Statevector
142probabilities = Statevector.from_instruction(
143 unitary_circuit
144).probabilities_dict()
145```

Test Sampled Results Statistically

Never assert an exact count split from finite shots:

counts = pub_result.data.meas.get_counts()
shots = sum(counts.values())

p_zero = counts.get("0", 0) / shots
assert abs(p_zero - 0.5) < 0.1

Choose tolerance from the expected binomial uncertainty and desired failure probability, not an arbitrary constant copied into every test.

For exact probability assertions, use Statevector.probabilities_dict() on a small unitary circuit:

from qiskit.quantum_info import Statevector

probabilities = Statevector.from_instruction(
    unitary_circuit
).probabilities_dict()
Raw lines 123 to 146
147## Test Estimator Values
149```python
150import numpy as np
152actual = np.asarray(pub_result.data.evs)
153np.testing.assert_allclose(
154 actual,
155 expected,
156 rtol=1e-10,
157 atol=1e-12,
159```
161Use tight tolerances only for exact local simulation. For noisy or hardware estimates, use a statistical test and report uncertainty.

Test Estimator Values

import numpy as np

actual = np.asarray(pub_result.data.evs)
np.testing.assert_allclose(
    actual,
    expected,
    rtol=1e-10,
    atol=1e-12,
)

Use tight tolerances only for exact local simulation. For noisy or hardware estimates, use a statistical test and report uncertainty.

Raw lines 147 to 162
163## Test Transpilation
165Check invariants, not a full textual snapshot:
167```python
168isa_circuit = pass_manager.run(circuit)
170assert isa_circuit.num_qubits <= backend.num_qubits
171assert isa_circuit.layout is not None
172target_operations = set(backend.operation_names)
173circuit_operations = set(isa_circuit.count_ops()) - {
174 "barrier",
176assert circuit_operations.issubset(
177 target_operations
179```
181Control-flow and directive names may require target-aware handling; use target APIs for rigorous compatibility checks.
183Track bounded structural regressions:
185```python
186def two_qubit_instruction_count(circuit):
187 return sum(
188 len(item.qubits) == 2
189 for item in circuit.data
190 )
192assert isa_circuit.depth() <= depth_budget
193assert (
194 two_qubit_instruction_count(isa_circuit)
195 <= two_qubit_budget
197```
199Compiler improvements can legitimately change exact circuit text and layout. Snapshot stable application metrics and broad budgets instead.

Test Transpilation

Check invariants, not a full textual snapshot:

isa_circuit = pass_manager.run(circuit)

assert isa_circuit.num_qubits <= backend.num_qubits
assert isa_circuit.layout is not None
target_operations = set(backend.operation_names)
circuit_operations = set(isa_circuit.count_ops()) - {
    "barrier",
}
assert circuit_operations.issubset(
    target_operations
)

Control-flow and directive names may require target-aware handling; use target APIs for rigorous compatibility checks.

Track bounded structural regressions:

def two_qubit_instruction_count(circuit):
    return sum(
        len(item.qubits) == 2
        for item in circuit.data
    )

assert isa_circuit.depth() <= depth_budget
assert (
    two_qubit_instruction_count(isa_circuit)
    <= two_qubit_budget
)

Compiler improvements can legitimately change exact circuit text and layout. Snapshot stable application metrics and broad budgets instead.

Raw lines 163 to 200
201## Test Observable Layout
203```python
204isa_observable = observable.apply_layout(
205 isa_circuit.layout
207assert isa_observable.num_qubits == isa_circuit.num_qubits
208```
210For a small target, compare the logical expectation value and the compiled/mapped expectation value using a local Estimator.
212Never submit a logical observable with a physically laid-out circuit.

Test Observable Layout

isa_observable = observable.apply_layout(
    isa_circuit.layout
)
assert isa_observable.num_qubits == isa_circuit.num_qubits

For a small target, compare the logical expectation value and the compiled/mapped expectation value using a local Estimator.

Never submit a logical observable with a physically laid-out circuit.

Raw lines 201 to 213
214## QPY Round-Trip Test
216```python
217from io import BytesIO
218from qiskit import qpy
220buffer = BytesIO()
221qpy.dump(circuit, buffer)
222buffer.seek(0)
223loaded = qpy.load(buffer)[0]
225assert loaded == circuit
226```
228Also test required metadata and parameter names. Newer Qiskit normally loads QPY written by older versions; older Qiskit is not expected to load newer QPY.
230Do not use pickle as a fallback for untrusted data.

QPY Round-Trip Test

from io import BytesIO
from qiskit import qpy

buffer = BytesIO()
qpy.dump(circuit, buffer)
buffer.seek(0)
loaded = qpy.load(buffer)[0]

assert loaded == circuit

Also test required metadata and parameter names. Newer Qiskit normally loads QPY written by older versions; older Qiskit is not expected to load newer QPY.

Do not use pickle as a fallback for untrusted data.

Raw lines 214 to 231
232## Runtime Code Without QPU Jobs
234Use:
236- fake backends for `Target` and compilation tests,
237- Aer for ideal/noisy execution,
238- Runtime primitives in local-testing mode,
239- `scripts/inspect_runtime.py` for read-only account/backend checks.
241```python
242from qiskit_ibm_runtime.fake_provider import FakeSherbrooke
244backend = FakeSherbrooke()
245```
247Fake backend names can change. Choose one present in the pinned Runtime version.

Runtime Code Without QPU Jobs

Use:

  • fake backends for Target and compilation tests,
  • Aer for ideal/noisy execution,
  • Runtime primitives in local-testing mode,
  • scripts/inspect_runtime.py for read-only account/backend checks.
from qiskit_ibm_runtime.fake_provider import FakeSherbrooke

backend = FakeSherbrooke()

Fake backend names can change. Choose one present in the pinned Runtime version.

Raw lines 232 to 248
Runs code scripts/inspect_runtime.py :239
249## Show Deprecation Warnings
251Run examples with warnings enabled:
253```bash
254python -W default scripts/run_local_primitives.py
255```
257For migration tests:
259```bash
260python -W error::DeprecationWarning your_test.py
261```
263Do not globally suppress deprecation warnings. Qiskit 2.x warnings identify code likely to break in Qiskit 3.0.

Show Deprecation Warnings

Run examples with warnings enabled:

python -W default scripts/run_local_primitives.py

For migration tests:

python -W error::DeprecationWarning your_test.py

Do not globally suppress deprecation warnings. Qiskit 2.x warnings identify code likely to break in Qiskit 3.0.

Raw lines 249 to 264
Runs code scripts/run_local_primitives.py :254
265## Resource Guards
267Before dense simulation:
269```python
270def statevector_bytes(num_qubits, bytes_per_complex=16):
271 return (2 ** num_qubits) * bytes_per_complex
273def density_matrix_bytes(num_qubits, bytes_per_complex=16):
274 return (4 ** num_qubits) * bytes_per_complex
275```
277Set project-specific memory and qubit limits. Include parameter-sweep dimensions and number of observables when estimating total work.
279Before Runtime:
281- bound PUB count,
282- bound shots or precision,
283- account for twirling and mitigation expansion,
284- bound optimizer iterations,
285- set session or batch `max_time`,
286- confirm plan allocation.

Resource Guards

Before dense simulation:

def statevector_bytes(num_qubits, bytes_per_complex=16):
    return (2 ** num_qubits) * bytes_per_complex

def density_matrix_bytes(num_qubits, bytes_per_complex=16):
    return (4 ** num_qubits) * bytes_per_complex

Set project-specific memory and qubit limits. Include parameter-sweep dimensions and number of observables when estimating total work.

Before Runtime:

  • bound PUB count,
  • bound shots or precision,
  • account for twirling and mitigation expansion,
  • bound optimizer iterations,
  • set session or batch max_time,
  • confirm plan allocation.
Raw lines 265 to 287
288## Provenance Record
290Store:
292```python
293from importlib.metadata import version
294import platform
295import sys
297provenance = {
298 "python": sys.version,
299 "platform": platform.platform(),
300 "qiskit": version("qiskit"),
301 "qiskit_ibm_runtime": version(
302 "qiskit-ibm-runtime"
303 ),
304 "backend": backend.name,
305 "seed_transpiler": seed_transpiler,
306 "seed_simulator": seed_simulator,
307 "optimization_level": optimization_level,
308 "primitive_options": primitive_options,
309 "job_ids": job_ids,
311```
313Do not include API keys, saved-account dictionaries, full environments, headers, or tokens.

Provenance Record

Store:

from importlib.metadata import version
import platform
import sys

provenance = {
    "python": sys.version,
    "platform": platform.platform(),
    "qiskit": version("qiskit"),
    "qiskit_ibm_runtime": version(
        "qiskit-ibm-runtime"
    ),
    "backend": backend.name,
    "seed_transpiler": seed_transpiler,
    "seed_simulator": seed_simulator,
    "optimization_level": optimization_level,
    "primitive_options": primitive_options,
    "job_ids": job_ids,
}

Do not include API keys, saved-account dictionaries, full environments, headers, or tokens.

Raw lines 288 to 314
315## Troubleshooting Matrix

Troubleshooting Matrix

Raw lines 315 to 316
317### Invalid mixed Qiskit environment
319Symptoms:
321- import error mentioning an invalid environment,
322- both `qiskit-terra` and modern `qiskit`,
323- modules missing after an in-place upgrade.
325Fix: create a fresh virtual environment and install `qiskit`, not `qiskit-terra`.

Invalid mixed Qiskit environment

Symptoms:

  • import error mentioning an invalid environment,
  • both qiskit-terra and modern qiskit,
  • modules missing after an in-place upgrade.

Fix: create a fresh virtual environment and install qiskit, not qiskit-terra.

Raw lines 317 to 326
327### `ImportError` for `Sampler` or `Estimator`
329Use an explicit current implementation:
331```python
332from qiskit.primitives import (
333 StatevectorEstimator,
334 StatevectorSampler,
336from qiskit_ibm_runtime.executor_estimator import Estimator
337from qiskit_ibm_runtime.executor_sampler import Sampler
338```

ImportError for Sampler or Estimator

Use an explicit current implementation:

from qiskit.primitives import (
    StatevectorEstimator,
    StatevectorSampler,
)
from qiskit_ibm_runtime.executor_estimator import Estimator
from qiskit_ibm_runtime.executor_sampler import Sampler
Raw lines 327 to 339
340### Result has no `.quasi_dists` or `.values`
342The code expects V1 output. Use:
344```python
345counts = result[0].data.meas.get_counts()
346expectation_values = result[0].data.evs
347```

Result has no .quasi_dists or .values

The code expects V1 output. Use:

counts = result[0].data.meas.get_counts()
expectation_values = result[0].data.evs
Raw lines 340 to 348
349### Result has no `.data.meas`
351Inspect register names:
353```python
354print([register.name for register in circuit.cregs])
355print(pub_result.data)
356```
358Use `.data.<actual_register_name>`.

Result has no .data.meas

Inspect register names:

print([register.name for register in circuit.cregs])
print(pub_result.data)

Use .data.<actual_register_name>.

Raw lines 349 to 359
360### Parameter/broadcast error
362Print:
364```python
365print([parameter.name for parameter in circuit.parameters])
366print(parameter_values.shape)
367print(observables.shape if hasattr(observables, "shape") else type(observables))
368```
370Reduce to one circuit, one observable, and one parameter row, then rebuild the broadcast.

Parameter/broadcast error

Print:

print([parameter.name for parameter in circuit.parameters])
print(parameter_values.shape)
print(observables.shape if hasattr(observables, "shape") else type(observables))

Reduce to one circuit, one observable, and one parameter row, then rebuild the broadcast.

Raw lines 360 to 371
372### Runtime rejects a non-ISA circuit
374Compile with a pass manager generated from the exact backend object and submit its output.

Runtime rejects a non-ISA circuit

Compile with a pass manager generated from the exact backend object and submit its output.

Raw lines 372 to 375
376### Wrong Estimator value after transpilation
378Apply:
380```python
381isa_observable = observable.apply_layout(
382 isa_circuit.layout
384```

Wrong Estimator value after transpilation

Apply:

isa_observable = observable.apply_layout(
    isa_circuit.layout
)
Raw lines 376 to 385
386### `backend.configuration()` fails
388Use BackendV2 direct attributes and `backend.target`.

backend.configuration() fails

Use BackendV2 direct attributes and backend.target.

Raw lines 386 to 389
390### `qiskit.pulse` import fails
392Pulse was removed in Qiskit 2.0. Use fractional gates for supported IBM rotations. Qiskit Dynamics is archived and requires a separately validated legacy stack for historical control-model research.

qiskit.pulse import fails

Pulse was removed in Qiskit 2.0. Use fractional gates for supported IBM rotations. Qiskit Dynamics is archived and requires a separately validated legacy stack for historical control-model research.

Raw lines 390 to 393
394### Session is rejected
396Open Plan users must use job or batch mode.

Session is rejected

Open Plan users must use job or batch mode.

Raw lines 394 to 397
398### Hardware and simulation disagree
400Check:
4021. bit order,
4032. observable layout,
4043. measurement basis,
4054. simulator target and noise snapshot,
4065. QPU calibration time,
4076. primitive options and mitigation,
4087. statistical uncertainty,
4098. total shot allocation after twirling.
411Do not tune a noise model solely to make one experiment match.

Hardware and simulation disagree

Check:

  1. bit order,
  2. observable layout,
  3. measurement basis,
  4. simulator target and noise snapshot,
  5. QPU calibration time,
  6. primitive options and mitigation,
  7. statistical uncertainty,
  8. total shot allocation after twirling.

Do not tune a noise model solely to make one experiment match.

Raw lines 398 to 412
413## Release Upgrade Test
415When changing a Qiskit pin:
4171. create a new environment,
4182. run the environment checker,
4193. run local primitive smoke tests,
4204. run all deprecation warnings as errors,
4215. round-trip representative QPY artifacts,
4226. compare circuit semantics,
4237. compare compiled structural budgets on pinned fake targets,
4248. validate Runtime option models without submitting,
4259. run one bounded noisy simulation,
42610. only then schedule a minimal hardware smoke test.

Release Upgrade Test

When changing a Qiskit pin:

  1. create a new environment,
  2. run the environment checker,
  3. run local primitive smoke tests,
  4. run all deprecation warnings as errors,
  5. round-trip representative QPY artifacts,
  6. compare circuit semantics,
  7. compare compiled structural budgets on pinned fake targets,
  8. validate Runtime option models without submitting,
  9. run one bounded noisy simulation,
  10. only then schedule a minimal hardware smoke test.
Raw lines 413 to 426