Outer Product of Two States
The simplest way to construct a QuDirac operator is to take the outer product of two states:
julia> k = d" 1/√2 * (| 0,0 > - | 1,1 >) "
Ket{KroneckerDelta,2,Float64} with 2 state(s):
0.7071067811865475 | 0,0 ⟩
-0.7071067811865475 | 1,1 ⟩
julia> k*k'
OuterProduct with 4 operator(s); Ket{KroneckerDelta,2,Float64} * Bra{KroneckerDelta,2,Float64}:
0.4999999999999999 | 0,0 ⟩⟨ 0,0 |
-0.4999999999999999 | 0,0 ⟩⟨ 1,1 |
-0.4999999999999999 | 1,1 ⟩⟨ 0,0 |
0.4999999999999999 | 1,1 ⟩⟨ 1,1 |
Specifically, an outer product of two states will yield an instance of the OuterProduct
type, as can be seen above.
The OuterProduct
type is a lazy representation of the outer product of two states - it simply stores a reference to
the two factor states, and uses the state information to behave like an operator. Thus, the OuterProduct
type acts as
a view onto the factor states.
With the exception of scaling functions, most mutating functions are not defined on OuterProduct
. The non-mutating versions of these functions will work, however, by converting the operator into the more flexible OpSum
type. The OpSum
type represents a sum of operators rather than an outer product of states, and is not a view.
Scalar Multiplication
Like states, operators can be multiplied by a scalar:
julia> op = d" | 'a' >< 'b' | "
OuterProduct with 1 operator(s); Ket{KroneckerDelta,1,Int64} * Bra{KroneckerDelta,1,Int64}:
1 | 'a' ⟩⟨ 'b' |
julia> im * op
OuterProduct with 1 operator(s); Ket{KroneckerDelta,1,Int64} * Bra{KroneckerDelta,1,Int64}:
0 + 1im | 'a' ⟩⟨ 'b' |
julia> op/2
OuterProduct with 1 operator(s); Ket{KroneckerDelta,1,Int64} * Bra{KroneckerDelta,1,Int64}:
0.5 | 'a' ⟩⟨ 'b' |
Addition and Subtraction
Operators can be added and subtracted just like states:
julia> op + op
OpSum{KroneckerDelta,1,Int64} with 1 operator(s):
2 | 'a' ⟩⟨ 'b' |
julia> d" 1/√2 * (op - | 0 >< 1 |) "
OpSum{KroneckerDelta,1,Float64} with 2 operator(s):
0.7071067811865475 | 'a' ⟩⟨ 'b' |
-0.7071067811865475 | 0 ⟩⟨ 1 |
As you can see, generic sums of operators are represented using the OpSum
type rather than the OuterProduct
type.
Normalization
Similarly to states, one can normalize operators using the normalize
and normalize!
functions:
julia> op = normalize(sum(i -> d"| i >< i^2 |", 1:5))
OpSum{KroneckerDelta,1,Float64} with 5 operator(s):
0.4472135954999579 | 5 ⟩⟨ 25 |
0.4472135954999579 | 3 ⟩⟨ 9 |
0.4472135954999579 | 4 ⟩⟨ 16 |
0.4472135954999579 | 1 ⟩⟨ 1 |
0.4472135954999579 | 2 ⟩⟨ 4 |
julia> norm(op)
0.9999999999999999
For QuDirac operators, the norm
function specifically computes the Frobenius norm.
Getting an Operator's Adjoint
Take the conjugate transpose of an operator, simply call ctranspose
on it:
julia> A = normalize(d"(1+3im)| 1 >< 2 | + (5-2im)| 3 >< 4 |")
OpSum{KroneckerDelta,1,Complex{Float64}} with 2 operator(s):
0.8006407690254357 - 0.32025630761017426im | 3 ⟩⟨ 4 |
0.16012815380508713 + 0.48038446141526137im | 1 ⟩⟨ 2 |
julia> A'
DualOpSum{KroneckerDelta,1,Complex{Float64}} with 2 operator(s):
0.8006407690254357 + 0.32025630761017426im | 4 ⟩⟨ 3 |
0.16012815380508713 - 0.48038446141526137im | 2 ⟩⟨ 1 |
Like the conjugate transpose of a Ket is a Bra, the conjugate transpose of an OpSum
is a DualOpSum
.
The DualOpSum
type is a view on the original operator, so mutating a DualOpSum
will mutate the underlying OpSum
(one can explicitly make a copy via the copy
function).
The dual of an OuterProduct
is simply an OuterProduct
, and is still a view on the original factor states.
Inner Product
Use the *
function to take the inner product of states/operators:
julia> k = d" 1/√2 * (| 0,0 > - | 1,1 >) "; P = k*k'
OuterProduct with 4 operator(s); Ket{KroneckerDelta,2,Float64} * Bra{KroneckerDelta,2,Float64}:
0.4999999999999999 | 0,0 ⟩⟨ 0,0 |
-0.4999999999999999 | 0,0 ⟩⟨ 1,1 |
-0.4999999999999999 | 1,1 ⟩⟨ 0,0 |
0.4999999999999999 | 1,1 ⟩⟨ 1,1 |
julia> d" P * | 1,1 > "
Ket{KroneckerDelta,2,Float64} with 2 state(s):
-0.4999999999999999 | 0,0 ⟩
0.4999999999999999 | 1,1 ⟩
julia> d" < 0,0 | * P * | 1,1 > "
-0.4999999999999999
julia> d" P * (| 1,1 >< 0,0 |) "
OuterProduct with 2 operator(s); Ket{KroneckerDelta,2,Float64} * Bra{KroneckerDelta,2,Int64}:
-0.4999999999999999 | 0,0 ⟩⟨ 0,0 |
0.4999999999999999 | 1,1 ⟩⟨ 0,0 |
Thus, expectation values are naturally obtained in this manner:
julia> d" < 1,1 | * P * | 1,1 > "
0.4999999999999999
julia> k' * P * k
0.9999999999999997
Like states, operator inner products have support for both lazy and custom evaluation rules. See the Working with Inner Products section for information regarding these features.
Acting an operator on a specific state factor
One can use the act_on
function to apply an operator to a specific factor of a state,
i.e. computing Oᵢ | k ⟩
.
julia> a = sum(i-> d"(√i)| i-1 >< i |", 1:5)
OpSum{KroneckerDelta,1,Float64} with 5 operator(s):
2.23606797749979 | 4 ⟩⟨ 5 |
2.0 | 3 ⟩⟨ 4 |
1.0 | 0 ⟩⟨ 1 |
1.4142135623730951 | 1 ⟩⟨ 2 |
1.7320508075688772 | 2 ⟩⟨ 3 |
julia> k = d" | 1,2,3 > + 2| 3,5,1> "
Ket{KroneckerDelta,3,Int64} with 2 state(s):
1 | 1,2,3 ⟩
2 | 3,5,1 ⟩
julia> act_on(a, k, 2)
Ket{KroneckerDelta,3,Float64} with 2 state(s):
4.47213595499958 | 3,4,1 ⟩
1.4142135623730951 | 1,1,3 ⟩
This works on Bras as well, i.e. computing ⟨ b | Oᵢ
:
julia> act_on(a, d" < 1,2,3 | ", 2)
Bra{KroneckerDelta,3,Float64} with 1 state(s):
1.7320508075688772 ⟨ 1,3,3 |
Tensor Product
Unlike states, one does not take the tensor product of operators using *
; that function is
already used for inner products. Thus, one must use the tensor
function:
julia> op = d" 1/√2 * (| 'a' >< 'b' | + | 'c' >< 'd' |)"
OpSum{KroneckerDelta,1,Float64} with 2 operator(s):
0.7071067811865475 | 'a' ⟩⟨ 'b' |
0.7071067811865475 | 'c' ⟩⟨ 'd' |
julia> tensor(op,op,op)
OpSum{KroneckerDelta,3,Float64} with 8 operator(s):
0.3535533905932737 | 'c','c','a' ⟩⟨ 'd','d','b' |
0.3535533905932737 | 'c','a','a' ⟩⟨ 'd','b','b' |
0.3535533905932737 | 'a','c','a' ⟩⟨ 'b','d','b' |
0.3535533905932737 | 'a','a','c' ⟩⟨ 'b','b','d' |
0.3535533905932737 | 'a','c','c' ⟩⟨ 'b','d','d' |
0.3535533905932737 | 'c','c','c' ⟩⟨ 'd','d','d' |
0.3535533905932737 | 'a','a','a' ⟩⟨ 'b','b','b' |
0.3535533905932737 | 'c','a','c' ⟩⟨ 'd','b','d' |
Trace and Partial Trace
To take the trace of an operator, simply use the trace
function:
julia> k = normalize(sum(ket, 0:5))
Ket{KroneckerDelta,1,Float64} with 6 state(s):
0.4082482904638631 | 0 ⟩
0.4082482904638631 | 2 ⟩
0.4082482904638631 | 3 ⟩
0.4082482904638631 | 5 ⟩
0.4082482904638631 | 4 ⟩
0.4082482904638631 | 1 ⟩
julia> trace(k*k')
1.0000000000000002
Note that the trace calculation is defined to sum the coefficients for which the Ket label and Bra label are equal (analogous to summing over the diagonal of a matrix representation).
The partial trace of an operator can be taken using the ptrace
function:
julia> bell = d" 1/√2 * (| 'b','a' > + | 'a','b' >) "
Ket{KroneckerDelta,2,Float64} with 2 state(s):
0.7071067811865475 | 'a','b' ⟩
0.7071067811865475 | 'b','a' ⟩
julia> dense = bell * bell'
OuterProduct with 4 operator(s); Ket{KroneckerDelta,2,Float64} * Bra{KroneckerDelta,2,Float64}:
0.4999999999999999 | 'b','a' ⟩⟨ 'b','a' |
0.4999999999999999 | 'b','a' ⟩⟨ 'a','b' |
0.4999999999999999 | 'a','b' ⟩⟨ 'b','a' |
0.4999999999999999 | 'a','b' ⟩⟨ 'a','b' |
julia> ptrace(dense,1) # trace over the 1st subsystem
OpSum{KroneckerDelta,1,Float64} with 2 operator(s):
0.4999999999999999 | 'b' ⟩⟨ 'b' |
0.4999999999999999 | 'a' ⟩⟨ 'a' |
julia> purity(ans) # get the purity of the previous result
0.4999999999999998
Partial Transpose
Take the partial transpose of an operator via the ptranspose
function:
julia> op = normalize(d" | 'a','b','c' >< 'd','e','f' | + 2| 'i','j','k' >< 'l','m','n' | ")
OpSum{KroneckerDelta,3,Float64} with 2 operator(s):
0.8944271909999159 | 'i','j','k' ⟩⟨ 'l','m','n' |
0.4472135954999579 | 'a','b','c' ⟩⟨ 'd','e','f' |
julia> ptranspose(op, 2) # transpose the 2nd subsystem
OpSum{KroneckerDelta,3,Float64} with 2 operator(s):
0.4472135954999579 | 'a','e','c' ⟩⟨ 'd','b','f' |
0.8944271909999159 | 'i','m','k' ⟩⟨ 'l','j','n' |