FiQCIEstimator#
FiQCIEstimator computes expectation values of Pauli observables from quantum circuits with built-in error mitigation. It supports both readout error mitigation (M3) and zero-noise extrapolation (ZNE).
Basic Configuration#
Initialize the estimator with an IQM backend, mitigation level, and optional parameters:
from fiqci.ems import FiQCIEstimator
# Initialize estimator with mitigation level 1
estimator = FiQCIEstimator(backend, mitigation_level=1, calibration_shots=2000, calibration_file="cals.json")
For more details see the API reference documentation for FiQCIEstimator.
Transpile the circuit with transpile_to_IQM() and map the observables onto the
transpiled circuit’s layout. remove_final_rzs=False is required: the estimator measures in the X
and Y bases, and dropping the final RZ gates changes those expectation values.
from iqm.qiskit_iqm import transpile_to_IQM
from qiskit.quantum_info import SparsePauliOp
tr_qc = transpile_to_IQM(qc, backend, remove_final_rzs=False, optimization_level=3)
observables = SparsePauliOp.from_list([("ZZ", 1), ("IX", 1)])
device_observables = observables.apply_layout(tr_qc.layout)
The circuit must not end in measurements. The estimator appends its own measurement basis, and
transpile_to_IQM drops the terminal RZ frame of a circuit it sees measurements on even with
remove_final_rzs=False, which flips the X and Y expectation values. run() raises
ValueError for such a circuit. Mid-circuit measurements are supported.
The examples below use tr_qc and device_observables.
Mitigation Levels#
Mitigation levels apply predefined sets of error mitigation techniques.
Level |
Mitigation Applied |
Technique |
|---|---|---|
0 |
None |
Raw results |
1 |
Readout Error Mitigation |
M3 (matrix-free measurement mitigation) |
2 |
Level 1 + Dynamical Decoupling |
Dynamical Decoupling standard sequence (see below) |
3 |
Level 2 + Zero Noise Extrapolation |
Exponential Extrapolation, Local Folding |
Mitigation Options#
Mitigators can also be configured manually using the provided methods.
REM (Readout Error Mitigation)#
Readout error mitigation uses M3 (matrix-free measurement mitigation) to correct measurement errors. It is enabled by default at mitigation level 1.
Configure REM using the rem() method:
estimator.rem(enabled=True, calibration_shots=2000, calibration_file="cals.json")
Parameter |
Default |
Description |
|---|---|---|
|
|
Enable or disable readout error mitigation |
|
|
Number of shots used for M3 calibration circuits |
|
|
Path to save/load calibration data (JSON). Reuses cached calibrations when available. |
ZNE (Zero-Noise Extrapolation)#
ZNE artificially scales circuit noise by folding gates, then extrapolates to the zero-noise limit. It is enabled at mitigation level 3.
Configure ZNE using the zne() method:
estimator.zne(
enabled=True,
fold_gates=["cx", "cz"],
scale_factors=[1, 3, 5],
folding_method="local",
extrapolation_method="exponential",
)
Parameter |
Default |
Description |
|---|---|---|
|
|
Enable or disable ZNE |
|
|
Gate names to fold (e.g. |
|
|
List of real numbers >= 1 for noise scaling. At least 2 required. Odd integers fold exactly; other values are approximated by partial/random folding. May be a list of lists (one per submitted circuit) to use different scale factors per circuit. |
|
|
|
|
|
Extrapolation fit method. One of: |
|
|
Polynomial degree (only for |
|
|
Seed for the random gate sampling used to approximate non-odd-integer scale factors. |
Dynamical Decoupling (DD)#
Dynamical decoupling inserts sequences of gates to mitigate decoherence. It is enabled at mitigation level 2.
Configure DD using the dd() method:
estimator.dd(enabled=True, gate_sequences=None) # None uses a standard set of sequences
The standard sequence is:
[
(9, 'XYXYYXYX', 'asap'),
(5, 'YXYX', 'asap'),
(2, 'XX', 'center'),
]
Parameter |
Default |
Description |
|---|---|---|
|
|
Enable or disable DD |
|
|
|
Pauli Twirling#
Pauli twirling sandwiches two-qubit gates with random single-qubit Pauli gates to mitigate coherent errors. It is enabled at mitigation level 3.
Configure Pauli Twirling using the pauli_twirl() method:
estimator.pauli_twirl(enabled=True, num_twirls=10, gates_to_twirl=None) # None twirls all two-qubit gates
Parameter |
Default |
Description |
|---|---|---|
|
|
Enable or disable Pauli twirling |
|
|
Number of twirled variant circuits to generate for each original circuit |
|
|
List of two-qubit gates to twirl. If |
Inspecting Options#
Use the mitigator_options property to view currently applied mitigation settings:
estimator.mitigator_options
The returned dictionary is a copy, so mutating it does not reconfigure the estimator; use
zne() / rem() /
dd() / pauli_twirl() for that, which
validate their input. The live M3IQM mitigator under rem is shared by reference rather than
copied, since it owns the calibration data.
Counting Circuits#
Use total_circuits_generated() to see how many circuits will actually be executed under the current mitigation settings (accounts for measurement-basis splitting, ZNE scale factors and Pauli twirls):
estimator.total_circuits_generated(num_base_circuits=1, observables=device_observables, detailed=True)
Set detailed=True to print the breakdown and return a dictionary with each multiplier; otherwise the method returns the total as an int. The count does not include REM calibration circuits.
Running Circuits#
run() accepts a max_batch_size parameter (default 100) that controls how many circuits are sent to the backend in a single job. All measurement-basis subcircuits across every circuit/observable pair (and every ZNE scale factor) are flattened and split into batches of this size, then re-combined transparently.
job = estimator.run(tr_qc, observables=device_observables, shots=2048, max_batch_size=100)
run returns immediately with a FiQCIEstimatorJob handle without waiting for results. Polling the underlying job works right away (job.status(), job.job_ids()); the expectation values are computed the first time you call expectation_values() / raw_expectation_values(), and cached.
The handle also lets you inspect a multi-batch run before it finishes:
job.job_ids() # backend job id of every batch (None for any unsubmitted batch)
job.statuses() # per-batch JobStatus, in submission order
job.status() # single aggregated status across all batches
job.done() # True once every batch has reached a terminal state
job.partial_results() # per-batch results for batches that have already completed
job.mitigator_options # frozen snapshot of the mitigation settings this run used
Unlike mitigator_options on the estimator (which reflects the current, mutable settings), the handle’s mitigator_options is a snapshot frozen at submission. It reports the zne configuration together with the underlying mitigation_level, rem, dd and pauli_twirl settings this run actually used, and never changes even if you reconfigure the estimator afterwards. (The per-pair scale factors are available separately via requested_scale_factors() / achieved_scale_factors() below.)
Note
As with the sampler, submission is not atomic. If the backend rejects a circuit partway through, run logs a warning and still returns a handle (rejected/skipped batches report ERROR/CANCELLED. To inspect use job.statuses()). In that case expectation_values() and result() raise BatchFailedError, since the values cannot be computed without all circuits.
Results#
run() returns a FiQCIEstimatorJob that wraps the single backend job produced by the run. Expectation values are computed lazily on first access and cached. It exposes the following methods:
Method |
Description |
|---|---|
|
Mitigated expectation values |
|
Raw (pre-extrapolation) expectation values |
|
The underlying backend job |
|
Combined |
|
Observables used in the computation |
|
ZNE scale factors requested for the run, one list per circuit/observable pair |
|
ZNE scale factors folding actually realised (the extrapolation x-axis), same shape |
|
Frozen snapshot of the mitigation settings this run used ( |
|
Standard errors of the expectation values (shot noise and, when ZNE is enabled, the extrapolation uncertainty) |
Standard Errors#
standard_errors() reports the uncertainty of each expectation value, computed lazily and cached alongside the values. It mirrors the shape of expectation_values: one entry per circuit/observable pair, each a dict of per-Pauli-term standard errors.
job = estimator.run(tr_qc, observables=device_observables, shots=2048)
job.expectation_values(0) # e.g. [0.92, -0.01, ...]
job.standard_errors(0) # {"shot_error": [...], "zne_extrapolation_error": ..., "total": [...]}
Key |
Description |
|---|---|
|
Statistical standard error of the raw measurement, \(\sqrt{(1 - \langle P \rangle^2) / N}\) per term. When ZNE is enabled this is taken at the unfolded (scale 1) point. This matches the convention used by Qiskit’s sampling-based |
|
Standard error of the extrapolated value: the per-scale shot errors propagated through the (linear) extrapolator. |
|
Standard error of the value |
Note
Only statistical error is reported. The estimator does not account for ZNE model bias (the systematic error from the chosen extrapolation shape), nor does it inflate shot_error by the M3 readout-mitigation overhead at mitigation level ≥ 1.