Skip to content

Commit b11c3c8

Browse files
committed
Implement KrausOperators, conversions, tensor, checks, tests
1 parent 730eb9e commit b11c3c8

File tree

4 files changed

+344
-16
lines changed

4 files changed

+344
-16
lines changed

src/QuantumOpticsBase.jl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ export Basis, GenericBasis, CompositeBasis, basis,
3939
current_time, time_shift, time_stretch, time_restrict, static_operator,
4040
#superoperators
4141
SuperOperator, DenseSuperOperator, DenseSuperOpType,
42-
SparseSuperOperator, SparseSuperOpType, spre, spost, sprepost, liouvillian,
43-
identitysuperoperator,
42+
SparseSuperOperator, SparseSuperOpType, ChoiState, KrausOperators,
43+
canonicalize, orthogonalize, make_trace_preserving, is_cptp, is_cptni,
44+
is_completely_positive, is_trace_preserving, is_trace_nonincreasing,
45+
spre, spost, sprepost, liouvillian, identitysuperoperator,
4446
#fock
4547
FockBasis, number, destroy, create,
4648
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: 254 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,22 @@
1+
import Base: isapprox
12
import QuantumInterface: AbstractSuperOperator
23
import FastExpm: fastExpm
34
import Adapt
45

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

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

7492
Base.length(a::SuperOperator) = length(a.basis_l[1])*length(a.basis_l[2])*length(a.basis_r[1])*length(a.basis_r[2])
7593
samebases(a::SuperOperator, b::SuperOperator) = samebases(a.basis_l[1], b.basis_l[1]) && samebases(a.basis_l[2], b.basis_l[2]) &&
@@ -130,7 +148,7 @@ function spre(op::AbstractOperator)
130148
if !samebases(op.basis_l, op.basis_r)
131149
throw(ArgumentError("It's not clear what spre of a non-square operator should be. See issue #113"))
132150
end
133-
SuperOperator((op.basis_l, op.basis_l), (op.basis_r, op.basis_r), tensor(op, identityoperator(op)).data)
151+
SuperOperator((op.basis_l, op.basis_l), (op.basis_r, op.basis_r), kron(identityoperator(op).data, op.data))
134152
end
135153

136154
"""
@@ -319,10 +337,14 @@ end
319337
throw(IncompatibleBases())
320338
end
321339

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

349392
# reshape swaps within systems due to colum major ordering
350393
# https://docs.qojulia.org/quantumobjects/operators/#tensor_order
@@ -359,8 +402,218 @@ end
359402
ChoiState(op::SuperOperator) = ChoiState(_super_choi(op.basis_l, op.basis_r, op.data)...)
360403
SuperOperator(op::ChoiState) = SuperOperator(_super_choi(op.basis_l, op.basis_r, op.data)...)
361404

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

365618
# GPU adaptation
366619
Adapt.adapt_structure(to, x::SuperOperator) = SuperOperator(x.basis_l, x.basis_r, Adapt.adapt(to, x.data))

0 commit comments

Comments
 (0)