Skip to content

Execution Metrics

Overview

When running a QESEM job, the results contain essential data to help you analyze the outcome of the QESEM execution. This data allows you to better understand the impact of different parameters on the QESEM execution. You might also find it relevant when writing a paper about your research. This guide explains each component of the job output and how to interpret them.

Results Structure

The QESEM results follow a consistent structure. Here's an example showing the full structure for one observable (average magnetization on 51 qubits):

for circuit_result in job.results:
    for i, (observable, result) in enumerate(circuit_result):
        print(f"Result {i}:")
        print(f"Observable: {observable}")
        print(f"QESEM result: {result.qesem}")
        print(f"Unmitigated result: {result.unmitigated}")
        print(f"Noise scaling method: {result.noise_scaling.scaling_method}")
        print(f"Noise scaling results (with REM):")
        for scale_result in result.noise_scaling.results_with_REM:
            print(f"  Scale {scale_result.scale}: {scale_result.value} ± {scale_result.error_bar}")
        print(f"QESEM ZNE results:")
        for zne_result in result.qesem_zne:
            print(f"  {zne_result.extrapolation}: {zne_result.value} ± {zne_result.error_bar} (scale factors: {zne_result.scale_factors})")
        print()
output
Result 0:
Observable: <SparseObservable with 51 terms on 51 qubits (average magnetization)>
QESEM result: {ev: 0.46416476809076984 ± 0.010920525719409813}
Unmitigated result: {ev: 0.41432941176470595 ± 0.0028514619949440174}
Noise scaling method: QESEM
Noise scaling results (with REM):
  Scale 0.0: 0.46416476809076984 ± 0.010920525719409813
  Scale 1.0: 0.41701989420491187 ± 0.0028700768562801874
  Scale 2.0: 0.3848860077543664 ± 0.0026132823963957263
QESEM ZNE results:
  exponential: 0.4518366182687061 ± 0.0.006934886422066519 (scale factors: [1.0, 2.0])

For comparison, the ideal value (from BP simulation) is 0.4621392970576006. Each result contains four main components:

  • qesem: The QESEM mitigated expectation value (corresponds to scale 0.0 in noise_scaling)
  • unmitigated: The raw expectation value without error mitigation
  • noise_scaling: Results at different noise scales, all with readout mitigation (REM). Scale 0.0 is the fully mitigated result - the QESEM result
  • qesem_zne: Zero Noise Extrapolation results using different extrapolation methods

The following sections explain each component in detail and also elaborate on other execution metrics available.

Mitigated and Unmitigated Results

Each result contains both the QESEM mitigated expectation value and the unmitigated (noisy) expectation value obtained by running the user's circuit without QESEM. This allows you to compare the performance of QESEM to bare execution. The following code snippet shows how to print the mitigated and unmitigated results side by side:

job = qedma_client.get_job(job_id="<job_id>", include_results=True)
for circuit_result in job.results:
    for observable, result in circuit_result:
        print(f"Observable: {observable}")
        print(f"QESEM (mitigated) result: {result.qesem}") 
        print(f"Unmitigated (noisy) result: {result.unmitigated}")
        print()
output
Observable: <SparseObservable with 5 terms on 5 qubits: (0.2+0j)(Z_0) + (0.2+0j)(Z_1) + (0.2+0j)(Z_2) + (0.2+0j)(Z_3) + (0.2+0j)(Z_4)>
QESEM (mitigated) result: {ev: 1.001564797971052 ± 0.0031723321009665215}
Unmitigated (noisy) result: {ev: 0.9956789123456789 ± 0.004123456789012345}

Zero Noise Extrapolation (ZNE) Results

In addition to the standard QESEM result (obtained at zero noise scale), the results also include Zero Noise Extrapolation (ZNE) estimates using different extrapolation methods. These are available in the qesem_zne field:

job = qedma_client.get_job(job_id="<job_id>", include_results=True)
for circuit_result in job.results:
    for observable, result in circuit_result:
        print(f"Observable: {observable}")
        print(f"QESEM result: {result.qesem}")

        if result.qesem_zne:
            for zne_result in result.qesem_zne:
                print(f"  ZNE ({zne_result.extrapolation}): {zne_result.value} ± {zne_result.error_bar}")
                print(f"  Scale factors: {zne_result.scale_factors}")
output
Observable: <SparseObservable with 5 terms on 5 qubits: (0.2+0j)(Z_0) + (0.2+0j)(Z_1) + (0.2+0j)(Z_2) + (0.2+0j)(Z_3) + (0.2+0j)(Z_4)>
QESEM result: {ev: 1.001564797971052 ± 0.0031723321009665215}
  ZNE (exponential): 0.9949222631501025 ± 0.004502643483116952
  Scale factors: [1.0, 2.0]
Observable: <SparseObservable with 2 terms on 5 qubits: (1+0j)(Z_1 Z_0) + (0.5+0j)(Z_4 X_1)>
QESEM result: {ev: 1.006890905320085 ± 0.0020487676884390882}
  ZNE (exponential): 1.0043660947633497 ± 0.01921560940528758
  Scale factors: [1.0, 2.0]

Each ZNE result includes the extrapolation method used (e.g., exponential) and the noise scale factors used for the extrapolation.

Noise Scaling Results

When using Quasi-probabilistic Error Tuning (QET) to obtain results at different noise scales, the noise_scaling field contains the results for all requested noise scales:

job = qedma_client.get_job(job_id="<job_id>", include_results=True)
for circuit_result in job.results:
    for observable, result in circuit_result:
        print(f"Observable: {observable}")
        print(f"Noise scaling method: {result.noise_scaling.scaling_method}")
        print(f"Scale factors: {result.noise_scaling.scale_factors}")

        for scale_result in result.noise_scaling.results_with_REM:
            print(f"  Scale {scale_result.scale}: {scale_result.value} ± {scale_result.error_bar}")
output
Observable: <SparseObservable with 5 terms on 5 qubits: (0.2+0j)(Z_0) + (0.2+0j)(Z_1) + (0.2+0j)(Z_2) + (0.2+0j)(Z_3) + (0.2+0j)(Z_4)>
Noise scaling method: QESEM
Scale factors: [0.0, 1.0, 2.0]
  Scale 0.0: 1.001564797971052 ± 0.0031723321009665215
  Scale 1.0: 0.9770000000000001 ± 0.0014406740093636336
  Scale 2.0: 0.9456789123456789 ± 0.002123456789012345

Each result includes:

  • scale: The noise scale factor (0.0 = fully mitigated, 1.0 = physical noise, >1.0 = amplified noise)
  • value: The expectation value at that noise scale
  • error_bar: The statistical uncertainty at that noise scale

The standard qesem result (accessed via result.qesem) corresponds to the scale 0.0 result from noise_scaling, which includes full QESEM error mitigation. All other noise scale results include readout mitigation (REM) which is measurement error mitigation. Notably, the scale factor 1.0 result (accessed via result.noise_scaling.results_with_REM where scale=1.0) is different from the unmitigated result (accessed via result.unmitigated): while both represent the physical noise level of the device, the 1.0 scale factor result includes readout mitigation, which corrects for measurement errors. Since measurement errors cannot be amplified when scaling other noise sources, all results used for ZNE fitting include readout mitigation to ensure consistent treatment of measurement errors across all scale factors.

Complementary Noise Scales

When using QET with PrecisionPerFactor, QESEM automatically measures complementary noise scales around 1.0 to provide symmetric data points for extrapolation.

For example, when requesting scales 0.0, 0.5, and 0.7:

job = qedma_client.create_job(
    circuit=circuit,
    observables=[avg_magnetization, other_observable],
    empirical_time_estimation=True,
    precision=qedma_api.PrecisionPerFactor(root={
        0.0: 0.1,
        0.5: 0.15,
        0.7: 0.2,
    }),
    backend="ibm_fez",
)

The results will include all requested scales plus their complementary scales:

job = qedma_client.get_job(job_id=job.job_id, include_results=True)
for circuit_result in job.results:
    for observable, result in circuit_result:
        print(f"Scale factors: {result.noise_scaling.scale_factors}")
        for scale_result in result.noise_scaling.results_with_REM:
            print(f"  Scale {scale_result.scale}: {scale_result.value} ± {scale_result.error_bar}")
output
Scale factors: [0.0, 0.5, 0.7, 1.0, 1.3, 1.5, 2.0]
  Scale 0.0: 0.9905427958272651 ± 0.007510231724724064
  Scale 0.5: 0.9951679607673365 ± 0.0038371191415651155
  Scale 0.7: 0.9946335155355385 ± 0.0012708919356078856
  Scale 1.0: 0.9898535475020883 ± 0.0012826557704453028
  Scale 1.3: 0.986937990104944 ± 0.0017600846686600714
  Scale 1.5: 0.9879899869869353 ± 0.0012683269224481708
  Scale 2.0: 0.9821643303431624 ± 0.0033657634725983167

In this example: - Scale 0.0 was requested → scale 2.0 was automatically added (complementary around 1.0) - Scale 0.5 was requested → scale 1.5 was automatically added (complementary around 1.0) - Scale 0.7 was requested → scale 1.3 was automatically added (complementary around 1.0) - Scale 1.0 is always included in the results

Note

Complementary noise scale pairs are based on the same circuit data, which means they are not independent datapoints. This may be relevant if you want to compute ZNE error bars.

Number of Shots

These fields provide insight into total shots (includes calibration, characterization and mitigation) and only mitigation shots allocation, helping users understand:

  • Characterization overhead – Can be much higher than necessary, especially for short mitigation jobs (≥1/2).
  • Parameterized circuit efficiency – Shared characterization reduces the fraction of characterization shots.
  • Scalability – Characterization becomes more efficient as qubits increase.

While users set the target precision, these fields help interpret the shot distribution and efficiency of QESEM.

job = qedma_client.get_job(job_id = "<job_id>")
print(f"Total shots: {job.execution_details.total_shots}")
print(f"Mitigation shots: {job.execution_details.mitigation_shots}") 
output
Total shots: 1259210
Mitigation shots: 253210

QPU Time

Another metric that QESEM exposes is the total billable QPU time which was required to execute the QESEM job. When execution mode is set to "session", job.qpu_time is the session wall time. When execution mode is set to "batch", job.qpu_time is the total qpu time used for all batches.

job = qedma_client.get_job(job_id = "<job_id>")
print(f"QPU time: {job.qpu_time['execution']}")
output
QPU time: 0:00:08

Note

The job.qpu_time doesn't include the QPU time required for empirical time estimation

Gates Fidelities

job.execution_details.gate_fidelities holds the average fidelity of each operation type in the transpiled circuit. Since these fidelities affect the time required for mitigation, they are valuable for understanding results and for making informed decisions. For example, comparing fidelities across devices can help identify the best device, and comparing fidelities across operation types can assist in circuit design.

Qedma’s characterization infers the Pauli error rates for all entangling layers in the circuit. The added infidelity due to single-qubit gates is accounted for within the two-qubit gate layer error budget. The infidelity of each operation within an entangling layer is computed by summing the Pauli error rates affecting its qubits, with crosstalk errors distributed evenly among the involved operations. The reported fidelities represent averages over all operations of a given type.

job = qedma_client.get_job(job_id = "<job_id>")
print(f"Gates fidelities: {job.execution_details.gate_fidelities}")
output
Gates fidelities:  {'CNOT': 0.9900899581083492, 'ID1Q': 0.9989167673500885}

Transpiled Circuit

job.execution_details.transpiled_circuits holds the quantum circuit after it has been adapted to the target hardware and optimized for qesem's mitigation QPU runtime. This involves converting the circuit to match device connectivity and native gates, utilizing both Clifford and fractional-angle entangling gates, and mapping logical qubits to physical qubits. The transpiled circuit serves as the basis for the mitigation: the circuits executed on the hardware maintain its structure, with only local modifications introduced during the mitigation process.

job = qedma_client.get_job(job_id = "<job_id>")
print(f"Transpiled circuit:")
print(job.execution_details.transpiled_circuits[0].circuit.draw(output="text", idle_wires=False, fold=10,vertical_compression='high'))
print(f"Transpiled circuit qubit mapping: {job.execution_details.transpiled_circuits[0].qubit_map}")
print(f"Transpiled circuit number of measurement bases: {job.execution_details.transpiled_circuits[0].num_measurement_bases}")
output
Transpiled circuit:
       ┌────────┐ ░ ┌────┐ ░ ┌──────────┐ ░ ┌────┐ ░ ┌────────┐ ░ ┌───┐ ░  ░ ┌────────────┐ ░ ┌────┐ ░ »
q1_31: ┤ Rz(-π) ├─░─┤ √X ├─░─┤ Rz(-π/2) ├─░─┤ √X ├─░─┤ Rz(-π) ├─░─┤ X ├─░──░─┤ Rz(1.4066) ├─░─┤ √X ├─░─»
       ├────────┤ ░ ├────┤ ░ ├──────────┤ ░ ├────┤ ░ ├────────┤ ░ └─┬─┘ ░  ░ └─┬────────┬─┘ ░ ├────┤ ░ »
q1_32: ┤ Rz(-π) ├─░─┤ √X ├─░─┤ Rz(-π/2) ├─░─┤ √X ├─░─┤ Rz(-π) ├─░───■───░──░───┤ Rz(-π) ├───░─┤ √X ├─░─»
       └────────┘ ░ └────┘ ░ └──────────┘ ░ └────┘ ░ └────────┘ ░       ░  ░   └────────┘   ░ └────┘ ░ »
c0: 33/════════════════════════════════════════════════════════════════════════════════════════════════»
                                                                                                       »
«        ┌────────┐  ░ ┌────┐ ░ ┌─────────────┐ ░ ┌─────────┐┌───────┐┌────┐┌───────┐┌──────────┐ ░ ┌─┐   
«q1_31: ─┤ Rz(-π) ├──░─┤ √X ├─░─┤ Rz(-1.4066) ├─░─┤ Rz(π/2) ├┤ Rz(π) ├┤ √X ├┤ Rz(π) ├┤ Rz(-π/2) ├─░─┤M├───
«       ┌┴────────┴┐ ░ ├────┤ ░ └──┬────────┬─┘ ░ └─────────┘└───────┘└────┘└───────┘└──────────┘ ░ └╥┘┌─┐
«q1_32: ┤ Rz(-π/2) ├─░─┤ √X ├─░────┤ Rz(-π) ├───░─────────────────────────────────────────────────░──╫─┤M├
«       └──────────┘ ░ └────┘ ░    └────────┘   ░                                                 ░  ║ └╥┘
«c0: 33/═════════════════════════════════════════════════════════════════════════════════════════════╩══╩═
«                                                  Gates fidelities: {'CNOT': 0.989021363408563}
Transpiled circuit qubit mapping: {0: 32, 1: 31}
Transpiled circuit number of measurement bases: 1