Skip to content

Noise-Aware Preprocessing

quprep.preprocess.noise_aware.NoiseProfile(qubit_error_rates, coupling_map, t1=None, t2=None, cx_error_rates=None) dataclass

Noise characteristics of a quantum backend.

Parameters:

Name Type Description Default
qubit_error_rates list of float

Per-qubit single-qubit gate or readout error rate (lower = better). Length must equal the number of physical qubits on the device.

required
coupling_map list of (int, int)

Available two-qubit connections. Each pair (i, j) means a native two-qubit gate can run directly between physical qubits i and j without an inserted SWAP. Pass an empty list for all-to-all devices (trapped-ion) or for single-qubit-only encodings.

required
t1 list of float

T1 relaxation times in microseconds, one per qubit. Longer is better.

None
t2 list of float

T2 dephasing times in microseconds, one per qubit. Longer is better.

None
cx_error_rates dict mapping (int, int) → float

Per-pair two-qubit (CX/CNOT) gate error rates. Stored for informational purposes; the qubit-assignment algorithm uses qubit_error_rates and T1/T2 as the primary quality signal.

None

Examples:

>>> profile = NoiseProfile(
...     qubit_error_rates=[0.001, 0.002, 0.003, 0.001, 0.002],
...     coupling_map=[(0, 1), (1, 2), (2, 3), (3, 4)],
...     t1=[150.0, 120.0, 180.0, 160.0, 140.0],
...     t2=[80.0, 70.0, 90.0, 85.0, 75.0],
... )
>>> profile.n_qubits
5

Attributes

n_qubits property

Number of physical qubits described by this profile.

Functions

qubit_score(qubit)

Combined quality score for a single qubit. Lower is better.

Combines gate/readout error rate with inverse coherence times so that shorter T1/T2 (noisier qubits) produce a higher (worse) score.

Parameters:

Name Type Description Default
qubit int

Physical qubit index.

required

Returns:

Type Description
float

Quality score. Qubits with lower scores should be preferred for high-variance features.

Source code in quprep/preprocess/noise_aware.py
def qubit_score(self, qubit: int) -> float:
    """
    Combined quality score for a single qubit.  **Lower is better.**

    Combines gate/readout error rate with inverse coherence times so
    that shorter T1/T2 (noisier qubits) produce a higher (worse) score.

    Parameters
    ----------
    qubit : int
        Physical qubit index.

    Returns
    -------
    float
        Quality score.  Qubits with lower scores should be preferred
        for high-variance features.
    """
    score = self.qubit_error_rates[qubit]
    if self.t1 is not None and self.t1[qubit] > 0:
        score += 1.0 / self.t1[qubit]
    if self.t2 is not None and self.t2[qubit] > 0:
        score += 1.0 / self.t2[qubit]
    return score

quprep.preprocess.noise_aware.NoiseAwarePreprocessor(noise_profile, encoding='angle', angle_deadzone=0.0)

Reorder dataset features for noise-aware qubit assignment.

Given a backend :class:NoiseProfile, this transformer:

  • assigns high-variance features to the least-noisy physical qubits,
  • reorders the selected qubits to form a path through the hardware coupling map (minimising SWAP overhead for entangled encodings), and
  • optionally remaps angle-encoded values away from 0 and π (requires data already normalised to [0, π]).

The output is a :class:~quprep.core.dataset.Dataset whose columns are reordered so that column i maps to the i-th qubit in :attr:qubit_assignment_. Downstream encoders that follow the standard feature i → logical qubit i convention will therefore automatically use the noise-optimised assignment.

Parameters:

Name Type Description Default
noise_profile NoiseProfile

Noise characteristics of the target backend.

required
encoding str

Target encoding name. Used to select the interaction graph for SWAP minimisation and to decide whether angle remapping applies. Recognised values: 'angle', 'entangled_angle', 'basis', 'amplitude', 'iqp', 'zz_feature_map', 'pauli_feature_map', 'reupload', 'tensor_product'. Unknown values are treated as single-qubit (no SWAP pressure).

'angle'
angle_deadzone float

Fraction of the encoding range to exclude at each pole. For example, 0.05 remaps data from [0, π] to [0.05π, 0.95π] for angle-family encodings, keeping all angles at least 5 % from the computational-basis poles. Must be in [0, 0.5). Default 0 (no remapping). Only applied when encoding uses rotation angles. The input must already be normalised to [0, π] for angle-family encodings (use Scaler('minmax_pi')) or to [0, 2π] for zz_feature_map (use Scaler('minmax_2pi')).

0.0

Attributes:

Name Type Description
permutation_ np.ndarray of int, shape (n_features,)

permutation_[i] is the original feature index placed at output column i. Available after :meth:fit.

qubit_assignment_ list of int, length n_features

qubit_assignment_[j] is the physical qubit assigned to original feature j. Available after :meth:fit.

estimated_swaps_before_ int

Estimated number of SWAP gates needed without topology optimisation. Available after :meth:fit.

estimated_swaps_after_ int

Estimated number of SWAP gates needed after topology optimisation. Always ≤ estimated_swaps_before_. Available after :meth:fit.

Examples:

>>> import numpy as np
>>> from quprep.core.dataset import Dataset
>>> from quprep.preprocess.noise_aware import NoiseAwarePreprocessor, NoiseProfile
>>>
>>> profile = NoiseProfile(
...     qubit_error_rates=[0.001, 0.005, 0.002, 0.003],
...     coupling_map=[(0, 1), (1, 2), (2, 3)],
... )
>>> rng = np.random.default_rng(0)
>>> data = rng.standard_normal((100, 3))
>>> data[:, 2] *= 5          # feature 2 has highest variance
>>> ds = Dataset(data=data, feature_names=["a", "b", "c"])
>>> prep = NoiseAwarePreprocessor(profile, encoding="entangled_angle")
>>> result = prep.fit_transform(ds)
>>> prep.qubit_assignment_[2]   # high-variance feature → low-error qubit
0
Source code in quprep/preprocess/noise_aware.py
def __init__(
    self,
    noise_profile: NoiseProfile,
    encoding: str = "angle",
    angle_deadzone: float = 0.0,
) -> None:
    if not 0.0 <= angle_deadzone < 0.5:
        raise ValueError(
            f"angle_deadzone must be in [0, 0.5), got {angle_deadzone}"
        )
    self.noise_profile = noise_profile
    self.encoding = encoding
    self.angle_deadzone = angle_deadzone
    self._fitted = False

    self.permutation_: np.ndarray | None = None
    self.qubit_assignment_: list[int] | None = None
    self.estimated_swaps_before_: int | None = None
    self.estimated_swaps_after_: int | None = None

Functions

fit(dataset)

Compute the noise-optimal feature permutation from dataset variances.

Parameters:

Name Type Description Default
dataset Dataset

Input dataset. Per-feature variances are computed from dataset.data.

required

Returns:

Type Description
NoiseAwarePreprocessor

Returns self for chaining.

Raises:

Type Description
ValueError

If the dataset has more features than qubits in the noise profile.

Source code in quprep/preprocess/noise_aware.py
def fit(self, dataset: Dataset) -> NoiseAwarePreprocessor:
    """
    Compute the noise-optimal feature permutation from dataset variances.

    Parameters
    ----------
    dataset : Dataset
        Input dataset.  Per-feature variances are computed from
        ``dataset.data``.

    Returns
    -------
    NoiseAwarePreprocessor
        Returns ``self`` for chaining.

    Raises
    ------
    ValueError
        If the dataset has more features than qubits in the noise profile.
    """
    n_feat = dataset.n_features
    n_qubits = self.noise_profile.n_qubits

    if n_feat > n_qubits:
        raise ValueError(
            f"Dataset has {n_feat} features — more features than qubits "
            f"({n_qubits}) in the noise profile.  Reduce the feature count "
            "first (e.g. with PCAReducer or HardwareAwareReducer)."
        )

    variances = np.var(dataset.data, axis=0)

    qubit_scores = np.array([
        self.noise_profile.qubit_score(q) for q in range(n_qubits)
    ])
    # Best n_feat qubits, ranked by ascending score (lowest noise first).
    best_qubits_naive: list[int] = np.argsort(qubit_scores)[:n_feat].tolist()

    # SWAP pressure only exists for encodings that insert 2-qubit gates.
    if self.encoding in self._ENTANGLED_ENCODINGS:
        self.estimated_swaps_before_ = self._count_adjacent_swaps(best_qubits_naive)
        if self.noise_profile.coupling_map:
            qubit_path = self._connectivity_path(best_qubits_naive, qubit_scores)
            self.estimated_swaps_after_ = self._count_adjacent_swaps(qubit_path)
        else:
            qubit_path = best_qubits_naive
            self.estimated_swaps_after_ = 0  # all-to-all / no topology = no SWAPs
    else:
        qubit_path = best_qubits_naive
        self.estimated_swaps_before_ = 0
        self.estimated_swaps_after_ = 0

    # Assign: position 0 in path (least noisy) ← feature with highest variance.
    feat_order = np.argsort(variances)[::-1]  # indices, highest variance first
    self.permutation_ = feat_order.copy()

    self.qubit_assignment_ = [0] * n_feat
    for position, feat_idx in enumerate(feat_order):
        self.qubit_assignment_[int(feat_idx)] = qubit_path[position]

    self._fitted = True
    return self

fit_transform(dataset)

Fit and transform in one call.

Source code in quprep/preprocess/noise_aware.py
def fit_transform(self, dataset: Dataset) -> Dataset:
    """Fit and transform in one call."""
    return self.fit(dataset).transform(dataset)

transform(dataset)

Reorder columns according to the fitted permutation.

Parameters:

Name Type Description Default
dataset Dataset

Input dataset. Must have the same feature count as the dataset used in :meth:fit.

required

Returns:

Type Description
Dataset

Dataset with columns reordered so that column i corresponds to qubit_assignment_[original_feature_i]. Metadata is updated with noise-aware routing information. If angle_deadzone > 0 and the encoding uses rotation angles, all values are linearly remapped from [0, π] to the interior [deadzone·π, (1−deadzone)·π].

Raises:

Type Description
NotFittedError

If :meth:fit has not been called.

ValueError

If the dataset's feature count differs from the fitted count.

Source code in quprep/preprocess/noise_aware.py
def transform(self, dataset: Dataset) -> Dataset:
    """
    Reorder columns according to the fitted permutation.

    Parameters
    ----------
    dataset : Dataset
        Input dataset.  Must have the same feature count as the dataset
        used in :meth:`fit`.

    Returns
    -------
    Dataset
        Dataset with columns reordered so that column *i* corresponds
        to ``qubit_assignment_[original_feature_i]``.  Metadata is
        updated with noise-aware routing information.  If
        ``angle_deadzone > 0`` and the encoding uses rotation angles,
        all values are linearly remapped from ``[0, π]`` to the
        interior ``[deadzone·π, (1−deadzone)·π]``.

    Raises
    ------
    sklearn.exceptions.NotFittedError
        If :meth:`fit` has not been called.
    ValueError
        If the dataset's feature count differs from the fitted count.
    """
    from sklearn.exceptions import NotFittedError

    if not self._fitted:
        raise NotFittedError(
            f"This {type(self).__name__} instance is not fitted yet. "
            "Call 'fit()' before 'transform()'."
        )

    n_feat = dataset.n_features
    if n_feat != len(self.permutation_):
        raise ValueError(
            f"Dataset has {n_feat} features but this transformer was "
            f"fitted on {len(self.permutation_)} features."
        )

    X = dataset.data[:, self.permutation_]

    if self.angle_deadzone > 0.0:
        if self.encoding in self._PI_ENCODINGS:
            lo = self.angle_deadzone * np.pi
            hi = (1.0 - self.angle_deadzone) * np.pi
            X = lo + (X / np.pi) * (hi - lo)
        elif self.encoding in self._TWO_PI_ENCODINGS:
            lo = self.angle_deadzone * 2.0 * np.pi
            hi = (1.0 - self.angle_deadzone) * 2.0 * np.pi
            X = lo + (X / (2.0 * np.pi)) * (hi - lo)
        elif self.encoding in self._PM_PI_ENCODINGS:
            X = X * (1.0 - 2.0 * self.angle_deadzone)

    feat_names = (
        [dataset.feature_names[int(i)] for i in self.permutation_]
        if dataset.feature_names
        else []
    )
    feat_types = (
        [dataset.feature_types[int(i)] for i in self.permutation_]
        if dataset.feature_types
        else []
    )

    meta = dict(dataset.metadata)
    meta.update({
        "noise_aware": True,
        "qubit_assignment": list(self.qubit_assignment_),
        "angle_deadzone": self.angle_deadzone,
        "encoding": self.encoding,
        "estimated_swaps_before": self.estimated_swaps_before_,
        "estimated_swaps_after": self.estimated_swaps_after_,
    })

    return Dataset(
        data=X,
        feature_names=feat_names,
        feature_types=feat_types,
        categorical_data=dict(dataset.categorical_data),
        metadata=meta,
        labels=dataset.labels,
    )