Quantum Objects (Qobj)

Introduction

The key difference between classical and quantum mechanics is the use of operators instead of numbers as variables. Moreover, we need to specify state vectors and their properties. Therefore, in computing the dynamics of quantum systems, we need a data structure that encapsulates the properties of a quantum operator and ket/bra vectors. The quantum object structure, QuantumObject, accomplishes this using array representation.

QuantumObject supports general Julia arrays (Base.AbstractArray). For example, it can be:

  • Base.Vector (dense vector)
  • Base.Matrix (dense matrix)
  • SparseArrays.SparseVector (sparse vector)
  • SparseArrays.SparseMatrixCSC (sparse matrix)
  • CUDA.CuArray (dense GPU vector / matrix)
  • CUDA.CUSPARSE.CuSparseVector (sparse GPU vector)
  • CUDA.CUSPARSE.CuSparseMatrixCSC (sparse GPU matrix)
  • CUDA.CUSPARSE.CuSparseMatrixCSR (sparse GPU matrix)
  • and even more ...
Support for GPU arrays

See CUDA extension for more details.

We can create a QuantumObject with a user defined data set by passing an array of data into the QuantumObject:

QuantumObject([1, 2, 3, 4, 5])
Quantum Object:   type=Ket   dims=[5]   size=(5,)
5-element Vector{Int64}:
 1
 2
 3
 4
 5

We can also use the same function Qobj in QuTiP (Python):

Qobj([1, 2, 3, 4, 5])
Quantum Object:   type=Ket   dims=[5]   size=(5,)
5-element Vector{Int64}:
 1
 2
 3
 4
 5
Qobj([1 2 3 4 5])
Quantum Object:   type=Bra   dims=[5]   size=(1, 5)
1×5 Matrix{Int64}:
 1  2  3  4  5
Qobj(rand(4, 4))
Quantum Object:   type=Operator   dims=[4]   size=(4, 4)   ishermitian=false
4×4 Matrix{Float64}:
 0.59538   0.728108  0.258083  0.390643
 0.767497  0.373374  0.588002  0.267767
 0.71726   0.890218  0.532463  0.939094
 0.882667  0.352245  0.956588  0.0590518
M = rand(ComplexF64, 4, 4)
Qobj(M, dims = [2, 2])  # dims as Vector
Qobj(M, dims = (2, 2))  # dims as Tuple (recommended)
Qobj(M, dims = SVector(2, 2)) # dims as StaticArrays.SVector (recommended)
Quantum Object:   type=Operator   dims=[2, 2]   size=(4, 4)   ishermitian=false
4×4 Matrix{ComplexF64}:
 0.671407+0.585873im  0.806787+0.803663im  …  0.387869+0.474902im
 0.988582+0.452471im  0.567583+0.39619im      0.776807+0.869516im
 0.567829+0.968046im  0.253067+0.946989im     0.793258+0.362771im
 0.502224+0.764517im  0.964779+0.806579im     0.828038+0.129221im
Beware of type-stability!

Please note that here we put the dims as a tuple (2, 2). Although it supports also Vector type (dims = [2, 2]), it is recommended to use Tuple or SVector from StaticArrays.jl to improve performance. For a brief explanation on the impact of the type of dims, see the Section The Importance of Type-Stability.

Qobj(rand(4, 4), type = SuperOperator)
Quantum Object:   type=SuperOperator   dims=[2]   size=(4, 4)
4×4 Matrix{Float64}:
 0.795201   0.682635  0.962696   0.832733
 0.0389268  0.566245  0.314792   0.947069
 0.878896   0.940959  0.756089   0.250443
 0.432414   0.235157  0.0154155  0.909555
Difference between `dims` and `size`

Notice that type, dims, and size will change according to the input data. Although dims and size appear to be the same, dims keep tracking the dimension of individual Hilbert spaces of a multipartite system, while size does not. We refer the reader to the section Tensor Products and Partial Traces for more information.

States and operators

Manually specifying the data for each quantum object is inefficient. Even more so when most objects correspond to commonly used types such as the ladder operators of a harmonic oscillator, the Pauli spin operators for a two-level system, or state vectors such as Fock states. Therefore, QuantumToolbox includes predefined functions to construct variety of states and operators (you can click the function links and see the corresponding docstring):

States

Operators

As an example, we give the output for a few of these functions:

basis(5, 3)
Quantum Object:   type=Ket   dims=[5]   size=(5,)
5-element Vector{ComplexF64}:
 0.0 + 0.0im
 0.0 + 0.0im
 0.0 + 0.0im
 1.0 + 0.0im
 0.0 + 0.0im
coherent(5, 0.5 - 0.5im)
Quantum Object:   type=Ket   dims=[5]   size=(5,)
5-element Vector{ComplexF64}:
   0.7788017020755937 + 0.0im
   0.3893914169377068 - 0.3893914169377068im
                  0.0 - 0.27545895152395im
  -0.0789861710068352 - 0.0789861710068352im
 -0.04314270829853118 + 0.0im
destroy(4)
Quantum Object:   type=Operator   dims=[4]   size=(4, 4)   ishermitian=false
4×4 SparseMatrixCSC{ComplexF64, Int64} with 3 stored entries:
     ⋅      1.0+0.0im          ⋅              ⋅    
     ⋅          ⋅      1.41421+0.0im          ⋅    
     ⋅          ⋅              ⋅      1.73205+0.0im
     ⋅          ⋅              ⋅              ⋅    
sigmaz()
Quantum Object:   type=Operator   dims=[2]   size=(2, 2)   ishermitian=true
2×2 SparseMatrixCSC{ComplexF64, Int64} with 2 stored entries:
 1.0+0.0im       ⋅    
     ⋅      -1.0+0.0im

Qobj fields (attributes)

We have seen that a structure QuantumObject has several fields (attributes), such as data, type and dims. These can be accessed in the following way:

a = destroy(4)
a.data
4×4 SparseMatrixCSC{ComplexF64, Int64} with 3 stored entries:
     ⋅      1.0+0.0im          ⋅              ⋅    
     ⋅          ⋅      1.41421+0.0im          ⋅    
     ⋅          ⋅              ⋅      1.73205+0.0im
     ⋅          ⋅              ⋅              ⋅    
a[2, 3] # the indexing in Julia starts from `1`
1.4142135623730951 + 0.0im
a.type
Operator
a.dims
1-element SVector{1, Int64} with indices SOneTo(1):
 4

In general, the properties of a QuantumObject can be retrieved using several functions with inputting QuantumObject:

size(a)
(4, 4)
shape(a) # synonym of size(a)
(4, 4)
length(a)
16
eltype(a) # element type
ComplexF64 (alias for Complex{Float64})
println(isket(a)) # ket
println(isbra(a)) # bra
println(isoper(a)) # operator
println(isoperket(a)) # operator-ket
println(isoperbra(a)) # operator-bra
println(issuper(a)) # super operator
println(ishermitian(a)) # Hermitian
println(isherm(a)) # synonym of ishermitian(a)
println(issymmetric(a)) # symmetric
println(isposdef(a)) # positive definite (and Hermitian)
println(isunitary(a)) # unitary
false
false
true
false
false
false
false
false
false
false
false

data conversions

As we mentioned above, QuantumObject.data supports general Julia arrays. The conversion between different type of QuantumObject.data is done using the standard type-conversion for arrays in Julia:

v_d = basis(2, 0)
Quantum Object:   type=Ket   dims=[2]   size=(2,)
2-element Vector{ComplexF64}:
 1.0 + 0.0im
 0.0 + 0.0im
Vector{Int64}(v_d)
Quantum Object:   type=Ket   dims=[2]   size=(2,)
2-element Vector{Int64}:
 1
 0
v_s = SparseVector(v_d)
Quantum Object:   type=Ket   dims=[2]   size=(2,)
2-element SparseVector{ComplexF64, Int64} with 1 stored entry:
  [1]  =  1.0+0.0im
SparseVector{Float64}(v_s)
Quantum Object:   type=Ket   dims=[2]   size=(2,)
2-element SparseVector{Float64, Int64} with 1 stored entry:
  [1]  =  1.0
x_s = sigmax()
Quantum Object:   type=Operator   dims=[2]   size=(2, 2)   ishermitian=true
2×2 SparseMatrixCSC{ComplexF64, Int64} with 2 stored entries:
     ⋅      1.0+0.0im
 1.0+0.0im      ⋅    
SparseMatrixCSC{Int64}(x_s)
Quantum Object:   type=Operator   dims=[2]   size=(2, 2)   ishermitian=true
2×2 SparseMatrixCSC{Int64, Int64} with 2 stored entries:
 ⋅  1
 1  ⋅
Matrix{Float64}(x_s)
Quantum Object:   type=Operator   dims=[2]   size=(2, 2)   ishermitian=true
2×2 Matrix{Float64}:
 0.0  1.0
 1.0  0.0

To convert between dense and sparse arrays, one can also use dense_to_sparse and sparse_to_dense:

x_d = sparse_to_dense(x_s)
Quantum Object:   type=Operator   dims=[2]   size=(2, 2)   ishermitian=true
2×2 Matrix{ComplexF64}:
 0.0+0.0im  1.0+0.0im
 1.0+0.0im  0.0+0.0im
dense_to_sparse(x_d)
Quantum Object:   type=Operator   dims=[2]   size=(2, 2)   ishermitian=true
2×2 SparseMatrixCSC{ComplexF64, Int64} with 2 stored entries:
     ⋅      1.0+0.0im
 1.0+0.0im      ⋅    
Convert to GPU arrays

See CUDA extension for more details.

QuantumToolbox will do conversion when needed to keep everything working in any format. However these conversions could slow down computation and it is recommended to keep to one format family where possible.

Qobj math

The rules for mathematical operations on QuantumObject are similar to the standard scalar, vector, and matrix arithmetic:

a = destroy(4)
Quantum Object:   type=Operator   dims=[4]   size=(4, 4)   ishermitian=false
4×4 SparseMatrixCSC{ComplexF64, Int64} with 3 stored entries:
     ⋅      1.0+0.0im          ⋅              ⋅    
     ⋅          ⋅      1.41421+0.0im          ⋅    
     ⋅          ⋅              ⋅      1.73205+0.0im
     ⋅          ⋅              ⋅              ⋅    
a' # synonym of adjoint(a)
Quantum Object:   type=Operator   dims=[4]   size=(4, 4)   ishermitian=false
4×4 Adjoint{ComplexF64, SparseMatrixCSC{ComplexF64, Int64}} with 3 stored entries:
     ⋅              ⋅              ⋅          ⋅    
 1.0-0.0im          ⋅              ⋅          ⋅    
     ⋅      1.41421-0.0im          ⋅          ⋅    
     ⋅              ⋅      1.73205-0.0im      ⋅    
a + 5
Quantum Object:   type=Operator   dims=[4]   size=(4, 4)   ishermitian=false
4×4 SparseMatrixCSC{ComplexF64, Int64} with 7 stored entries:
 5.0+0.0im  1.0+0.0im          ⋅              ⋅    
     ⋅      5.0+0.0im  1.41421+0.0im          ⋅    
     ⋅          ⋅          5.0+0.0im  1.73205+0.0im
     ⋅          ⋅              ⋅          5.0+0.0im
a' * a
Quantum Object:   type=Operator   dims=[4]   size=(4, 4)   ishermitian=true
4×4 SparseMatrixCSC{ComplexF64, Int64} with 3 stored entries:
     ⋅          ⋅          ⋅          ⋅    
     ⋅      1.0+0.0im      ⋅          ⋅    
     ⋅          ⋅      2.0+0.0im      ⋅    
     ⋅          ⋅          ⋅      3.0+0.0im
a ^ 3
Quantum Object:   type=Operator   dims=[4]   size=(4, 4)   ishermitian=false
4×4 SparseMatrixCSC{ComplexF64, Int64} with 1 stored entry:
     ⋅          ⋅          ⋅      2.44949+0.0im
     ⋅          ⋅          ⋅              ⋅    
     ⋅          ⋅          ⋅              ⋅    
     ⋅          ⋅          ⋅              ⋅    
x = sigmax()
Quantum Object:   type=Operator   dims=[2]   size=(2, 2)   ishermitian=true
2×2 SparseMatrixCSC{ComplexF64, Int64} with 2 stored entries:
     ⋅      1.0+0.0im
 1.0+0.0im      ⋅    
x / sqrt(2)
Quantum Object:   type=Operator   dims=[2]   size=(2, 2)   ishermitian=true
2×2 SparseMatrixCSC{ComplexF64, Int64} with 2 stored entries:
          ⋅      0.707107+0.0im
 0.707107+0.0im           ⋅    
x ^ 3 == x
true
# type \approx + <TAB> to get symbol ≈
x ^ 3 ≈ x
true

Of course, like matrices, multiplying two objects of incompatible dims or size throws an error:

julia> a * xERROR: DimensionMismatch: The two quantum objects are not of the same Hilbert dimension.

Note that there is a special case for multiplying Ket and Bra, which results in outer product $|u\rangle \otimes \langle v|$:

u = Qobj([1, 2, 3])
Quantum Object:   type=Ket   dims=[3]   size=(3,)
3-element Vector{Int64}:
 1
 2
 3
v = Qobj([4, 5, 6])
v'
Quantum Object:   type=Bra   dims=[3]   size=(1, 3)
1×3 adjoint(::Vector{Int64}) with eltype Int64:
 4  5  6
u * v'
Quantum Object:   type=Operator   dims=[3]   size=(3, 3)   ishermitian=false
3×3 Matrix{Int64}:
  4   5   6
  8  10  12
 12  15  18

Of course, if you switch the order of multiplication, it becomes inner product $\langle v | u \rangle$:

v' * u
32