Readout Post-Processing

This page explains how QAT’s granular readout pipeline transforms raw IQ acquisition data into state labels and numeric values. For shot-level filtering (pre-selection and post-selection), see Shot Selection (Pre & Post).

Pipeline order:

EqualiseDiscriminatePostSelectDemap

Overview

The data flow spans three layers:

  1. Hardware model — Qubits carry a configured discrimination method (LinearMapToRealMethod or MaxLikelihoodMethod).

  2. IR builder — Frontends emit granular readout instructions from that method.

  3. Runtime pass — Runtime applies instructions, builds masks, filters invalid shots, and formats final outputs.

Qubit.post_process_method
  └─ LinearMapToRealMethod | MaxLikelihoodMethod
           │
           ▼
measure_with_granular_post_processing()       ← frontend parsers call this
  ├─ MeasureBlock
  ├─ emit_granular_post_processing()
  │    └─ Equalise → Discriminate → Demap
  └─ emit_post_select()  (if disallowed states configured)
       └─ PostSelect (inserted before Demap)  → see shot_selection
           │
           ▼
AcquisitionPostprocessing pass (runtime)
  ├─ apply_equalise()
  ├─ apply_discriminate_instruction()
  ├─ apply_post_select() → validity_mask      → see shot_selection
  └─ apply_demap_instruction()
  │
  ├─ global_mask = AND of all per-output masks
  ├─ filter all result arrays by global_mask
  └─ store PostSelectionResult(shots_requested, shots_retained, mask)
           │
           ▼
ResultTransform
  └─ uses shots_retained as denominator for binary_count

Pipeline steps

Step 1 — Equalise

class Equalise(**data)

Bases: Instruction

Apply an affine transform in the IQ (complex) plane to readout data.

This is the first stage of the granular post-processing pipeline.

In superconducting qubit readout the downconverted IQ signal is distorted by three hardware imperfections: phase imbalance (LO quadrature paths not exactly 90° apart), gain imbalance (unequal I/Q amplifier chains), and DC offsets (mixer leakage and biases). As a result, raw (I, Q) samples cluster on a distorted, offset ellipse rather than a compact point cloud, degrading any downstream discriminator.

The Equalise instruction corrects all three imperfections in a single real affine transform:

\[\begin{split}\begin{pmatrix} I' \\ Q' \end{pmatrix} = A \begin{pmatrix} I \\ Q \end{pmatrix} + \begin{pmatrix} b_I \\ b_Q \end{pmatrix}\end{split}\]

where A is a real 2×2 matrix (transform) and [b_I, b_Q] is the real offset vector (offset). The output is returned as a complex value I' + j Q'.

Each Equalise instruction operates on a single readout channel. To equalise multiple channels, emit one instruction per channel with its own output_variable.

The default transform (2×2 identity) and default offset (zero vector) are a no-op pass-through for already-calibrated hardware.

Runtime implementation: qat.runtime.post_processing.apply_equalise().

Parameters:
  • output_variable – Variable name whose data should be transformed.

  • transform – Real (2, 2) matrix A.

  • offset – Real offset vector [b_I, b_Q], shape (2,). Defaults to the zero vector.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

model_config: ClassVar[ConfigDict] = {'extra': 'ignore', 'use_enum_values': False, 'validate_assignment': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Runtime implementation: apply_equalise().

Step 2 — Discriminate

class Discriminate(**data)

Bases: Instruction

Discriminate equalised values to string state labels.

For the linear-map path a sign-based threshold comparison is used: values above threshold → label "0", values at or below → label "1". For the maximum-likelihood path the nearest centroid in the complex plane determines the state’s configured string label (MLStateMap.label).

Exactly one of threshold or method must be provided.

Runtime implementation: qat.runtime.post_processing.apply_discriminate_instruction().

Parameters:
  • output_variable – Variable name whose data should be discriminated.

  • threshold – Scalar threshold for the linear-map discrimination path. None when method is provided.

  • method – Post-process method object for the ML path. None when threshold is provided.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

model_config: ClassVar[ConfigDict] = {'extra': 'ignore', 'use_enum_values': False, 'validate_assignment': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Runtime implementation: apply_discriminate_instruction().

Configuring classification methods

Configuration lives on Qubit via post_process_method.

Note

post_process_method and legacy mean_z_map_args are mutually exclusive.

class LinearMapToRealMethod(**data)

Bases: MethodBase

Threshold-based discriminator using a calibrated complex-to-real projection.

Projects each complex IQ readout onto a real value via:

\[v = \mathrm{Re}(a \cdot \mathrm{IQ} + b)\]

where a and b are the two entries of mean_z_map_args. The result is sign-classified: positive → state "0", non-positive → state "1".

Set disallowed_states to filter out shots by their classified state label (e.g. {"1"} to discard shots not in the ground state).

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

property declared_states: set[str]

All state labels known to this method.

Returns:

{"0", "1"} for a standard threshold discriminator.

disallowed_states: set[str]
mean_z_map_args: list[complex]
method: Literal[<MethodIndicator.LINEAR_MAP_COMPLEX_TO_REAL: 'linear_map_complex_to_real'>]
model_config: ClassVar[ConfigDict] = {'extra': 'forbid', 'ser_json_inf_nan': 'constants', 'use_enum_values': False, 'validate_assignment': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class MaxLikelihoodMethod(**data)

Bases: MethodBase

Maximum likelihood discriminator for qubit measurement results.

Each shot is assigned to the state k with the highest normalised likelihood:

\[\tilde{p}_k(z) = \frac{L_k(z)}{\sum_j L_j(z)}, \quad L_k(z) = \exp\!\left(-\frac{|z - \mathrm{loc}_k|^2}{2\,\nu}\right)\]

where \(\nu\) is the noise power (variance) noise_est.

Likelihoods are computed in log-domain with log-sum-exp stabilisation to avoid underflow on extreme outliers.

Outlier rejection — set p_min > 0 to automatically reject shots whose winning normalised likelihood falls below the threshold. Those shots are labelled BG_LABEL and discarded by PostSelect. Default p_min=0.0 disables the check entirely (zero overhead).

Disallowed states — individual states can be marked disallowed=True on MLStateMap for erasure checks and pre-selection; these are independent of p_min and handled by separate PostSelect instructions.

Runtime implementation: qat.runtime.post_processing.apply_discriminate_instruction().

Parameters:
  • states – Per-state IQ-plane centroids, labels and output values.

  • noise_est – Global Gaussian noise power (variance, \(\sigma^2\)). Must be strictly positive. Default 1.0.

  • p_min – Minimum normalised likelihood for acceptance. Must be in [0, 1].

  • transform – Real (2, 2) IQ affine pre-transform matrix A. If None (default), no Equalise step is emitted.

  • offset – Real offset vector [b_I, b_Q] for the affine pre-transform. If None (default), no Equalise step is emitted.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

property declared_states: set[str]

All state labels known to this method.

Includes labels from states and BG_LABEL when p_min > 0.

Returns:

Set of all recognised state label strings.

property disallowed_states: set[str]

State labels marked disallowed=True, plus BG_LABEL if p_min > 0.

Returns:

Set of disallowed state label strings.

method: Literal[<MethodIndicator.MAX_LIKELIHOOD: 'max_likelihood'>]
model_config: ClassVar[ConfigDict] = {'extra': 'forbid', 'ser_json_inf_nan': 'constants', 'use_enum_values': False, 'validate_assignment': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

noise_est: float
offset: Optional[Annotated[PydArray]]
p_min: float
states: list[MLStateMap]
transform: Optional[Annotated[PydArray]]
class MLStateMap(**data)

Demapping from a quantum state to its IQ-plane location and C-register output value.

Each MLStateMap entry represents one possible measurement outcome. The label is string routing key that threads through the discrimination pipeline (DiscriminatePostSelectDemap). The output_value is the final integer written to the classical register after Demap has been applied.

The disallowed field marks a state as invalid for use-cases such as erasure checks and pre-selection. Shots assigned to a disallowed=True state are subsequently removed by PostSelect.

Example — a standard qubit with states |0⟩ and |1⟩:

MLStateMap(label="0", output_value=0, location=1+0j)
MLStateMap(label="1", output_value=1, location=-1+0j)

Example — qutrit classifier that rejects the leakage state |2⟩:

MLStateMap(label="0", output_value=0, location=1+0j)
MLStateMap(label="1", output_value=1, location=-1+0j)
MLStateMap(label="2", output_value=2, location=0+1j, disallowed=True)

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

disallowed: bool
label: str
location: complex
model_config: ClassVar[ConfigDict] = {'extra': 'forbid', 'ser_json_inf_nan': 'constants', 'use_enum_values': False, 'validate_assignment': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

output_value: int

Example — attaching a method to a qubit:

from qat.model.device import Qubit
from qat.model.post_processing import LinearMapToRealMethod

qubit = Qubit(
    ...,
    mean_z_map_args=None,
    post_process_method=LinearMapToRealMethod(disallowed_states=["1"]),
)

Step 3 — PostSelect

PostSelect sits between Discriminate and Demap and marks shots for filtering based on discriminated state labels. For full details on post-selection and pre-selection, see Shot Selection (Pre & Post).

Step 4 — Demap

class Demap(**data)

Bases: Instruction

De-map string state labels to final integer output values.

This is the final stage of the granular post-processing pipeline. Each shot is mapped from a string state label (produced by Discriminate) to the configured integer output value written to the classical register.

Runtime implementation: qat.runtime.post_processing.apply_demap_instruction().

Parameters:
  • output_variable – Variable name whose state labels should be mapped.

  • state_map – Mapping from string state label to integer output value, e.g. {"0": 0, "1": 1} for the standard binary convention.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

model_config: ClassVar[ConfigDict] = {'extra': 'ignore', 'use_enum_values': False, 'validate_assignment': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

Runtime implementation: apply_demap_instruction().

Step 5 — Results format

ResultTransform formats final results by results_format.

raw()

Complex IQ arrays (equalised path output).

binary()

Per-shot mapped int values.

binary_count()

{label: count} from discriminated string labels (retained shots only).

Semantic matrix

Format

Selection off

Selection on (e.g. 3 of 10 filtered)

raw()

10 complex IQ values

7 complex IQ values

binary()

10 mapped int values

7 mapped int values

binary_count()

{"0": 6, "1": 4} over 10 shots

{"0": 4, "1": 3} over 7 retained shots

When results_format is None, dynamic structure return is used.

IR encoding

measure_with_granular_post_processing()

Frontend path (QASM2/QASM3/QIR/tket). Emits MeasureBlock, optional PostProcessing(MEAN, TIME) (SCOPE only), then granular instructions via emit_granular_post_processing().

measure_single_shot_z()

Customer-facing path. Emits legacy PostProcessing(LINEAR_MAP_COMPLEX_TO_REAL) and does not emit granular instructions.

Legacy PostProcessing instructions remain supported for backward compatibility.

Runtime execution

AcquisitionPostprocessing applies the full Equalise/Discriminate/PostSelect/Demap chain. See Shot Selection (Pre & Post) for details on mask construction and filtering.

See also