Skip to content

Commit ad3b7a3

Browse files
committed
Implement KrausOperators, conversions, tensor, checks, tests
1 parent 2b6763b commit ad3b7a3

File tree

4 files changed

+343
-15
lines changed

4 files changed

+343
-15
lines changed

src/QuantumOpticsBase.jl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ export Basis, GenericBasis, CompositeBasis, basis,
3636
current_time, time_shift, time_stretch, time_restrict, static_operator,
3737
#superoperators
3838
SuperOperator, DenseSuperOperator, DenseSuperOpType,
39-
SparseSuperOperator, SparseSuperOpType, spre, spost, sprepost, liouvillian,
40-
identitysuperoperator,
39+
SparseSuperOperator, SparseSuperOpType, ChoiState, KrausOperators,
40+
canonicalize, orthogonalize, make_trace_preserving, is_cptp, is_cptni,
41+
is_completely_positive, is_trace_preserving, is_trace_nonincreasing,
42+
spre, spost, sprepost, liouvillian, identitysuperoperator,
4143
#fock
4244
FockBasis, number, destroy, create,
4345
fockstate, coherentstate, coherentstate!,

src/pauli.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,9 +257,9 @@ ChiMatrix(ptm::DensePauliTransferMatrix) = ChiMatrix(SuperOperator(ptm))
257257

258258
"""Equality for all varieties of superoperators."""
259259
==(sop1::T, sop2::T) where T<:Union{DensePauliTransferMatrix, DenseSuperOpType, DenseChiMatrix} = sop1.data == sop2.data
260-
==(sop1::Union{DensePauliTransferMatrix, DenseSuperOpType, DenseChiMatrix}, sop2::Union{DensePauliTransferMatrix, DenseSuperOpType, DenseChiMatrix}) = false
260+
==(sop1::Union{DensePauliTransferMatrix, DenseSuperOpType, DenseChiMatrix}, sop2::Union{DensePauliTransferMatrix, DenseChiMatrix}) = false
261261

262262
"""Approximate equality for all varieties of superoperators."""
263-
function isapprox(sop1::T, sop2::T; kwargs...) where T<:Union{DensePauliTransferMatrix, DenseSuperOpType, DenseChiMatrix}
263+
function isapprox(sop1::T, sop2::T; kwargs...) where T<:Union{DensePauliTransferMatrix, DenseChiMatrix}
264264
return isapprox(sop1.data, sop2.data; kwargs...)
265265
end

src/superoperators.jl

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
1+
import Base: isapprox
12
import QuantumInterface: AbstractSuperOperator
23
import FastExpm: fastExpm
34

5+
# TODO: this should belong in QuantumInterface.jl
6+
abstract type OperatorBasis{BL<:Basis,BR<:Basis} end
7+
abstract type SuperOperatorBasis{BL<:OperatorBasis,BR<:OperatorBasis} end
8+
9+
"""
10+
tensor(E::AbstractSuperOperator, F::AbstractSuperOperator, G::AbstractSuperOperator...)
11+
12+
Tensor product ``\\mathcal{E}⊗\\mathcal{F}⊗\\mathcal{G}⊗…`` of the given super operators.
13+
"""
14+
tensor(a::AbstractSuperOperator, b::AbstractSuperOperator) = arithmetic_binary_error("Tensor product", a, b)
15+
tensor(op::AbstractSuperOperator) = op
16+
tensor(operators::AbstractSuperOperator...) = reduce(tensor, operators)
17+
18+
419
"""
520
SuperOperator <: AbstractSuperOperator
621
@@ -69,6 +84,9 @@ sparse(a::SuperOperator) = SuperOperator(a.basis_l, a.basis_r, sparse(a.data))
6984

7085
==(a::SuperOperator{B1,B2}, b::SuperOperator{B1,B2}) where {B1,B2} = (samebases(a,b) && a.data == b.data)
7186
==(a::SuperOperator, b::SuperOperator) = false
87+
isapprox(a::SuperOperator{B1,B2}, b::SuperOperator{B1,B2}; kwargs...) where {B1,B2} =
88+
(samebases(a,b) && isapprox(a.data, b.data; kwargs...))
89+
isapprox(a::SuperOperator, b::SuperOperator; kwargs...) = false
7290

7391
Base.length(a::SuperOperator) = length(a.basis_l[1])*length(a.basis_l[2])*length(a.basis_r[1])*length(a.basis_r[2])
7492
samebases(a::SuperOperator, b::SuperOperator) = samebases(a.basis_l[1], b.basis_l[1]) && samebases(a.basis_l[2], b.basis_l[2]) &&
@@ -318,10 +336,14 @@ end
318336
throw(IncompatibleBases())
319337
end
320338

339+
# TODO should all of PauliTransferMatrix, ChiMatrix, ChoiState, and KrausOperators subclass AbstractSuperOperator?
321340
"""
322341
ChoiState <: AbstractSuperOperator
323342
324343
Superoperator represented as a choi state.
344+
345+
The convention is chosen such that the input operators live in `(basis_l[1], basis_r[1])` while
346+
the output operators live in `(basis_r[2], basis_r[2])`.
325347
"""
326348
mutable struct ChoiState{B1,B2,T} <: AbstractSuperOperator{B1,B2}
327349
basis_l::B1
@@ -344,6 +366,27 @@ dagger(a::ChoiState) = ChoiState(dagger(SuperOperator(a)))
344366
*(a::ChoiState, b::ChoiState) = ChoiState(SuperOperator(a)*SuperOperator(b))
345367
*(a::ChoiState, b::Operator) = SuperOperator(a)*b
346368
==(a::ChoiState, b::ChoiState) = (SuperOperator(a) == SuperOperator(b))
369+
isapprox(a::ChoiState, b::ChoiState; kwargs...) = isapprox(SuperOperator(a), SuperOperator(b); kwargs...)
370+
371+
# Container to hold each of the four bases for a Choi operator when converting it to
372+
# an operator so that if any are CompositeBases tensor doesn't lossily collapse them
373+
struct ChoiSubBasis{S,B<:Basis} <: Basis
374+
shape::S
375+
basis::B
376+
end
377+
ChoiSubBasis(b::Basis) = ChoiSubBasis(b.shape, b)
378+
379+
# TODO: decide whether to document and export this
380+
choi_to_operator(c::ChoiState) = Operator(
381+
ChoiSubBasis(c.basis_l[2])ChoiSubBasis(c.basis_l[1]), ChoiSubBasis(c.basis_r[2])ChoiSubBasis(c.basis_r[1]), c.data)
382+
383+
function tensor(a::ChoiState, b::ChoiState)
384+
op = choi_to_operator(a) choi_to_operator(b)
385+
op = permutesystems(op, [1,3,2,4])
386+
ChoiState((a.basis_l[1] b.basis_l[1], a.basis_l[2] b.basis_l[2]),
387+
(a.basis_r[1] b.basis_r[1], a.basis_r[2] b.basis_r[2]), op.data)
388+
end
389+
tensor(a::SuperOperator, b::SuperOperator) = SuperOperator(tensor(ChoiState(a), ChoiState(b)))
347390

348391
# reshape swaps within systems due to colum major ordering
349392
# https://docs.qojulia.org/quantumobjects/operators/#tensor_order
@@ -372,6 +415,216 @@ end
372415
ChoiState(op::SuperOperator) = ChoiState(_super_choi(op.basis_l, op.basis_r, op.data)...)
373416
SuperOperator(op::ChoiState) = SuperOperator(_super_choi(op.basis_l, op.basis_r, op.data)...)
374417

418+
419+
"""
420+
KrausOperators <: AbstractSuperOperator
421+
422+
Superoperator represented as a list of Kraus operators.
423+
424+
Note that KrausOperators can only represent linear maps taking density operators to other
425+
(potentially unnormalized) density operators.
426+
In contrast the `SuperOperator` or `ChoiState` representations can represent arbitrary linear maps
427+
taking arbitrary operators defined on ``H_A \\to H_B`` to ``H_C \\to H_D``.
428+
In otherwords, the Kraus representation is only defined for completely positive linear maps of the form
429+
``(H_A \\to H_A) \\to (H_B \\to H_B)``.
430+
Thus converting from `SuperOperator` or `ChoiState` to `KrausOperators` will throw an exception if the
431+
map cannot be faithfully represented up to the specificed tolerance `tol`.
432+
433+
----------------------------
434+
Old text:
435+
Note unlike the SuperOperator or ChoiState types where it is possible to have
436+
`basis_l[1] != basis_l[2]` and `basis_r[1] != basis_r[2]`
437+
which allows representations of maps between general linear operators defined on ``H_A \\to H_B``,
438+
a quantum channel can only act on valid density operators which live in ``H_A \\to H_A``.
439+
Thus the Kraus representation is only defined for quantum channels which map
440+
``(H_A \\to H_A) \\to (H_B \\to H_B)``.
441+
"""
442+
mutable struct KrausOperators{B1,B2,T} <: AbstractSuperOperator{B1,B2}
443+
basis_l::B1
444+
basis_r::B2
445+
data::Vector{T}
446+
function KrausOperators{BL,BR,T}(basis_l::BL, basis_r::BR, data::Vector{T}) where {BL,BR,T}
447+
if (any(!samebases(basis_r, M.basis_r) for M in data) ||
448+
any(!samebases(basis_l, M.basis_l) for M in data))
449+
throw(DimensionMismatch("Tried to assign data with incompatible bases"))
450+
end
451+
452+
new(basis_l, basis_r, data)
453+
end
454+
end
455+
KrausOperators{BL,BR}(b1::BL,b2::BR,data::Vector{T}) where {BL,BR,T} = KrausOperators{BL,BR,T}(b1,b2,data)
456+
KrausOperators(b1::BL,b2::BR,data::Vector{T}) where {BL,BR,T} = KrausOperators{BL,BR,T}(b1,b2,data)
457+
458+
dense(a::KrausOperators) = KrausOperators(a.basis_l, a.basis_r, [dense(op) for op in a.data])
459+
sparse(a::KrausOperators) = KrausOperators(a.basis_l, a.basis_r, [sparse(op) for op in a.data])
460+
dagger(a::KrausOperators) = KrausOperators(a.basis_r, a.basis_l, [dagger(op) for op in a.data])
461+
*(a::KrausOperators{B1,B2}, b::KrausOperators{B2,B3}) where {B1,B2,B3} =
462+
KrausOperators(a.basis_l, b.basis_r, [A*B for A in a.data for B in b.data])
463+
*(a::KrausOperators, b::KrausOperators) = throw(IncompatibleBases())
464+
*(a::KrausOperators{BL,BR}, b::Operator{BR,BR}) where {BL,BR} = sum(op*b*dagger(op) for op in a.data)
465+
==(a::KrausOperators, b::KrausOperators) = (SuperOperator(a) == SuperOperator(b))
466+
isapprox(a::KrausOperators, b::KrausOperators; kwargs...) = isapprox(SuperOperator(a), SuperOperator(b); kwargs...)
467+
tensor(a::KrausOperators, b::KrausOperators) =
468+
KrausOperators(a.basis_l b.basis_l, a.basis_r b.basis_r,
469+
[A B for A in a.data for B in b.data])
470+
471+
"""
472+
orthogonalize(kraus::KrausOperators; tol=√eps)
473+
474+
Orthogonalize the set kraus operators by performing a qr decomposition on their vec'd operators.
475+
Note that this is different than `canonicalize` which returns a kraus decomposition such
476+
that the kraus operators are Hilbert–Schmidt orthorgonal.
477+
478+
If the input dimension is d and output dimension is d' then the number of kraus
479+
operators returned is guaranteed to be no greater than dd', however it may be greater
480+
than the Kraus rank.
481+
482+
`orthogonalize` should always be much faster than canonicalize as it avoids an explicit eigendecomposition
483+
and thus also preserves sparsity if the kraus operators are sparse.
484+
"""
485+
function orthogonalize(kraus::KrausOperators; tol=_get_tol(kraus))
486+
bl, br = kraus.basis_l, kraus.basis_r
487+
dim = length(bl)*length(br)
488+
489+
A = stack(reshape(op.data, dim) for op in kraus.data; dims=1)
490+
F = qr(A; tol=tol)
491+
# rank(F) for some reason doesn't work but should
492+
rank = maximum(findnz(F.R)[1])
493+
# Sanity checks that help illustrate what qr() returns:
494+
# @assert (F.R ≈ (sparse(F.Q') * A[F.prow,F.pcol])[1:dim,:])
495+
# @assert (all(iszero(F.R[rank+1:end,:])))
496+
497+
ops = [Operator(bl, br, copy(reshape( # copy materializes reshaped view
498+
F.R[i,invperm(F.pcol)], (length(bl), length(br))))) for i=1:rank]
499+
return KrausOperators(bl, br, ops)
500+
end
501+
502+
"""
503+
canonicalize(kraus::KrausOperators; tol=√eps)
504+
505+
Transform the quantum channel into canonical form such that the kraus operators ``{A_k}``
506+
are Hilbert–Schmidt orthorgonal:
507+
508+
```math
509+
\\Tr A_i^\\dagger A_j \\sim \\delta_{i,j}
510+
```
511+
512+
If the input dimension is d and output dimension is d' then the number of kraus
513+
operators returned is guaranteed to be no greater than dd' and will furthermore
514+
be equal the Kraus rank of the channel up to numerical imprecision controlled by `tol`.
515+
"""
516+
canonicalize(kraus::KrausOperators; tol=_get_tol(kraus)) = KrausOperators(ChoiState(kraus); tol=tol)
517+
518+
# TODO: document
519+
function make_trace_preserving(kraus; tol=_get_tol(kraus))
520+
m = I - sum(dagger(M)*M for M in kraus.data).data
521+
if isa(_positive_eigen(m, tol), Number)
522+
throw(ArgumentError("Channel must be trace nonincreasing"))
523+
end
524+
K = Operator(kraus.basis_l, kraus.basis_r, sqrt(Matrix(m)))
525+
return KrausOperators(kraus.basis_l, kraus.basis_r, [kraus.data; K])
526+
end
527+
528+
SuperOperator(kraus::KrausOperators) =
529+
SuperOperator((kraus.basis_l, kraus.basis_l), (kraus.basis_r, kraus.basis_r),
530+
(sum(conj(op)op for op in kraus.data)).data)
531+
532+
ChoiState(kraus::KrausOperators) =
533+
ChoiState((kraus.basis_r, kraus.basis_l), (kraus.basis_r, kraus.basis_l),
534+
(sum((M=op.data; reshape(M, (length(M), 1))*reshape(M, (1, length(M))))
535+
for op in kraus.data)))
536+
537+
_choi_state_maps_density_ops(choi::ChoiState) = (samebases(choi.basis_l[1], choi.basis_r[1]) &&
538+
samebases(choi.basis_l[2], choi.basis_r[2]))
539+
540+
# TODO: consider using https://github.com/jlapeyre/IsApprox.jl
541+
_is_hermitian(M, tol) = ishermitian(M) || isapprox(M, M', atol=tol)
542+
_is_identity(M, tol) = isapprox(M, I, atol=tol)
543+
544+
# TODO: document
545+
# data must be Hermitian!
546+
function _positive_eigen(data, tol)
547+
# LinearAlgebra's eigen returns eigenvals sorted smallest to largest for Hermitian matrices
548+
vals, vecs = eigen(Hermitian(Matrix(data)))
549+
vals[1] < -tol && return vals[1]
550+
ret = [(val, vecs[:,i]) for (i, val) in enumerate(vals) if val > tol]
551+
return ret
552+
end
553+
554+
function KrausOperators(choi::ChoiState; tol=_get_tol(choi))
555+
if !_choi_state_maps_density_ops(choi)
556+
throw(DimensionMismatch("Tried to convert Choi state of something that isn't a quantum channel mapping density operators to density operators"))
557+
end
558+
if !_is_hermitian(choi.data, tol)
559+
throw(ArgumentError("Tried to convert nonhermitian Choi state"))
560+
end
561+
bl, br = choi.basis_l[2], choi.basis_l[1]
562+
eigs = _positive_eigen(choi.data, tol)
563+
if isa(eigs, Number)
564+
throw(ArgumentError("Tried to convert a non-positive semidefinite Choi state,"*
565+
"failed for smallest eigval $(eigs), consider increasing tol=$(tol)"))
566+
end
567+
568+
ops = [Operator(bl, br, sqrt(val)*reshape(vec, length(bl), length(br))) for (val, vec) in eigs]
569+
return KrausOperators(bl, br, ops)
570+
end
571+
572+
KrausOperators(op::SuperOperator; tol=_get_tol(op)) = KrausOperators(ChoiState(op); tol=tol)
573+
574+
# TODO: document superoperator representation precident: everything of mixed type returns SuperOperator
375575
*(a::ChoiState, b::SuperOperator) = SuperOperator(a)*b
376576
*(a::SuperOperator, b::ChoiState) = a*SuperOperator(b)
577+
*(a::KrausOperators, b::SuperOperator) = SuperOperator(a)*b
578+
*(a::SuperOperator, b::KrausOperators) = a*SuperOperator(b)
579+
*(a::KrausOperators, b::ChoiState) = SuperOperator(a)*SuperOperator(b)
580+
*(a::ChoiState, b::KrausOperators) = SuperOperator(a)*SuperOperator(b)
581+
582+
_get_tol(kraus::KrausOperators) = sqrt(eps(real(eltype(eltype(fieldtypes(typeof(kraus))[3])))))
583+
_get_tol(super::SuperOperator) = sqrt(eps(real(eltype(fieldtypes(typeof(super))[3]))))
584+
_get_tol(super::ChoiState) = sqrt(eps(real(eltype(fieldtypes(typeof(super))[3]))))
585+
586+
# TODO: document this
587+
is_completely_positive(choi::KrausOperators; tol=_get_tol(choi)) = true
588+
589+
function is_completely_positive(choi::ChoiState; tol=_get_tol(choi))
590+
_choi_state_maps_density_ops(choi) || return false
591+
_is_hermitian(choi.data, tol) || return false
592+
isa(_positive_eigen(Hermitian(choi.data), tol), Number) && return false
593+
return true
594+
end
595+
596+
is_completely_positive(super::SuperOperator; tol=_get_tol(super)) =
597+
is_completely_positive(ChoiState(super); tol=tol)
598+
599+
# TODO: document this
600+
is_trace_preserving(kraus::KrausOperators; tol=_get_tol(kraus)) =
601+
_is_identity(sum(dagger(M)*M for M in kraus.data).data, tol)
602+
603+
is_trace_preserving(choi::ChoiState; tol=_get_tol(choi)) =
604+
_is_identity(ptrace(choi_to_operator(choi), 1).data, tol)
605+
606+
is_trace_preserving(super::SuperOperator; tol=_get_tol(super)) =
607+
is_trace_preserving(ChoiState(super); tol=tol)
608+
609+
# TODO: document this
610+
function is_trace_nonincreasing(kraus::KrausOperators; tol=_get_tol(kraus))
611+
m = I - sum(dagger(M)*M for M in kraus.data).data
612+
_is_hermitian(m, tol) || return false
613+
return !isa(_positive_eigen(Hermitian(m), tol), Number)
614+
end
615+
616+
function is_trace_nonincreasing(choi::ChoiState; tol=_get_tol(choi))
617+
m = I - ptrace(choi_to_operator(choi), 1).data
618+
_is_hermitian(m, tol) || return false
619+
return !isa(_positive_eigen(Hermitian(m), tol), Number)
620+
end
621+
622+
is_trace_nonincreasing(super::SuperOperator; tol=_get_tol(super)) =
623+
is_trace_nonincreasing(ChoiState(super); tol=tol)
624+
625+
# TODO: document this
626+
is_cptp(sop; tol=_get_tol(sop)) = is_completely_positive(sop; tol=tol) && is_trace_preserving(sop; tol=tol)
627+
628+
# TODO: document this
629+
is_cptni(sop; tol=_get_tol(sop)) = is_completely_positive(sop; tol=tol) && is_trace_nonincreasing(sop; tol=tol)
377630

0 commit comments

Comments
 (0)