Skip to content

Noise

qliff.noise simulates noisy circuits inside the stabilizer formalism. Each channel is a quasiprobability mixture of stabilizer (Clifford) channels:

E()=μqμSμ().

A sampler runs many pure-Clifford trajectories and reweights each by sign(qμ)γ, where γ=μ|qμ| is the per-location overhead. Pauli channels have qμ0 summing to one, so γ=1. Coherent and non-unitary channels carry negative weights and γ>1. This is the method of arXiv:2512.07304.

You rarely touch a channel directly. Add noise to a Circuit, then call estimate or sample — the classes below are what those methods build.

Channel

Channel is an abstract base; subclasses expose a stabilizer-channel decomposition. A branch is a pair (weight, ops), where ops is a list of (gate, targets). The identity (no-fault) branch comes first.

Property/MethodDescription
is_pauliTrue if all weights are probabilities (records are sampleable)
branches(targets)the (weight, ops) decomposition for the given qubits
sample(targets, rng)draw one branch as (sign·gamma, ops)

Channel catalog

Each factory returns a configured Channel. The first five are Pauli (positive weights); the last two are general (signed weights, is_pauli = False).

ChannelCircuit methodArgumentsDescription
Depolarize(p)DEPOLARIZE1(q, p)pX,Y,Z each with probability p/3
Depolarize(p, twoq=True)DEPOLARIZE2(pair, p)pthe 15 two-qubit Paulis, each p/15
BitFlip(p)X_ERROR(q, p)pX with probability p
PhaseFlip(p)Z_ERROR(q, p)pZ with probability p
PauliChannel(px, py, pz)PAULI_CHANNEL_1(q, (px,py,pz))px,py,pzarbitrary 1-qubit Pauli channel (twoq=True → 2-qubit depolarizing)
Rotation(axis, theta)RZ(q, theta) / RX(q, theta)axis, θcoherent rotation eiθP/2
AmplitudeDamping(p)AMPLITUDE_DAMP(q, p)penergy decay |1|0

Amplitude damping

AmplitudeDamping(p) decomposes exactly over {I,Z,reset}:

qI=(1p)+1p2,qZ=(1p)1p2<0,qR=p.

The overhead γ=(1+p)1p21+p2 sits barely above the Pauli value of 1, with negativity η=13. So this non-unitary channel costs nearly as little to sample as Pauli noise. It is not a Pauli channel — estimate it with expect (estimate reweights automatically).

Sampler

Sampler wraps a circuit and runs trajectories. It has two methods.

MethodHandlesNotes
expect(obs, shots, seed=None, stratify=False)any channelimportance estimate, unbiased for any noise
sample(shots, seed=None)Pauli onlyuint8 array (shots, measurements); raises on a general channel

expect draws one branch per noise location and reweights each trajectory by sign(qμ)γ. On Pauli noise every weight is 1, so it is plain Monte-Carlo. With stratify=True it rewrites the estimate as F=kP(k)Fk: P(k) is a Poisson-binomial probability of k faulty locations, and Fk the conditional estimate. Since Fk varies slowly with k, this cuts variance sharply at the same shot budget.

python
from qliff import Circuit
from qliff.noise import Sampler

c = Circuit(1)
c.H(0).RZ(0, 0.3)

Sampler(c).expect("X", 20000)
Sampler(c).expect("X", 20000, stratify=True)

sample is Pauli-only: a measured bitstring cannot be reweighted by a negative quasiprobability, so a non-Pauli channel raises.

TIP

Prefer c.estimate(observable, shots). It uses flat importance sampling for Pauli circuits and stratifies otherwise, so you rarely build a Sampler by hand.

Custom channels

Subclass Channel, set is_pauli, and return the branches: an identity branch first, then (weight, ops) faults (weights may be negative quasiprobabilities). No Rust or recompilation needed. Drop it into a circuit with c.noise(channel, q).

python
from qliff import Circuit
from qliff.noise import Channel


class Dephase(Channel):
    is_pauli = True

    def __init__(self, p):
        self.p = p

    def branches(self, targets):
        q = targets[0]

        return [(1.0 - self.p, []), (self.p, [("Z", (q,))])]


c = Circuit(1)
c.H(0).noise(Dephase(0.1), 0)