From 60da00508f774622cbd9c7446b857b5e6ff03f9c Mon Sep 17 00:00:00 2001 From: Yi-Hsiu Yang <236533857+yi-hsiu-yang@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:43:40 +0800 Subject: [PATCH 1/4] Add noisify API with structured NoiseModel (issue #729) --- src/QuantumClifford.jl | 2 + src/noise.jl | 83 ++++++++++++++++++++++++++++++++++++++++++ test/test_noisify.jl | 28 ++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 test/test_noisify.jl diff --git a/src/QuantumClifford.jl b/src/QuantumClifford.jl index 39f2594c6..d1e3c437e 100644 --- a/src/QuantumClifford.jl +++ b/src/QuantumClifford.jl @@ -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, diff --git a/src/noise.jl b/src/noise.jl index 29e9815cf..58162a1e0 100644 --- a/src/noise.jl +++ b/src/noise.jl @@ -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 diff --git a/test/test_noisify.jl b/test/test_noisify.jl new file mode 100644 index 000000000..790dbf447 --- /dev/null +++ b/test/test_noisify.jl @@ -0,0 +1,28 @@ +@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 ErrorException 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)) + @test length(result) == 1 + @test result[1] === op + end +end \ No newline at end of file From 200c78050f4d3d9fb5d847391658612f0cf76a63 Mon Sep 17 00:00:00 2001 From: Yi-Hsiu Yang <236533857+yi-hsiu-yang@users.noreply.github.com> Date: Wed, 10 Jun 2026 18:00:46 +0800 Subject: [PATCH 2/4] Add CHANGELOG entry for noisify --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bcd1f662..649f1bda5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. From 4b621e84719e104d6f7af8df92e0ebd9680e9a70 Mon Sep 17 00:00:00 2001 From: Yi-Hsiu Yang <236533857+yi-hsiu-yang@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:29:16 +0800 Subject: [PATCH 3/4] Add tests for measurement, reset, and idle paths --- test/test_noisify.jl | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/test/test_noisify.jl b/test/test_noisify.jl index 790dbf447..5856c232d 100644 --- a/test/test_noisify.jl +++ b/test/test_noisify.jl @@ -21,8 +21,21 @@ @testset "unknown operations pass through unchanged" begin op = ClassicalXOR((1, 2), 3) - result = noisify([op], PauliNoise(1e-3, 1e-3, 1e-3)) + result = noisify([op], PauliNoise(1e-3, 1e-3, 1e-3); nqubits=3) @test length(result) == 1 @test result[1] === op end + @testset "structured noise model with all op types and idle" begin + nm = NoiseModel( + single_qubit = PauliNoise(1e-4, 1e-4, 1e-4), + two_qubit = PauliNoise(1e-3, 1e-3, 1e-3), + measurement = PauliNoise(2e-3, 2e-3, 2e-3), + reset = PauliNoise(5e-3, 5e-3, 5e-3), + idle = PauliNoise(1e-5, 1e-5, 1e-5), + ) + circuit = [sHadamard(1), sCNOT(1, 2), sMZ(2, 1), sMRZ(1, 2)] + result = noisify(circuit, nm; nqubits=3) + @test length(result) > length(circuit) + @test result[end] == sMRZ(1, 2) + end end \ No newline at end of file From 4e269300e4aa49aeb346e6093d0af7b66ce366ad Mon Sep 17 00:00:00 2001 From: Yi-Hsiu Yang <236533857+yi-hsiu-yang@users.noreply.github.com> Date: Fri, 12 Jun 2026 00:46:08 +0800 Subject: [PATCH 4/4] Improve noisify tests: verify dispatch routing and tighten error check --- test/test_noisify.jl | 78 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 12 deletions(-) diff --git a/test/test_noisify.jl b/test/test_noisify.jl index 5856c232d..fd20682b4 100644 --- a/test/test_noisify.jl +++ b/test/test_noisify.jl @@ -8,7 +8,9 @@ end @testset "errors when idle requested without nqubits" begin - @test_throws ErrorException noisify([sHadamard(1)], DefaultNoiseModel()) + @test_throws "nqubits must be provided" noisify( + [sHadamard(1)], DefaultNoiseModel() + ) end @testset "noise inserted before op with correct qubits" begin @@ -25,17 +27,69 @@ @test length(result) == 1 @test result[1] === op end - @testset "structured noise model with all op types and idle" begin - nm = NoiseModel( - single_qubit = PauliNoise(1e-4, 1e-4, 1e-4), - two_qubit = PauliNoise(1e-3, 1e-3, 1e-3), - measurement = PauliNoise(2e-3, 2e-3, 2e-3), - reset = PauliNoise(5e-3, 5e-3, 5e-3), - idle = PauliNoise(1e-5, 1e-5, 1e-5), + @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), sMZ(2, 1), sMRZ(1, 2)] - result = noisify(circuit, nm; nqubits=3) - @test length(result) > length(circuit) - @test result[end] == sMRZ(1, 2) + + 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 \ No newline at end of file