QuantumSavory / QuantumClifford.jl

Clifford circuits, graph states, and other quantum Stabilizer formalism tools.
MIT License
120 stars 47 forks source link

`enumerate_cliffords_slow(n, i, ;...)` does not follow the same ordering convention as the original paper #343

Open Fe-r-oz opened 2 months ago

Fe-r-oz commented 2 months ago

Describe the bug 🐞

enumerate_clifford_slow(n, i, ;...) gives a canonical mapping from integer to symplectic group Sp(2n). This algorithm is based on generalized Gram-Schmidt orthogonalization procedure (see section B Symplection Gram-Schmidt and II. A for details of this algorithm from this paper).

The same paper provides a useful correctness check in form of the SYMPLECTICinverse(n, gn) algorithm which provides an inverse map, meaning indexing a given group element. SYMPLECTICinverse(n, gn) requires n and gn where gn is returned by SymplecticImproved. See the additional context for more details.

I am working on the PR towards the SymplecticImproved O(n³) algorithm following Stefan's instructions from #11. For correctness sake, I implemented the SymplecticImproved nearly as exactly as possible (1) from the paper itself since I was not satisfied with just the Juqst.jl's implementation (2). There are a few minor differences between (2)'s implementation and the pseudocode in the paper, so for safety sake, it was better to not just rely onJuqst.jl without testing it first.

For these two approaches (1, 2) , this test should pass as it serve as a correctness check:

for n in rand(3: upperbound, ...)
    for i in rand(1:symplectic cardinality, ...)
        `@test QuantumClifford.symplecticinverse(QuantumClifford.symplecticImproved(i, n)) == i`
    end
end

This test passes for both of these two approaches (1, 2):

julia> @testset "symplecticImproved correctness checks" begin
           for n in rand(3:12, 100)
               for i in rand(1:clifford_cardinality(n, phases=false), 100)
                   @test QuantumClifford.symplecticinverse(QuantumClifford.symplecticImproved(i, n)) == i
               end
           end
       end
Test Summary:                         |  Pass  Total  Time
symplecticImproved correctness checks | 10000  10000  4.0s
Test.DefaultTestSet("symplecticImproved correctness checks", Any[], 10000, false, false, true, 1.724164591392796e9, 1.72416459540161e9, false, "REPL[6]")

It is noted that for these two approaches (1, 2) this works for n > 2.

Expected behavior

For all the values of n>2 that obey Symplectic cardinality, these should hold true for enumerate_clifford_slow(n, i, ;...):

@test QuantumClifford.symplecticinverse(stab_to_gf2(tab(QuantumClifford.enumerate_cliffords_slow(n, i,  onlycoset=false)))) == i

I also had a look at test_enumerate.jl but I didn't find tests for enumerate_cliffords_slow. I think we should add a similar test as for enumerate_cliffords_slow after it becomes bug-free. In the meantime, I will polish the PR and soon submit it for further discussion and link to this issue. I am sharing a conceptual MRE that I am getting locally as (2) can be referenced for existing implementation.

Note: We are only interested in symplectic cardinality as the authors designed algorithms that return ith element from symplectic cardinality. That's why onlycoset=false, see the doctest in enumeration.jl for more details. Also, under the hood, onlycoset=false by default for enumerate_clifford_slow(n, i, ;...) for this reason as well.

Minimal Reproducible Example 👇

julia> @testset "enumerate_clifford_slow correctness checks" begin
           for n in rand(3:10, 2)
               for i in rand(1:clifford_cardinality(n, phases=false), 2)
                   @test QuantumClifford.symplecticinverse(stab_to_gf2(tab(QuantumClifford.enumerate_cliffords_slow(n, i, onlycoset=false)))) == i
               end
           end
       end

Error & Stacktrace or other complete output produced by the MRE ⚠️

Even for a few random cases, the above test does not hold:

enumerate_clifford_slow correctness checks: Test Failed at REPL[7]:4
  Expression: QuantumClifford.symplecticinverse(stab_to_gf2(tab(QuantumClifford.enumerate_cliffords_slow(n, i, onlycoset = false)))) == i
   Evaluated: 34551563347 == 31496842868

Stacktrace:
  [1] macro expansion
    @ ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/Test/src/Test.jl:672 [inlined]
  [2] macro expansion
    @ ./REPL[7]:4 [inlined]
  [3] macro expansion
    @ ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/Test/src/Test.jl:1577 [inlined]
  [4] top-level scope
    @ ./REPL[7]:2
  [5] eval
    @ ./boot.jl:385 [inlined]
  [6] eval_user_input(ast::Any, backend::REPL.REPLBackend, mod::Module)
    @ REPL ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/REPL/src/REPL.jl:150
  [7] repl_backend_loop(backend::REPL.REPLBackend, get_module::Function)
    @ REPL ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/REPL/src/REPL.jl:246
  [8] start_repl_backend(backend::REPL.REPLBackend, consumer::Any; get_module::Function)
    @ REPL ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/REPL/src/REPL.jl:231
  [9] run_repl(repl::REPL.AbstractREPL, consumer::Any; backend_on_current_task::Bool, backend::Any)
    @ REPL ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/REPL/src/REPL.jl:389
 [10] run_repl(repl::REPL.AbstractREPL, consumer::Any)
    @ REPL ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/REPL/src/REPL.jl:375
 [11] (::Base.var"#1013#1015"{Bool, Bool, Bool})(REPL::Module)
    @ Base ./client.jl:432
 [12] #invokelatest#2
    @ ./essentials.jl:892 [inlined]
 [13] invokelatest
    @ ./essentials.jl:889 [inlined]
 [14] run_main_repl(interactive::Bool, quiet::Bool, banner::Bool, history_file::Bool, color_set::Bool)
    @ Base ./client.jl:416
 [15] exec_options(opts::Base.JLOptions)
    @ Base ./client.jl:333
 [16] _start()
    @ Base ./client.jl:552
enumerate_clifford_slow correctness checks: Test Failed at REPL[7]:4
  Expression: QuantumClifford.symplecticinverse(stab_to_gf2(tab(QuantumClifford.enumerate_cliffords_slow(n, i, onlycoset = false)))) == i
   Evaluated: 9076932653 == 8430116994

Stacktrace:
  [1] macro expansion
    @ ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/Test/src/Test.jl:672 [inlined]
  [2] macro expansion
    @ ./REPL[7]:4 [inlined]
  [3] macro expansion
    @ ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/Test/src/Test.jl:1577 [inlined]
  [4] top-level scope
    @ ./REPL[7]:2
  [5] eval
    @ ./boot.jl:385 [inlined]
  [6] eval_user_input(ast::Any, backend::REPL.REPLBackend, mod::Module)
    @ REPL ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/REPL/src/REPL.jl:150
  [7] repl_backend_loop(backend::REPL.REPLBackend, get_module::Function)
    @ REPL ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/REPL/src/REPL.jl:246
  [8] start_repl_backend(backend::REPL.REPLBackend, consumer::Any; get_module::Function)
    @ REPL ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/REPL/src/REPL.jl:231
  [9] run_repl(repl::REPL.AbstractREPL, consumer::Any; backend_on_current_task::Bool, backend::Any)
    @ REPL ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/REPL/src/REPL.jl:389
 [10] run_repl(repl::REPL.AbstractREPL, consumer::Any)
    @ REPL ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/REPL/src/REPL.jl:375
 [11] (::Base.var"#1013#1015"{Bool, Bool, Bool})(REPL::Module)
    @ Base ./client.jl:432
 [12] #invokelatest#2
    @ ./essentials.jl:892 [inlined]
 [13] invokelatest
    @ ./essentials.jl:889 [inlined]
 [14] run_main_repl(interactive::Bool, quiet::Bool, banner::Bool, history_file::Bool, color_set::Bool)
    @ Base ./client.jl:416
 [15] exec_options(opts::Base.JLOptions)
    @ Base ./client.jl:333
 [16] _start()
    @ Base ./client.jl:552
enumerate_clifford_slow correctness checks: Test Failed at REPL[7]:4
  Expression: QuantumClifford.symplecticinverse(stab_to_gf2(tab(QuantumClifford.enumerate_cliffords_slow(n, i, onlycoset = false)))) == i
   Evaluated: 16852729387 == 27027632033

Stacktrace:
  [1] macro expansion
    @ ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/Test/src/Test.jl:672 [inlined]
  [2] macro expansion
    @ ./REPL[7]:4 [inlined]
  [3] macro expansion
    @ ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/Test/src/Test.jl:1577 [inlined]
  [4] top-level scope
    @ ./REPL[7]:2
  [5] eval
    @ ./boot.jl:385 [inlined]
  [6] eval_user_input(ast::Any, backend::REPL.REPLBackend, mod::Module)
    @ REPL ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/REPL/src/REPL.jl:150
  [7] repl_backend_loop(backend::REPL.REPLBackend, get_module::Function)
    @ REPL ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/REPL/src/REPL.jl:246
  [8] start_repl_backend(backend::REPL.REPLBackend, consumer::Any; get_module::Function)
    @ REPL ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/REPL/src/REPL.jl:231
  [9] run_repl(repl::REPL.AbstractREPL, consumer::Any; backend_on_current_task::Bool, backend::Any)
    @ REPL ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/REPL/src/REPL.jl:389
 [10] run_repl(repl::REPL.AbstractREPL, consumer::Any)
    @ REPL ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/REPL/src/REPL.jl:375
 [11] (::Base.var"#1013#1015"{Bool, Bool, Bool})(REPL::Module)
    @ Base ./client.jl:432
 [12] #invokelatest#2
    @ ./essentials.jl:892 [inlined]
 [13] invokelatest
    @ ./essentials.jl:889 [inlined]
 [14] run_main_repl(interactive::Bool, quiet::Bool, banner::Bool, history_file::Bool, color_set::Bool)
    @ Base ./client.jl:416
 [15] exec_options(opts::Base.JLOptions)
    @ Base ./client.jl:333
 [16] _start()
    @ Base ./client.jl:552
enumerate_clifford_slow correctness checks: Test Failed at REPL[7]:4
  Expression: QuantumClifford.symplecticinverse(stab_to_gf2(tab(QuantumClifford.enumerate_cliffords_slow(n, i, onlycoset = false)))) == i
   Evaluated: 34838106142 == 42563658818

Stacktrace:
  [1] macro expansion
    @ ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/Test/src/Test.jl:672 [inlined]
  [2] macro expansion
    @ ./REPL[7]:4 [inlined]
  [3] macro expansion
    @ ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/Test/src/Test.jl:1577 [inlined]
  [4] top-level scope
    @ ./REPL[7]:2
  [5] eval
    @ ./boot.jl:385 [inlined]
  [6] eval_user_input(ast::Any, backend::REPL.REPLBackend, mod::Module)
    @ REPL ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/REPL/src/REPL.jl:150
  [7] repl_backend_loop(backend::REPL.REPLBackend, get_module::Function)
    @ REPL ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/REPL/src/REPL.jl:246
  [8] start_repl_backend(backend::REPL.REPLBackend, consumer::Any; get_module::Function)
    @ REPL ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/REPL/src/REPL.jl:231
  [9] run_repl(repl::REPL.AbstractREPL, consumer::Any; backend_on_current_task::Bool, backend::Any)
    @ REPL ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/REPL/src/REPL.jl:389
 [10] run_repl(repl::REPL.AbstractREPL, consumer::Any)
    @ REPL ~/.julia/juliaup/julia-1.10.4+0.x64.linux.gnu/share/julia/stdlib/v1.10/REPL/src/REPL.jl:375
 [11] (::Base.var"#1013#1015"{Bool, Bool, Bool})(REPL::Module)
    @ Base ./client.jl:432
 [12] #invokelatest#2
    @ ./essentials.jl:892 [inlined]
 [13] invokelatest
    @ ./essentials.jl:889 [inlined]
 [14] run_main_repl(interactive::Bool, quiet::Bool, banner::Bool, history_file::Bool, color_set::Bool)
    @ Base ./client.jl:416
 [15] exec_options(opts::Base.JLOptions)
    @ Base ./client.jl:333
 [16] _start()
    @ Base ./client.jl:552
Test Summary:                              | Fail  Total  Time
enumerate_clifford_slow correctness checks |    4      4  1.2s
ERROR: Some tests did not pass: 0 passed, 4 failed, 0 errored, 0 broken.

Environment (please complete the following information):

  - Output of using Pkg; Pkg.status()

QuantumClifford v0.9.9

  - Output of using Pkg; Pkg.status(; mode = PKGMODE_MANIFEST)

QuantumClifford v0.9.9

  - Output of versioninfo()

Julia Version 1.10.4
Commit 48d4fd48430 (2024-06-04 10:41 UTC)
Build Info:
  Official https://julialang.org/ release
Platform Info:
  OS: Linux (x86_64-linux-gnu)

Additional context

SymplecticImproved(i, n) O(n³)

note: WIP. PR soon to be submitted.

julia> m = QuantumClifford.symplecticImproved(62345, 7)
14×14 Matrix{Bool}:
 1  0  1  1  0  0  0  1  1  1  0  0  1  1
 1  0  1  0  0  0  0  0  0  0  0  0  0  0
 0  1  0  1  0  0  0  1  1  1  0  0  1  1
 1  0  0  0  0  0  0  1  1  1  0  0  1  1
 0  0  0  0  1  0  0  0  0  0  0  0  0  0
 0  0  0  0  0  1  0  0  0  0  0  0  0  0
 0  1  1  1  0  0  1  1  1  1  0  0  1  1
 0  0  0  0  0  0  0  1  0  0  0  0  0  0
 0  1  1  1  0  0  0  1  0  1  0  0  1  1
 0  1  1  1  0  0  0  1  1  0  0  0  1  1
 0  0  0  0  0  0  0  0  0  0  1  0  0  0
 0  0  0  0  0  0  0  0  0  0  0  1  0  0
 0  1  1  1  0  0  0  1  1  1  0  0  0  1
 0  1  1  1  0  0  0  1  1  1  0  0  1  0
julia> CliffordOperator((@view tab(Stabilizer(QuantumClifford.symplecticImproved(62345,7)))[1:14]))
X₁ ⟼ + YZYX_ZZ
X₂ ⟼ + X_X____
X₃ ⟼ + ZYZX_ZZ
X₄ ⟼ + YZZ__ZZ
X₅ ⟼ + ____X__
X₆ ⟼ + _____X_
X₇ ⟼ + ZYYX_ZY
Z₁ ⟼ + Z______
Z₂ ⟼ + ZXYX_ZZ
Z₃ ⟼ + ZYXX_ZZ
Z₄ ⟼ + ___Z___
Z₅ ⟼ + ____Z__
Z₆ ⟼ + ZYYX__Z
Z₇ ⟼ + ZYYX_Z_

And this test passes:

julia> QuantumClifford.symplecticinverse(QuantumClifford.symplecticImproved(62345, 7)) == 62345
true

Symplectic(n, i) O(n⁴) algorithm

In order to relate with enumerate_cliffords_slow

julia> QuantumClifford.enumerate_cliffords_slow(7, 62345)
X₁ ⟼ + ZZYX_ZZ
X₂ ⟼ + X_____X
X₃ ⟼ + _____Z_
X₄ ⟼ + ____Z__
X₅ ⟼ + ___Z__X
X₆ ⟼ + __Z___X
X₇ ⟼ + _Z_____
Z₁ ⟼ + ZZYX_ZY
Z₂ ⟼ + Z______
Z₃ ⟼ + _____XX
Z₄ ⟼ + ____X__
Z₅ ⟼ + ___X___
Z₆ ⟼ + __X___X
Z₇ ⟼ + _X____X

julia> stab_to_gf2(tab(QuantumClifford.enumerate_cliffords_slow(7, 62345)))
14×14 Matrix{Bool}:
 0  0  1  1  0  0  0  1  1  1  0  0  1  1
 1  0  0  0  0  0  1  0  0  0  0  0  0  0
 0  0  0  0  0  0  0  0  0  0  0  0  1  0
 0  0  0  0  0  0  0  0  0  0  0  1  0  0
 0  0  0  0  0  0  1  0  0  0  1  0  0  0
 0  0  0  0  0  0  1  0  0  1  0  0  0  0
 0  0  0  0  0  0  0  0  1  0  0  0  0  0
 0  0  1  1  0  0  1  1  1  1  0  0  1  1
 0  0  0  0  0  0  0  1  0  0  0  0  0  0
 0  0  0  0  0  1  1  0  0  0  0  0  0  0
 0  0  0  0  1  0  0  0  0  0  0  0  0  0
 0  0  0  1  0  0  0  0  0  0  0  0  0  0
 0  0  1  0  0  0  1  0  0  0  0  0  0  0
 0  1  0  0  0  0  1  0  0  0  0  0  0  0

julia> QuantumClifford.symplecticinverse(stab_to_gf2(tab(QuantumClifford.enumerate_cliffords_slow(7, 62345)))) == 62345
false

Pedagogical Detail: O(n⁴) and O(n³) algorithms provided in the paper provide different canonical mapping as acknowelged in the paper. Also, n can be deduced from gn. Thus, gn is only parameter needed for SYMPLECTICinverse.

Krastanov commented 2 months ago

That would be a nice check to implement. I imagine the symplecticinverse does not match the same conventions as enumerate_cliffords. E.g. there are places where an integer has to be represented as bits or where a submatrix needs to be created. In such places, if one does not follow the same conventions, the inverse will not work correctly. E.g. are integers little or big endian, are submatrices inserted in the left or right corner of a bigger matrix, are symplectic matrices represented as a big block-diagonal matrix with just 4 quardrants or a matrix made out of many small 2x2 matrices.

Fe-r-oz commented 2 months ago

Thank you. Your insight is really helpful. I will be more careful and ponder about this.

I did noticed that enumerate_cliffords_slow(n, i, ;...) is implemented in-place, with Tableau manipulations and updates, and also you commented that 'their algorithm seems wrong as ⟨w'₁|wₗ⟩=bₗ which is not always zero.'

The symplecticinverse indeed gives different answer if gn is even slightly altered. E.g 0 instead of 1 ( or vice versa) at very few index locations. Yesterday, initially, when rewriting the pesudocode from paper, I overlooked indexing at one place, and I got the answer 62340 instead of 62345. Thus, the symplecticinverse is sensitive to even small changes might happen to gn .

Pedagogical details/ Additional Context

The following is not done/implemented in the paper per se but authors talk about circuit compilation: ... In addition, Juqst.jl also parse the symplectic matrix(gn) in the form of the Aaronson/Gottesman Tableau. The paper discusses by the list of parameters (α, β, γ, δ, r, s) where α, β, γ, δ are n × n matrices of bits, and r, s are n-bit vectors. He splits the gn into (α, β, γ, δ) in one function, and in another, he converts it to the form of Aaronson/Gottesman Tableau. There is no testing for this, so I hesitated relying on this for the time being.

I have yet to check whether after this, does symplecticinverse still work on the modifed gn meaning is this reordering affect the result? ...

I think the overgoal is circuit compilation that you point out in issue 11 as authors say "given the list (α, β, γ, δ, r, s), there is a classical algorithm for compiling a circuit implementing U which is composed of O(n²/ log n) gates from the gate set {H, CNOT,P}". This task can be divided into many small PR that address one thing at a time. I am pondering about step-by-step breakdown of what these tasks might look like and I am looking forward to discussing it with you soon.

Also, in #13, this paper also have one algorithm (Algorithm 1) that relies on the calculation of symplectic matrix. Theorem 24 of this papers talks about the same stuff about symplectic transvections as the aforementioned paper in first comment does. Thus, there is overlap between these two issues/papers (11 and 13) as they appears to be relying on symplectic transvections.

Fe-r-oz commented 2 months ago

I have yet to check whether after this, does symplecticinverse still work on the modifed gn meaning is this reordering affect the result?

Short answer: No, it would not work. symplecticinverse only works on symplectic matrices gn, not on matrices that are created by decomposing gn into (α,β,γ,δ) and then forming new matrix in accordance with Aaronson/Gottesman Tableau with additional last column representing r.

I may not have raised a good question, but I felt compelled to provide an answer. This question seems irrelavant to the present discussion.

Additional Context

If I am not mistaken, by 4 quadrants, you refer to the 4 submatrices of the Aaronsom/Gottesman tableau. Juqst.jl tries to proceed with decompositing the original symplectic matrix gn from the paper for which the paper provides pseudocode, and decomposes into to (α,β,γ,δ) submatrices . Then, he arranges these quadrants into the Aaronson/Gottesman tableau form. Most likely, column vector towards the end representing r is added when their tableau structure also has. The resulting matrix in now (2n x 2n+1) which is not a symplectic.

These is done by two functions by Juqst.jl, parseSymplectic and stabiliseSymp (which are not mentioned in the original paper) which makes sense as in paper, authors are only concerned with the symplectic matrix (gn), and not the circuit compilation which is not the goal of the paper per se.

julia> symplecticinverse(stabiliseSymp(symplectic(62345, 7), 1)[1:14, 1:14])
12242461102891138205349303168274

Thus, only the complete pesudocode in the paper and 'some' part of Juqst.jl implementation can be verified with correctness tests. The above does not work as I am trying to check whether a matrix in Aaronson/Gottesman form has symplectic inverse which will be incorrect.

P.S. This comment is not directly helpful to the discussion which is related to symplectic matrices. This is because I am checking the outcomes of some steps required for proceeding into circuit compilation.

Krastanov commented 2 months ago

To do the correctness check you need to implement symplecticinverse in a way that uses the conventions from this codebase, not the conventions from the paper.