Skip to content

Commit 5390341

Browse files
committed
Cleanup, TODOs, and tests
1 parent ef13d6b commit 5390341

File tree

2 files changed

+170
-77
lines changed

2 files changed

+170
-77
lines changed

src/superoperators.jl

Lines changed: 115 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -319,62 +319,13 @@ end
319319
end
320320

321321
# TODO should all of PauliTransferMatrix, ChiMatrix, ChoiState, and KrausOperators subclass AbstractSuperOperator?
322-
"""
323-
KrausOperators(B1, B2, data)
324-
325-
Superoperator represented as a list of Kraus operators.
326-
Note unlike the SuperOperator or ChoiState types where
327-
its possible to have basis_l[1] != basis_l[2] and basis_r[1] != basis_r[2]
328-
which allows representations of maps between general linear operators defined on H_A \to H_B,
329-
a quantum channel can only act on valid density operators which live in H_A \to H_A.
330-
Thus the Kraus representation is only defined for quantum channels which map
331-
(H_A \to H_A) \to (H_B \to H_B).
332-
"""
333-
mutable struct KrausOperators{B1,B2,T} <: AbstractSuperOperator{B1,B2}
334-
basis_l::B1
335-
basis_r::B2
336-
data::T
337-
function KrausOperators{BL,BR,T}(basis_l::BL, basis_r::BR, data::T) where {BL,BR,T}
338-
if (any(!samebases(basis_r, M.basis_r) for M in data) ||
339-
any(!samebases(basis_l, M.basis_l) for M in data))
340-
throw(DimensionMismatch("Tried to assign data with incompatible bases"))
341-
end
342-
343-
new(basis_l, basis_r, data)
344-
end
345-
end
346-
KrausOperators{BL,BR}(b1::BL,b2::BR,data::T) where {BL,BR,T} = KrausOperators{BL,BR,T}(b1,b2,data)
347-
KrausOperators(b1::BL,b2::BR,data::T) where {BL,BR,T} = KrausOperators{BL,BR,T}(b1,b2,data)
348-
KrausOperators(b,data) = KrausOperators(b,b,data)
349-
tensor(a::KrausOperators, b::KrausOperators) =
350-
KrausOperators(a.basis_l b.basis_l, a.basis_r b.basis_r,
351-
[A B for A in a.data for B in b.data])
352-
dagger(a::KrausOperators) = KrausOperators(a.basis_r, a.basis_l, [dagger(op) for op in a.data])
353-
*(a::KrausOperators{B1,B2}, b::KrausOperators{B2,B3}) where {B1,B2,B3} =
354-
KrausOperators(a.basis_l, b.basis_r, [A*B for A in a.data for B in b.data])
355-
*(a::KrausOperators, b::KrausOperators) = throw(IncompatibleBases())
356-
*(a::KrausOperators{BL,BR}, b::Operator{BR,BR}) where {BL,BR} =
357-
sum(op*b*dagger(op) for op in a.data)
358-
359-
function minimize_kraus_rank(kraus; tol=1e-9)
360-
bl, br = kraus.basis_l, kraus.basis_r
361-
dim = length(bl)*length(br)
362-
363-
A = stack(reshape(op.data, dim) for op in kraus.data; dims=1)
364-
F = qr(A; tol=tol)
365-
rank = maximum(findnz(F.R)[1]) # rank(F) doesn't work but should
366-
#@assert (F.R ≈ (sparse(F.Q') * A[F.prow,F.pcol])[1:dim,:])
367-
#@assert (all(iszero(F.R[rank+1:end,:])))
368-
369-
ops = [Operator(bl, br, copy(reshape( # copy materializes reshaped view
370-
F.R[i,invperm(F.pcol)], (length(bl), length(br))))) for i=1:rank]
371-
return KrausOperators(bl, br, ops)
372-
end
373-
374322
"""
375323
ChoiState <: AbstractSuperOperator
376324
377325
Superoperator represented as a choi state.
326+
327+
The convention is chosen such that the input operators live in `(basis_l[1], basis_r[1])` while
328+
the output operators live in `(basis_r[2], basis_r[2])`.
378329
"""
379330
mutable struct ChoiState{B1,B2,T} <: AbstractSuperOperator{B1,B2}
380331
basis_l::B1
@@ -399,6 +350,9 @@ dagger(a::ChoiState) = ChoiState(dagger(SuperOperator(a)))
399350
*(a::ChoiState, b::Operator) = SuperOperator(a)*b
400351
==(a::ChoiState, b::ChoiState) = (SuperOperator(a) == SuperOperator(b))
401352

353+
# TOOD: decide whether to document and export this
354+
choi_to_operator(c::ChoiState) = Operator(c.basis_l[1]c.basis_l[2], c.basis_r[1]c.basis_r[2], c.data)
355+
402356
# reshape swaps within systems due to colum major ordering
403357
# https://docs.qojulia.org/quantumobjects/operators/#tensor_order
404358
function _super_choi((l1, l2), (r1, r2), data)
@@ -426,50 +380,133 @@ end
426380
ChoiState(op::SuperOperator) = ChoiState(_super_choi(op.basis_l, op.basis_r, op.data)...)
427381
SuperOperator(op::ChoiState) = SuperOperator(_super_choi(op.basis_l, op.basis_r, op.data)...)
428382

429-
*(a::ChoiState, b::SuperOperator) = SuperOperator(a)*b
430-
*(a::SuperOperator, b::ChoiState) = a*SuperOperator(b)
383+
384+
"""
385+
KrausOperators <: AbstractSuperOperator
386+
387+
Superoperator represented as a list of Kraus operators.
388+
Note unlike the SuperOperator or ChoiState types where it is possible to have
389+
`basis_l[1] != basis_l[2]` and `basis_r[1] != basis_r[2]`
390+
which allows representations of maps between general linear operators defined on ``H_A \\to H_B``,
391+
a quantum channel can only act on valid density operators which live in ``H_A \\to H_A``.
392+
Thus the Kraus representation is only defined for quantum channels which map
393+
``(H_A \\to H_A) \\to (H_B \\to H_B)``.
394+
"""
395+
mutable struct KrausOperators{B1,B2,T} <: AbstractSuperOperator{B1,B2}
396+
basis_l::B1
397+
basis_r::B2
398+
data::T
399+
function KrausOperators{BL,BR,T}(basis_l::BL, basis_r::BR, data::T) where {BL,BR,T}
400+
if (any(!samebases(basis_r, M.basis_r) for M in data) ||
401+
any(!samebases(basis_l, M.basis_l) for M in data))
402+
throw(DimensionMismatch("Tried to assign data with incompatible bases"))
403+
end
404+
405+
new(basis_l, basis_r, data)
406+
end
407+
end
408+
KrausOperators{BL,BR}(b1::BL,b2::BR,data::T) where {BL,BR,T} = KrausOperators{BL,BR,T}(b1,b2,data)
409+
KrausOperators(b1::BL,b2::BR,data::T) where {BL,BR,T} = KrausOperators{BL,BR,T}(b1,b2,data)
410+
411+
tensor(a::KrausOperators, b::KrausOperators) =
412+
KrausOperators(a.basis_l b.basis_l, a.basis_r b.basis_r,
413+
[A B for A in a.data for B in b.data])
414+
dagger(a::KrausOperators) = KrausOperators(a.basis_r, a.basis_l, [dagger(op) for op in a.data])
415+
*(a::KrausOperators{B1,B2}, b::KrausOperators{B2,B3}) where {B1,B2,B3} =
416+
KrausOperators(a.basis_l, b.basis_r, [A*B for A in a.data for B in b.data])
417+
*(a::KrausOperators, b::KrausOperators) = throw(IncompatibleBases())
418+
*(a::KrausOperators{BL,BR}, b::Operator{BR,BR}) where {BL,BR} = sum(op*b*dagger(op) for op in a.data)
419+
420+
"""
421+
canonicalize(kraus::KrausOperators; tol=1e-9)
422+
423+
Canonicalize the set kraus operators by performing a qr decomposition.
424+
A quantum channel with kraus operators ``{A_k}`` is in cannonical form if and only if
425+
426+
```math
427+
\\Tr A_i^\\dagger A_j \\sim \\delta_{i,j}
428+
```
429+
430+
If the input dimension is d and output dimension is d' then the number of kraus
431+
operators returned is guaranteed to be no greater than dd' and will furthermore
432+
be equal the Kraus rank of the channel up to numerical imprecision controlled by `tol`.
433+
"""
434+
function canonicalize(kraus::KrausOperators; tol=1e-9)
435+
bl, br = kraus.basis_l, kraus.basis_r
436+
dim = length(bl)*length(br)
437+
438+
A = stack(reshape(op.data, dim) for op in kraus.data; dims=1)
439+
F = qr(A; tol=tol)
440+
# rank(F) for some reason doesn't work but should
441+
rank = maximum(findnz(F.R)[1])
442+
# Sanity checks that help illustrate what qr() returns:
443+
# @assert (F.R ≈ (sparse(F.Q') * A[F.prow,F.pcol])[1:dim,:])
444+
# @assert (all(iszero(F.R[rank+1:end,:])))
445+
446+
ops = [Operator(bl, br, copy(reshape( # copy materializes reshaped view
447+
F.R[i,invperm(F.pcol)], (length(bl), length(br))))) for i=1:rank]
448+
return KrausOperators(bl, br, ops)
449+
end
450+
451+
# TODO: check if canonicalize and orthogonalize are equivalent
452+
orthogonalize(kraus::KrausOperators; tol=1e-9) = KrausOperators(ChoiState(kraus); tol=tol)
431453

432454
SuperOperator(kraus::KrausOperators) =
433455
SuperOperator((kraus.basis_l, kraus.basis_l), (kraus.basis_r, kraus.basis_r),
434456
(sum(conj(op)op for op in kraus.data)).data)
435457

436-
ChoiState(kraus::KrausOperators; tol=1e-9) =
458+
ChoiState(kraus::KrausOperators) =
437459
ChoiState((kraus.basis_r, kraus.basis_l), (kraus.basis_r, kraus.basis_l),
438460
(sum((M=op.data; reshape(M, (length(M), 1))*reshape(M, (1, length(M))))
439-
for op in kraus.data)); tol=tol)
461+
for op in kraus.data)))
440462

441-
function KrausOperators(choi::ChoiState; tol=1e-9)
463+
function KrausOperators(choi::ChoiState; tol=1e-9, warn=true)
442464
if (!samebases(choi.basis_l[1], choi.basis_r[1]) ||
443465
!samebases(choi.basis_l[2], choi.basis_r[2]))
444-
throw(DimensionMismatch("Tried to convert choi state of something that isn't a quantum channel mapping density operators to density operators"))
466+
throw(DimensionMismatch("Tried to convert Choi state of something that isn't a quantum channel mapping density operators to density operators"))
467+
end
468+
# TODO: consider using https://github.com/jlapeyre/IsApprox.jl
469+
if !ishermitian(choi.data) || !isapprox(choi.data, choi.data', atol=tol)
470+
throw(ArgumentError("Tried to convert nonhermitian Choi state"))
445471
end
446472
bl, br = choi.basis_l[2], choi.basis_l[1]
447-
#ishermitian(choi.data) || @warn "ChoiState is not hermitian"
448473
# TODO: figure out how to do this with sparse matrices using e.g. Arpack.jl or ArnoldiMethod.jl
449474
vals, vecs = eigen(Hermitian(Matrix(choi.data)))
450475
for val in vals
451-
(abs(val) > tol && val < 0) && @warn "eigval $(val) < 0 but abs(eigval) > tol=$(tol)"
476+
if warn && (abs(val) > tol && val < 0)
477+
@warn "eigval $(val) < 0 but abs(eigval) > tol=$(tol)"
478+
end
452479
end
453480
ops = [Operator(bl, br, sqrt(val)*reshape(vecs[:,i], length(bl), length(br)))
454481
for (i, val) in enumerate(vals) if abs(val) > tol && val > 0]
455482
return KrausOperators(bl, br, ops)
456483
end
457484

458-
KrausOperators(op::SuperOperator; tol=1e-9) = KrausOperators(ChoiState(op; tol=tol); tol=tol)
485+
KrausOperators(op::SuperOperator; tol=1e-9) = KrausOperators(ChoiState(op); tol=tol)
459486

487+
# TODO: document superoperator representation precident: everything of mixed type returns SuperOperator
460488
*(a::ChoiState, b::SuperOperator) = SuperOperator(a)*b
461489
*(a::SuperOperator, b::ChoiState) = a*SuperOperator(b)
462490
*(a::KrausOperators, b::SuperOperator) = SuperOperator(a)*b
463491
*(a::SuperOperator, b::KrausOperators) = a*SuperOperator(b)
464492
*(a::KrausOperators, b::ChoiState) = SuperOperator(a)*SuperOperator(b)
465493
*(a::ChoiState, b::KrausOperators) = SuperOperator(a)*SuperOperator(b)
466494

467-
function is_trace_preserving(kraus::KrausOperators; tol=1e-9)
468-
m = I(length(kraus.basis_r)) - sum(dagger(M)*M for M in kraus.data).data
469-
m[abs.(m) .< tol] .= 0
470-
return iszero(m)
495+
# TODO: document this
496+
is_trace_preserving(kraus::KrausOperators; tol=1e-9) =
497+
isapprox.(I(length(kraus.basis_r)) - sum(dagger(M)*M for M in kraus.data).data, atol=tol)
498+
499+
function is_trace_preserving(choi::ChoiState; tol=1e-9)
500+
if (!samebases(choi.basis_l[1], choi.basis_r[1]) ||
501+
!samebases(choi.basis_l[2], choi.basis_r[2]))
502+
throw(DimensionMismatch("Choi state is of something that isn't a quantum channel mapping density operators to density operators"))
503+
end
504+
bl, br = choi.basis_l[2], choi.basis_l[1]
505+
return isapprox.(I(length(br)) - ptrace(choi_to_operator(choi), 2), atol=tol)
471506
end
472507

508+
is_trace_preserving(super::SuperOperator; tol=1e-9) = is_trace_preserving(ChoiState(super); tol=tol)
509+
473510
# this check seems suspect... since it fails while the below check on choi succeeeds
474511
#function is_valid_channel(kraus::KrausOperators; tol=1e-9)
475512
# m = I(length(kraus.basis_r)) - sum(dagger(M)*M for M in kraus.data).data
@@ -484,8 +521,20 @@ end
484521
# return iszero(eigs)
485522
#end
486523

487-
function is_valid_channel(choi::ChoiState; tol=1e-9)
524+
# TODO: pull out check for valid quantum channel
525+
# TODO: document this
526+
function is_completely_positive(choi::ChoiState; tol=1e-9)
527+
if (!samebases(choi.basis_l[1], choi.basis_r[1]) ||
528+
!samebases(choi.basis_l[2], choi.basis_r[2]))
529+
throw(DimensionMismatch("Choi state is of something that isn't a quantum channel mapping density operators to density operators"))
530+
end
488531
any(abs.(choi.data - choi.data') .> tol) && return false
489532
eigs = eigvals(Hermitian(Matrix(choi.data)))
490533
return all(@. abs(eigs) < tol || eigs > 0)
491534
end
535+
536+
is_completely_positive(kraus::KrausOperators; tol=1e-9) = is_completely_positive(ChoiState(kraus); tol=tol)
537+
is_completely_positive(super::SuperOperator; tol=1e-9) = is_completely_positive(ChoiState(super); tol=tol)
538+
539+
# TODO: document this
540+
is_cptp(sop) = is_completly_positive(sop) && is_trace_preserving(sop)

test/test_superoperators.jl

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -247,20 +247,64 @@ N = exp(log(2) * sparse(L)) # 50% loss channel
247247
@test (0.5 - real(tr^2))) < 1e-10 # one photon state becomes maximally mixed
248248
@test tracedistance(ρ, normalize(dm(fockstate(b, 0)) + dm(fockstate(b, 1)))) < 1e-10
249249

250-
# Testing 0-2-4 binomial code encoder
250+
# 0-2-4 binomial code encoder
251251
b_logical = SpinBasis(1//2)
252252
b_fock = FockBasis(5)
253253
z_l = normalize(fockstate(b_fock, 0) + fockstate(b_fock, 4))
254254
o_l = fockstate(b_fock, 2)
255-
encoder_kraus = z_l dagger(spinup(b_logical)) + o_l dagger(spindown(b_logical))
256-
encoder_sup = sprepost(encoder_kraus, dagger(encoder_kraus))
257-
decoder_sup = sprepost(dagger(encoder_kraus), encoder_kraus)
258-
@test SuperOperator(ChoiState(encoder_sup)).data == encoder_sup.data
259-
@test decoder_sup == dagger(encoder_sup)
260-
@test ChoiState(decoder_sup) == dagger(ChoiState(encoder_sup))
261-
@test decoder_sup*encoder_sup dense(identitysuperoperator(b_logical))
262-
@test decoder_sup*ChoiState(encoder_sup) dense(identitysuperoperator(b_logical))
263-
@test ChoiState(decoder_sup)*encoder_sup dense(identitysuperoperator(b_logical))
264-
@test SuperOperator(ChoiState(decoder_sup)*ChoiState(encoder_sup)) dense(identitysuperoperator(b_logical))
255+
enc_proj = z_l dagger(spinup(b_logical)) + o_l dagger(spindown(b_logical))
256+
dec_proj = dagger(enc_proj)
257+
enc_sup = sprepost(enc_proj, dec_proj)
258+
dec_sup = sprepost(dec_proj, enc_proj)
259+
enc_kraus = KrausOperators(b_fock, b_logical, [enc_proj])
260+
dec_kraus = KrausOperators(b_logical, b_fock, [dec_proj])
261+
## testing conversions
262+
@test dec_sup == dagger(enc_sup)
263+
@test dec_kraus == dagger(enc_kraus)
264+
@test ChoiState(enc_sup) == ChoiState(enc_kraus)
265+
@test ChoiState(dec_sup) == dagger(ChoiState(enc_sup))
266+
@test ChoiState(dec_kraus) == dagger(ChoiState(enc_kraus))
267+
@test SuperOperator(ChoiState(enc_sup)) == enc_sup
268+
@test SuperOperator(KrausOperators(enc_sup)) == enc_sup
269+
@test KrausOperators(ChoiState(enc_kraus)) == enc_kraus
270+
@test KrausOperators(SuperOperator(enc_kraus)) == enc_kraus
271+
## testing multipication
272+
@test dec_sup*enc_sup dense(identitysuperoperator(b_logical))
273+
@test SuperOperator(dec_kraus*enc_kraus) dense(identitysuperoperator(b_logical))
274+
@test dec_sup*enc_kraus dense(identitysuperoperator(b_logical))
275+
@test dec_kraus*enc_sup dense(identitysuperoperator(b_logical))
276+
@test SuperOperator(dec_kraus*enc_kraus) dense(identitysuperoperator(b_logical))
277+
@test dec_sup*ChoiState(enc_sup) dense(identitysuperoperator(b_logical))
278+
@test ChoiState(dec_sup)*enc_sup dense(identitysuperoperator(b_logical))
279+
@test SuperOperator(ChoiState(dec_sup)*ChoiState(enc_sup)) dense(identitysuperoperator(b_logical))
280+
281+
@test avg_gate_fidelity(dec_sup*enc_sup) 1
282+
@test avg_gate_fidelity(dec_kraus*enc_kraus) 1
283+
@test avg_gate_fidelity(ChoiState(dec_sup)*ChoiState(enc_sup)) 1
284+
285+
# test amplitude damping channel
286+
function amplitude_damp_kraus_op(b, γ, l)
287+
op = SparseOperator(b)
288+
for n=l:(length(b)-1)
289+
op.data[n-l+1, n+1] = sqrt(binomial(n,l) * (1-γ)^(n-l) * γ^l)
290+
end
291+
return op
292+
end
293+
294+
function test_kraus_channel(N, γ, tol)
295+
b = FockBasis(N)
296+
super = exp(liouvillian(identityoperator(b), [destroy(b)]))
297+
kraus = KrausOperators(b, b, [amplitude_damp_kraus_op(b, γ, i) for i=0:(N-1)])
298+
@test SuperOperator(kraus) super
299+
@test ChoiState(kraus) ChoiState(super)
300+
@test kraus KrausOperators(super; tol=tol)
301+
@test is_trace_preserving(kraus; tol=tol)
302+
@test is_valid_channel(kraus; tol=tol)
303+
end
304+
305+
test_kraus_channel(10, 0.1, 1e-8)
306+
test_kraus_channel(20, 0.1, 1e-8)
307+
test_kraus_channel(10, 0.01, 1e-8)
308+
test_kraus_channel(20, 0.01, 1e-8)
265309

266310
end # testset

0 commit comments

Comments
 (0)