1+ import Base: isapprox
12import QuantumInterface: AbstractSuperOperator
23import 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
7391Base. length (a:: SuperOperator ) = length (a. basis_l[1 ])* length (a. basis_l[2 ])* length (a. basis_r[1 ])* length (a. basis_r[2 ])
7492samebases (a:: SuperOperator , b:: SuperOperator ) = samebases (a. basis_l[1 ], b. basis_l[1 ]) && samebases (a. basis_l[2 ], b. basis_l[2 ]) &&
@@ -129,7 +147,7 @@ function spre(op::AbstractOperator)
129147 if ! samebases (op. basis_l, op. basis_r)
130148 throw (ArgumentError (" It's not clear what spre of a non-square operator should be. See issue #113" ))
131149 end
132- SuperOperator ((op. basis_l, op. basis_l), (op. basis_r, op. basis_r), tensor (op, identityoperator (op)) . data)
150+ SuperOperator ((op. basis_l, op. basis_l), (op. basis_r, op. basis_r), kron ( identityoperator (op). data, op . data) )
133151end
134152
135153"""
@@ -318,10 +336,14 @@ end
318336 throw (IncompatibleBases ())
319337end
320338
339+ # TODO should all of PauliTransferMatrix, ChiMatrix, ChoiState, and KrausOperators subclass AbstractSuperOperator?
321340"""
322341 ChoiState <: AbstractSuperOperator
323342
324343Superoperator 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"""
326348mutable 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
372415ChoiState (op:: SuperOperator ) = ChoiState (_super_choi (op. basis_l, op. basis_r, op. data)... )
373416SuperOperator (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