From 4801b11a498b46241e05b6f5f32ad1ff67d93f5f Mon Sep 17 00:00:00 2001 From: Rabqubit Date: Fri, 27 Sep 2024 05:24:48 +0800 Subject: [PATCH 1/2] Lifted and lifted product codes via Hecke's GroupAlgebra (#356) --------- Co-authored-by: Stefan Krastanov Co-authored-by: hanakl --- CHANGELOG.md | 4 + Project.toml | 5 +- docs/Project.toml | 1 + docs/make.jl | 8 +- docs/src/ECC_API.md | 7 + docs/src/references.bib | 52 +++++ .../QuantumCliffordHeckeExt.jl | 19 ++ ext/QuantumCliffordHeckeExt/lifted.jl | 101 ++++++++++ ext/QuantumCliffordHeckeExt/lifted_product.jl | 187 ++++++++++++++++++ ext/QuantumCliffordHeckeExt/types.jl | 36 ++++ src/QuantumClifford.jl | 10 +- src/ecc/ECC.jl | 30 ++- src/ecc/codes/classical/lifted.jl | 10 + src/ecc/codes/lifted_product.jl | 19 ++ src/ecc/decoder_pipeline.jl | 8 +- test/Project.toml | 1 + test/test_ecc.jl | 4 +- test/test_ecc_base.jl | 45 ++++- test/test_ecc_codeproperties.jl | 5 +- test/test_ecc_decoder_all_setups.jl | 27 +++ test/test_ecc_encoding.jl | 2 +- test/test_ecc_syndromes.jl | 6 +- test/test_jet.jl | 2 + 23 files changed, 564 insertions(+), 25 deletions(-) create mode 100644 ext/QuantumCliffordHeckeExt/QuantumCliffordHeckeExt.jl create mode 100644 ext/QuantumCliffordHeckeExt/lifted.jl create mode 100644 ext/QuantumCliffordHeckeExt/lifted_product.jl create mode 100644 ext/QuantumCliffordHeckeExt/types.jl create mode 100644 src/ecc/codes/classical/lifted.jl create mode 100644 src/ecc/codes/lifted_product.jl diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ee7c838d..762c48595 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ - **(fix)** Bug fix to the `parity_checks(ReedMuller(r, m))` of classical Reed-Muller code (it was returning generator matrix). - `RecursiveReedMuller` code implementation as an alternative implementation of `ReedMuller`. +## v0.9.10 - 2024-09-26 + +- **(fix)** `ECC.code_s` now gives the number of parity checks with redundancy. If you want the number of linearly independent parity checks, you can use `LinearAlgebra.rank`. + ## v0.9.9 - 2024-08-05 - `inv` is implemented for all Clifford operator types (symbolic, dense, sparse). diff --git a/Project.toml b/Project.toml index a5dc91bfb..802c43685 100644 --- a/Project.toml +++ b/Project.toml @@ -24,6 +24,7 @@ SumTypes = "8e1ec7a9-0e02-4297-b0fe-6433085c89f2" [weakdeps] CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +Hecke = "3e1990a7-5d81-5526-99ce-9ba3ff248f21" LDPCDecoders = "3c486d74-64b9-4c60-8b1a-13a564e77efb" Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" @@ -33,6 +34,7 @@ QuantumOpticsBase = "4f57444f-1401-5e15-980d-4471b28d5678" [extensions] QuantumCliffordGPUExt = "CUDA" +QuantumCliffordHeckeExt = "Hecke" QuantumCliffordLDPCDecodersExt = "LDPCDecoders" QuantumCliffordMakieExt = "Makie" QuantumCliffordPlotsExt = "Plots" @@ -46,6 +48,7 @@ Combinatorics = "1.0" DataStructures = "0.18" DocStringExtensions = "0.9" Graphs = "1.9" +Hecke = "0.28, 0.29, 0.30, 0.31, 0.32, 0.33" HostCPUFeatures = "0.1.6" ILog2 = "0.2.3" InteractiveUtils = "1.9" @@ -53,7 +56,7 @@ LDPCDecoders = "0.3.1" LinearAlgebra = "1.9" MacroTools = "0.5.9" Makie = "0.20, 0.21" -Nemo = "0.42, 0.43, 0.44, 0.45, 0.46" +Nemo = "^0.42.1, 0.43, 0.44, 0.45, 0.46" Plots = "1.38.0" PrecompileTools = "1.2" PyQDecoders = "0.2.1" diff --git a/docs/Project.toml b/docs/Project.toml index b414301a3..8ef02232a 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -5,6 +5,7 @@ Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" DocumenterCitations = "daee34ce-89f3-4625-b898-19384cb65244" GraphMakie = "1ecd5474-83a3-4783-bb4f-06765db800d2" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" +Hecke = "3e1990a7-5d81-5526-99ce-9ba3ff248f21" LDPCDecoders = "3c486d74-64b9-4c60-8b1a-13a564e77efb" Nemo = "2edaba10-b0f1-5616-af89-8c11ac63239a" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" diff --git a/docs/make.jl b/docs/make.jl index 699924cda..b8d13fc30 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -7,6 +7,11 @@ using QuantumClifford using QuantumClifford.Experimental.NoisyCircuits using QuantumInterface +ENV["HECKE_PRINT_BANNER"] = "false" +import Hecke + +const QuantumCliffordHeckeExt = Base.get_extension(QuantumClifford, :QuantumCliffordHeckeExt) + #DocMeta.setdocmeta!(QuantumClifford, :DocTestSetup, :(using QuantumClifford); recursive=true) ENV["LINES"] = 80 # for forcing `displaysize(io)` to be big enough @@ -20,8 +25,9 @@ doctest = false, clean = true, sitename = "QuantumClifford.jl", format = Documenter.HTML(size_threshold_ignore = ["API.md"]), -modules = [QuantumClifford, QuantumClifford.Experimental.NoisyCircuits, QuantumClifford.ECC, QuantumInterface], +modules = [QuantumClifford, QuantumClifford.Experimental.NoisyCircuits, QuantumClifford.ECC, QuantumInterface, QuantumCliffordHeckeExt], warnonly = [:missing_docs], +linkcheck = true, authors = "Stefan Krastanov", pages = [ "QuantumClifford.jl" => "index.md", diff --git a/docs/src/ECC_API.md b/docs/src/ECC_API.md index d77b26097..c400a2bf4 100644 --- a/docs/src/ECC_API.md +++ b/docs/src/ECC_API.md @@ -4,3 +4,10 @@ Modules = [QuantumClifford.ECC] Private = false ``` + +## Implemented in an extension requiring `Hecke.jl` + +```@autodocs +Modules = [QuantumCliffordHeckeExt] +Private = true +``` \ No newline at end of file diff --git a/docs/src/references.bib b/docs/src/references.bib index 6a50a9194..1cbde2910 100644 --- a/docs/src/references.bib +++ b/docs/src/references.bib @@ -402,3 +402,55 @@ @inproceedings{brown2013short pages = {346--350}, doi = {10.1109/ISIT.2013.6620245} } + +@article{panteleev2021degenerate, + title = {Degenerate {{Quantum LDPC Codes With Good Finite Length Performance}}}, + author = {Panteleev, Pavel and Kalachev, Gleb}, + year = {2021}, + month = nov, + journal = {Quantum}, + volume = {5}, + eprint = {1904.02703}, + primaryclass = {quant-ph}, + pages = {585}, + issn = {2521-327X}, + doi = {10.22331/q-2021-11-22-585}, + archiveprefix = {arXiv} +} + + +@inproceedings{panteleev2022asymptotically, + title = {Asymptotically Good {{Quantum}} and Locally Testable Classical {{LDPC}} Codes}, + booktitle = {Proceedings of the 54th {{Annual ACM SIGACT Symposium}} on {{Theory}} of {{Computing}}}, + author = {Panteleev, Pavel and Kalachev, Gleb}, + year = {2022}, + month = jun, + pages = {375--388}, + publisher = {ACM}, + address = {Rome Italy}, + doi = {10.1145/3519935.3520017}, + isbn = {978-1-4503-9264-8} +} + +@article{roffe2023bias, + title = {Bias-Tailored Quantum {{LDPC}} Codes}, + author = {Roffe, Joschka and Cohen, Lawrence Z. and Quintavalle, Armanda O. and Chandra, Daryus and Campbell, Earl T.}, + year = {2023}, + month = may, + journal = {Quantum}, + volume = {7}, + pages = {1005}, + doi = {10.22331/q-2023-05-15-1005} +} + +@article{raveendran2022finite, + title = {Finite {{Rate QLDPC-GKP Coding Scheme}} That {{Surpasses}} the {{CSS Hamming Bound}}}, + author = {Raveendran, Nithin and Rengaswamy, Narayanan and Rozp{\k e}dek, Filip and Raina, Ankur and Jiang, Liang and Vasi{\'c}, Bane}, + year = {2022}, + month = jul, + journal = {Quantum}, + volume = {6}, + pages = {767}, + issn = {2521-327X}, + doi = {10.22331/q-2022-07-20-767}, +} diff --git a/ext/QuantumCliffordHeckeExt/QuantumCliffordHeckeExt.jl b/ext/QuantumCliffordHeckeExt/QuantumCliffordHeckeExt.jl new file mode 100644 index 000000000..354de8a69 --- /dev/null +++ b/ext/QuantumCliffordHeckeExt/QuantumCliffordHeckeExt.jl @@ -0,0 +1,19 @@ +module QuantumCliffordHeckeExt + +using DocStringExtensions + +import QuantumClifford, LinearAlgebra +import Hecke: Group, GroupElem, AdditiveGroup, AdditiveGroupElem, + GroupAlgebra, GroupAlgebraElem, FqFieldElem, representation_matrix, dim, base_ring, + multiplication_table, coefficients, abelian_group, group_algebra +import Nemo: characteristic, matrix_repr, GF, ZZ + +import QuantumClifford.ECC: AbstractECC, CSS, ClassicalCode, + hgp, code_k, code_n, code_s, iscss, parity_checks, parity_checks_x, parity_checks_z, parity_checks_xz, + two_block_group_algebra_codes, generalized_bicycle_codes, bicycle_codes + +include("types.jl") +include("lifted.jl") +include("lifted_product.jl") + +end # module diff --git a/ext/QuantumCliffordHeckeExt/lifted.jl b/ext/QuantumCliffordHeckeExt/lifted.jl new file mode 100644 index 000000000..6601ce236 --- /dev/null +++ b/ext/QuantumCliffordHeckeExt/lifted.jl @@ -0,0 +1,101 @@ +""" +$TYPEDEF + +Classical codes lifted over a group algebra, used for lifted product code construction ([panteleev2021degenerate](@cite), [panteleev2022asymptotically](@cite)) + +The parity-check matrix is constructed by applying `repr` to each element of `A`, +which is mathematically a linear map from a group algebra element to a binary matrix. +The size of the parity check matrix will enlarged with each element of `A` being inflated into a matrix. +The procedure is called a lift [panteleev2022asymptotically](@cite). + +## Constructors + +A lifted code can be constructed via the following approaches: + +1. A matrix of group algebra elements. + +2. A matrix of group elements, where a group element will be considered as a group algebra element by assigning a unit coefficient. + +3. A matrix of integers, where each integer represent the shift of a cyclic permutation. The order of the cyclic permutation should be specified. + +The default `GA` is the group algebra of `A[1, 1]`, the default representation `repr` is the permutation representation. + +## The representation function `repr` + +In this struct, we use the default representation function `default_repr` to convert a `GF(2)`-group algebra element to a binary matrix. +The default representation, provided by `Hecke`, is the permutation representation. + +We also accept a custom representation function. +Such a customization would be useful to reduce the number of bits required by the code construction. + +For example, if we use a D4 group for lifting, our default representation will be `8×8` permutation matrices, +where 8 is the group's order. +However, we can find a `4×4` matrix representation for the group, +e.g. by using the typical [`2×2` representation](https://en.wikipedia.org/wiki/Dihedral_group) +and converting it into binary representation by replacing "1" with the Pauli I, and "-1" with the Pauli X matrix. + +See also: [`LPCode`](@ref). + +$TYPEDFIELDS +""" +struct LiftedCode <: ClassicalCode + """the base matrix of the code, whose elements are in a group algebra.""" + A::GroupAlgebraElemMatrix + """the group algebra for which elements in `A` are from.""" + GA::GroupAlgebra + """ + a function that converts a group algebra element to a binary matrix; + default to be the permutation representation for GF(2)-algebra.""" + repr::Function + + function LiftedCode(A::GroupAlgebraElemMatrix; GA::GroupAlgebra=parent(A[1, 1]), repr::Function) + all(elem.parent == GA for elem in A) || error("The base ring of all elements in the code must be the same as the group algebra") + new(A, GA, repr) + end +end + +default_repr(y::GroupAlgebraElem{FqFieldElem, <: GroupAlgebra}) = Matrix((x -> Bool(Int(lift(ZZ, x)))).(representation_matrix(y))) + +""" +`LiftedCode` constructor using the default `GF(2)` representation (coefficients converted to a permutation matrix by `representation_matrix` provided by Hecke). +""" # TODO doctest example +function LiftedCode(A::Matrix{GroupAlgebraElem{FqFieldElem, <: GroupAlgebra}}; GA::GroupAlgebra=parent(A[1,1])) + !(characteristic(base_ring(A[1, 1])) == 2) && error("The default permutation representation applies only to GF(2) group algebra; otherwise, a custom representation function should be provided") + LiftedCode(A; GA=GA, repr=default_repr) +end + +# TODO document and doctest example +function LiftedCode(group_elem_array::Matrix{<: GroupOrAdditiveGroupElem}; GA::GroupAlgebra=group_algebra(GF(2), parent(group_elem_array[1,1])), repr::Union{Function, Nothing}=nothing) + A = zeros(GA, size(group_elem_array)...) + for i in axes(group_elem_array, 1), j in axes(group_elem_array, 2) + A[i, j] = GA[A[i, j]] + end + if repr === nothing + return LiftedCode(A; GA=GA, repr=default_repr) + else + return LiftedCode(A; GA=GA, repr=repr) + end +end + +# TODO document and doctest example +function LiftedCode(shift_array::Matrix{Int}, l::Int; GA::GroupAlgebra=group_algebra(GF(2), abelian_group(l))) + A = zeros(GA, size(shift_array)...) + for i in 1:size(shift_array, 1) + for j in 1:size(shift_array, 2) + A[i, j] = GA[shift_array[i, j]%l+1] + end + end + return LiftedCode(A; GA=GA, repr=default_repr) +end + +function lift(repr::Function, mat::GroupAlgebraElemMatrix) + vcat([hcat([repr(mat[i, j]) for j in axes(mat, 2)]...) for i in axes(mat, 1)]...) +end + +function parity_checks(c::LiftedCode) + return lift(c.repr, c.A) +end + +code_n(c::LiftedCode) = size(c.A, 2) * size(zero(c.GA), 2) + +code_s(c::LiftedCode) = size(c.A, 1) * size(zero(c.GA), 1) diff --git a/ext/QuantumCliffordHeckeExt/lifted_product.jl b/ext/QuantumCliffordHeckeExt/lifted_product.jl new file mode 100644 index 000000000..aecad36b3 --- /dev/null +++ b/ext/QuantumCliffordHeckeExt/lifted_product.jl @@ -0,0 +1,187 @@ +""" +$TYPEDEF + +Lifted product codes ([panteleev2021degenerate](@cite), [panteleev2022asymptotically](@cite)) + +A lifted product code is defined by the hypergraph product of a base matrices `A` and the conjugate of another base matrix `B'`. +Here, the hypergraph product is taken over a group algebra, of which the base matrices are consisting. + +The binary parity check matrix is obtained by applying `repr` to each element of the matrix resulted from the hypergraph product, which is mathematically a linear map from each group algebra element to a binary matrix. + +## Constructors + +Multiple constructors are available: + +1. Two base matrices of group algebra elements. + +2. Two lifted codes, whose base matrices are for quantum code construction. + +3. Two base matrices of group elements, where each group element will be considered as a group algebra element by assigning a unit coefficient. + +4. Two base matrices of integers, where each integer represent the shift of a cyclic permutation. The order of the cyclic permutation should be specified. + +## Examples + +A [[882, 24, d ≤ 24]] code from Appendix B of [roffe2023bias](@cite). +We use the 1st constructor to generate the code and check its length and dimension. +During the construction, we do arithmetic operations to get the group algebra elements in base matrices `A` and `B`. +Here `x` is the generator of the group algebra, i.e., offset-1 cyclic permutation, and `GA(1)` is the unit element. + +```jldoctest +julia> import Hecke: group_algebra, GF, abelian_group, gens; import LinearAlgebra: diagind; + +julia> l = 63; GA = group_algebra(GF(2), abelian_group(l)); x = gens(GA)[]; + +julia> A = zeros(GA, 7, 7); + +julia> A[diagind(A)] .= x^27; + +julia> A[diagind(A, -1)] .= x^54; + +julia> A[diagind(A, 6)] .= x^54; + +julia> A[diagind(A, -2)] .= GA(1); + +julia> A[diagind(A, 5)] .= GA(1); + +julia> B = reshape([1 + x + x^6], (1, 1)); + +julia> c1 = LPCode(A, B); + +julia> code_n(c1), code_k(c1) +(882, 24) +``` + +A [[175, 19, d ≤ 0]] code from Eq. (18) in Appendix A of [raveendran2022finite](@cite), +following the 4th constructor. + +```jldoctest +julia> base_matrix = [0 0 0 0; 0 1 2 5; 0 6 3 1]; l = 7; + +julia> c2 = LPCode(base_matrix, l .- base_matrix', l); + +julia> code_n(c2), code_k(c2) +(175, 19) +``` + +## Code subfamilies and convenience constructors for them + +- When the base matrices of the `LPCode` are 1×1, the code is called a two-block group-algebra code [`two_block_group_algebra_codes`](@ref). +- When the base matrices of the `LPCode` are 1×1 and their elements are sums of cyclic permutations, the code is called a generalized bicycle code [`generalized_bicycle_codes`](@ref). +- When the two matrices are adjoint to each other, the code is called a bicycle code [`bicycle_codes`](@ref). + +## The representation function + +In this struct, we use the default representation function `default_repr` to convert a `GF(2)`-group algebra element to a binary matrix. +The default representation, provided by `Hecke`, is the permutation representation. + +We also accept a custom representation function as detailed in [`LiftedCode`](@ref). + +See also: [`LiftedCode`](@ref), [`two_block_group_algebra_codes`](@ref), [`generalized_bicycle_codes`](@ref), [`bicycle_codes`](@ref). + +$TYPEDFIELDS +""" +struct LPCode <: AbstractECC + """the first base matrix of the code, whose elements are in a group algebra.""" + A::GroupAlgebraElemMatrix + """the second base matrix of the code, whose elements are in the same group algebra as `A`.""" + B::GroupAlgebraElemMatrix + """the group algebra for which elements in `A` and `B` are from.""" + GA::GroupAlgebra + """ + a function that converts a group algebra element to a binary matrix; + default to be the permutation representation for GF(2)-algebra.""" + repr::Function + + function LPCode(A::GroupAlgebraElemMatrix, B::GroupAlgebraElemMatrix; GA::GroupAlgebra=parent(A[1,1]), repr::Function) + all(elem.parent == GA for elem in A) && all(elem.parent == GA for elem in B) || error("The base rings of all elements in both matrices must be the same as the group algebra") + new(A, B, GA, repr) + end + + function LPCode(c₁::LiftedCode, c₂::LiftedCode; GA::GroupAlgebra=c₁.GA, repr::Function=c₁.repr) + # we are using the group algebra and the representation function of the first lifted code + c₁.GA == GA && c₂.GA == GA || error("The base rings of both lifted codes must be the same as the group algebra") + new(c₁.A, c₂.A, GA, repr) + end +end + +# TODO document and doctest example +function LPCode(A::FqFieldGroupAlgebraElemMatrix, B::FqFieldGroupAlgebraElemMatrix; GA::GroupAlgebra=parent(A[1,1])) + LPCode(LiftedCode(A; GA=GA, repr=default_repr), LiftedCode(B; GA=GA, repr=default_repr); GA=GA, repr=default_repr) +end + +# TODO document and doctest example +function LPCode(group_elem_array1::Matrix{<: GroupOrAdditiveGroupElem}, group_elem_array2::Matrix{<: GroupOrAdditiveGroupElem}; GA::GroupAlgebra=group_algebra(GF(2), parent(group_elem_array1[1,1]))) + LPCode(LiftedCode(group_elem_array1; GA=GA), LiftedCode(group_elem_array2; GA=GA); GA=GA, repr=default_repr) +end + +# TODO document and doctest example +function LPCode(shift_array1::Matrix{Int}, shift_array2::Matrix{Int}, l::Int; GA::GroupAlgebra=group_algebra(GF(2), abelian_group(l))) + LPCode(LiftedCode(shift_array1, l; GA=GA), LiftedCode(shift_array2, l; GA=GA); GA=GA, repr=default_repr) +end + +iscss(::Type{LPCode}) = true + +function parity_checks_xz(c::LPCode) + hx, hz = hgp(c.A, c.B') + hx, hz = lift(c.repr, hx), lift(c.repr, hz) + return hx, hz +end + +parity_checks_x(c::LPCode) = parity_checks_xz(c)[1] + +parity_checks_z(c::LPCode) = parity_checks_xz(c)[2] + +parity_checks(c::LPCode) = parity_checks(CSS(parity_checks_xz(c)...)) + +code_n(c::LPCode) = size(c.repr(zero(c.GA)), 2) * (size(c.A, 2) * size(c.B, 1) + size(c.A, 1) * size(c.B, 2)) + +code_s(c::LPCode) = size(c.repr(zero(c.GA)), 1) * (size(c.A, 1) * size(c.B, 1) + size(c.A, 2) * size(c.B, 2)) + +""" +Two-block group algebra (2GBA) codes, which are a special case of lifted product codes +from two group algebra elements `a` and `b`, used as `1x1` base matrices. + +See also: [`LPCode`](@ref), [`generalized_bicycle_codes`](@ref), [`bicycle_codes`](@ref) +""" # TODO doctest example +function two_block_group_algebra_codes(a::GroupAlgebraElem, b::GroupAlgebraElem) + A = reshape([a], (1, 1)) + B = reshape([b], (1, 1)) + LPCode(A, B) +end + +""" +Generalized bicycle codes, which are a special case of 2GBA codes (and therefore of lifted product codes). +Here the group is chosen as the cyclic group of order `l`, +and the base matrices `a` and `b` are the sum of the group algebra elements corresponding to the shifts `a_shifts` and `b_shifts`. + +See also: [`two_block_group_algebra_codes`](@ref), [`bicycle_codes`](@ref). + +A [[254, 28, 14 ≤ d ≤ 20]] code from (A1) in Appendix B of [panteleev2021degenerate](@cite). + +```jldoctest +julia> c = generalized_bicycle_codes([0, 15, 20, 28, 66], [0, 58, 59, 100, 121], 127); + +julia> code_n(c), code_k(c) +(254, 28) +``` +""" # TODO doctest example +function generalized_bicycle_codes(a_shifts::Array{Int}, b_shifts::Array{Int}, l::Int) + GA = group_algebra(GF(2), abelian_group(l)) + a = sum(GA[n%l+1] for n in a_shifts) + b = sum(GA[n%l+1] for n in b_shifts) + two_block_group_algebra_codes(a, b) +end + +""" +Bicycle codes are a special case of generalized bicycle codes, +where `a` and `b` are conjugate to each other. +The order of the cyclic group is `l`, and the shifts `a_shifts` and `b_shifts` are reverse to each other. + +See also: [`two_block_group_algebra_codes`](@ref), [`generalized_bicycle_codes`](@ref). +""" # TODO doctest example +function bicycle_codes(a_shifts::Array{Int}, l::Int) + GA = group_algebra(GF(2), abelian_group(l)) + a = sum(GA[n÷l+1] for n in a_shifts) + two_block_group_algebra_codes(a, a') +end diff --git a/ext/QuantumCliffordHeckeExt/types.jl b/ext/QuantumCliffordHeckeExt/types.jl new file mode 100644 index 000000000..52ccb70dd --- /dev/null +++ b/ext/QuantumCliffordHeckeExt/types.jl @@ -0,0 +1,36 @@ +const GroupOrAdditiveGroupElem = Union{GroupElem,AdditiveGroupElem} + +const GroupAlgebraElemMatrix = Union{ + Matrix{<:GroupAlgebraElem}, + LinearAlgebra.Adjoint{<:GroupAlgebraElem,<:Matrix{<:GroupAlgebraElem}} +} + +const FqFieldGroupAlgebraElemMatrix = Union{ + Matrix{<:GroupAlgebraElem{FqFieldElem,<:GroupAlgebra}}, + LinearAlgebra.Adjoint{<:GroupAlgebraElem{FqFieldElem,<:GroupAlgebra},<:Matrix{<:GroupAlgebraElem{FqFieldElem,<:GroupAlgebra}}} +} + +""" +Compute the adjoint of a group algebra element. +The adjoint is defined as the conjugate of the element in the group algebra, +i.e. the inverse of the element in the associated group. +""" +function _adjoint(a::GroupAlgebraElem{T}) where T # TODO Is this used? Should it be deleted? + A = parent(a) + d = dim(A) + v = Vector{T}(undef, d) + for i in 1:d + v[i] = zero(base_ring(A)) + end + id_index = findfirst(x -> x == 1, one(A).coeffs) + # t = zero(base_ring(A)) + mt = multiplication_table(A, copy = false) + acoeff = coefficients(a, copy = false) + for i in 1:d + if acoeff[i] != 0 + k = findfirst(x -> x==id_index, mt[i, :]) # find the inverse of i-th element in the group + v[k] += acoeff[i] + end + end + return A(v) +end diff --git a/src/QuantumClifford.jl b/src/QuantumClifford.jl index 407af5959..ffe679a0d 100644 --- a/src/QuantumClifford.jl +++ b/src/QuantumClifford.jl @@ -1101,8 +1101,14 @@ end """ Check basic consistency requirements of a stabilizer. Used in tests. """ -function stab_looks_good(s) - c = tab(canonicalize!(copy(s))) +function stab_looks_good(s; remove_redundant_rows=false) + # first remove redundant rows + c = if remove_redundant_rows + s1, _, rank = canonicalize!(copy(s), ranks=true) + tab(s1[1:rank]) + else + tab(canonicalize!(copy(s))) + end nrows, ncols = size(c) all((c.phases .== 0x0) .| (c.phases .== 0x2)) || return false H = stab_to_gf2(c) diff --git a/src/ecc/ECC.jl b/src/ecc/ECC.jl index 955ec5818..d8e863136 100644 --- a/src/ecc/ECC.jl +++ b/src/ecc/ECC.jl @@ -17,10 +17,11 @@ export parity_checks, parity_checks_x, parity_checks_z, iscss, code_n, code_s, code_k, rate, distance, isdegenerate, faults_matrix, naive_syndrome_circuit, shor_syndrome_circuit, naive_encoding_circuit, - RepCode, + RepCode, LiftedCode, CSS, Shor9, Steane7, Cleve8, Perfect5, Bitflip3, Toric, Gottesman, Surface, Concat, CircuitCode, + LPCode, two_block_group_algebra_codes, generalized_bicycle_codes, bicycle_codes, random_brickwork_circuit_code, random_all_to_all_circuit_code, evaluate_decoder, CommutationCheckECCSetup, NaiveSyndromeECCSetup, ShorSyndromeECCSetup, @@ -87,14 +88,22 @@ code_n(c::AbstractECC) = code_n(parity_checks(c)) code_n(s::Stabilizer) = nqubits(s) -"""The number of stabilizer checks in a code.""" +"""The number of stabilizer checks in a code. They might not be all linearly independent, thus `code_s >= code_n-code_k`. For the number of linearly independent checks you can use `LinearAlgebra.rank`.""" function code_s end - -code_s(c::AbstractECC) = code_s(parity_checks(c)) code_s(s::Stabilizer) = length(s) +code_s(c::AbstractECC) = code_s(parity_checks(c)) + +""" +The number of logical qubits in a code. + +Note that when redundant rows exist in the parity check matrix, the number of logical qubits `code_k(c)` will be greater than `code_n(c) - code_s(c)`, where the difference equals the redundancy. +""" +function code_k(s::Stabilizer) + _, _, r = canonicalize!(Base.copy(s), ranks=true) + return code_n(s) - r +end -"""The number of logical qubits in a code.""" -code_k(c) = code_n(c) - code_s(c) +code_k(c::AbstractECC) = code_k(parity_checks(c)) """The rate of a code.""" function rate(c) @@ -354,6 +363,7 @@ include("circuits.jl") include("decoder_pipeline.jl") include("codes/util.jl") + include("codes/classical_codes.jl") include("codes/css.jl") include("codes/bitflipcode.jl") @@ -366,7 +376,13 @@ include("codes/gottesman.jl") include("codes/surface.jl") include("codes/concat.jl") include("codes/random_circuit.jl") + include("codes/classical/reedmuller.jl") -include("codes/classical/bch.jl") include("codes/classical/recursivereedmuller.jl") +include("codes/classical/bch.jl") + +# qLDPC +include("codes/classical/lifted.jl") +include("codes/lifted_product.jl") + end #module diff --git a/src/ecc/codes/classical/lifted.jl b/src/ecc/codes/classical/lifted.jl new file mode 100644 index 000000000..a7c20fa04 --- /dev/null +++ b/src/ecc/codes/classical/lifted.jl @@ -0,0 +1,10 @@ +"""Classical codes lifted over a group algebra, used for lifted product code construction ([panteleev2021degenerate](@cite), [panteleev2022asymptotically](@cite)) + +Implemented as a package extension with Hecke. Check the [QuantumClifford documentation](http://qc.quantumsavory.org/stable/ECC_API/) for more details on that extension.""" +function LiftedCode(args...; kwargs...) + ext = Base.get_extension(QuantumClifford, :QuantumCliffordHeckeExt) + if isnothing(ext) + throw("The `LiftedCode` depends on the package `Hecke` but you have not installed or imported it yet. Immediately after you import `Hecke`, the `LiftedCode` will be available.") + end + return ext.LiftedCode(args...; kwargs...) +end diff --git a/src/ecc/codes/lifted_product.jl b/src/ecc/codes/lifted_product.jl new file mode 100644 index 000000000..338880702 --- /dev/null +++ b/src/ecc/codes/lifted_product.jl @@ -0,0 +1,19 @@ +"""Lifted product codes ([panteleev2021degenerate](@cite), [panteleev2022asymptotically](@cite)) + +Implemented as a package extension with Hecke. Check the [QuantumClifford documentation](http://qc.quantumsavory.org/stable/ECC_API/) for more details on that extension.""" +function LPCode(args...; kwargs...) + ext = Base.get_extension(QuantumClifford, :QuantumCliffordHeckeExt) + if isnothing(ext) + throw("The `LPCode` depends on the package `Hecke` but you have not installed or imported it yet. Immediately after you import `Hecke`, the `LPCode` will be available.") + end + return ext.LPCode(args...; kwargs...) +end + +"""Implemented in a package extension with Hecke.""" +function two_block_group_algebra_codes end + +"""Implemented in a package extension with Hecke.""" +function generalized_bicycle_codes end + +"""Implemented in a package extension with Hecke.""" +function bicycle_codes end diff --git a/src/ecc/decoder_pipeline.jl b/src/ecc/decoder_pipeline.jl index 81fd1af30..0488dd28a 100644 --- a/src/ecc/decoder_pipeline.jl +++ b/src/ecc/decoder_pipeline.jl @@ -101,7 +101,7 @@ function physical_ECC_circuit(H, setup::NaiveSyndromeECCSetup) end mem_error_circ = [PauliError(i, setup.mem_noise) for i in 1:nqubits(H)] - circ = [mem_error_circ..., noisy_syndrome_circ...] + circ = vcat(mem_error_circ, noisy_syndrome_circ) return circ, syndrome_bits, n_anc end @@ -119,7 +119,7 @@ function physical_ECC_circuit(H, setup::ShorSyndromeECCSetup) mem_error_circ = [PauliError(i, setup.mem_noise) for i in 1:nqubits(H)] - circ = [prep_anc..., mem_error_circ..., noisy_syndrome_circ...] + circ = vcat(prep_anc, mem_error_circ, noisy_syndrome_circ) circ, syndrome_bits, n_anc end @@ -141,12 +141,12 @@ function evaluate_decoder(d::AbstractSyndromeDecoder, setup::AbstractECCSetup, n # Evaluate the probability for X logical error (the Z-observable part of the faults matrix is used) X_error = evaluate_decoder( d, nsamples, - [encoding_circ..., physical_noisy_circ..., logZ_circ...], + vcat(encoding_circ, physical_noisy_circ, logZ_circ), syndrome_bits, logZ_bits, O[end÷2+1:end,:]) # Evaluate the probability for Z logical error (the X-observable part of the faults matrix is used) Z_error = evaluate_decoder( d, nsamples, - [preX..., encoding_circ..., physical_noisy_circ..., logX_circ...], + vcat(preX, encoding_circ, physical_noisy_circ, logX_circ), syndrome_bits, logX_bits, O[1:end÷2,:]) return (X_error, Z_error) end diff --git a/test/Project.toml b/test/Project.toml index 67977b460..cd15ae310 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -7,6 +7,7 @@ Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" +Hecke = "3e1990a7-5d81-5526-99ce-9ba3ff248f21" HostCPUFeatures = "3e5b6fbb-0976-4d2c-9146-d79de83f2fb0" ILog2 = "2cd5bd5f-40a1-5050-9e10-fc8cdb6109f5" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" diff --git a/test/test_ecc.jl b/test/test_ecc.jl index 64531986d..075d27ac5 100644 --- a/test/test_ecc.jl +++ b/test/test_ecc.jl @@ -9,7 +9,7 @@ function test_naive_syndrome(c::AbstractECC, e::Bool) # create a random logical state unencoded_qubits = random_stabilizer(code_k(c)) - bufferqubits = one(Stabilizer,code_s(c)) + bufferqubits = one(Stabilizer, code_n(c) - code_k(c)) logicalqubits = bufferqubits⊗unencoded_qubits mctrajectory!(logicalqubits, naive_encoding_circuit(c)) if e @@ -50,7 +50,7 @@ ancqubits = code_s(code) regbits = ancqubits frames = PauliFrame(nframes, dataqubits+ancqubits, regbits) - circuit = [ecirc..., scirc...] + circuit = vcat(ecirc, scirc) pftrajectories(frames, circuit) @test sum(pfmeasurements(frames)) == 0 end diff --git a/test/test_ecc_base.jl b/test/test_ecc_base.jl index 9b4dac227..d9afc09bb 100644 --- a/test/test_ecc_base.jl +++ b/test/test_ecc_base.jl @@ -3,6 +3,10 @@ using QuantumClifford using QuantumClifford.ECC using InteractiveUtils +import Nemo: GF +import LinearAlgebra +import Hecke: group_algebra, abelian_group, gens + # generate instances of all implemented codes to make sure nothing skips being checked # We do not include smaller random circuit code because some of them has a bad distance and fails the TableDecoder test @@ -14,13 +18,52 @@ random_circuit_code_args = vcat( [map(f -> getfield(random_all_to_all_circuit_code(c...), f), fieldnames(CircuitCode)) for c in random_all_to_all_circuit_args] ) +# test codes LP04 and LP118 from Appendix A of [raveendran2022finite](@cite), +B04 = Dict( + 7 => [0 0 0 0; 0 1 2 5; 0 6 3 1], + 9 => [0 0 0 0; 0 1 6 7; 0 4 5 2], + 17 => [0 0 0 0; 0 1 2 11; 0 8 12 13], + 19 => [0 0 0 0; 0 2 6 9; 0 16 7 11] +) + +B118 = Dict( + 16 => [0 0 0 0 0; 0 2 4 7 11; 0 3 10 14 15], + 21 => [0 0 0 0 0; 0 4 5 7 17; 0 14 18 12 11], + 30 => [0 0 0 0 0; 0 2 14 24 25; 0 16 11 14 13], +) + +LP04 = [LPCode(base_matrix, l .- base_matrix', l) for (l, base_matrix) in B04] +LP118 = [LPCode(base_matrix, l .- base_matrix', l) for (l, base_matrix) in B118] + +# generalized bicyle codes from from Appendix B of [panteleev2021degenerate](@cite). +test_gb_codes = [ + generalized_bicycle_codes([0, 15, 20, 28, 66], [0, 58, 59, 100, 121], 127), + generalized_bicycle_codes([0, 1, 14, 16, 22], [0, 3, 13, 20, 42], 63), +] + +other_lifted_product_codes = [] + +# from Eq. (18) in Appendix A of [raveendran2022finite](@cite) +l = 63 +GA = group_algebra(GF(2), abelian_group(l)) +A = zeros(GA, 7, 7) +x = gens(GA)[] +A[LinearAlgebra.diagind(A)] .= x^27 +A[LinearAlgebra.diagind(A, -1)] .= x^54 +A[LinearAlgebra.diagind(A, 6)] .= x^54 +A[LinearAlgebra.diagind(A, -2)] .= GA(1) +A[LinearAlgebra.diagind(A, 5)] .= GA(1) +B = reshape([1 + x + x^6], (1, 1)) +push!(other_lifted_product_codes, LPCode(A, B)) + const code_instance_args = Dict( Toric => [(3,3), (4,4), (3,6), (4,3), (5,5)], Surface => [(3,3), (4,4), (3,6), (4,3), (5,5)], Gottesman => [3, 4, 5], CSS => (c -> (parity_checks_x(c), parity_checks_z(c))).([Shor9(), Steane7(), Toric(4, 4)]), Concat => [(Perfect5(), Perfect5()), (Perfect5(), Steane7()), (Steane7(), Cleve8()), (Toric(2, 2), Shor9())], - CircuitCode => random_circuit_code_args + CircuitCode => random_circuit_code_args, + LPCode => (c -> (c.A, c.B)).(vcat(LP04, LP118, test_gb_codes, other_lifted_product_codes)) ) function all_testablable_code_instances(;maxn=nothing) diff --git a/test/test_ecc_codeproperties.jl b/test/test_ecc_codeproperties.jl index 404592763..523606422 100644 --- a/test/test_ecc_codeproperties.jl +++ b/test/test_ecc_codeproperties.jl @@ -30,10 +30,9 @@ H = parity_checks(code) @test nqubits(code) == size(H, 2) == code_n(code) @test size(H, 1) == code_s(code) - @test code_s(code) + code_k(code) == code_n(code) - @test size(H, 1) < size(H, 2) + @test code_s(code) + code_k(code) >= code_n(code) # possibly exist redundant checks _, _, rank = canonicalize!(copy(H), ranks=true) - @test rank == size(H, 1) # TODO maybe weaken this if we want to permit codes with redundancies + @test rank <= size(H, 1) @test QuantumClifford.stab_looks_good(copy(H)) end end diff --git a/test/test_ecc_decoder_all_setups.jl b/test/test_ecc_decoder_all_setups.jl index f6ceee981..4a8382c17 100644 --- a/test/test_ecc_decoder_all_setups.jl +++ b/test/test_ecc_decoder_all_setups.jl @@ -96,3 +96,30 @@ end end end + +@testset "belief prop decoders, good for sparse codes" begin + codes = vcat(LP04, LP118, test_gb_codes, other_lifted_product_codes) + + noise = 0.001 + + setups = [ + CommutationCheckECCSetup(noise), + NaiveSyndromeECCSetup(noise, 0), + ShorSyndromeECCSetup(noise, 0), + ] + # lifted product codes currently trigger errors in syndrome circuits + + for c in codes + for s in setups + for d in [c -> PyBeliefPropOSDecoder(c, maxiter=10)] + nsamples = code_n(c) > 400 ? 1000 : 100000 + # take fewer samples for larger codes to save time + e = evaluate_decoder(d(c), s, nsamples) + # @show c + # @show s + # @show e + @assert max(e...) < noise / 4 (c, s, e) + end + end + end +end diff --git a/test/test_ecc_encoding.jl b/test/test_ecc_encoding.jl index cab0562ff..78312c57d 100644 --- a/test/test_ecc_encoding.jl +++ b/test/test_ecc_encoding.jl @@ -31,7 +31,7 @@ pre_encₖ = one(Stabilizer, code_k(code)) # n-k ancillary qubits in state zero prepended - pre_encₙ = one(Stabilizer, code_s(code)) ⊗ pre_encₖ + pre_encₙ = one(Stabilizer, code_n(code) - code_k(code)) ⊗ pre_encₖ # running the encoding circuit encodedₙ = mctrajectory!(pre_encₙ, circ)[1] |> canonicalize! diff --git a/test/test_ecc_syndromes.jl b/test/test_ecc_syndromes.jl index 45f4e0f89..0f6f67ea0 100644 --- a/test/test_ecc_syndromes.jl +++ b/test/test_ecc_syndromes.jl @@ -17,8 +17,8 @@ # no noise naive_frames = PauliFrame(nframes, naive_qubits, syndromebits) shor_frames = PauliFrame(nframes, shor_qubits, last(shor_bits)) - naive_circuit = [ecirc..., naive_scirc...] - shor_circuit = [ecirc..., shor_cat_scirc..., shor_scirc...] + naive_circuit = vcat(ecirc, naive_scirc) + shor_circuit = vcat(ecirc, shor_cat_scirc, shor_scirc) pftrajectories(naive_frames, naive_circuit) pftrajectories(shor_frames, shor_circuit) @test pfmeasurements(naive_frames) == pfmeasurements(shor_frames)[:,shor_bits] @@ -27,7 +27,7 @@ naive_frames = PauliFrame(nframes, naive_qubits, syndromebits) shor_frames = PauliFrame(nframes, shor_qubits, last(shor_bits)) pftrajectories(naive_frames, ecirc) - pftrajectories(shor_frames, [ecirc..., shor_cat_scirc...]) + pftrajectories(shor_frames, vcat(ecirc, shor_cat_scirc)) # manually injecting the same type of noise in the frames -- not really a user accessible API p = random_pauli(dataqubits, realphase=true) pₙ = embed(naive_qubits, 1:dataqubits, p) diff --git a/test/test_jet.jl b/test/test_jet.jl index 304f3b78c..1b8b01504 100644 --- a/test/test_jet.jl +++ b/test/test_jet.jl @@ -8,6 +8,7 @@ using LinearAlgebra using Nemo using AbstractAlgebra + using Hecke rep = report_package("QuantumClifford"; ignored_modules=( @@ -19,6 +20,7 @@ AnyFrameModule(LinearAlgebra), AnyFrameModule(Nemo), AnyFrameModule(AbstractAlgebra), + AnyFrameModule(Hecke), )) @show rep From 01cc0f09d17130b3ab1aac9f393c826f0ad8c5fe Mon Sep 17 00:00:00 2001 From: Stefan Krastanov Date: Thu, 26 Sep 2024 17:31:45 -0400 Subject: [PATCH 2/2] bump version number and update CHANGELOG --- CHANGELOG.md | 7 +++---- Project.toml | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 762c48595..30cdbd09e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,15 +5,14 @@ # News -## dev +## v0.9.10 - 2024-09-26 +- The lifted product class of quantum LDPC codes is implemented in the ECC submodule. +- **(fix)** `ECC.code_s` now gives the number of parity checks with redundancy. If you want the number of linearly independent parity checks, you can use `LinearAlgebra.rank`. - Implementing many more named single-qubit gates following naming convention similar to the stim package in python. - **(fix)** Bug fix to the `parity_checks(ReedMuller(r, m))` of classical Reed-Muller code (it was returning generator matrix). - `RecursiveReedMuller` code implementation as an alternative implementation of `ReedMuller`. -## v0.9.10 - 2024-09-26 - -- **(fix)** `ECC.code_s` now gives the number of parity checks with redundancy. If you want the number of linearly independent parity checks, you can use `LinearAlgebra.rank`. ## v0.9.9 - 2024-08-05 diff --git a/Project.toml b/Project.toml index 802c43685..02555d45f 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "QuantumClifford" uuid = "0525e862-1e90-11e9-3e4d-1b39d7109de1" authors = ["Stefan Krastanov and QuantumSavory community members"] -version = "0.9.9" +version = "0.9.10" [deps] Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa"