Skip to content

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:

julia
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):

julia
Qobj([1, 2, 3, 4, 5])

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

Quantum Object:   type=Bra   dims=[5]   size=(1, 5)
1×5 Matrix{Int64}:
 1  2  3  4  5
julia
Qobj(rand(4, 4))

Quantum Object:   type=Operator   dims=[4]   size=(4, 4)   ishermitian=false
4×4 Matrix{Float64}:
 0.436136  0.0523541  0.125534   0.523377
 0.333953  0.937047   0.715633   0.359658
 0.57749   0.968143   0.245974   0.199163
 0.116653  0.201873   0.0421517  0.45364
julia
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.242822+0.595937im    0.845335+0.815778im  …  0.204792+0.885239im
  0.46183+0.397738im    0.038032+0.207129im     0.167271+0.469069im
 0.639868+0.00135075im  0.370842+0.352966im     0.271874+0.473101im
 0.940338+0.710936im    0.900482+0.765282im     0.804241+0.657217im

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.

julia
Qobj(rand(4, 4), type = SuperOperator)

Quantum Object:   type=SuperOperator   dims=[2]   size=(4, 4)
4×4 Matrix{Float64}:
 0.410389  0.721126  0.398799   0.487883
 0.643279  0.71733   0.0409458  0.399743
 0.222834  0.345121  0.6159     0.969404
 0.255762  0.188746  0.350295   0.548891

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:

julia
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
julia
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
julia
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
     ⋅          ⋅              ⋅              ⋅
julia
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:

julia
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
     ⋅          ⋅              ⋅              ⋅
julia
a[2, 3] # the indexing in Julia starts from `1`
1.4142135623730951 + 0.0im
julia
a.type
Operator
julia
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:

julia
size(a)
(4, 4)
julia
shape(a) # synonym of size(a)
(4, 4)
julia
length(a)
16
julia
eltype(a) # element type
ComplexF64 (alias for Complex{Float64})
julia
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(isconstant(a)) # time-independent or not
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
true
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:

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
julia
Vector{Int64}(v_d)

Quantum Object:   type=Ket   dims=[2]   size=(2,)
2-element Vector{Int64}:
 1
 0
julia
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
julia
SparseVector{Float64}(v_s)

Quantum Object:   type=Ket   dims=[2]   size=(2,)
2-element SparseVector{Float64, Int64} with 1 stored entry:
  [1]  =  1.0
julia
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      ⋅
julia
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  ⋅
julia
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 to_sparse and to_dense:

julia
x_d = 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
julia
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:

julia
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
     ⋅          ⋅              ⋅              ⋅
julia
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      ⋅
julia
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
julia
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
julia
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
     ⋅          ⋅          ⋅              ⋅    
     ⋅          ⋅          ⋅              ⋅    
     ⋅          ⋅          ⋅              ⋅
julia
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      ⋅
julia
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           ⋅
julia
x ^ 3 == x
true
julia
# 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
julia> a * x
ERROR: DimensionMismatch: The quantum objects should have the same Hilbert `dimensions`.

Note that there is a special case for multiplying Ket and Bra, which results in outer product |uv|:

julia
u = Qobj([1, 2, 3])

Quantum Object:   type=Ket   dims=[3]   size=(3,)
3-element Vector{Int64}:
 1
 2
 3
julia
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
julia
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 v|u:

julia
v' * u
32