diff --git a/CHANGELOG.md b/CHANGELOG.md index c3de21653..345bc1c09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ## v0.10.1-dev +- Add decoding pipeline in `QECCore`. - The `TrivariateTricycleCode` is implemented using a novel realization via `Oscar.jl`'s multivariate polynomial quotient ring formalism in the ECC submodule. - The `GeneralizedToricCode` on twisted tori via `Oscar.jl`'s Laurent polynomials is now implemented in the ECC submodule. - The `HomologicalProductCode` and `DoubleHomologicalProductCode` are now implemented via `Oscar.jl`'s homological algebra in the ECC submodule. diff --git a/lib/QECCore/CHANGELOG.md b/lib/QECCore/CHANGELOG.md index 25f0a8dc3..f8cd0f767 100644 --- a/lib/QECCore/CHANGELOG.md +++ b/lib/QECCore/CHANGELOG.md @@ -2,6 +2,7 @@ ## v0.1.2 - dev +- Add decoding pipeline in `QECCore`. - Add novel `[[n² + m²,(n - rank([C ∣ M]))² + (m − rank([C ∣ M]ᵀ))², d]]` quantum Tillich-Zémor `random_TillichZemor_code` codes to `QECCore` and introduce `QECCoreNemoExt` for accurate matrix `rank` computation. - Introduce `metacheck_matrix_x`, `metacheck_matrix_z`, and `metacheck_matrix` for CSS codes built using chain complexes and homology. - Move the following codes from `QuantumClifford.ECC` to `QECCore`: `ReedMuller`, `RecursiveReedMuller`, `QuantumReedMuller`, `Hamming`, `Golay`, `Triangular488 `, `Triangular666 `, `Gottesman`. diff --git a/lib/QECCore/docs/src/decoding_pipeline.md b/lib/QECCore/docs/src/decoding_pipeline.md new file mode 100644 index 000000000..7a376a055 --- /dev/null +++ b/lib/QECCore/docs/src/decoding_pipeline.md @@ -0,0 +1,53 @@ +# Quantum Error Correction Decoding Interface + +## Overview + +`decoding_interface.jl` defines the core abstract interfaces for quantum error correction decoding systems, providing a modular framework for handling decoding problems of quantum codes. + +## Core Abstract Types + +### Problem Definition +- `AbstractDecodingProblem`: Abstract base class for decoding problems +- `AbstractNoiseModel`: Abstract base class for noise models +- `AbstractDecodingScenario`: Abstract base class for decoding scenarios + +### Sampling and Data +- `AbstractDecodingSamples`: Abstract base class for decoding samples +- `AbstractSampler`: Abstract base class for samplers +- `AbstractSyndrome`: Abstract base class for syndromes + +### Decoder +- `AbstractDecoder`: Abstract base class for decoders +- `AbstractDecodingResult`: Abstract base class for decoding results + +## Main Interface Functions + +### 1. Problem Generation +```julia +decoding_problem(code::AbstractCode, noise_model::AbstractNoiseModel, decoding_scenario::AbstractDecodingScenario) -> AbstractDecodingProblem +``` +Generate a decoding problem from a quantum code, noise model, and decoding scenario. + +### 2. Sampling +```julia +sample(problem::AbstractDecodingProblem, sampler::AbstractSampler) -> AbstractDecodingSamples +``` +Sample from the decoding problem to generate decoding samples. + +### 3. Syndrome Extraction +```julia +syndrome(samples::AbstractDecodingSamples) -> AbstractSyndrome +``` +Extract syndrome from sampled data. + +### 4. Decoding +```julia +decode(problem::AbstractDecodingProblem, syndrome::AbstractSyndrome, decoder::AbstractDecoder) -> AbstractDecodingResult +``` +Decode the syndrome using the decoder and return the decoding result. + +### 5. Error Rate Calculation +```julia +decoding_error_rate(problem::AbstractDecodingProblem, samples::AbstractDecodingSamples, decoding_result::AbstractDecodingResult) -> Float64 +``` +Calculate the error rate of the decoding result for performance evaluation. diff --git a/lib/QECCore/src/QECCore.jl b/lib/QECCore/src/QECCore.jl index 3e0dc5e3f..7009aa98f 100644 --- a/lib/QECCore/src/QECCore.jl +++ b/lib/QECCore/src/QECCore.jl @@ -14,6 +14,12 @@ rate, metacheck_matrix_x, metacheck_matrix_z, metacheck_matrix, bivariate_bicycl generator_polynomial export AbstractECC, AbstractQECC, AbstractCECC, AbstractCSSCode, AbstractDistanceAlg +# Decoding interfaces +export AbstractDecoder, AbstractDecodingResult, AbstractDecodingProblem, AbstractNoiseModel, AbstractDecodingScenario, decode, sample, decoding_error_rate, AbstractSyndrome, syndrome + +# DetectorModelProblem +export DetectorModelProblem, FactoredBitNoiseModel, depolarization_error_model, isvector, isindependent, IndependentVectorSampler, BitStringSamples, MatrixDecodingResult, MatrixSyndrome + # QEC Codes export Perfect5, Cleve8, Gottesman @@ -29,6 +35,9 @@ export RepCode, ReedMuller, RecursiveReedMuller, Golay, Hamming, GallagerLDPC, G export search_self_orthogonal_rm_codes include("interface.jl") +include("decoding_interface.jl") +include("decoding_impl.jl") + include("codes/util.jl") # Classical Codes diff --git a/lib/QECCore/src/decoding_impl.jl b/lib/QECCore/src/decoding_impl.jl new file mode 100644 index 000000000..1698e5cd7 --- /dev/null +++ b/lib/QECCore/src/decoding_impl.jl @@ -0,0 +1,189 @@ +""" + $TYPEDEF + +Represents a probabilistic error model for a system of bits using a factored +distribution approach. The model decomposes the joint probability distribution +over all bits into smaller, manageable distributions over subsets of bits. + +### Fields + $TYPEDFIELDS +""" +struct FactoredBitNoiseModel{VT<:AbstractArray{Float64}} <: AbstractNoiseModel + """The number of bits in the noise model.""" + num_bits::Int + """Dictionary defining the factored probability distributions. + + - **Keys**: `Vector{Int}` specifying which subset of bits the distribution covers + (e.g., `[1,3]` means this distribution covers bits 1 and 3) + - **Values**: `VT` multi-dimensional probability array where: + - Dimensionality = length of the key vector + - Each dimension has size 2 (representing classical bit states 0/1 or error/no-error) + - Values are non-negative probabilities summing to 1 + + Example: `[1,2] => [0.4 0.3; 0.2 0.1]` represents: + - P(bit1=0, bit2=0) = 0.4 + - P(bit1=0, bit2=1) = 0.3 + - P(bit1=1, bit2=0) = 0.2 + - P(bit1=1, bit2=1) = 0.1""" + probabilities::Dict{Vector{Int},VT} + function FactoredBitNoiseModel(num_bits::Int, probabilities::Dict{Vector{Int},VT}) where VT<:AbstractArray{Float64} + for (error_bits, prob_array) in probabilities + @assert maximum(error_bits) <= num_bits "Maximum element in error bits $error_bits is $(maximum(error_bits)), but num_bits is $num_bits" + expected_size = (fill(2, length(error_bits))...,) + actual_size = size(prob_array) + @assert size(prob_array) == expected_size "The dimension of the probability array must match the length of the error bits vector, and each dimension must be size 2. Expected: $expected_size, Got: $actual_size" + @assert sum(prob_array) == 1 "The sum of the probabilities must be 1, but got $(sum(prob_array))" + end + missing_variables = setdiff(1:num_bits, unique(vcat(keys(probabilities)...))) + @assert isempty(missing_variables) "The following variables are not used in the error model: $missing_variables" + new{VT}(num_bits, probabilities) + end +end + +""" + $TYPEDSIGNATURES + +Create an error model from a vector of error probabilities. The `i`-th element of the vector is the error probability of bit `i`. + +See also: [`depolarization_error_model`](@ref) +""" +function FactoredBitNoiseModel(probabilities::Vector{Float64}) + num_bits = length(probabilities) + probabilities = Dict([i] => [1.0 - probabilities[i], probabilities[i]] for i in 1:num_bits) + return FactoredBitNoiseModel(num_bits, probabilities) +end + +""" + $TYPEDSIGNATURES + +Create a depolarization error model from a vector of error probabilities. +The `i`-th element of the vector is the depolarization probability of qubit `i`. +""" +function depolarization_error_model(pvec::Vector{Float64}, qubit_num::Int) + @assert length(pvec) == qubit_num "The length of the vector of error probabilities must match the number of qubits" + probabilities = Dict([i, i + qubit_num] => [1.0-pvec[i] pvec[i]/3; pvec[i]/3 pvec[i]/3] for i in 1:qubit_num) + return FactoredBitNoiseModel(2 * qubit_num, probabilities) +end + +""" + $TYPEDSIGNATURES + +Create a depolarization error model from a depolarization probability. All qubits have the same depolarization probability. +""" +depolarization_error_model(p::Float64, qubit_num::Int) = depolarization_error_model(fill(p, qubit_num), qubit_num) + +""" + $TYPEDSIGNATURES + +Check if the error model is a vector error model. +""" +isvector(em::FactoredBitNoiseModel) = all(length(key) == 1 for key in keys(em.probabilities)) + +""" + $TYPEDSIGNATURES + +Check if the error model is an independent error model. Independent error model means that +""" +function isindependent(em::FactoredBitNoiseModel) + keys_list = collect(keys(em.probabilities)) + for i in 1:length(keys_list)-1 + for j in i+1:length(keys_list) + if !isempty(intersect(keys_list[i], keys_list[j])) + return false + end + end + end + return true +end + +"""`IndependentVectorSampler` is a simple sampler that only works on vector error model.""" +struct IndependentVectorSampler <: AbstractSampler end + +function sample(em::FactoredBitNoiseModel, num_samples::Int, sampler::IndependentVectorSampler) + @assert isvector(em) "The error model must be a vector error model" + return [rand() < em.probabilities[[i]][2] for i in 1:em.num_bits, _ in 1:num_samples] +end + +""" + $TYPEDEF + +Represents a quantum error correction decoding problem by combining an error +model with the code's stabilizer checks and logical operators. This structure +forms the complete specification needed to perform quantum error correction decoding. + +See also: [`FactoredBitErrorModel`](@ref) + +### Fields + $TYPEDFIELDS +""" +struct DetectorModelProblem{NMT<:AbstractNoiseModel} <: AbstractDecodingProblem + """Probabilistic error model for the bits""" + error_model::NMT + """Parity check matrix + - **Matrix dimensions**: [`num_checks` × `num_bits`] + - **Matrix elements**: Boolean (false=0, true=1) + - **Operation**: Syndrome = `check_matrix` × `error_vector` (mod 2)""" + check_matrix::Matrix{Bool} + """Logical operators + - **Matrix dimensions**: [`num_logicals` × `num_bits`] + - **Matrix elements**: Boolean (false=0, true=1)""" + logical_matrix::Matrix{Bool} + function DetectorModelProblem(error_model::NMT, check_matrix::Matrix{Bool}, logical_matrix::Matrix{Bool}) where NMT<:AbstractNoiseModel + @assert size(check_matrix, 2) == error_model.num_bits "The number of columns of the check matrix must match the number of bits in the error model" + @assert size(logical_matrix, 2) == error_model.num_bits "The number of columns of the logical matrix must match the number of bits in the error model" + new{NMT}(error_model, check_matrix, logical_matrix) + end +end + +""" + $TYPEDEF + +Represents a set of bit string samples. + +### Fields + $TYPEDFIELDS +""" +struct BitStringSamples <: AbstractDecodingSamples + """Physical bits""" + physical_bits::Matrix{Bool} + """Check bits""" + check_bits::Matrix{Bool} + """Logical bits""" + logical_bits::Matrix{Bool} +end + +function sample(problem::DetectorModelProblem{NMT}, num_samples::Int, sampler::IndependentVectorSampler) where NMT<:FactoredBitNoiseModel + physical_bits = sample(problem.error_model, num_samples, sampler) + check_bits = measure_syndrome(problem.check_matrix, physical_bits) + logical_bits = measure_syndrome(problem.logical_matrix, physical_bits) + return BitStringSamples(physical_bits, check_bits, logical_bits) +end + +function measure_syndrome(check_matrix::AbstractMatrix, error_patterns::AbstractMatrix) + return Bool.(mod.(check_matrix * error_patterns, 2)) +end +measure_syndrome(problem::DetectorModelProblem, error_patterns::Matrix{Bool}) = measure_syndrome(problem.check_matrix, error_patterns) + +struct MatrixSyndrome <: AbstractSyndrome + mat::Matrix{Bool} +end +syndrome(samples::BitStringSamples) = MatrixSyndrome(samples.check_bits) + +struct MatrixDecodingResult <: AbstractDecodingResult + mat::Matrix{Bool} +end +""" + $TYPEDSIGNATURES + +Check if the error pattern is a logical error. +""" +function decoding_error_rate(problem::DetectorModelProblem, samples::BitStringSamples, decoding_result::MatrixDecodingResult) + ep = Bool.(mod.(decoding_result.mat - samples.physical_bits, 2)) + return count(check_decoding_result(problem, ep)) / size(decoding_result.mat, 2) +end + +function check_decoding_result(problem::DetectorModelProblem,decoding_result_diff::AbstractMatrix{Bool}) + syndrome_test = any(measure_syndrome(problem.check_matrix, decoding_result_diff), dims=1) + logical_test = any(measure_syndrome(problem.logical_matrix, decoding_result_diff), dims=1) + return syndrome_test .|| logical_test +end diff --git a/lib/QECCore/src/decoding_interface.jl b/lib/QECCore/src/decoding_interface.jl new file mode 100644 index 000000000..f7cb86663 --- /dev/null +++ b/lib/QECCore/src/decoding_interface.jl @@ -0,0 +1,81 @@ +abstract type AbstractDecodingProblem end +abstract type AbstractNoiseModel end +abstract type AbstractDecodingScenario end + +""" + decoding_problem(code::AbstractCode, noise_model::AbstractNoiseModel, decoding_scenario::AbstractDecodingScenario) + +Generate a decoding problem from a code, a noise model, and a decoding scenario. + +### Inputs +- `code::AbstractCode`: The code to decode. +- `noise_model::AbstractNoiseModel`: The noise model to use. +- `decoding_scenario::AbstractDecodingScenario`: The decoding scenario to use. + +### Outputs +- `problem::AbstractDecodingProblem`: The decoding problem. +""" +function decoding_problem end + +abstract type AbstractDecodingSamples end +abstract type AbstractSampler end + +""" + sample(problem::AbstractDecodingProblem, sampler::AbstractSampler) + +Sample from the decoding problem. + +### Inputs +- `problem::AbstractDecodingProblem`: The decoding problem to sample from. +- `sampler::AbstractSampler`: The sampler to use. + +### Outputs +- `samples::AbstractDecodingSamples`: The sampled syndrome. +""" +function sample end + +abstract type AbstractSyndrome end +""" + syndrome(samples::AbstractDecodingSamples) + +Extract the syndrome from the samples. + +### Inputs +- `samples::AbstractDecodingSamples`: The sampled data. + +### Outputs +- `syndrome::AbstractSyndrome`: The extracted syndrome. +""" +function syndrome end + +abstract type AbstractDecoder end +abstract type AbstractDecodingResult end +""" + decode(problem::AbstractDecodingProblem, syndrome::AbstractSyndrome, decoder::AbstractDecoder) + +Decode the syndrome using the decoder. + +### Inputs +- `problem::AbstractDecodingProblem`: The decoding problem to decode. +- `syndrome::AbstractSyndrome`: The syndrome to decode. +- `decoder::AbstractDecoder`: The decoder to use. + +### Outputs +- `decoding_result::AbstractDecodingResult`: The decoded result. +""" +function decode end + +""" + decoding_error_rate(problem::AbstractDecodingProblem, samples::AbstractDecodingSamples, decoding_result::AbstractDecodingResult) + +Calculate the error rate of the decoding result. + +### Inputs +- `problem::AbstractDecodingProblem`: The decoding problem to validate. +- `syndrome::AbstractSyndrome`: The syndrome to validate. +- `decoding_result::AbstractDecodingResult`: The decoded result to validate. + +### Outputs +- `rate::Float64`: The error rate of the decoding result. +""" +function decoding_error_rate end diff --git a/lib/QECCore/test/test_decoding.jl b/lib/QECCore/test/test_decoding.jl new file mode 100644 index 000000000..57a6388d7 --- /dev/null +++ b/lib/QECCore/test/test_decoding.jl @@ -0,0 +1,95 @@ +@testitem "Decoding interfaces" begin + using QECCore + using Test + + @testset "FactoredBitNoiseModel" begin + em1 = FactoredBitNoiseModel([0.4,0.5]) + @test em1.probabilities == Dict([1] => [0.6,0.4], [2] => [0.5,0.5]) + @test em1.num_bits == 2 + @test isvector(em1) + @test isindependent(em1) + + em2 = FactoredBitNoiseModel(3, Dict([1,2] => [1.0 0.0;0.0 0.0], [3] => [0.0, 1.0])) + @test !isvector(em2) + @test isindependent(em2) + + em3 = FactoredBitNoiseModel(3, Dict([1,2] => [1.0 0.0;0.0 0.0], [2,3] => [1.0 0.0;0.0 0.0])) + @test !isvector(em3) + @test !isindependent(em3) + + ep4 = depolarization_error_model(0.1, 3) + @test ep4.probabilities == Dict([1,4] => [0.9 0.1/3;0.1/3 0.1/3], [2,5] => [0.9 0.1/3;0.1/3 0.1/3], [3,6] => [0.9 0.1/3;0.1/3 0.1/3]) + @test ep4.num_bits == 6 + @test !isvector(ep4) + @test isindependent(ep4) + + @test_throws AssertionError FactoredBitNoiseModel(3, Dict([1,2,3] => [0.6,0.4,0.5])) + @test_throws AssertionError FactoredBitNoiseModel(2, Dict([1] => [0.6,0.4,0.5],[2] => [0.5,0.5])) + @test_throws AssertionError FactoredBitNoiseModel(2, Dict([1] => [0.6,0.4],[3] => [0.5,0.5])) + @test_throws AssertionError FactoredBitNoiseModel(2, Dict([1] => [0.6,0.2],[2] => [0.5,0.5])) + @test_throws AssertionError FactoredBitNoiseModel(3, Dict([1] => [0.6,0.4],[3] => [0.5,0.5])) + end + + @testset "IndependentVectorSampler" begin + em = FactoredBitNoiseModel(fill(0.1, 100)) + ep = sample(em, 10000, IndependentVectorSampler()) + @test size(ep) == (100, 10000) + @test count(ep)/100/10000 ≈ 0.1 atol = 0.01 + end + + @testset "DetectorModelProblem" begin + c = Steane7() + pm = parity_matrix(c) + em = FactoredBitNoiseModel(fill(0.1, 14)) + logical_matrix = fill(false, 2, 14) + logical_matrix[1,1] = true + logical_matrix[2,1+7] = true + logical_matrix[1,2] = true + logical_matrix[2,2+7] = true + logical_matrix[1,3] = true + logical_matrix[2,3+7] = true + + problem = DetectorModelProblem(em, pm, logical_matrix) + + @test_throws AssertionError DetectorModelProblem(em, pm, fill(false, 2, 13)) + @test_throws AssertionError DetectorModelProblem(em, pm[:,1:13], logical_matrix) + @test_throws AssertionError DetectorModelProblem(FactoredBitNoiseModel(fill(0.1, 15)), pm, logical_matrix) + + em = FactoredBitNoiseModel(fill(0.3, 14)) + ep = sample(em, 10, IndependentVectorSampler()) + + syndrome = QECCore.measure_syndrome(problem, ep) + @test size(syndrome) == (6, 10) + + ep = fill(false, 14, 1) + ep[7] = true # Z error on qubit 7 + @test QECCore.measure_syndrome(problem, ep) == Bool[true; true; true; false; false; false;;] + + ep2 = copy(ep) + @test !(QECCore.check_decoding_result(problem, Bool.(mod.(ep2.-ep, 2)))[]) + + ep2[1] = true + @test (QECCore.check_decoding_result(problem, Bool.(mod.(ep2.-ep, 2)))[]) + + # applying a logical operator will lead to a logical error + ep[2] = true + ep[3] = true + @test (QECCore.check_decoding_result(problem, Bool.(mod.(ep2.-ep, 2)))[]) + ep2 = copy(ep) + ep2[8:10] .= true + @test (QECCore.check_decoding_result(problem, Bool.(mod.(ep2.-ep, 2)))[]) + + # applying a stabilizer will not lead to a logical error + ep2 = copy(ep) + ep2[4:6] .= true + ep2[7] = false + @test !(QECCore.check_decoding_result(problem, Bool.(mod.(ep2.-ep, 2)))[]) + + ep2 = copy(ep) + ep2[11:14] .= true + @test !(QECCore.check_decoding_result(problem, Bool.(mod.(ep2.-ep, 2)))[]) + + samples = sample(problem, 1000, IndependentVectorSampler()) + @test decoding_error_rate(problem, samples, MatrixDecodingResult(samples.physical_bits)) == 0.0 + end +end