skills/qiskit/references/patterns.md
Use a four-stage workflow:
Map -> Optimize -> Execute -> Analyze
Keep these stages separate so circuit construction, compilation, paid execution, and interpretation can each be tested and reproduced.
from qiskit import QuantumCircuit
circuit = QuantumCircuit(2)
circuit.h(0)
circuit.cx(0, 1)
circuit.measure_all()
Local statevector primitives accept abstract circuits. A pass manager is still useful for testing the same workflow shape:
from qiskit.transpiler import generate_preset_pass_manager
pass_manager = generate_preset_pass_manager(
optimization_level=1,
seed_transpiler=31,
)
compiled = pass_manager.run(circuit)
from qiskit.primitives import StatevectorSampler
sampler = StatevectorSampler(seed=31)
pub_result = sampler.run([compiled], shots=2048).result()[0]
counts = pub_result.data.meas.get_counts()
total = sum(counts.values())
probabilities = {
bitstring: count / total
for bitstring, count in counts.items()
}
unexpected = sum(
probability
for state, probability in probabilities.items()
if state not in {"00", "11"}
)
print(probabilities, unexpected)
Use this ideal baseline before adding a noise model or QPU.
from qiskit import QuantumCircuit
circuit = QuantumCircuit(3)
circuit.h(0)
circuit.cx(0, 1)
circuit.cx(1, 2)
circuit.measure_all()
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True,
simulator=False,
min_num_qubits=circuit.num_qubits,
)
pass_manager = generate_preset_pass_manager(
backend=backend,
optimization_level=1,
seed_transpiler=31,
)
isa_circuit = pass_manager.run(circuit)
print("depth:", isa_circuit.depth())
print("operations:", isa_circuit.count_ops())
print("layout:", isa_circuit.layout)
from qiskit_ibm_runtime import SamplerV2 as Sampler
sampler = Sampler(mode=backend)
job = sampler.run([isa_circuit], shots=4096)
job_id = job.job_id()
print("job_id:", job_id)
pub_result = job.result()[0]
counts = pub_result.data.meas.get_counts()
shots = sum(counts.values())
ghz_support = (
counts.get("000", 0) + counts.get("111", 0)
) / shots
record = {
"job_id": job_id,
"backend": backend.name,
"shots": shots,
"ghz_support": ghz_support,
"result_metadata": pub_result.metadata,
}
Do not present GHZ support as state fidelity without a justified measurement protocol.
Build and compile one parameterized circuit:
import numpy as np
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
from qiskit.transpiler import generate_preset_pass_manager
theta = Parameter("theta")
circuit = QuantumCircuit(2)
circuit.ry(theta, 0)
circuit.cx(0, 1)
circuit.measure_all()
pass_manager = generate_preset_pass_manager(
backend=backend,
optimization_level=1,
seed_transpiler=31,
)
isa_circuit = pass_manager.run(circuit)
parameter_values = np.linspace(0, np.pi, 21).reshape(-1, 1)
Submit values through one PUB:
sampler = Sampler(mode=backend)
pub_result = sampler.run(
[(isa_circuit, parameter_values)],
shots=1024,
).result()[0]
counts_by_point = [
pub_result.data.meas.get_counts(index)
for index in range(len(parameter_values))
]
This preserves a consistent layout and avoids repeated compilation.
Use a parameterized ansatz and map the observable after compilation:
import numpy as np
from qiskit.circuit.library import efficient_su2
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler import generate_preset_pass_manager
ansatz = efficient_su2(
num_qubits=2,
reps=1,
entanglement="linear",
)
hamiltonian = SparsePauliOp.from_list(
[
("ZI", 1.0),
("IZ", 1.0),
("XX", 0.2),
]
)
pass_manager = generate_preset_pass_manager(
backend=backend,
optimization_level=1,
seed_transpiler=31,
)
isa_ansatz = pass_manager.run(ansatz)
isa_hamiltonian = hamiltonian.apply_layout(isa_ansatz.layout)
initial_point = np.zeros(ansatz.num_parameters)
Run iterative Estimator calls. Session mode requires an eligible paid plan:
from scipy.optimize import minimize
from qiskit_ibm_runtime import EstimatorV2 as Estimator, Session
history = []
with Session(backend=backend, max_time="20m") as session:
estimator = Estimator(
mode=session,
options={"resilience_level": 1},
)
def objective(parameters):
pub = (
isa_ansatz,
isa_hamiltonian,
[parameters],
)
pub_result = estimator.run(
[pub],
precision=0.03,
).result()[0]
value = float(np.asarray(pub_result.data.evs).reshape(-1)[0])
history.append(
{
"parameters": parameters.copy(),
"value": value,
"metadata": pub_result.metadata,
}
)
return value
optimum = minimize(
objective,
initial_point,
method="COBYLA",
options={"maxiter": 25},
)
For Open Plan access, instantiate Estimator(mode=backend) and use job mode. The circuit remains compiled once in either case.
Compile all circuits against the same target:
pass_manager = generate_preset_pass_manager(
backend=backend,
optimization_level=1,
seed_transpiler=31,
)
isa_circuits = pass_manager.run(circuits)
Submit independent jobs:
from qiskit_ibm_runtime import Batch, SamplerV2 as Sampler
with Batch(backend=backend, max_time="10m") as batch:
sampler = Sampler(mode=batch)
jobs = [
sampler.run([circuit], shots=2048)
for circuit in isa_circuits
]
job_records = [
{
"job_id": job.job_id(),
"result": job.result(),
}
for job in jobs
]
Keep the job list aligned with an explicit experiment manifest.
Evaluate in three stages:
StatevectorSampler or StatevectorEstimator.Do not force every stage to use identical compiled circuits: simulator and QPU targets differ. Preserve the same logical circuit and compile separately for each target.
Compare:
Avoid claiming a noise model predicts QPU output merely because the two results are close once.
Run an unmitigated baseline:
from qiskit_ibm_runtime import EstimatorV2 as Estimator
baseline_estimator = Estimator(
mode=backend,
options={"resilience_level": 0},
)
baseline = baseline_estimator.run(
[(isa_circuit, isa_observable)],
precision=0.03,
).result()[0]
Run a mitigated configuration:
mitigated_estimator = Estimator(
mode=backend,
options={"resilience_level": 2},
)
mitigated = mitigated_estimator.run(
[(isa_circuit, isa_observable)],
precision=0.03,
).result()[0]
Compare both against a justified ideal or classically verifiable reference. Report uncertainty, usage, and total circuit/shot overhead. Do not assume the mitigated value is closer.
Persist enough information to reconstruct the workflow:
from importlib.metadata import version
manifest = {
"packages": {
"qiskit": version("qiskit"),
"qiskit-ibm-runtime": version("qiskit-ibm-runtime"),
},
"backend": backend.name,
"job_ids": job_ids,
"seed_transpiler": 31,
"optimization_level": 1,
"shots": 4096,
"primitive_options": resolved_options,
"logical_parameter_order": [
parameter.name for parameter in logical_circuit.parameters
],
"compiled_layout": str(isa_circuit.layout),
"compiled_operations": dict(isa_circuit.count_ops()),
}
Store logical and ISA circuits in QPY, and store the manifest in a text format such as JSON after converting Qiskit-specific objects to explicit strings or dictionaries.
Never serialize credentials, service account objects, raw environments, or API request headers.
max_time.