Skip to content

Noise-Aware Preprocessing

When running on real quantum hardware, not all qubits are equal. Gate error rates vary across the chip, coherence times differ by qubit, and two-qubit gates between non-adjacent qubits require inserted SWAP operations that add noise. NoiseAwarePreprocessor addresses all three issues before encoding begins.


Quick example

import numpy as np
import quprep as qd
from quprep.preprocess.noise_aware import NoiseProfile

# Describe your backend
profile = NoiseProfile(
    qubit_error_rates=[0.001, 0.003, 0.001, 0.002, 0.004],
    coupling_map=[(0, 1), (1, 2), (2, 3), (3, 4)],
    t1=[180.0, 120.0, 175.0, 160.0, 110.0],   # µs
    t2=[ 90.0,  65.0,  88.0,  80.0,  55.0],   # µs
)

prep = qd.NoiseAwarePreprocessor(profile, encoding="entangled_angle")

pipeline = qd.Pipeline(
    preprocessor=prep,
    encoder=qd.EntangledAngleEncoder(),
)
result = pipeline.fit_transform("data.csv")

After fit_transform, the dataset columns are reordered so the most informative features land on the quietest qubits, and adjacent logical qubits are physically connected on the chip.


NoiseProfile

NoiseProfile is a dataclass that describes the noise characteristics of a backend.

from quprep.preprocess.noise_aware import NoiseProfile

profile = NoiseProfile(
    qubit_error_rates=[0.001, 0.005, 0.002],   # required — one per qubit
    coupling_map=[(0, 1), (1, 2)],             # required — native 2Q connections
    t1=[150.0, 90.0, 160.0],                   # optional — relaxation times (µs)
    t2=[ 80.0, 45.0,  85.0],                   # optional — dephasing times (µs)
    cx_error_rates={(0, 1): 0.01, (1, 2): 0.012},  # optional — per-pair CX error
)

qubit_error_rates — Per-qubit single-qubit gate or readout error. The primary quality signal; lower is better.

coupling_map — Pairs of physically connected qubits. Any two-qubit gate between qubits not in this list requires SWAP insertion by the hardware compiler.

t1 / t2 — Coherence times in microseconds. Qubits with shorter coherence are penalised in the quality ranking. Omit if unavailable.

cx_error_rates — Per-pair CX error rates, stored for informational purposes.

Where to get these values: IBM Quantum / IQM / IonQ provider dashboards, or from qiskit_ibm_runtime.IBMBackend.properties().


Three optimisations

1 — Qubit assignment

Features are ranked by variance; qubits are ranked by a combined quality score (error rate + 1/T1 + 1/T2). The highest-variance feature is assigned to the lowest-score (least noisy) qubit, and so on.

This matters because high-variance features carry the most information. Placing them on noisy qubits wastes discriminative capacity.

prep.fit(dataset)

prep.qubit_assignment_
# e.g. [2, 0, 1] — feature 0 → qubit 2, feature 1 → qubit 0, feature 2 → qubit 1

2 — Topology-aware reordering

Applies to entangled encodings: entangled_angle, iqp, zz_feature_map, pauli_feature_map, reupload.

After selecting the best n qubits, the preprocessor greedily threads them into a path through the coupling map — so that adjacent logical qubits (which the encoder connects with CNOT/CZ gates) are physically adjacent on the chip. This eliminates or reduces compiler-inserted SWAPs.

prep.estimated_swaps_before_   # SWAPs with noise-ranked but topology-naive assignment
prep.estimated_swaps_after_    # SWAPs after topology path optimisation

For single-qubit encodings (angle, basis, amplitude) both estimates are 0 — there are no two-qubit gates.

3 — Angle dead-zone remapping

The rotation gates Ry(0) = |0⟩ and Ry(π) = |1⟩ produce computational-basis states with no superposition. Encoded angles near these poles are less discriminative and more sensitive to certain noise channels.

Set angle_deadzone to push all angles away from 0 and π:

prep = qd.NoiseAwarePreprocessor(
    profile,
    encoding="angle",
    angle_deadzone=0.05,   # maps [0, π] → [0.05π, 0.95π]
)

Normalise first

angle_deadzone remaps values within the encoder's target range. Apply the correct normaliser before NoiseAwarePreprocessor:

  • Angle-family encodings (angle, iqp, pauli_feature_map, …) → Scaler('minmax_pi')[0, π]
  • zz_feature_mapScaler('minmax_2pi')[0, 2π]

Standalone usage

NoiseAwarePreprocessor follows the sklearn fit / transform / fit_transform pattern and can be used outside a Pipeline:

from quprep.core.dataset import Dataset
from quprep.preprocess.noise_aware import NoiseAwarePreprocessor, NoiseProfile
import numpy as np

profile = NoiseProfile(
    qubit_error_rates=[0.001, 0.002, 0.003, 0.001],
    coupling_map=[(0, 1), (1, 2), (2, 3)],
)

rng = np.random.default_rng(0)
data = rng.standard_normal((200, 3))
data[:, 2] *= 4   # 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)

print(prep.qubit_assignment_)        # [2, 0, 1] — feature 2 (high-var) → qubit 0 (best)
print(prep.estimated_swaps_before_)  # SWAPs without topology opt
print(prep.estimated_swaps_after_)   # SWAPs after topology opt
print(result.metadata["noise_aware"])  # True

In a Pipeline

The preprocessor slot accepts a single transformer or a list:

pipeline = qd.Pipeline(
    preprocessor=[
        qd.WindowTransformer(window_size=8),   # modality-specific step first
        qd.NoiseAwarePreprocessor(profile, encoding="angle"),
    ],
    encoder=qd.AngleEncoder(),
)

When more features than qubits

If the dataset has more features than the noise profile has qubits, fit() raises a ValueError. Reduce first:

pipeline = qd.Pipeline(
    reducer=qd.HardwareAwareReducer(backend=5, encoding="angle"),  # cap at 5 features
    preprocessor=qd.NoiseAwarePreprocessor(profile, encoding="angle"),
    encoder=qd.AngleEncoder(),
)