Client-side Estimator
qiskit_ibm_runtime.executor_estimator
Overview
qiskit_ibm_runtime.executor_estimator.Estimator is an implementation of the Qiskit EstimatorV2 interface built on top of the Executor primitive. It estimates expectation values of quantum observables by executing ISA circuits on an IBM Quantum backend.
The key difference between the legacy server-side Estimator and this new implementation is that all pre- and post-processing runs on the client machine. This includes circuit preparation (twirling, gate folding, dynamical decoupling, and noise injection) as well as result post-processing (TREX rescaling, ZNE extrapolation, and PEC quasi-probability weighting). Running these steps locally provides faster debugging feedback and greater user control.
When a user submits a job through run(), the underlying processing consists of:
- Coercing the Primitive Unified Blocs (PUBs), resolving the resilience-level defaults, and determining the shot count.
- Converting the PUBs into a
QuantumProgram, applying circuit transformations (twirling, gate folding, DD, noise injection) according to the specified options. - Calling
Executorto submit the quantum program to the backend. - Upon job completion, estimating expectation values from the raw measurement data and applying error-mitigation post-processing as needed.
For large or complex workloads, the client-side pre- and post-processing steps can be resource intensive and may cause a delay before the job is submitted. Set the qiskit_ibm_runtime logger to INFO to monitor client-side processing:
import logging
logger = logging.getLogger("qiskit_ibm_runtime")
logger.setLevel(logging.INFO)Basic usage
Example 1 — Minimal (no error mitigation)
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime.executor_estimator import Estimator
# Select a backend.
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
observable = SparsePauliOp("ZZ")
# Transform the circuit and observable into ISA format.
pm = generate_preset_pass_manager(backend=backend, optimization_level=1)
isa_qc = pm.run(qc)
isa_obs = observable.apply_layout(isa_qc.layout)
estimator = Estimator(mode=backend)
estimator.options.resilience_level = 0
job = estimator.run([(isa_qc, isa_obs)])
result = job.result()
print(result[0].data.evs) # expectation valueExample 2 — Resilience level 2 (measurement error mitigation + gate-folding ZNE)
estimator = Estimator(mode=backend)
estimator.options.resilience_level = 2 # TREX + ZNE (gate folding)
job = estimator.run([(isa_qc, isa_obs)])
result = job.result()
print(result[0].data.evs) # zero-noise extrapolated expectation value
print(result[0].data.evs_noise_factors) # raw values at each noise amplification levelExample 3 — PEC (requires explicit noise learning)
Unlike the legacy server-side implementation, this client-side Estimator requires explicit noise learning for the error mitigation methods that need a noise model (PEC and PEA).
Use find_unique_layers() to extract the unique gate layers from your PUBs, pass the layers to NoiseLearnerV3 to learn their noise in a separate job, then assign the learned noise maps to layer_noise_model.
from qiskit_ibm_runtime.noise_learner_v3 import NoiseLearnerV3
pubs = [(isa_qc, isa_obs)]
estimator = Estimator(mode=backend)
estimator.options.resilience.pec_mitigation = True
# Step 1. Extract the unique boxed gate layers from your PUBs.
layers = estimator.find_unique_layers(pubs)
# Step 2. Learn the noise model for those layers.
nl_result = NoiseLearnerV3(mode=backend).run(layers).result()
# Step 3. Convert the NoiseLearnerV3 result to Pauli-Lindblad maps and pass them
# to the Estimator. The maps follow the same order as the input layers, so they can
# be zipped positionally.
pauli_lindblad_maps = nl_result.to_pauli_lindblad_maps()
estimator.options.resilience.layer_noise_model = zip(layers, pauli_lindblad_maps)
job = estimator.run(pubs)
result = job.result()
print(result[0].data.evs)Inputs
Each call to run() takes a list of PUBs. Each PUB is in this format:
(<single circuit>, <one or more observables>, <optional parameter values>, <optional precision>)See Estimator inputs and outputs for more information on Estimator inputs and outputs.
Elements from observables and parameter values are combined by following NumPy broadcasting rules as described in Primitive inputs and outputs.
Options
When instantiating Estimator, you can pass in options by using EstimatorOptions or a dictionary. Commonly used options, such as resilience_level, are at the first level. Other options are grouped into categories, such as execution. Specify the options in this format: options.<category>.<option> = <value>. For example: options.dynamical_decoupling.enable = True.
See Introduction to options for an overview on specifying primitive options. See Specify Estimator options and Configure noise management with Estimator for more information about Estimator options.
Outputs
run() returns a RuntimeJobV2. Calling job.result() returns a PrimitiveResult of EstimatorPubResult objects — one per input PUB:
result = job.result()
pub_result = result[0] # EstimatorPubResult for the first PUB
pub_result.data # DataBin holding numerical arrays
pub_result.metadata # dictionary with per-PUB metadata
result.metadata # dictionary with job-level metadataThe contents of pub_result.data depend on the mitigation technique specified.
No mitigation / measurement mitigation only (resilience levels 0 and 1)
All of the following fields have the shape pub_shape, which is broadcast(param_shape, obs_shape).
data.evs— Expectation values.data.stds— Standard deviations. Reflects the spread across twirling randomizations when twirling is enabled; equalsensemble_standard_errorwhen twirling is disabled.data.ensemble_standard_error— Standard error under the independently and identically-distributed shot-noise assumption (no drift contribution).
When measurement mitigation (TREX) is active, the evs values are corrected for readout errors using a calibration circuit that is run automatically alongside the main circuits.
PEC
PEC produces the same three fields as the no-mitigation case: evs, stds, and ensemble_standard_error. The gamma quasi-probability factor is applied internally during post-processing and does not appear as a separate output field.
The stds values are scaled by the gamma factor, so they are typically larger than in the no-mitigation case for the same shot count. This is the fundamental cost of PEC: unbiased estimates come with increased variance proportional to the sampling overhead (gamma^2).
ZNE (gate folding) and PEA
When resilience.zne_mitigation=True, the estimator runs the circuit at multiple noise amplification levels and fits a curve to extrapolate to zero noise. The result contains both the extrapolated estimate and the raw data at each noise level.
data.evs— Zero-noise extrapolated expectation values (best heterogeneous fit — the extrapolator is chosen per term for multi-term observables). Shape:pub_shape.data.stds— Standard deviations of the extrapolated values. Same shape asevs. Derived from the spread over twirling randomizations when twirling is on.data.evs_noise_factors— Raw (non-extrapolated) expectation values at each noise amplification level. Shape:(*pub_shape, num_noise_factors).data.stds_noise_factors— Standard deviations at each noise factor. Same shape asevs_noise_factors. Reflects the spread over twirling randomizations when twirling is on; equalsensemble_stds_noise_factorswhen twirling is off.data.ensemble_stds_noise_factors— Ensemble standard errors at each noise factor under the independently and identically-distributed shot-noise assumption. Shape:(*pub_shape, num_noise_factors).data.evs_extrapolated— Expectation values from each requested extrapolator, evaluated at each point inresilience.zne.extrapolated_noise_factors. These are forced homogeneous fits — the same extrapolator is applied to all terms of a multi-term observable — one fit per extrapolator. Shape:(*pub_shape, num_extrapolators, num_eval_points).data.stds_extrapolated— Standard deviations corresponding toevs_extrapolated. Same shape.
For multi-term observables (for example, {"XX": 0.5, "XY": 0.5}), evs and stds use a heterogeneous fit: the best-fitting extrapolator is selected independently for each Pauli term. evs_extrapolated and stds_extrapolated use a homogeneous fit per extrapolator, which is useful for comparing models. If your analysis needs a single extrapolator applied consistently, split the multi-term observable into single-term observables so that each term is fit on its own.
ZNE results can be visualized with draw_zne_evs() and draw_zne_extrapolators() (requires plotly).
Job-level metadata
result.metadata is a dict containing:
"options"— the finalizedEstimatorOptions, as a dictionary. Inactive resilience sub-options are pruned (for example, theznesub-dictionary is omitted whenzne_mitigation=False), and noise model (if provided) is removed."target_precision"— the precision resolved from the PUBs and theprecisionargument ofrun(), orNoneif neither specified one. In that case, the shot count comes fromdefault_shots, falling back todefault_precision."shots"— the total shot count used for execution."executor"— the metadata of the underlying Executor result.
Migration guide
This client-side Estimator implementation is largely a drop-in replacement for the legacy one. Follow the steps listed below to migrate to the new implementation, keeping in mind these behavioral changes:
- Pre- and post-processing happen on the client side. In the legacy Estimator, all circuit transformations (twirling, gate folding, and noise injection), as well as result post-processing (ZNE extrapolation, TREX rescaling, and PEC weighting), are performed on the server side. In this client-side implementation, these steps run entirely on the client machine.
- Noise learning is a separate step. In the legacy Estimator, noise learning for PEC and PEA is integrated into the Estimator job itself and handled implicitly on the server side. In this new implementation, noise learning is a separate workflow that must be performed explicitly.
- Executor jobs are submitted. The client-side Estimator uses Executor to submit jobs, making them Executor jobs rather than Estimator jobs. Job attributes, such as
primitive_idandinputs, return information about the Executor job.
Step 1 — Update the imports.
Before:
from qiskit_ibm_runtime import Estimator
from qiskit_ibm_runtime.options import EstimatorOptionsAfter:
from qiskit_ibm_runtime.executor_estimator import Estimator
from qiskit_ibm_runtime.options_models import EstimatorOptionsStep 2 — Perform noise learning explicitly (if using PEC or PEA).
If your code uses PEC (pec_mitigation=True) or ZNE with PEA (zne.amplifier="pea"), you need to perform noise learning explicitly by using NoiseLearnerV3.
Before:
from qiskit_ibm_runtime import Estimator
pubs = [...] # Your PUBs
estimator = Estimator(mode=backend)
estimator.options.resilience.pec_mitigation = True # or zne_mitigation + pea amplifier
job = estimator.run(pubs)After:
from qiskit_ibm_runtime.executor_estimator import Estimator
from qiskit_ibm_runtime import NoiseLearnerV3
pubs = [...] # Your PUBs
estimator = Estimator(mode=backend)
estimator.options.resilience.pec_mitigation = True # or zne_mitigation + pea amplifier
# Identify the unique layers to learn.
layers = estimator.find_unique_layers(pubs)
# Learn the noise model for those layers (runs as a separate job).
learner = NoiseLearnerV3(mode=backend)
learner_job = learner.run(layers)
learner_result = learner_job.result()
# Convert the result to Pauli-Lindblad noise maps.
pauli_lindblad_maps = learner_result.to_pauli_lindblad_maps()
# Assign the learned noise maps so PEA/PEC uses them.
# The result objects returned by NoiseLearnerV3 follow the same order as the input
# layers, so the positional zip can be used here.
estimator.options.resilience.layer_noise_model = zip(layers, pauli_lindblad_maps)
# Now execute the target PUBs.
job = estimator.run(pubs)Step 3 — Split one job into several (if the PUBs use different precision values).
Mixed precisions are no longer supported: a job can no longer contain PUBs that request different precision values, and run() raises IBMInputValueError if it does. Group the PUBs by precision and submit one job per group, using a Batch so the groups still run together.
Before:
from qiskit_ibm_runtime import Estimator
# PUBs with different precision values.
pubs = [(isa_circuit, isa_obs, None, 0.1), (isa_circuit1, isa_obs1, None, 0.5)]
estimator = Estimator(mode=backend)
job = estimator.run(pubs)After:
from qiskit_ibm_runtime.executor_estimator import Estimator
from qiskit_ibm_runtime import Batch
# Group the PUBs by precision, one group per job.
with Batch(backend=backend) as batch:
estimator = Estimator(mode=batch)
jobs = [
estimator.run([(isa_circuit, isa_obs)], precision=0.1),
estimator.run([(isa_circuit1, isa_obs1)], precision=0.5),
]
results = [job.result() for job in jobs]Classes
コラム「 1 」 | コラム「 2 」 |
|---|---|
Estimator | Client-side Estimator primitive for IBM Quantum Compute (formerly Qiskit Runtime). |
EstimatorV2 | alias of Estimator |