Skip to content

Commit 0035c32

Browse files
authored
Faster random circuits (#464)
1 parent 891f4a8 commit 0035c32

File tree

5 files changed

+261
-8
lines changed

5 files changed

+261
-8
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55

66
# News
77

8+
## v0.9.17 - 2025-02-18
9+
10+
- New memory structure and matrix inversion function for `random_destabilizer`, to reduce allocations and speed up repeated generation of many random destabilizers.
11+
- Improvements to allocations in `apply!`
12+
813
## v0.9.16 - 2024-12-29
914

1015
- 100× faster unbiased `random_pauli`.

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "QuantumClifford"
22
uuid = "0525e862-1e90-11e9-3e4d-1b39d7109de1"
33
authors = ["Stefan Krastanov <[email protected]> and QuantumSavory community members"]
4-
version = "0.9.16"
4+
version = "0.9.17"
55

66
[deps]
77
Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa"

src/randoms.jl

Lines changed: 222 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,199 @@ random_destabilizer(n::Int; phases::Bool=true) = random_destabilizer(GLOBAL_RNG,
171171
random_destabilizer(rng::AbstractRNG, r::Int, n::Int; phases::Bool=true) = MixedDestabilizer(random_destabilizer(rng,n;phases),r)
172172
random_destabilizer(r::Int, n::Int; phases::Bool=true) = random_destabilizer(GLOBAL_RNG,r,n; phases)
173173

174+
# Reuse memory for faster generation of many random destabilizers
175+
struct RandDestabMemory{N,T<:Integer}
176+
F1::Matrix{Int8}
177+
F2::Matrix{Int8}
178+
hadamard::BitVector
179+
perm::Vector{T}
180+
had_idxs::Vector{T}
181+
perm_inds::Vector{T}
182+
U::Matrix{Int8}
183+
xzs::BitMatrix
184+
phasesarray::Vector{UInt8}
185+
phase_options::Vector{UInt8}
186+
arr::Vector{T}
187+
n::Int64
188+
189+
function RandDestabMemory(n::Integer)
190+
T = typeof(n)
191+
F1 = zeros(Int8, 2n, 2n)
192+
F2 = zeros(Int8, 2n, 2n)
193+
hadamard = falses(n)
194+
perm = zeros(T, n)
195+
had_idxs = zeros(T, n)
196+
perm_inds = zeros(T, 2n)
197+
U = zeros(Int8, 2n, 2n)
198+
xzs = falses(2n, 2n)
199+
phasesarray = zeros(UInt8, 2n)
200+
phase_options = [0x0, 0x2]
201+
arr = collect(1:n)
202+
new{n,T}(F1, F2, hadamard, perm, had_idxs, perm_inds, U, xzs, phasesarray, phase_options, arr, n)
203+
end
204+
end
205+
function _reset!(memory::RandDestabMemory)
206+
fill!(memory.F1, 0)
207+
fill!(memory.F2, 0)
208+
fill!(memory.hadamard, false)
209+
fill!(memory.perm, 0)
210+
fill!(memory.had_idxs, 0)
211+
fill!(memory.perm_inds, 0)
212+
fill!(memory.U, 0)
213+
fill!(memory.xzs, false)
214+
fill!(memory.phasesarray, 0)
215+
for i in eachindex(memory.arr)
216+
@inbounds memory.arr[i] = i
217+
end
218+
end
219+
220+
# Allocation free inverse of upper trinagular int matrix with 1 on diagonal.
221+
function _inv!(inverse, A)
222+
for i in 2:size(A, 2)
223+
for j in 1:i-1
224+
@inbounds factor = A[j, i] # diagonal always 1 => only integers in inverse matrix
225+
for k in 1:j
226+
@inbounds inverse[k, i] -= factor * inverse[k, j]
227+
end
228+
end
229+
end
230+
nothing
231+
end
232+
233+
function random_destabilizer(rng::AbstractRNG, memory::RandDestabMemory; phases::Bool=true)
234+
n = memory.n
235+
# reset the working memory
236+
_reset!(memory)
237+
238+
hadamard = memory.hadamard
239+
perm = memory.perm
240+
_quantum_mallows!(rng, hadamard, perm, memory.arr)
241+
had_idxs = memory.had_idxs
242+
j = 1
243+
for i in eachindex(hadamard)
244+
@inbounds if hadamard[i]
245+
@inbounds had_idxs[j] = i
246+
j += 1
247+
end
248+
end
249+
250+
# delta, delta', gamma, gamma' appear in the canonical form
251+
# of a Clifford operator (Eq. 3/Theorem 1)
252+
# delta is unit lower triangular, gamma is symmetric
253+
F1 = memory.F1
254+
F2 = memory.F2
255+
delta = @view F1[1:n, 1:n]
256+
delta_p = @view F2[1:n, 1:n]
257+
prod = @view F1[n+1:2n, 1:n]
258+
prod_p = @view F2[n+1:2n, 1:n]
259+
gamma = @view F1[1:n, n+1:2n]
260+
gamma_p = @view F2[1:n, n+1:2n]
261+
inv_delta = @view F1[n+1:2n, n+1:2n]
262+
inv_delta_p = @view F2[n+1:2n, n+1:2n]
263+
for i in 1:n
264+
delta[i, i] = 1
265+
delta_p[i, i] = 1
266+
gamma_p[i, i] = rand(rng, 0x0:0x1)::UInt8
267+
end
268+
269+
# gamma_ii is zero if h[i] = 0
270+
for idx in had_idxs
271+
if idx != 0
272+
gamma[idx, idx] = rand(rng, 0x0:0x1)::UInt8
273+
end
274+
end
275+
276+
# gamma' and delta' are unconstrained on the lower triangular
277+
fill_tril(rng, gamma_p, n, symmetric=true) # I think this should be fill_tril!
278+
fill_tril(rng, delta_p, n)
279+
280+
# off diagonal: gamma, delta must obey conditions C1-C5
281+
for row in 1:n, col in 1:row-1
282+
if hadamard[row] && hadamard[col]
283+
gamma[row, col] = gamma[col, row] = rand(rng, 0x0:0x1)::UInt8
284+
# otherwise delta[row,col] must be zero by C4
285+
if perm[row] > perm[col]
286+
delta[row, col] = rand(rng, 0x0:0x1)::UInt8
287+
end
288+
elseif hadamard[row] && (!hadamard[col]) && perm[row] < perm[col]
289+
# C5 imposes delta[row, col] = 0 for h[row]=1, h[col]=0
290+
# if perm[row] > perm[col] then C2 imposes gamma[row,col] = 0
291+
gamma[row, col] = gamma[col, row] = rand(rng, 0x0:0x1)::UInt8
292+
elseif (!hadamard[row]) && hadamard[col]
293+
delta[row, col] = rand(rng, 0x0:0x1)::UInt8
294+
# not sure what condition imposes this
295+
if perm[row] > perm[col]
296+
gamma[row, col] = gamma[col, row] = rand(rng, 0x0:0x1)::UInt8
297+
end
298+
elseif (!hadamard[row]) && (!hadamard[col]) && perm[row] < perm[col]
299+
# C1 imposes gamma[row, col] = 0 for h[row]=h[col] = 0
300+
# if perm[row] > perm[col] then C3 imposes delta[row,col] = 0
301+
delta[row, col] = rand(rng, 0x0:0x1)::UInt8
302+
end
303+
end
304+
305+
# now construct the tableau representation for F(I, Gamma, Delta)
306+
mul!(prod, gamma, delta)
307+
mul!(prod_p, gamma_p, delta_p)
308+
for i in n+1:2n
309+
@inbounds F1[i, i] = 1
310+
@inbounds F2[i, i] = 1
311+
end
312+
_inv!(inv_delta, delta')
313+
_inv!(inv_delta_p, delta_p')
314+
315+
# block matrix form
316+
F1 .= mod.(F1, 2)
317+
F2 .= mod.(F2, 2)
318+
gamma .= 0
319+
gamma_p .= 0
320+
321+
# apply qubit permutation S to F2
322+
perm_inds = memory.perm_inds
323+
U = memory.U
324+
for i in 1:n
325+
@inbounds perm_inds[i] = perm[i]
326+
@inbounds perm_inds[i+n] = perm[i] + n
327+
end
328+
for (i, e) in enumerate(perm_inds)
329+
for j in 1:2n
330+
U[i, j] = F2[e, j]
331+
end
332+
end
333+
334+
# apply layer of hadamards
335+
for i in 1:n
336+
if had_idxs[i] != 0
337+
for j in 1:2n
338+
@inbounds (U[had_idxs[i], j], U[had_idxs[i]+n, j]) = (U[had_idxs[i]+n, j], U[had_idxs[i], j])
339+
end
340+
end
341+
end
342+
343+
# apply F1
344+
xzs = memory.xzs
345+
fill!(F2, 0)
346+
mul!(F2, F1, U)
347+
348+
F2 .= mod.(F2, 2)
349+
xzs .= F2 .== 1
350+
351+
# random Pauli matrix just amounts to phases on the stabilizer tableau
352+
phasesarray = memory.phasesarray
353+
if phases
354+
for i in 1:2n
355+
phasesarray[i] = rand(rng, memory.phase_options)
356+
end
357+
end
358+
359+
return Destabilizer(Tableau(phasesarray, xzs))
360+
end
361+
362+
363+
174364
"""A random Clifford operator generated by the Bravyi-Maslov Algorithm 2 from [bravyi2020hadamard](@cite)."""
175365
random_clifford(rng::AbstractRNG, n::Int; phases::Bool=true) = CliffordOperator(random_destabilizer(rng, n; phases))
366+
random_clifford(rng::AbstractRNG, memory::RandDestabMemory; phases::Bool=true) = CliffordOperator(random_destabilizer(rng, memory; phases))
176367
random_clifford(n::Int; phases::Bool=true) = random_clifford(GLOBAL_RNG, n::Int; phases)
177368

178369
"""A random Stabilizer tableau generated by the Bravyi-Maslov Algorithm 2 from [bravyi2020hadamard](@cite)."""
@@ -196,23 +387,49 @@ function nemo_inv(a, n)::Matrix{UInt8}
196387
return collect(UInt8.(inverted.==1)) # maybe there is a better way to do the conversion
197388
end
198389

390+
199391
"""Sample (h, S) from the distribution P_n(h, S) from Bravyi and Maslov Algorithm 1."""
200392
function quantum_mallows(rng::AbstractRNG, n::Int) # each one is benchmarked in benchmarks/quantum_mallows.jl
201393
arr = collect(1:n)
202394
hadamard = falses(n)
203395
perm = zeros(Int64, n)
396+
397+
return _quantum_mallows!(rng, hadamard, perm, arr)
398+
end
399+
400+
function _quantum_mallows!(
401+
rng::AbstractRNG,
402+
hadamard::BitVector,
403+
perm::Vector{T},
404+
arr::Vector{T}) where {T<:Integer} # inplace version
405+
406+
n = length(perm)
204407
for idx in 1:n
205-
m = length(arr)
408+
m = n - idx + 1
206409
# sample h_i from given prob distribution
207410
l = sample_geometric_2(rng, 2 * m)
208411
weight = 2 * m - l
209412
hadamard[idx] = (weight < m)
210-
k = weight < m ? weight : 2*m - weight - 1
211-
perm[idx] = popat!(arr, k + 1)
413+
k = weight < m ? weight : 2 * m - weight - 1
414+
perm[idx] = _popat!(arr, k + 1)
212415
end
213416
return hadamard, perm
214417
end
215418

419+
function _popat!(arr, n)
420+
m = length(arr)
421+
at = arr[n]
422+
423+
for i in n:(m-1)
424+
arr[i] = arr[i+1]
425+
end
426+
427+
arr[end] = 0
428+
429+
return at
430+
end
431+
432+
216433
""" This function samples a number from 1 to `n` where `n >= 1`
217434
probability of outputting `i` is proportional to `2^i`"""
218435
function sample_geometric_2(rng::AbstractRNG, n::Integer)
@@ -263,6 +480,7 @@ function random_brickwork_clifford_circuit(rng::AbstractRNG, lattice_size::NTupl
263480
cartesian = CartesianIndices(lattice_size)
264481
dim = length(lattice_size)
265482
nqubits = prod(lattice_size)
483+
working_memory = RandDestabMemory(2)
266484
for i in 1:nlayers
267485
gate_direction = (i - 1) % dim + 1
268486
l = lattice_size[gate_direction]
@@ -273,7 +491,7 @@ function random_brickwork_clifford_circuit(rng::AbstractRNG, lattice_size::NTupl
273491
cardk = cardj
274492
cardk[gate_direction] = cardk[gate_direction] + 1
275493
k = LinearIndices(cartesian)[cardk...]
276-
push!(circ, SparseGate(random_clifford(rng, 2), [j, k]))
494+
push!(circ, SparseGate(random_clifford(rng, working_memory), [j, k]))
277495
end
278496
end
279497
end

test/test_allocations.jl

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
@testitem "Allocation checks" tags=[:alloccc] begin
2-
using QuantumClifford
3-
using QuantumClifford: mul_left!
1+
@testitem "Allocation checks" begin
2+
using QuantumClifford: mul_left!, RandDestabMemory, Tableau
3+
using Random
44
n = Threads.nthreads()
55
allocated(f::F) where {F} = @allocations f()
66
@testset "apply! mul_left! canonicalize!" begin
@@ -44,6 +44,20 @@
4444
@test allocated(f6) <= 2
4545
end
4646
end
47+
@testset "random_destabilizer" begin
48+
N = 100
49+
memory = RandDestabMemory(N)
50+
f1() = RandDestabMemory(N)
51+
f1()
52+
f2() = random_destabilizer(Random.GLOBAL_RNG, memory)
53+
f2()
54+
f3() = Destabilizer(Tableau(memory.phasesarray, memory.xzs))
55+
f3()
56+
@test allocated(f1) < 12.5 * N^2 + 50 * N + 1000
57+
if VERSION >= v"1.11"
58+
@test abs(allocated(f2) - allocated(f3)) / allocated(f3) < 0.05
59+
end
60+
end
4761
@testset "project!" begin
4862
N = 100
4963
d = random_destabilizer(N)

test/test_random.jl

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
@testitem "Random" begin
2+
using Random
23
using QuantumClifford
34
using QuantumClifford: stab_looks_good, destab_looks_good, mixed_stab_looks_good, mixed_destab_looks_good
45

@@ -32,6 +33,21 @@
3233
end
3334
end
3435

36+
@testset "Random sampling of operators memory reuse" begin
37+
for n in [1, test_sizes..., 200, 500]
38+
workingmemory = QuantumClifford.RandDestabMemory(n)
39+
for _ in 1:2
40+
seed = rand(1:100000)
41+
rng = Random.GLOBAL_RNG
42+
Random.seed!(rng, seed)
43+
non_reuse_version = random_destabilizer(rng, n)
44+
Random.seed!(rng, seed)
45+
reuse_version = random_destabilizer(rng, workingmemory)
46+
@test non_reuse_version == reuse_version
47+
end
48+
end
49+
end
50+
3551
@testset "Random Paulis" begin
3652
for n in [1, test_sizes..., 200,500]
3753
@test all((random_pauli(n).phase[] == 0 for _ in 1:100))

0 commit comments

Comments
 (0)