Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

# News

## v0.11.5 - dev

- Add `noisify` API with structured `NoiseModel` for inserting noise operations into circuits.

## v0.11.4 - 2026-05-05

- Add `DepolarizationNoise` for n-qubit depolarizing noise channels.
Expand Down
2 changes: 2 additions & 0 deletions src/QuantumClifford.jl
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ export
# Noise
applynoise!, UnbiasedUncorrelatedNoise, DepolarizationNoise, NoiseOp, NoiseOpAll, NoisyGate,
PauliNoise, PauliError,
# noisify
noisify, NoiseModel, NoNoise, DefaultNoiseModel,
# Pauli frames
PauliFrame, pftrajectories, pfmeasurements,
measurements,
Expand Down
83 changes: 83 additions & 0 deletions src/noise.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,89 @@ abstract type AbstractNoise end

abstract type AbstractNoiseOp <: AbstractOperation end

"""NoNoise is a noise model that represents the absence of noise."""
struct NoNoise <: AbstractNoise end

"""NoiseModel is a struct that holds the noise models for different types of operations in a circuit"""
Base.@kwdef struct NoiseModel
single_qubit::AbstractNoise = NoNoise()
two_qubit::AbstractNoise = NoNoise()
idle::AbstractNoise = NoNoise()
measurement::AbstractNoise = NoNoise()
reset::AbstractNoise = NoNoise()
end

NoiseModel(noise::AbstractNoise) = NoiseModel(
single_qubit = noise,
two_qubit = noise,
idle = NoNoise(),
measurement = noise,
reset = noise,
)

"""A NoiseModel with non-trivial defaults for every operation type"""
DefaultNoiseModel() = NoiseModel(
single_qubit = PauliNoise(1e-4, 1e-4, 1e-4),
two_qubit = PauliNoise(1e-3, 1e-3, 1e-3),
idle = PauliNoise(1e-5, 1e-5, 1e-5),
measurement = PauliNoise(2e-3, 2e-3, 2e-3),
reset = PauliNoise(2e-3, 2e-3, 2e-3),
)

"""_noise_ops is a helper function that generates the noise operations to be inserted before a given operation."""
_noise_ops(::NoNoise, qubits) = Any[]

function _noise_ops(noise::AbstractNoise, qubits)
isempty(qubits) && return Any[]
return [NoiseOp(noise, qubits)]
end


"""
noisify(circuit, noise; nqubits=nothing)

Return a new circuit with noise operations inserted before each operation according to `noise`.

`noise` can be either a bare `AbstractNoise` (applied uniformly to all gate / measurement / reset
operations) or a `NoiseModel` (which lets different operation types receive different noise).
When `nqubits` is provided, idle noise from `noise_model.idle` is also inserted on the qubits
not touched by each operation. An error is thrown when idle noise is requested without `nqubits`.

# Examples
noisify([sHadamard(1), sCNOT(1,2)], PauliNoise(1e-3, 1e-3, 1e-3))
noisify([sHadamard(1)], DefaultNoiseModel(); nqubits=3)
"""
function noisify(circuit::AbstractVector, noise::AbstractNoise; nqubits=nothing)
return noisify(circuit, NoiseModel(noise); nqubits=nqubits)
end

function noisify(circuit::AbstractVector, noise_model::NoiseModel; nqubits=nothing)
if nqubits === nothing
if !(noise_model.idle isa NoNoise)
error("nqubits must be provided to noisify when noise_model.idle is not NoNoise")
end
return mapreduce(g -> noisify(g, noise_model), vcat, circuit; init = Any[],)
else
return mapreduce(g -> noisify(g, noise_model, nqubits), vcat, circuit; init = Any[],)
end
end

# Fallback: leave unknown operations unchanged.
noisify(g, noise_model::NoiseModel) = [g]

noisify(g::AbstractSingleQubitOperator, noise_model::NoiseModel) = vcat(_noise_ops(noise_model.single_qubit, affectedqubits(g)), [g],)
noisify(g::AbstractTwoQubitOperator, noise_model::NoiseModel) = vcat(_noise_ops(noise_model.two_qubit, affectedqubits(g)), [g],)
noisify(g::AbstractMeasurement, noise_model::NoiseModel) = vcat(_noise_ops(noise_model.measurement, affectedqubits(g)),[g],)
noisify(g::AbstractResetMeasurement, noise_model::NoiseModel) = vcat(_noise_ops(noise_model.reset, affectedqubits(g)), [g],)

# Fallback for operations that do not have affectedqubits.
noisify(g, noise_model::NoiseModel, nqubits) = noisify(g, noise_model)

function noisify(g::AbstractOperation, noise_model::NoiseModel, nqubits)
idle_qubits = setdiff(1:nqubits, affectedqubits(g))
return vcat(_noise_ops(noise_model.idle, idle_qubits), noisify(g, noise_model),)
end

"""A method modifying a given state by applying the corresponding noise model. It is non-deterministic, part of the Noise interface."""
function applynoise! end

Expand Down
95 changes: 95 additions & 0 deletions test/test_noisify.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
@testitem "noisify" begin
using QuantumClifford

@testset "simple API" begin
result = noisify([sHadamard(1)], PauliNoise(1e-3, 1e-3, 1e-3))
@test length(result) == 2
@test result[2] == sHadamard(1)
end

@testset "errors when idle requested without nqubits" begin
@test_throws "nqubits must be provided" noisify(
[sHadamard(1)], DefaultNoiseModel()
)
end

@testset "noise inserted before op with correct qubits" begin
result = noisify([sCNOT(1, 2)], PauliNoise(1e-3, 1e-3, 1e-3))
@test length(result) == 2
@test result[1] isa NoiseOp
@test result[2] == sCNOT(1, 2)
@test affectedqubits(result[1]) == (1, 2)
end

@testset "unknown operations pass through unchanged" begin
op = ClassicalXOR((1, 2), 3)
result = noisify([op], PauliNoise(1e-3, 1e-3, 1e-3); nqubits=3)
@test length(result) == 1
@test result[1] === op
end
@testset "noisify dispatches NoiseModel fields correctly" begin
model = NoiseModel(
single_qubit = PauliNoise(0.01, 0.0, 0.0),
two_qubit = PauliNoise(0.0, 0.02, 0.0),
measurement = PauliNoise(0.0, 0.0, 0.03),
reset = PauliNoise(0.04, 0.04, 0.04),
idle = PauliNoise(0.05, 0.0, 0.0),
)

circuit = [
sHadamard(1),
sCNOT(1, 2),
sX(2),
sMZ(1),
sMRZ(3),
]

noisified = noisify(circuit, model; nqubits=3)
@test length(noisified) == 15

# sHadamard(1): idle on q2,q3; single-qubit noise on q1
@testset "sHadamard(1)" begin
@test noisified[1].noise == model.idle
@test noisified[1].indices == (2, 3)
@test noisified[2].noise == model.single_qubit
@test noisified[2].indices == (1,)
@test noisified[3] == circuit[1]
end

# sCNOT(1,2): idle on q3; two-qubit noise on q1,q2
@testset "sCNOT(1,2)" begin
@test noisified[4].noise == model.idle
@test noisified[4].indices == (3,)
@test noisified[5].noise == model.two_qubit
@test noisified[5].indices == (1, 2)
@test noisified[6] == circuit[2]
end

# sX(2): idle on q1,q3; single-qubit noise on q2
@testset "sX(2)" begin
@test noisified[7].noise == model.idle
@test noisified[7].indices == (1, 3)
@test noisified[8].noise == model.single_qubit
@test noisified[8].indices == (2,)
@test noisified[9] == circuit[3]
end

# sMZ(1): idle on q2,q3; measurement noise on q1
@testset "sMZ(1)" begin
@test noisified[10].noise == model.idle
@test noisified[10].indices == (2, 3)
@test noisified[11].noise == model.measurement
@test noisified[11].indices == (1,)
@test noisified[12] == circuit[4]
end

# sMRZ(3): idle on q1,q2; reset noise on q3
@testset "sMRZ(3)" begin
@test noisified[13].noise == model.idle
@test noisified[13].indices == (1, 2)
@test noisified[14].noise == model.reset
@test noisified[14].indices == (3,)
@test noisified[15] == circuit[5]
end
end
end
Loading