diff --git a/Project.toml b/Project.toml index e40366f4..66e2ec4e 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "QuantumToolbox" uuid = "6c2fb7c5-b903-41d2-bc5e-5a7c320b9fab" authors = ["Alberto Mercurio", "Luca Gravina", "Yi-Te Huang"] -version = "0.12.1" +version = "0.13.0" [deps] ArrayInterface = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9" @@ -19,6 +19,7 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" +StaticArraysCore = "1e83bf80-4336-4d27-bf5d-d5a4f845583c" StochasticDiffEq = "789caeaf-c7a9-5a7d-9973-96adeb23e2a0" [weakdeps] @@ -45,6 +46,7 @@ Reexport = "1" SparseArrays = "<0.0.1, 1" SpecialFunctions = "2" StochasticDiffEq = "6" +StaticArraysCore = "1" Test = "<0.0.1, 1" julia = "1.7" diff --git a/docs/make.jl b/docs/make.jl index 01d5a01d..fccd116d 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -28,6 +28,7 @@ const PAGES = [ "users_guide/QuantumObject/QuantumObject.md", "users_guide/QuantumObject/QuantumObject_functions.md", ], + "The Importance of Type-Stability" => "users_guide/type_stability.md", "Manipulating States and Operators" => "users_guide/states_and_operators.md", "Tensor Products and Partial Traces" => "users_guide/tensor.md", "Time Evolution and Dynamics" => [ diff --git a/docs/src/users_guide/QuantumObject/QuantumObject.md b/docs/src/users_guide/QuantumObject/QuantumObject.md index 42fef30f..2f53e90c 100644 --- a/docs/src/users_guide/QuantumObject/QuantumObject.md +++ b/docs/src/users_guide/QuantumObject/QuantumObject.md @@ -39,9 +39,15 @@ Qobj(rand(4, 4)) ``` ```@example Qobj -Qobj(rand(4, 4), dims = [2, 2]) +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) ``` +> [!IMPORTANT] +> 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`](https://github.com/JuliaArrays/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](@ref doc:Type-Stability). + ```@example Qobj Qobj(rand(4, 4), type = SuperOperator) ``` diff --git a/docs/src/users_guide/type_stability.md b/docs/src/users_guide/type_stability.md new file mode 100644 index 00000000..c3cd0e12 --- /dev/null +++ b/docs/src/users_guide/type_stability.md @@ -0,0 +1,3 @@ +# [The Importance of Type-Stability](@id doc:Type-Stability) + +This page is still under construction. diff --git a/src/QuantumToolbox.jl b/src/QuantumToolbox.jl index 03c17776..a0c8684d 100644 --- a/src/QuantumToolbox.jl +++ b/src/QuantumToolbox.jl @@ -34,6 +34,8 @@ import OrdinaryDiffEq: OrdinaryDiffEqAlgorithm import Pkg import Random import SpecialFunctions: loggamma +@reexport import StaticArraysCore: SVector +import StaticArraysCore: MVector # Setting the number of threads to 1 allows # to achieve better performances for more massive parallelizations diff --git a/src/metrics.jl b/src/metrics.jl index 0a026ffb..62f7af6d 100644 --- a/src/metrics.jl +++ b/src/metrics.jl @@ -62,21 +62,21 @@ function entropy_vn( end """ - entanglement(QO::QuantumObject, sel::Vector) + entanglement(QO::QuantumObject, sel::Union{Int,AbstractVector{Int},Tuple}) Calculates the entanglement by doing the partial trace of `QO`, selecting only the dimensions with the indices contained in the `sel` vector, and then using the Von Neumann entropy [`entropy_vn`](@ref). """ function entanglement( QO::QuantumObject{<:AbstractArray{T},OpType}, - sel::Vector{Int}, + sel::Union{AbstractVector{Int},Tuple}, ) where {T,OpType<:Union{BraQuantumObject,KetQuantumObject,OperatorQuantumObject}} ψ = normalize(QO) ρ_tr = ptrace(ψ, sel) entropy = entropy_vn(ρ_tr) return (entropy > 0) * entropy end -entanglement(QO::QuantumObject, sel::Int) = entanglement(QO, [sel]) +entanglement(QO::QuantumObject, sel::Int) = entanglement(QO, (sel,)) @doc raw""" tracedist(ρ::QuantumObject, σ::QuantumObject) diff --git a/src/qobj/arithmetic_and_attributes.jl b/src/qobj/arithmetic_and_attributes.jl index ae765c70..6dc2f240 100644 --- a/src/qobj/arithmetic_and_attributes.jl +++ b/src/qobj/arithmetic_and_attributes.jl @@ -471,7 +471,7 @@ proj(ψ::QuantumObject{<:AbstractArray{T},KetQuantumObject}) where {T} = ψ * ψ proj(ψ::QuantumObject{<:AbstractArray{T},BraQuantumObject}) where {T} = ψ' * ψ @doc raw""" - ptrace(QO::QuantumObject, sel::Vector{Int}) + ptrace(QO::QuantumObject, sel) [Partial trace](https://en.wikipedia.org/wiki/Partial_trace) of a quantum state `QO` leaving only the dimensions with the indices present in the `sel` vector. @@ -487,7 +487,7 @@ Quantum Object: type=Ket dims=[2, 2] size=(4,) 0.0 + 0.0im 0.0 + 0.0im -julia> ptrace(ψ, [2]) +julia> ptrace(ψ, 2) Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=true 2×2 Matrix{ComplexF64}: 0.0+0.0im 0.0+0.0im @@ -504,63 +504,79 @@ Quantum Object: type=Ket dims=[2, 2] size=(4,) 0.0 + 0.0im 0.7071067811865475 + 0.0im -julia> ptrace(ψ, [1]) +julia> ptrace(ψ, 1) Quantum Object: type=Operator dims=[2] size=(2, 2) ishermitian=true 2×2 Matrix{ComplexF64}: 0.5+0.0im 0.0+0.0im 0.0+0.0im 0.5+0.0im ``` """ -function ptrace(QO::QuantumObject{<:AbstractArray{T1},KetQuantumObject}, sel::Vector{T2}) where {T1,T2<:Integer} +function ptrace(QO::QuantumObject{<:AbstractArray,KetQuantumObject}, sel::Union{AbstractVector{Int},Tuple}) length(QO.dims) == 1 && return QO - ρtr, dkeep = _ptrace_ket(QO.data, QO.dims, sel) + ρtr, dkeep = _ptrace_ket(QO.data, QO.dims, SVector(sel)) return QuantumObject(ρtr, dims = dkeep) end -ptrace(QO::QuantumObject{<:AbstractArray{T1},BraQuantumObject}, sel::Vector{T2}) where {T1,T2<:Integer} = - ptrace(QO', sel) +ptrace(QO::QuantumObject{<:AbstractArray,BraQuantumObject}, sel::Union{AbstractVector{Int},Tuple}) = ptrace(QO', sel) -function ptrace(QO::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject}, sel::Vector{T2}) where {T1,T2<:Integer} +function ptrace(QO::QuantumObject{<:AbstractArray,OperatorQuantumObject}, sel::Union{AbstractVector{Int},Tuple}) length(QO.dims) == 1 && return QO - ρtr, dkeep = _ptrace_oper(QO.data, QO.dims, sel) + ρtr, dkeep = _ptrace_oper(QO.data, QO.dims, SVector(sel)) return QuantumObject(ρtr, dims = dkeep) end -ptrace(QO::QuantumObject, sel::Int) = ptrace(QO, [sel]) - -function _ptrace_ket(QO::AbstractArray{T1}, dims::Vector{<:Integer}, sel::Vector{T2}) where {T1,T2<:Integer} - rd = dims - nd = length(rd) - - nd == 1 && return QO, rd - - dkeep = rd[sel] - qtrace = filter!(e -> e ∉ sel, Vector(1:nd)) - dtrace = @view(rd[qtrace]) +ptrace(QO::QuantumObject, sel::Int) = ptrace(QO, SVector(sel)) + +function _ptrace_ket(QO::AbstractArray, dims::Union{SVector,MVector}, sel) + nd = length(dims) + + nd == 1 && return QO, dims + + qtrace = filter(i -> i ∉ sel, 1:nd) + dkeep = dims[sel] + dtrace = dims[qtrace] + # Concatenate sel and qtrace without loosing the length information + sel_qtrace = ntuple(Val(nd)) do i + if i <= length(sel) + @inbounds sel[i] + else + @inbounds qtrace[i-length(sel)] + end + end - vmat = reshape(QO, reverse(rd)...) - topermute = nd + 1 .- vcat(sel, qtrace) + vmat = reshape(QO, reverse(dims)...) + topermute = nd + 1 .- sel_qtrace vmat = PermutedDimsArray(vmat, topermute) vmat = reshape(vmat, prod(dkeep), prod(dtrace)) return vmat * vmat', dkeep end -function _ptrace_oper(QO::AbstractArray{T1}, dims::Vector{<:Integer}, sel::Vector{T2}) where {T1,T2<:Integer} - rd = dims - nd = length(rd) - - nd == 1 && return QO, rd - - dkeep = rd[sel] - qtrace = filter!(e -> e ∉ sel, Vector(1:nd)) - dtrace = @view(rd[qtrace]) +function _ptrace_oper(QO::AbstractArray, dims::Union{SVector,MVector}, sel) + nd = length(dims) + + nd == 1 && return QO, dims + + qtrace = filter(i -> i ∉ sel, 1:nd) + dkeep = dims[sel] + dtrace = dims[qtrace] + # Concatenate sel and qtrace without loosing the length information + qtrace_sel = ntuple(Val(2 * nd)) do i + if i <= length(qtrace) + @inbounds qtrace[i] + elseif i <= 2 * length(qtrace) + @inbounds qtrace[i-length(qtrace)] + nd + elseif i <= 2 * length(qtrace) + length(sel) + @inbounds sel[i-length(qtrace)-length(sel)] + else + @inbounds sel[i-length(qtrace)-2*length(sel)] + nd + end + end - ρmat = reshape(QO, reverse!(repeat(rd, 2))...) - topermute = 2 * nd + 1 .- vcat(qtrace, qtrace .+ nd, sel, sel .+ nd) - reverse!(topermute) - ρmat = PermutedDimsArray(ρmat, topermute) + ρmat = reshape(QO, reverse(vcat(dims, dims))...) + topermute = 2 * nd + 1 .- qtrace_sel + ρmat = PermutedDimsArray(ρmat, reverse(topermute)) ## TODO: Check if it works always @@ -639,7 +655,7 @@ function get_coherence(ρ::QuantumObject{<:AbstractArray,OperatorQuantumObject}) end @doc raw""" - permute(A::QuantumObject, order::Vector{Int}) + permute(A::QuantumObject, order::Union{AbstractVector{Int},Tuple}) Permute the tensor structure of a [`QuantumObject`](@ref) `A` according to the specified `order` list @@ -660,28 +676,36 @@ true """ function permute( A::QuantumObject{<:AbstractArray{T},ObjType}, - order::AbstractVector{Int}, + order::Union{AbstractVector{Int},Tuple}, ) where {T,ObjType<:Union{KetQuantumObject,BraQuantumObject,OperatorQuantumObject}} (length(order) != length(A.dims)) && throw(ArgumentError("The order list must have the same length as the number of subsystems (A.dims)")) !isperm(order) && throw(ArgumentError("$(order) is not a valid permutation of the subsystems (A.dims)")) + _non_static_array_warning("order", order) + + order_svector = SVector{length(order),Int}(order) # convert it to SVector for performance + # obtain the arguments: dims for reshape; perm for PermutedDimsArray - dims, perm = _dims_and_perm(A.type, A.dims, order, length(order)) + dims, perm = _dims_and_perm(A.type, A.dims, order_svector, length(order_svector)) - return QuantumObject(reshape(PermutedDimsArray(reshape(A.data, dims...), perm), size(A)), A.type, A.dims[order]) + return QuantumObject( + reshape(PermutedDimsArray(reshape(A.data, dims...), Tuple(perm)), size(A)), + A.type, + A.dims[order_svector], + ) end function _dims_and_perm( ::ObjType, - dims::AbstractVector{Int}, + dims::SVector{N,Int}, order::AbstractVector{Int}, L::Int, -) where {ObjType<:Union{KetQuantumObject,BraQuantumObject}} - return reverse(dims), reverse!((L + 1) .- order) +) where {ObjType<:Union{KetQuantumObject,BraQuantumObject},N} + return reverse(dims), reverse((L + 1) .- order) end -function _dims_and_perm(::OperatorQuantumObject, dims::AbstractVector{Int}, order::AbstractVector{Int}, L::Int) - return reverse!([dims; dims]), reverse!((2 * L + 1) .- [order; order .+ L]) +function _dims_and_perm(::OperatorQuantumObject, dims::SVector{N,Int}, order::AbstractVector{Int}, L::Int) where {N} + return reverse(vcat(dims, dims)), reverse((2 * L + 1) .- vcat(order, order .+ L)) end diff --git a/src/qobj/eigsolve.jl b/src/qobj/eigsolve.jl index f92777f2..d2108273 100644 --- a/src/qobj/eigsolve.jl +++ b/src/qobj/eigsolve.jl @@ -6,11 +6,11 @@ export EigsolveResult export eigenenergies, eigenstates, eigsolve, eigsolve_al @doc raw""" - struct EigsolveResult{T1<:Vector{<:Number}, T2<:AbstractMatrix{<:Number}, ObjType<:Union{Nothing,OperatorQuantumObject,SuperOperatorQuantumObject}} + struct EigsolveResult{T1<:Vector{<:Number}, T2<:AbstractMatrix{<:Number}, ObjType<:Union{Nothing,OperatorQuantumObject,SuperOperatorQuantumObject},N} values::T1 vectors::T2 type::ObjType - dims::Vector{Int} + dims::SVector{N,Int} iter::Int numops::Int converged::Bool @@ -22,7 +22,7 @@ A struct containing the eigenvalues, the eigenvectors, and some information from - `values::AbstractVector`: the eigenvalues - `vectors::AbstractMatrix`: the transformation matrix (eigenvectors) - `type::Union{Nothing,QuantumObjectType}`: the type of [`QuantumObject`](@ref), or `nothing` means solving eigen equation for general matrix -- `dims::Vector{Int}`: the `dims` of [`QuantumObject`](@ref) +- `dims::SVector`: the `dims` of [`QuantumObject`](@ref) - `iter::Int`: the number of iteration during the solving process - `numops::Int` : number of times the linear map was applied in krylov methods - `converged::Bool`: Whether the result is converged @@ -54,11 +54,12 @@ struct EigsolveResult{ T1<:Vector{<:Number}, T2<:AbstractMatrix{<:Number}, ObjType<:Union{Nothing,OperatorQuantumObject,SuperOperatorQuantumObject}, + N, } values::T1 vectors::T2 type::ObjType - dims::Vector{Int} + dims::SVector{N,Int} iter::Int numops::Int converged::Bool @@ -271,7 +272,7 @@ function _eigsolve( A, b::AbstractVector{T}, type::ObjType, - dims::Vector{Int}, + dims::SVector, k::Int = 1, m::Int = max(20, 2 * k + 1); tol::Real = 1e-8, @@ -398,7 +399,7 @@ function eigsolve( A; v0::Union{Nothing,AbstractVector} = nothing, type::Union{Nothing,OperatorQuantumObject,SuperOperatorQuantumObject} = nothing, - dims::Vector{Int} = Int[], + dims = SVector{0,Int}(), sigma::Union{Nothing,Real} = nothing, k::Int = 1, krylovdim::Int = max(20, 2 * k + 1), @@ -411,6 +412,8 @@ function eigsolve( isH = ishermitian(A) v0 === nothing && (v0 = normalize!(rand(T, size(A, 1)))) + dims = SVector(dims) + if sigma === nothing res = _eigsolve(A, v0, type, dims, k, krylovdim, tol = tol, maxiter = maxiter) vals = res.values diff --git a/src/qobj/operators.jl b/src/qobj/operators.jl index 255014e7..ac04102b 100644 --- a/src/qobj/operators.jl +++ b/src/qobj/operators.jl @@ -19,7 +19,7 @@ Returns a random unitary [`QuantumObject`](@ref). The `dimensions` can be either the following types: - `dimensions::Int`: Number of basis states in the Hilbert space. -- `dimensions::Vector{Int}`: list of dimensions representing the each number of basis in the subsystems. +- `dimensions::Union{AbstractVector{Int},Tuple}`: list of dimensions representing the each number of basis in the subsystems. The `distribution` specifies which of the method used to obtain the unitary matrix: - `:haar`: Haar random unitary matrix using the algorithm from reference 1 @@ -28,9 +28,10 @@ The `distribution` specifies which of the method used to obtain the unitary matr # References 1. [F. Mezzadri, How to generate random matrices from the classical compact groups, arXiv:math-ph/0609050 (2007)](https://arxiv.org/abs/math-ph/0609050) """ -rand_unitary(dimensions::Int, distribution::Symbol = :haar) = rand_unitary([dimensions], Val(distribution)) -rand_unitary(dimensions::Vector{Int}, distribution::Symbol = :haar) = rand_unitary(dimensions, Val(distribution)) -function rand_unitary(dimensions::Vector{Int}, ::Val{:haar}) +rand_unitary(dimensions::Int, distribution::Symbol = :haar) = rand_unitary(SVector(dimensions), Val(distribution)) +rand_unitary(dimensions::Union{AbstractVector{Int},Tuple}, distribution::Symbol = :haar) = + rand_unitary(dimensions, Val(distribution)) +function rand_unitary(dimensions::Union{AbstractVector{Int},Tuple}, ::Val{:haar}) N = prod(dimensions) # generate N x N matrix Z of complex standard normal random variates @@ -45,7 +46,7 @@ function rand_unitary(dimensions::Vector{Int}, ::Val{:haar}) Λ ./= abs.(Λ) # rescaling the elements return QuantumObject(dense_to_sparse(Q * Diagonal(Λ)); type = Operator, dims = dimensions) end -function rand_unitary(dimensions::Vector{Int}, ::Val{:exp}) +function rand_unitary(dimensions::Union{AbstractVector{Int},Tuple}, ::Val{:exp}) N = prod(dimensions) # generate N x N matrix Z of complex standard normal random variates @@ -56,7 +57,8 @@ function rand_unitary(dimensions::Vector{Int}, ::Val{:exp}) return exp(-1.0im * H) end -rand_unitary(dimensions::Vector{Int}, ::Val{T}) where {T} = throw(ArgumentError("Invalid distribution: $(T)")) +rand_unitary(dimensions::Union{AbstractVector{Int},Tuple}, ::Val{T}) where {T} = + throw(ArgumentError("Invalid distribution: $(T)")) @doc raw""" commutator(A::QuantumObject, B::QuantumObject; anti::Bool=false) @@ -86,17 +88,17 @@ This operator acts on a fock state as ``\hat{a} \ket{n} = \sqrt{n} \ket{n-1}``. julia> a = destroy(20) Quantum Object: type=Operator dims=[20] size=(20, 20) ishermitian=false 20×20 SparseMatrixCSC{ComplexF64, Int64} with 19 stored entries: -⠈⠢⡀⠀⠀⠀⠀⠀⠀⠀ -⠀⠀⠈⠢⡀⠀⠀⠀⠀⠀ -⠀⠀⠀⠀⠈⠢⡀⠀⠀⠀ -⠀⠀⠀⠀⠀⠀⠈⠢⡀⠀ -⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢ +⎡⠈⠢⡀⠀⠀⠀⠀⠀⠀⠀⎤ +⎢⠀⠀⠈⠢⡀⠀⠀⠀⠀⠀⎥ +⎢⠀⠀⠀⠀⠈⠢⡀⠀⠀⠀⎥ +⎢⠀⠀⠀⠀⠀⠀⠈⠢⡀⠀⎥ +⎣⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⎦ julia> fock(20, 3)' * a * fock(20, 4) 2.0 + 0.0im ``` """ -destroy(N::Int) = QuantumObject(spdiagm(1 => Array{ComplexF64}(sqrt.(1:N-1))), Operator, [N]) +destroy(N::Int) = QuantumObject(spdiagm(1 => Array{ComplexF64}(sqrt.(1:N-1))), Operator, N) @doc raw""" create(N::Int) @@ -111,17 +113,17 @@ This operator acts on a fock state as ``\hat{a}^\dagger \ket{n} = \sqrt{n+1} \ke julia> a_d = create(20) Quantum Object: type=Operator dims=[20] size=(20, 20) ishermitian=false 20×20 SparseMatrixCSC{ComplexF64, Int64} with 19 stored entries: -⠢⡀⠀⠀⠀⠀⠀⠀⠀⠀ -⠀⠈⠢⡀⠀⠀⠀⠀⠀⠀ -⠀⠀⠀⠈⠢⡀⠀⠀⠀⠀ -⠀⠀⠀⠀⠀⠈⠢⡀⠀⠀ -⠀⠀⠀⠀⠀⠀⠀⠈⠢⡀ +⎡⠢⡀⠀⠀⠀⠀⠀⠀⠀⠀⎤ +⎢⠀⠈⠢⡀⠀⠀⠀⠀⠀⠀⎥ +⎢⠀⠀⠀⠈⠢⡀⠀⠀⠀⠀⎥ +⎢⠀⠀⠀⠀⠀⠈⠢⡀⠀⠀⎥ +⎣⠀⠀⠀⠀⠀⠀⠀⠈⠢⡀⎦ julia> fock(20, 4)' * a_d * fock(20, 3) 2.0 + 0.0im ``` """ -create(N::Int) = QuantumObject(spdiagm(-1 => Array{ComplexF64}(sqrt.(1:N-1))), Operator, [N]) +create(N::Int) = QuantumObject(spdiagm(-1 => Array{ComplexF64}(sqrt.(1:N-1))), Operator, N) @doc raw""" displace(N::Int, α::Number) @@ -162,7 +164,7 @@ Bosonic number operator with Hilbert space cutoff `N`. This operator is defined as ``\hat{N}=\hat{a}^\dagger \hat{a}``, where ``\hat{a}`` is the bosonic annihilation operator. """ -num(N::Int) = QuantumObject(spdiagm(0 => Array{ComplexF64}(0:N-1)), Operator, [N]) +num(N::Int) = QuantumObject(spdiagm(0 => Array{ComplexF64}(0:N-1)), Operator, N) @doc raw""" position(N::Int) @@ -218,7 +220,7 @@ function phase(N::Int, ϕ0::Real = 0) N_list = collect(0:(N-1)) ϕ = ϕ0 .+ (2 * π / N) .* N_list states = [exp.((1.0im * ϕ[m]) .* N_list) ./ sqrt(N) for m in 1:N] - return QuantumObject(sum([ϕ[m] * states[m] * states[m]' for m in 1:N]); type = Operator, dims = [N]) + return QuantumObject(sum([ϕ[m] * states[m] * states[m]' for m in 1:N]); type = Operator, dims = N) end @doc raw""" @@ -258,7 +260,7 @@ function jmat(j::Real, ::Val{:x}) throw(ArgumentError("The spin quantum number (j) must be a non-negative integer or half-integer.")) σ = _jm(j) - return QuantumObject((σ' + σ) / 2, Operator, [Int(J)]) + return QuantumObject((σ' + σ) / 2, Operator, Int(J)) end function jmat(j::Real, ::Val{:y}) J = 2 * j + 1 @@ -266,28 +268,28 @@ function jmat(j::Real, ::Val{:y}) throw(ArgumentError("The spin quantum number (j) must be a non-negative integer or half-integer.")) σ = _jm(j) - return QuantumObject((σ' - σ) / 2im, Operator, [Int(J)]) + return QuantumObject((σ' - σ) / 2im, Operator, Int(J)) end function jmat(j::Real, ::Val{:z}) J = 2 * j + 1 ((floor(J) != J) || (j < 0)) && throw(ArgumentError("The spin quantum number (j) must be a non-negative integer or half-integer.")) - return QuantumObject(_jz(j), Operator, [Int(J)]) + return QuantumObject(_jz(j), Operator, Int(J)) end function jmat(j::Real, ::Val{:+}) J = 2 * j + 1 ((floor(J) != J) || (j < 0)) && throw(ArgumentError("The spin quantum number (j) must be a non-negative integer or half-integer.")) - return QuantumObject(adjoint(_jm(j)), Operator, [Int(J)]) + return QuantumObject(adjoint(_jm(j)), Operator, Int(J)) end function jmat(j::Real, ::Val{:-}) J = 2 * j + 1 ((floor(J) != J) || (j < 0)) && throw(ArgumentError("The spin quantum number (j) must be a non-negative integer or half-integer.")) - return QuantumObject(_jm(j), Operator, [Int(J)]) + return QuantumObject(_jm(j), Operator, Int(J)) end jmat(j::Real, ::Val{T}) where {T} = throw(ArgumentError("Invalid spin operator: $(T)")) @@ -414,7 +416,7 @@ eye( QuantumObject(Diagonal(ones(ComplexF64, N)); type = type, dims = dims) @doc raw""" - fdestroy(N::Int, j::Int) + fdestroy(N::Union{Int,Val}, j::Int) Construct a fermionic destruction operator acting on the `j`-th site, where the fock space has totally `N`-sites: @@ -423,12 +425,15 @@ Here, we use the [Jordan-Wigner transformation](https://en.wikipedia.org/wiki/Jo d_j = \sigma_z^{\otimes j} \otimes \sigma_{-} \otimes I^{\otimes N-j-1} ``` -Note that the site index `j` should satisfy: `0 ≤ j ≤ N - 1` +Note that the site index `j` should satisfy: `0 ≤ j ≤ N - 1`. + +> [!IMPORTANT] +> If you want to keep type stability, it is recommended to use `fdestroy(Val(N), j)` instead of `fdestroy(N, j)`. See [this link](https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-value-type) and the [related Section](@ref doc:Type-Stability) for more details. """ -fdestroy(N::Int, j::Int) = _Jordan_Wigner(N, j, sigmam()) +fdestroy(N::Union{Int,Val}, j::Int) = _Jordan_Wigner(N, j, sigmam()) @doc raw""" - fcreate(N::Int, j::Int) + fcreate(N::Union{Int,Val}, j::Int) Construct a fermionic creation operator acting on the `j`-th site, where the fock space has totally `N`-sites: @@ -437,11 +442,17 @@ Here, we use the [Jordan-Wigner transformation](https://en.wikipedia.org/wiki/Jo d_j^\dagger = \sigma_z^{\otimes j} \otimes \sigma_{+} \otimes I^{\otimes N-j-1} ``` -Note that the site index `j` should satisfy: `0 ≤ j ≤ N - 1` +Note that the site index `j` should satisfy: `0 ≤ j ≤ N - 1`. + +> [!IMPORTANT] +> If you want to keep type stability, it is recommended to use `fcreate(Val(N), j)` instead of `fcreate(N, j)`. See [this link](https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-value-type) and the [related Section](@ref doc:Type-Stability) for more details. """ -fcreate(N::Int, j::Int) = _Jordan_Wigner(N, j, sigmap()) +fcreate(N::Union{Int,Val}, j::Int) = _Jordan_Wigner(N, j, sigmap()) + +_Jordan_Wigner(N::Int, j::Int, op::QuantumObject{<:AbstractArray{T},OperatorQuantumObject}) where {T} = + _Jordan_Wigner(Val(N), j, op) -function _Jordan_Wigner(N::Int, j::Int, op::QuantumObject{<:AbstractArray{T},OperatorQuantumObject}) where {T} +function _Jordan_Wigner(::Val{N}, j::Int, op::QuantumObject{<:AbstractArray{T},OperatorQuantumObject}) where {N,T} (N < 1) && throw(ArgumentError("The total number of sites (N) cannot be less than 1")) ((j >= N) || (j < 0)) && throw(ArgumentError("The site index (j) should satisfy: 0 ≤ j ≤ N - 1")) @@ -451,7 +462,7 @@ function _Jordan_Wigner(N::Int, j::Int, op::QuantumObject{<:AbstractArray{T},Ope S = 2^(N - j - 1) I_tensor = sparse((1.0 + 0.0im) * LinearAlgebra.I, S, S) - return QuantumObject(kron(Z_tensor, op.data, I_tensor); type = Operator, dims = fill(2, N)) + return QuantumObject(kron(Z_tensor, op.data, I_tensor); type = Operator, dims = ntuple(i -> 2, Val(N))) end @doc raw""" @@ -462,7 +473,7 @@ Generates the projection operator ``\hat{O} = \dyad{i}{j}`` with Hilbert space d projection(N::Int, i::Int, j::Int) = QuantumObject(sparse([i + 1], [j + 1], [1.0 + 0.0im], N, N)) @doc raw""" - tunneling(N::Int, m::Int=1; sparse::Bool=false) + tunneling(N::Int, m::Int=1; sparse::Union{Bool,Val{<:Bool}}=Val(false)) Generate a tunneling operator defined as: @@ -471,15 +482,20 @@ Generate a tunneling operator defined as: ``` where ``N`` is the number of basis states in the Hilbert space, and ``m`` is the number of excitations in tunneling event. + +If `sparse=true`, the operator is returned as a sparse matrix, otherwise a dense matrix is returned. + +> [!IMPORTANT] +> If you want to keep type stability, it is recommended to use `tunneling(N, m, Val(sparse))` instead of `tunneling(N, m, sparse)`. See [this link](https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-value-type) and the [related Section](@ref doc:Type-Stability) for more details. """ -function tunneling(N::Int, m::Int = 1; sparse::Bool = false) +function tunneling(N::Int, m::Int = 1; sparse::Union{Bool,Val} = Val(false)) (m < 1) && throw(ArgumentError("The number of excitations (m) cannot be less than 1")) data = ones(ComplexF64, N - m) - if sparse - return QuantumObject(spdiagm(m => data, -m => data); type = Operator, dims = [N]) + if getVal(makeVal(sparse)) + return QuantumObject(spdiagm(m => data, -m => data); type = Operator, dims = N) else - return QuantumObject(diagm(m => data, -m => data); type = Operator, dims = [N]) + return QuantumObject(diagm(m => data, -m => data); type = Operator, dims = N) end end @@ -490,7 +506,7 @@ Generates a discrete Fourier transform matrix ``\hat{F}_N`` for [Quantum Fourier The `dimensions` can be either the following types: - `dimensions::Int`: Number of basis states in the Hilbert space. -- `dimensions::Vector{Int}`: list of dimensions representing the each number of basis in the subsystems. +- `dimensions::Union{AbstractVector{Int},Tuple}`: list of dimensions representing the each number of basis in the subsystems. ``N`` represents the total dimension, and therefore the matrix is defined as @@ -507,8 +523,9 @@ The `dimensions` can be either the following types: where ``\omega = \exp(\frac{2 \pi i}{N})``. """ -qft(dimensions::Int) = QuantumObject(_qft_op(dimensions), Operator, [dimensions]) -qft(dimensions::Vector{Int}) = QuantumObject(_qft_op(prod(dimensions)), Operator, dimensions) +qft(dimensions::Int) = QuantumObject(_qft_op(dimensions), Operator, dimensions) +qft(dimensions::Union{AbstractVector{T},Tuple}) where {T} = + QuantumObject(_qft_op(prod(dimensions)), Operator, dimensions) function _qft_op(N::Int) ω = exp(2.0im * π / N) arr = 0:(N-1) diff --git a/src/qobj/quantum_object.jl b/src/qobj/quantum_object.jl index 7abf2e66..1420cc70 100644 --- a/src/qobj/quantum_object.jl +++ b/src/qobj/quantum_object.jl @@ -111,10 +111,10 @@ A constant representing the type of [`OperatorKetQuantumObject`](@ref): a ket st const OperatorKet = OperatorKetQuantumObject() @doc raw""" - struct QuantumObject{MT<:AbstractArray,ObjType<:QuantumObjectType} + struct QuantumObject{MT<:AbstractArray,ObjType<:QuantumObjectType,N} data::MT type::ObjType - dims::Vector{Int} + dims::SVector{N, Int} end Julia struct representing any quantum objects. @@ -125,20 +125,35 @@ Julia struct representing any quantum objects. julia> a = destroy(20) Quantum Object: type=Operator dims=[20] size=(20, 20) ishermitian=false 20×20 SparseMatrixCSC{ComplexF64, Int64} with 19 stored entries: -⠈⠢⡀⠀⠀⠀⠀⠀⠀⠀ -⠀⠀⠈⠢⡀⠀⠀⠀⠀⠀ -⠀⠀⠀⠀⠈⠢⡀⠀⠀⠀ -⠀⠀⠀⠀⠀⠀⠈⠢⡀⠀ -⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢ +⎡⠈⠢⡀⠀⠀⠀⠀⠀⠀⠀⎤ +⎢⠀⠀⠈⠢⡀⠀⠀⠀⠀⠀⎥ +⎢⠀⠀⠀⠀⠈⠢⡀⠀⠀⠀⎥ +⎢⠀⠀⠀⠀⠀⠀⠈⠢⡀⠀⎥ +⎣⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⎦ julia> a isa QuantumObject true ``` """ -struct QuantumObject{MT<:AbstractArray,ObjType<:QuantumObjectType} <: AbstractQuantumObject +struct QuantumObject{MT<:AbstractArray,ObjType<:QuantumObjectType,N} <: AbstractQuantumObject data::MT type::ObjType - dims::Vector{Int} + dims::SVector{N,Int} + + function QuantumObject(data::MT, type::ObjType, dims) where {MT<:AbstractArray,ObjType<:QuantumObjectType} + _check_dims(dims) + + _size = _get_size(data) + _check_QuantumObject(type, dims, _size[1], _size[2]) + + N = length(dims) + + return new{MT,ObjType,N}(data, type, SVector{N,Int}(dims)) + end +end + +function QuantumObject(A::AbstractArray, type::ObjType, dims::Integer) where {ObjType<:QuantumObjectType} + return QuantumObject(A, type, SVector{1,Int}(dims)) end function QuantumObject( @@ -146,7 +161,7 @@ function QuantumObject( type::ObjType = nothing, dims = nothing, ) where {T,ObjType<:Union{Nothing,QuantumObjectType}} - _size = size(A) + _size = _get_size(A) if type isa Nothing type = (_size[1] == 1 && _size[2] > 1) ? Bra : Operator # default type @@ -160,15 +175,12 @@ function QuantumObject( if dims isa Nothing if type isa OperatorQuantumObject || type isa BraQuantumObject - dims = [_size[2]] + dims = SVector{1,Int}(_size[2]) elseif type isa SuperOperatorQuantumObject || type isa OperatorBraQuantumObject - dims = [isqrt(_size[2])] + dims = SVector{1,Int}(isqrt(_size[2])) end - else - _check_dims(dims) end - _check_QuantumObject(type, dims, _size[1], _size[2]) return QuantumObject(A, type, dims) end @@ -183,19 +195,15 @@ function QuantumObject( throw(ArgumentError("The argument type must be Ket or OperatorKet if the input array is a vector.")) end - _size = (length(A), 1) - if dims isa Nothing + _size = _get_size(A) if type isa KetQuantumObject - dims = [_size[1]] + dims = SVector{1,Int}(_size[1]) elseif type isa OperatorKetQuantumObject - dims = [isqrt(_size[1])] + dims = SVector{1,Int}(isqrt(_size[1])) end - else - _check_dims(dims) end - _check_QuantumObject(type, dims, _size[1], _size[2]) return QuantumObject(A, type, dims) end @@ -207,44 +215,62 @@ function QuantumObject( throw(DomainError(size(A), "The size of the array is not compatible with vector or matrix.")) end -_check_dims(dims::Vector{Int}) = - all(>(0), dims) || throw(DomainError(dims, "The argument dims must be a vector with positive integers.")) -_check_dims(dims::Any) = throw(ArgumentError("The argument dims must be a vector with positive integers.")) +_get_size(A::AbstractMatrix) = size(A) +_get_size(A::AbstractVector) = (length(A), 1) + +_non_static_array_warning(argname, arg::Tuple{}) = + throw(ArgumentError("The argument $argname must be a Tuple or a StaticVector of non-zero length.")) +_non_static_array_warning(argname, arg::Union{SVector{N,T},MVector{N,T},NTuple{N,T}}) where {N,T} = nothing +_non_static_array_warning(argname, arg::AbstractVector{T}) where {T} = + @warn "The argument $argname should be a Tuple or a StaticVector for better performance. Try to use `$argname = $(Tuple(arg))` or `$argname = SVector(" * + join(arg, ", ") * + ")` instead of `$argname = $arg`." maxlog = 1 + +function _check_dims(dims::Union{AbstractVector{T},NTuple{N,T}}) where {T<:Integer,N} + _non_static_array_warning("dims", dims) + return (all(>(0), dims) && length(dims) > 0) || + throw(DomainError(dims, "The argument dims must be of non-zero length and contain only positive integers.")) +end +_check_dims(dims::Any) = throw( + ArgumentError( + "The argument dims must be a Tuple or a StaticVector of non-zero length and contain only positive integers.", + ), +) -function _check_QuantumObject(type::KetQuantumObject, dims::Vector{Int}, m::Int, n::Int) +function _check_QuantumObject(type::KetQuantumObject, dims, m::Int, n::Int) (n != 1) && throw(DomainError((m, n), "The size of the array is not compatible with Ket")) (prod(dims) != m) && throw(DimensionMismatch("Ket with dims = $(dims) does not fit the array size = $((m, n)).")) return nothing end -function _check_QuantumObject(type::BraQuantumObject, dims::Vector{Int}, m::Int, n::Int) +function _check_QuantumObject(type::BraQuantumObject, dims, m::Int, n::Int) (m != 1) && throw(DomainError((m, n), "The size of the array is not compatible with Bra")) (prod(dims) != n) && throw(DimensionMismatch("Bra with dims = $(dims) does not fit the array size = $((m, n)).")) return nothing end -function _check_QuantumObject(type::OperatorQuantumObject, dims::Vector{Int}, m::Int, n::Int) +function _check_QuantumObject(type::OperatorQuantumObject, dims, m::Int, n::Int) (m != n) && throw(DomainError((m, n), "The size of the array is not compatible with Operator")) (prod(dims) != m) && throw(DimensionMismatch("Operator with dims = $(dims) does not fit the array size = $((m, n)).")) return nothing end -function _check_QuantumObject(type::SuperOperatorQuantumObject, dims::Vector{Int}, m::Int, n::Int) +function _check_QuantumObject(type::SuperOperatorQuantumObject, dims, m::Int, n::Int) (m != n) && throw(DomainError((m, n), "The size of the array is not compatible with SuperOperator")) (prod(dims) != sqrt(m)) && throw(DimensionMismatch("SuperOperator with dims = $(dims) does not fit the array size = $((m, n)).")) return nothing end -function _check_QuantumObject(type::OperatorKetQuantumObject, dims::Vector{Int}, m::Int, n::Int) +function _check_QuantumObject(type::OperatorKetQuantumObject, dims, m::Int, n::Int) (n != 1) && throw(DomainError((m, n), "The size of the array is not compatible with OperatorKet")) (prod(dims) != sqrt(m)) && throw(DimensionMismatch("OperatorKet with dims = $(dims) does not fit the array size = $((m, n)).")) return nothing end -function _check_QuantumObject(type::OperatorBraQuantumObject, dims::Vector{Int}, m::Int, n::Int) +function _check_QuantumObject(type::OperatorBraQuantumObject, dims, m::Int, n::Int) (m != 1) && throw(DomainError((m, n), "The size of the array is not compatible with OperatorBra")) (prod(dims) != sqrt(n)) && throw(DimensionMismatch("OperatorBra with dims = $(dims) does not fit the array size = $((m, n)).")) diff --git a/src/qobj/states.jl b/src/qobj/states.jl index 0dd76534..d1b8a0e9 100644 --- a/src/qobj/states.jl +++ b/src/qobj/states.jl @@ -14,20 +14,26 @@ Returns a zero [`Ket`](@ref) vector with given argument `dimensions`. The `dimensions` can be either the following types: - `dimensions::Int`: Number of basis states in the Hilbert space. -- `dimensions::Vector{Int}`: list of dimensions representing the each number of basis in the subsystems. +- `dimensions::Union{AbstractVector{Int}, Tuple}`: list of dimensions representing the each number of basis in the subsystems. """ -zero_ket(dimensions::Int) = QuantumObject(zeros(ComplexF64, dimensions), Ket, [dimensions]) -zero_ket(dimensions::Vector{Int}) = QuantumObject(zeros(ComplexF64, prod(dimensions)), Ket, dimensions) +zero_ket(dimensions::Int) = QuantumObject(zeros(ComplexF64, dimensions), Ket, dimensions) +zero_ket(dimensions::Union{AbstractVector{Int},Tuple}) = + QuantumObject(zeros(ComplexF64, prod(dimensions)), Ket, dimensions) @doc raw""" - fock(N::Int, pos::Int=0; dims::Vector{Int}=[N], sparse::Bool=false) + fock(N::Int, pos::Int=0; dims::Union{Int,AbstractVector{Int},Tuple}=N, sparse::Union{Bool,Val}=Val(false)) Generates a fock state ``\ket{\psi}`` of dimension `N`. It is also possible to specify the list of dimensions `dims` if different subsystems are present. """ -function fock(N::Int, pos::Int = 0; dims::Vector{Int} = [N], sparse::Bool = false) - if sparse +function fock( + N::Int, + pos::Int = 0; + dims::Union{Int,AbstractVector{Int},Tuple} = N, + sparse::Union{Bool,Val} = Val(false), +) + if getVal(makeVal(sparse)) array = sparsevec([pos + 1], [1.0 + 0im], N) else array = zeros(ComplexF64, N) @@ -37,13 +43,13 @@ function fock(N::Int, pos::Int = 0; dims::Vector{Int} = [N], sparse::Bool = fals end @doc raw""" - basis(N::Int, pos::Int = 0; dims::Vector{Int}=[N]) + basis(N::Int, pos::Int = 0; dims::Union{Int,AbstractVector{Int},Tuple}=N) Generates a fock state like [`fock`](@ref). It is also possible to specify the list of dimensions `dims` if different subsystems are present. """ -basis(N::Int, pos::Int = 0; dims::Vector{Int} = [N]) = fock(N, pos, dims = dims) +basis(N::Int, pos::Int = 0; dims::Union{Int,AbstractVector{Int},Tuple} = N) = fock(N, pos, dims = dims) @doc raw""" coherent(N::Int, α::Number) @@ -61,25 +67,30 @@ Generate a random normalized [`Ket`](@ref) vector with given argument `dimension The `dimensions` can be either the following types: - `dimensions::Int`: Number of basis states in the Hilbert space. -- `dimensions::Vector{Int}`: list of dimensions representing the each number of basis in the subsystems. +- `dimensions::Union{AbstractVector{Int},Tuple}`: list of dimensions representing the each number of basis in the subsystems. """ -rand_ket(dimensions::Int) = rand_ket([dimensions]) -function rand_ket(dimensions::Vector{Int}) +rand_ket(dimensions::Int) = rand_ket(SVector(dimensions)) +function rand_ket(dimensions::Union{AbstractVector{Int},Tuple}) N = prod(dimensions) ψ = rand(ComplexF64, N) .- (0.5 + 0.5im) return QuantumObject(normalize!(ψ); type = Ket, dims = dimensions) end @doc raw""" - fock_dm(N::Int, pos::Int=0; dims::Vector{Int}=[N], sparse::Bool=false) + fock_dm(N::Int, pos::Int=0; dims::Union{Int,AbstractVector{Int},Tuple}=N, sparse::Union{Bool,Val}=Val(false)) Density matrix representation of a Fock state. Constructed via outer product of [`fock`](@ref). """ -function fock_dm(N::Int, pos::Int = 0; dims::Vector{Int} = [N], sparse::Bool = false) +function fock_dm( + N::Int, + pos::Int = 0; + dims::Union{Int,AbstractVector{Int},Tuple} = N, + sparse::Union{Bool,Val} = Val(false), +) ψ = fock(N, pos; dims = dims, sparse = sparse) - return ψ * ψ' + return ket2dm(ψ) end @doc raw""" @@ -91,24 +102,28 @@ Constructed via outer product of [`coherent`](@ref). """ function coherent_dm(N::Int, α::T) where {T<:Number} ψ = coherent(N, α) - return ψ * ψ' + return ket2dm(ψ) end @doc raw""" - thermal_dm(N::Int, n::Real; sparse::Bool=false) + thermal_dm(N::Int, n::Real; sparse::Union{Bool,Val}=Val(false)) Density matrix for a thermal state (generating thermal state probabilities) with the following arguments: - `N::Int`: Number of basis states in the Hilbert space - `n::Real`: Expectation value for number of particles in the thermal state. +- `sparse::Union{Bool,Val}`: If `true`, return a sparse matrix representation. + +> [!IMPORTANT] +> If you want to keep type stability, it is recommended to use `thermal_dm(N, n, Val(sparse))` instead of `thermal_dm(N, n, sparse)`. See [this link](https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-value-type) and the [related Section](@ref doc:Type-Stability) for more details. """ -function thermal_dm(N::Int, n::Real; sparse::Bool = false) +function thermal_dm(N::Int, n::Real; sparse::Union{Bool,Val} = Val(false)) β = log(1.0 / n + 1.0) N_list = Array{Float64}(0:N-1) data = exp.(-β .* N_list) - if sparse - return QuantumObject(spdiagm(0 => data ./ sum(data)), Operator, [N]) + if getVal(makeVal(sparse)) + return QuantumObject(spdiagm(0 => data ./ sum(data)), Operator, N) else - return QuantumObject(diagm(0 => data ./ sum(data)), Operator, [N]) + return QuantumObject(diagm(0 => data ./ sum(data)), Operator, N) end end @@ -119,10 +134,10 @@ Returns the maximally mixed density matrix with given argument `dimensions`. The `dimensions` can be either the following types: - `dimensions::Int`: Number of basis states in the Hilbert space. -- `dimensions::Vector{Int}`: list of dimensions representing the each number of basis in the subsystems. +- `dimensions::Union{AbstractVector{Int},Tuple}`: list of dimensions representing the each number of basis in the subsystems. """ -maximally_mixed_dm(dimensions::Int) = QuantumObject(I(dimensions) / complex(dimensions), Operator, [dimensions]) -function maximally_mixed_dm(dimensions::Vector{Int}) +maximally_mixed_dm(dimensions::Int) = QuantumObject(I(dimensions) / complex(dimensions), Operator, SVector(dimensions)) +function maximally_mixed_dm(dimensions::Union{AbstractVector{Int},Tuple}) N = prod(dimensions) return QuantumObject(I(N) / complex(N), Operator, dimensions) end @@ -134,7 +149,7 @@ Generate a random density matrix from Ginibre ensemble with given argument `dime The `dimensions` can be either the following types: - `dimensions::Int`: Number of basis states in the Hilbert space. -- `dimensions::Vector{Int}`: list of dimensions representing the each number of basis in the subsystems. +- `dimensions::Union{AbstractVector{Int},Tuple}`: list of dimensions representing the each number of basis in the subsystems. The default keyword argument `rank = prod(dimensions)` (full rank). @@ -142,8 +157,8 @@ The default keyword argument `rank = prod(dimensions)` (full rank). - [J. Ginibre, Statistical ensembles of complex, quaternion, and real matrices, Journal of Mathematical Physics 6.3 (1965): 440-449](https://doi.org/10.1063/1.1704292) - [K. Życzkowski, et al., Generating random density matrices, Journal of Mathematical Physics 52, 062201 (2011)](http://dx.doi.org/10.1063/1.3595693) """ -rand_dm(dimensions::Int; rank::Int = prod(dimensions)) = rand_dm([dimensions], rank = rank) -function rand_dm(dimensions::Vector{Int}; rank::Int = prod(dimensions)) +rand_dm(dimensions::Int; rank::Int = prod(dimensions)) = rand_dm(SVector(dimensions), rank = rank) +function rand_dm(dimensions::Union{AbstractVector{Int},Tuple}; rank::Int = prod(dimensions)) N = prod(dimensions) (rank < 1) && throw(DomainError(rank, "The argument rank must be larger than 1.")) (rank > N) && throw(DomainError(rank, "The argument rank cannot exceed dimensions.")) @@ -230,10 +245,10 @@ Quantum Object: type=Ket dims=[2, 2] size=(4,) ``` """ bell_state(x::Int, z::Int) = bell_state(Val(x), Val(z)) -bell_state(::Val{0}, ::Val{0}) = QuantumObject(ComplexF64[1, 0, 0, 1] / sqrt(2), Ket, [2, 2]) -bell_state(::Val{0}, ::Val{1}) = QuantumObject(ComplexF64[1, 0, 0, -1] / sqrt(2), Ket, [2, 2]) -bell_state(::Val{1}, ::Val{0}) = QuantumObject(ComplexF64[0, 1, 1, 0] / sqrt(2), Ket, [2, 2]) -bell_state(::Val{1}, ::Val{1}) = QuantumObject(ComplexF64[0, 1, -1, 0] / sqrt(2), Ket, [2, 2]) +bell_state(::Val{0}, ::Val{0}) = QuantumObject(ComplexF64[1, 0, 0, 1] / sqrt(2), Ket, (2, 2)) +bell_state(::Val{0}, ::Val{1}) = QuantumObject(ComplexF64[1, 0, 0, -1] / sqrt(2), Ket, (2, 2)) +bell_state(::Val{1}, ::Val{0}) = QuantumObject(ComplexF64[0, 1, 1, 0] / sqrt(2), Ket, (2, 2)) +bell_state(::Val{1}, ::Val{1}) = QuantumObject(ComplexF64[0, 1, -1, 0] / sqrt(2), Ket, (2, 2)) bell_state(::Val{T1}, ::Val{T2}) where {T1,T2} = throw(ArgumentError("Invalid Bell state: $(T1), $(T2)")) @doc raw""" @@ -241,7 +256,7 @@ bell_state(::Val{T1}, ::Val{T2}) where {T1,T2} = throw(ArgumentError("Invalid Be Return the two particle singlet state: ``\frac{1}{\sqrt{2}} ( |01\rangle - |10\rangle )`` """ -singlet_state() = QuantumObject(ComplexF64[0, 1, -1, 0] / sqrt(2), Ket, [2, 2]) +singlet_state() = QuantumObject(ComplexF64[0, 1, -1, 0] / sqrt(2), Ket, (2, 2)) @doc raw""" triplet_states() @@ -254,29 +269,33 @@ Return a list of the two particle triplet states: """ function triplet_states() return QuantumObject[ - QuantumObject(ComplexF64[0, 0, 0, 1], Ket, [2, 2]), - QuantumObject(ComplexF64[0, 1, 1, 0] / sqrt(2), Ket, [2, 2]), - QuantumObject(ComplexF64[1, 0, 0, 0], Ket, [2, 2]), + QuantumObject(ComplexF64[0, 0, 0, 1], Ket, (2, 2)), + QuantumObject(ComplexF64[0, 1, 1, 0] / sqrt(2), Ket, (2, 2)), + QuantumObject(ComplexF64[1, 0, 0, 0], Ket, (2, 2)), ] end @doc raw""" - w_state(n::Int) + w_state(n::Union{Int,Val}) Returns the `n`-qubit [W-state](https://en.wikipedia.org/wiki/W_state): ```math \frac{1}{\sqrt{n}} \left( |100...0\rangle + |010...0\rangle + \cdots + |00...01\rangle \right) ``` + +> [!IMPORTANT] +> If you want to keep type stability, it is recommended to use `w_state(Val(n))` instead of `w_state(n)`. See [this link](https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-value-type) and the [related Section](@ref doc:Type-Stability) for more details. """ -function w_state(n::Int) +function w_state(::Val{n}) where {n} nzind = 2 .^ (0:(n-1)) .+ 1 nzval = fill(ComplexF64(1 / sqrt(n)), n) - return QuantumObject(SparseVector(2^n, nzind, nzval), Ket, fill(2, n)) + return QuantumObject(SparseVector(2^n, nzind, nzval), Ket, ntuple(x -> 2, Val(n))) end +w_state(n::Int) = w_state(Val(n)) @doc raw""" - ghz_state(n::Int; d::Int=2) + ghz_state(n::Union{Int,Val}; d::Int=2) Returns the generalized `n`-qudit [Greenberger–Horne–Zeilinger (GHZ) state](https://en.wikipedia.org/wiki/Greenberger%E2%80%93Horne%E2%80%93Zeilinger_state): @@ -285,9 +304,13 @@ Returns the generalized `n`-qudit [Greenberger–Horne–Zeilinger (GHZ) state]( ``` Here, `d` specifies the dimension of each qudit. Default to `d=2` (qubit). + +> [!IMPORTANT] +> If you want to keep type stability, it is recommended to use `ghz_state(Val(n); kwargs...)` instead of `ghz_state(n; kwargs...)`. See [this link](https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-value-type) and the [related Section](@ref doc:Type-Stability) for more details. """ -function ghz_state(n::Int; d::Int = 2) +function ghz_state(::Val{n}; d::Int = 2) where {n} nzind = collect((0:(d-1)) .* Int((d^n - 1) / (d - 1)) .+ 1) nzval = ones(ComplexF64, d) / sqrt(d) - return QuantumObject(SparseVector(d^n, nzind, nzval), Ket, fill(d, n)) + return QuantumObject(SparseVector(d^n, nzind, nzval), Ket, ntuple(x -> d, Val(n))) end +ghz_state(n::Int; d::Int = 2) = ghz_state(Val(n), d = d) diff --git a/src/spin_lattice.jl b/src/spin_lattice.jl index cf3ba11d..26aa48ae 100644 --- a/src/spin_lattice.jl +++ b/src/spin_lattice.jl @@ -18,7 +18,7 @@ end #Definition of many-body operators function mb(s::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject}, i::Integer, N::Integer) where {T1} T = s.dims[1] - return QuantumObject(kron(eye(T^(i - 1)), s, eye(T^(N - i))); dims = fill(2, N)) + return QuantumObject(kron(eye(T^(i - 1)), s, eye(T^(N - i))); dims = ntuple(j -> 2, Val(N))) end mb(s::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject}, i::Integer, latt::Lattice) where {T1} = mb(s, i, latt.N) mb(s::QuantumObject{<:AbstractArray{T1},OperatorQuantumObject}, row::Integer, col::Integer, latt::Lattice) where {T1} = @@ -42,8 +42,7 @@ function TFIM(Jx::Real, Jy::Real, Jz::Real, hx::Real, γ::Real, latt::Lattice; b S = [mb(sm, i, latt) for i in 1:latt.N] c_ops = sqrt(γ) .* S - op_sum(S::Vector{QuantumObject{SparseMatrixCSC{ComplexF64,Int64},OperatorQuantumObject}}, i::CartesianIndex) = - S[latt.lin_idx[i]] * sum(S[latt.lin_idx[nn(i, latt, bc; order = order)]]) + op_sum(S, i::CartesianIndex) = S[latt.lin_idx[i]] * sum(S[latt.lin_idx[nn(i, latt, bc; order = order)]]) H = 0 if (Jx != 0 || hx != 0) diff --git a/src/time_evolution/time_evolution.jl b/src/time_evolution/time_evolution.jl index dfb94463..089c2baf 100644 --- a/src/time_evolution/time_evolution.jl +++ b/src/time_evolution/time_evolution.jl @@ -282,7 +282,7 @@ function liouvillian_generalized( ) where {MT<:AbstractMatrix} (length(fields) == length(T_list)) || throw(DimensionMismatch("The number of fields, ωs and Ts must be the same.")) - dims = N_trunc == size(H, 1) ? H.dims : [N_trunc] + dims = N_trunc == size(H, 1) ? H.dims : SVector(N_trunc) result = eigen(H) E = real.(result.values[1:N_trunc]) U = QuantumObject(result.vectors, result.type, result.dims) diff --git a/src/time_evolution/time_evolution_dynamical.jl b/src/time_evolution/time_evolution_dynamical.jl index c5738170..3def16a3 100644 --- a/src/time_evolution/time_evolution_dynamical.jl +++ b/src/time_evolution/time_evolution_dynamical.jl @@ -4,10 +4,10 @@ export dfd_mesolve, dsf_mesolve, dsf_mcsolve function _reduce_dims( QO::AbstractArray{T}, - dims::Vector{<:Integer}, - sel::AbstractVector, - reduce::AbstractVector, -) where {T} + dims::Union{SVector{N,DT},MVector{N,DT}}, + sel, + reduce, +) where {T,N,DT<:Integer} rd = dims nd = length(rd) rd_new = zero(rd) @@ -29,13 +29,13 @@ end function _increase_dims( QO::AbstractArray{T}, - dims::Vector{<:Integer}, - sel::AbstractVector, - increase::AbstractVector, -) where {T} + dims::Union{SVector{N,DT},MVector{N,DT}}, + sel, + increase, +) where {T,N,DT<:Integer} rd = dims nd = length(rd) - rd_new = zero(rd) + rd_new = MVector(zero(rd)) # Mutable SVector rd_new[sel] .= increase @. rd_new = rd + rd_new @@ -78,7 +78,7 @@ function _DFDIncreaseReduceCondition(u, t, integrator) dim_i = dim_list[i] pillow_i = pillow_list[i] if dim_i < maxdim_i && dim_i > 2 && maxdim_i != 0 - ρi = _ptrace_oper(vec2mat(dfd_ρt_cache), dim_list, [i])[1] + ρi = _ptrace_oper(vec2mat(dfd_ρt_cache), dim_list, SVector(i))[1] @views res = norm(ρi[diagind(ρi)[end-pillow_i:end]], 1) * sqrt(dim_i) / pillow_i if res > tol_list[i] increase_list[i] = true @@ -106,9 +106,9 @@ function _DFDIncreaseReduceAffect!(integrator) ρt = vec2mat(dfd_ρt_cache) - @views pillow_increase = pillow_list[increase_list] - @views pillow_reduce = pillow_list[reduce_list] - dim_increase = findall(increase_list) + pillow_increase = pillow_list[increase_list] # TODO: This returns a Vector. Find a way to return an SVector or NTuple + pillow_reduce = pillow_list[reduce_list] + dim_increase = findall(increase_list) # TODO: This returns a Vector. Find a way to return an SVector or NTuple dim_reduce = findall(reduce_list) if length(dim_increase) > 0 @@ -151,15 +151,15 @@ function dfd_mesolveProblem( length(ψ0.dims) != length(maxdims) && throw(DimensionMismatch("'dim_list' and 'maxdims' do not have the same dimension.")) - dim_list = deepcopy(ψ0.dims) + dim_list = MVector(ψ0.dims) H₀ = H(dim_list, dfd_params) c_ops₀ = c_ops(dim_list, dfd_params) e_ops₀ = e_ops(dim_list, dfd_params) dim_list_evo_times = [0.0] dim_list_evo = [dim_list] - reduce_list = zeros(Bool, length(dim_list)) - increase_list = zeros(Bool, length(dim_list)) + reduce_list = MVector(ntuple(i -> false, length(dim_list))) + increase_list = MVector(ntuple(i -> false, length(dim_list))) pillow_list = _dfd_set_pillow.(dim_list) params2 = merge( diff --git a/test/code_quality.jl b/test/code_quality.jl index 591561a4..724639a3 100644 --- a/test/code_quality.jl +++ b/test/code_quality.jl @@ -2,7 +2,7 @@ using Aqua, JET @testset "Code quality" verbose = true begin @testset "Aqua.jl" begin - Aqua.test_all(QuantumToolbox; ambiguities = false) + Aqua.test_all(QuantumToolbox; ambiguities = false, unbound_args = false) end @testset "JET.jl" begin diff --git a/test/low_rank_dynamics.jl b/test/low_rank_dynamics.jl index a83a84de..fda8d5aa 100644 --- a/test/low_rank_dynamics.jl +++ b/test/low_rank_dynamics.jl @@ -8,7 +8,7 @@ M = Nx * Ny + 1 # Define initial state - ϕ = Vector{QuantumObject{Vector{ComplexF64},KetQuantumObject}}(undef, M) + ϕ = Vector{QuantumObject{Vector{ComplexF64},KetQuantumObject,M - 1}}(undef, M) ϕ[1] = tensor(repeat([basis(2, 0)], N_modes)...) i = 1 for j in 1:N_modes @@ -29,7 +29,7 @@ B = Matrix(Diagonal([1 + 0im; zeros(M - 1)])) S = z' * z B = B / tr(S * B) - ρ = Qobj(z * B * z', dims = ones(Int, N_modes) * N_cut) + ρ = Qobj(z * B * z', dims = ntuple(i -> 1, Val(N_modes)) .* N_cut) # Define Hamiltonian and collapse operators Jx = 0.9 diff --git a/test/negativity_and_partial_transpose.jl b/test/negativity_and_partial_transpose.jl index a72178ce..d729d0f4 100644 --- a/test/negativity_and_partial_transpose.jl +++ b/test/negativity_and_partial_transpose.jl @@ -7,7 +7,7 @@ 1 -3 5 1 15 1 1 15 ]; - dims = [2, 2], + dims = (2, 2), ) Neg = negativity(rho, 1) @test Neg ≈ 0.25 @@ -18,7 +18,7 @@ @testset "partial_transpose" begin # A (24 * 24)-matrix which contains number 1 ~ 576 - A_dense = Qobj(reshape(1:(24^2), (24, 24)), dims = [2, 3, 4]) + A_dense = Qobj(reshape(1:(24^2), (24, 24)), dims = (2, 3, 4)) A_sparse = dense_to_sparse(A_dense) PT = (true, false) for s1 in PT diff --git a/test/quantum_objects.jl b/test/quantum_objects.jl index b1bd0d93..1ef56ae4 100644 --- a/test/quantum_objects.jl +++ b/test/quantum_objects.jl @@ -51,19 +51,22 @@ # unsupported type of dims @testset "unsupported dims" begin - @test_throws ArgumentError Qobj(rand(2, 2), dims = 2) - @test_throws ArgumentError Qobj(rand(2, 2), dims = [2.0]) - @test_throws ArgumentError Qobj(rand(2, 2), dims = [2.0 + 0.0im]) - @test_throws DomainError Qobj(rand(2, 2), dims = [0]) - @test_throws DomainError Qobj(rand(2, 2), dims = [2, -2]) + @test_throws ArgumentError Qobj(rand(2, 2), dims = 2.0) + @test_throws ArgumentError Qobj(rand(2, 2), dims = 2.0 + 0.0im) + @test_throws DomainError Qobj(rand(2, 2), dims = 0) + @test_throws DomainError Qobj(rand(2, 2), dims = (2, -2)) + @test_logs ( + :warn, + "The argument dims should be a Tuple or a StaticVector for better performance. Try to use `dims = (2, 2)` or `dims = SVector(2, 2)` instead of `dims = [2, 2]`.", + ) Qobj(rand(4, 4), dims = [2, 2]) end @testset "Ket and Bra" begin N = 10 a = rand(ComplexF64, 10) # @test_logs (:warn, "The norm of the input data is not one.") QuantumObject(a) - @test_throws DimensionMismatch Qobj(a, dims = [2]) - @test_throws DimensionMismatch Qobj(a', dims = [2]) + @test_throws DimensionMismatch Qobj(a, dims = 2) + @test_throws DimensionMismatch Qobj(a', dims = 2) a2 = Qobj(a') a3 = Qobj(a) @test dag(a3) == a2 # Here we are also testing the dag function @@ -103,7 +106,7 @@ @test isoperket(a3) == false @test isoperbra(a3) == false @test isunitary(a3) == false - @test_throws DimensionMismatch Qobj(a, dims = [2]) + @test_throws DimensionMismatch Qobj(a, dims = 2) end @testset "OperatorKet and OperatorBra" begin @@ -137,8 +140,8 @@ @test L * ρ_ket ≈ -1im * (+(spre(H) * ρ_ket) - spost(H) * ρ_ket) @test (ρ_bra * L')' == L * ρ_ket @test sum((conj(ρ) .* ρ).data) ≈ dot(ρ_ket, ρ_ket) ≈ ρ_bra * ρ_ket - @test_throws DimensionMismatch Qobj(ρ_ket.data, type = OperatorKet, dims = [4]) - @test_throws DimensionMismatch Qobj(ρ_bra.data, type = OperatorBra, dims = [4]) + @test_throws DimensionMismatch Qobj(ρ_ket.data, type = OperatorKet, dims = 4) + @test_throws DimensionMismatch Qobj(ρ_bra.data, type = OperatorBra, dims = 4) end @testset "arithmetic" begin @@ -452,17 +455,17 @@ ket_b = Qobj(rand(ComplexF64, 3)) ket_c = Qobj(rand(ComplexF64, 4)) ket_d = Qobj(rand(ComplexF64, 5)) - ket_bdca = permute(tensor(ket_a, ket_b, ket_c, ket_d), [2, 4, 3, 1]) + ket_bdca = permute(tensor(ket_a, ket_b, ket_c, ket_d), (2, 4, 3, 1)) bra_a = ket_a' bra_b = ket_b' bra_c = ket_c' bra_d = ket_d' - bra_bdca = permute(tensor(bra_a, bra_b, bra_c, bra_d), [2, 4, 3, 1]) + bra_bdca = permute(tensor(bra_a, bra_b, bra_c, bra_d), (2, 4, 3, 1)) op_a = Qobj(rand(ComplexF64, 2, 2)) op_b = Qobj(rand(ComplexF64, 3, 3)) op_c = Qobj(rand(ComplexF64, 4, 4)) op_d = Qobj(rand(ComplexF64, 5, 5)) - op_bdca = permute(tensor(op_a, op_b, op_c, op_d), [2, 4, 3, 1]) + op_bdca = permute(tensor(op_a, op_b, op_c, op_d), (2, 4, 3, 1)) correct_dims = [3, 5, 4, 2] wrong_order1 = [1] wrong_order2 = [2, 3, 4, 5] diff --git a/test/states_and_operators.jl b/test/states_and_operators.jl index e57a35f3..1a5448fe 100644 --- a/test/states_and_operators.jl +++ b/test/states_and_operators.jl @@ -1,7 +1,7 @@ @testset "States and Operators" verbose = true begin @testset "zero state" begin v1 = zero_ket(4) - v2 = zero_ket([2, 2]) + v2 = zero_ket((2, 2)) @test size(v1) == (4,) @test size(v2) == (4,) @test v1.data == v2.data @@ -14,8 +14,8 @@ @testset "fock state" begin # fock, basis, and fock_dm - @test fock_dm(4; dims = [2, 2], sparse = true) ≈ ket2dm(basis(4; dims = [2, 2])) - @test_throws DimensionMismatch fock(4; dims = [2]) + @test fock_dm(4; dims = (2, 2), sparse = true) ≈ ket2dm(basis(4; dims = (2, 2))) + @test_throws DimensionMismatch fock(4; dims = 2) end @testset "coherent state" begin @@ -51,7 +51,7 @@ @testset "maximally mixed state" begin ρ1 = maximally_mixed_dm(4) - ρ2 = maximally_mixed_dm([2, 2]) + ρ2 = maximally_mixed_dm((2, 2)) @test size(ρ1) == (4, 4) @test size(ρ2) == (4, 4) @test tr(ρ1) ≈ 1.0 @@ -67,7 +67,7 @@ @testset "random state" begin # rand_ket and rand_dm ψ = rand_ket(10) - ρ_AB = rand_dm([2, 2]) + ρ_AB = rand_dm((2, 2)) ρ_A = ptrace(ρ_AB, 1) ρ_B = ptrace(ρ_AB, 2) rank = 5 @@ -173,7 +173,7 @@ @testset "quantum Fourier transform" begin N = 9 - dims = [3, 3] + dims = (3, 3) ω = exp(2.0im * π / N) x = Qobj(rand(ComplexF64, N)) ψx = basis(N, 0; dims = dims) @@ -192,8 +192,8 @@ @testset "random unitary" begin U1 = rand_unitary(20) U2 = rand_unitary(20, :exp) - U3 = rand_unitary([5, 5]) - U4 = rand_unitary([5, 5], :exp) + U3 = rand_unitary((5, 5)) + U4 = rand_unitary((5, 5), :exp) @test isunitary(U1) @test isunitary(U2) @test isunitary(U3) @@ -286,7 +286,7 @@ # test commutation relations for fermionic creation and annihilation operators sites = 4 SIZE = 2^sites - dims = fill(2, sites) + dims = ntuple(i -> 2, Val(sites)) Q_iden = Qobj(sparse((1.0 + 0.0im) * LinearAlgebra.I, SIZE, SIZE); dims = dims) Q_zero = Qobj(spzeros(ComplexF64, SIZE, SIZE); dims = dims) for i in 0:(sites-1) @@ -312,9 +312,9 @@ @testset "identity operator" begin I_op1 = qeye(4) - I_op2 = qeye(4, dims = [2, 2]) + I_op2 = qeye(4, dims = (2, 2)) I_su1 = qeye(4, type = SuperOperator) - I_su2 = qeye(4, type = SuperOperator, dims = [2]) + I_su2 = qeye(4, type = SuperOperator, dims = 2) @test isunitary(I_op1) == true @test isunitary(I_op2) == true @test isunitary(I_su1) == false @@ -326,17 +326,17 @@ @test (I_op2 == I_su1) == false @test (I_op2 == I_su2) == false @test (I_su1 == I_su2) == true - @test_throws DimensionMismatch qeye(4, dims = [2]) + @test_throws DimensionMismatch qeye(4, dims = 2) @test_throws DimensionMismatch qeye(2, type = SuperOperator) - @test_throws DimensionMismatch qeye(4, type = SuperOperator, dims = [2, 2]) + @test_throws DimensionMismatch qeye(4, type = SuperOperator, dims = (2, 2)) end @testset "superoperators" begin # spre, spost, and sprepost Xs = sigmax() Xd = sparse_to_dense(Xs) - A_wrong1 = Qobj(rand(4, 4), dims = [4]) - A_wrong2 = Qobj(rand(4, 4), dims = [2, 2]) + A_wrong1 = Qobj(rand(4, 4), dims = 4) + A_wrong2 = Qobj(rand(4, 4), dims = (2, 2)) A_wrong3 = Qobj(rand(3, 3)) @test (typeof(spre(Xd).data) <: SparseMatrixCSC) == true @test (typeof(spre(Xs).data) <: SparseMatrixCSC) == true