1+ import Base: isapprox
12import QuantumInterface: AbstractSuperOperator
23import FastExpm: fastExpm
34import 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
7492Base. length (a:: SuperOperator ) = length (a. basis_l[1 ])* length (a. basis_l[2 ])* length (a. basis_r[1 ])* length (a. basis_r[2 ])
7593samebases (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) )
134152end
135153
136154"""
@@ -319,10 +337,14 @@ end
319337 throw (IncompatibleBases ())
320338end
321339
340+ # TODO should all of PauliTransferMatrix, ChiMatrix, ChoiState, and KrausOperators subclass AbstractSuperOperator?
322341"""
323342 ChoiState <: AbstractSuperOperator
324343
325344Superoperator 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"""
327349mutable 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
359402ChoiState (op:: SuperOperator ) = ChoiState (_super_choi (op. basis_l, op. basis_r, op. data)... )
360403SuperOperator (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
366619Adapt. adapt_structure (to, x:: SuperOperator ) = SuperOperator (x. basis_l, x. basis_r, Adapt. adapt (to, x. data))
0 commit comments