Toolkit
This module contains the toolkit of the package.
The constants, types, macros, functions defined in this module will not be exported by the package. Instead, they serve as the prerequisites. The range of the contents are quite wide, but basically, they fall into two categories:
- Utilities, such as global constants and miscellaneous tiny useful functions;
- Basic data structures as supplements to the
Julia.Base
and other common packages.
Utilities
QuantumLattices.Toolkit.atol
— ConstantAbsolute tolerance for float numbers.
QuantumLattices.Toolkit.rtol
— ConstantRelative tolerance for float numbers.
QuantumLattices.Toolkit.Float
— TypeDefault float type.
QuantumLattices.Toolkit.concatenate
— Functionconcatenate(ts::Tuple...) -> Tuple
Concatenate tuples.
QuantumLattices.Toolkit.tostr
— Functiontostr(number, ::Integer=5) -> String
tostr(number::Integer, n::Integer=5) -> String
tostr(number::Rational, n::Integer=5) -> String
tostr(number::AbstractFloat, n::Integer=5) -> String
tostr(number::Complex, n::Integer=5) -> String
Convert a number to a string with at most n
decimal places.
tostr(value::Symbol) -> String
tostr(value::Colon) -> String
tostr(value::Char) -> String
Convert a single value to string.
QuantumLattices.Toolkit.subscript
— Functionsubscript(i::Integer) -> String
Convert an integer to the subscript.
QuantumLattices.Toolkit.superscript
— Functionsuperscript(i::Integer) -> String
Convert an integer to the superscript.
QuantumLattices.Toolkit.delta
— Functiondelta(i, j) -> Int
Kronecker delta function.
QuantumLattices.id
— Functionid(od::OrderedDict, i) -> keytype(od)
Get the ith key of an OrderedDict
.
id(m::OperatorPack) -> idtype(m)
Get the id of an OperatorPack
.
id(term::Term) -> Symbol
id(::Type{<:Term) -> Symbol
Get the id of a term.
QuantumLattices.value
— Functionvalue(od::OrderedDict, i) -> valtype(od)
Get the ith value of an OrderedDict
.
value(m::OperatorPack) -> valtype(m)
Get the value of an OperatorPack
.
value(qn::SimpleAbelianQuantumNumber) -> Number
Get the value of a simple Abelian quantum number.
value(qn::AbelianQuantumNumberProd, i::Integer) -> Number
Get the value of the ith simple Abelian quantum number in a Deligne tensor product.
value(term::Term) -> valtype(term)
Get the value of a term.
QuantumLattices.Toolkit.DirectSummedIndices
— TypeDirectSummedIndices{T<:Tuple{Vararg{OrdinalRange{Int, Int}}}} <: AbstractVector{CartesianIndex{3}}
Direct sum of indexes.
QuantumLattices.Toolkit.Segment
— TypeSegment{S} <: AbstractVector{S}
A segment.
Combinatorics
The combinations and permutations of an indexable object are implemented, with duplicate elements allowed or not. Compared to another Julia package Combinatorics, the iterators return tuples instead of vectors, which could greatly decrease the memory allocation times and improves the code efficiency.
Combinatorics{M, C}
is the abstract type of all combinatorial algorithms. It has two type parameters:
M
: the number of elements to be takenC
: the type of the collection of candidate elements
To avoid memory allocation, the iteration of a concrete combinatorial algorithm returns a tuple, whose length is M
and eltype is eltype(C)
.
Combinations and DuplicateCombinations
Combinations{M, C}
and DuplicateCombinations{M, C}
generate all the combinations of M
elements from an indexable collection whose type is C
, with the differences being that the former forbids duplicate elements in the combinations while the latter allows.
All combinations of 2 integers taken from 1 to 3 without duplicate:
Combinations{2}(1:3) |> collect
3-element Vector{Tuple{Int64, Int64}}:
(1, 2)
(1, 3)
(2, 3)
All combinations of 2 integers taken from 1 to 3 with duplicate allowed:
DuplicateCombinations{2}(1:3) |> collect
6-element Vector{Tuple{Int64, Int64}}:
(1, 1)
(1, 2)
(1, 3)
(2, 2)
(2, 3)
(3, 3)
Permutations and DuplicatePermutations
Permutations{M, C}
and DuplicatePermutations{M, C}
generate all the permutations of M
elements from an indexable collection whose type is C
, with the differences being that the former forbids duplicate elements in the permutations while the latter allows.
All permutations of 2 integers taken from 1 to 3 without duplicate:
Permutations{2}(1:3) |> collect
6-element Vector{Tuple{Int64, Int64}}:
(1, 2)
(1, 3)
(2, 1)
(2, 3)
(3, 1)
(3, 2)
All permutations of 2 integers taken from 1 to 3 with duplicate allowed:
DuplicatePermutations{2}(1:3) |> collect
9-element Vector{Tuple{Int64, Int64}}:
(1, 1)
(1, 2)
(1, 3)
(2, 1)
(2, 2)
(2, 3)
(3, 1)
(3, 2)
(3, 3)
Manual
QuantumLattices.Toolkit.Combinatorics
— TypeCombinatorics{M, C}
Abstract combinatorial algorithms.
QuantumLattices.Toolkit.Combinations
— TypeCombinations{M}(contents::C) where {M, C}
Combinations of M
elements from contents
. Duplicates are not allowed.
QuantumLattices.Toolkit.DuplicateCombinations
— TypeDuplicateCombinations{M}(contents::C) where {M, C}
Combinations of M
elements from contents
. Duplicates are allowed.
QuantumLattices.Toolkit.Permutations
— TypePermutations{M}(contents::C) where {M, C}
Permutations of M
elements from contents
. Duplicates are not allowed.
QuantumLattices.Toolkit.DuplicatePermutations
— TypeDuplicatePermutations{M}(contents::C) where {M, C}
Permutations of M
elements from contents
. Duplicates are allowed.
Traits
Trait functions and trait types that are useful to the package are defined.
Generally speaking, traits in Julia could fall into two categories according to their usages, the first may be term as "type helpers" and the second are usually called "Holy traits" named after Tim Holy. Type helpers aim at the inquiry, alteration and computation of the compile-time information of types, while Holy traits can be applied as an alternative to multi-inheritance by use of the Julia multidispatch feature.
Type helpers
Type helpers are important for the generic programming in Julia, especially in the design of generic interfaces and abstract types.
Let's see a simple situation, i.e. the elemental addition of two vectors of numbers. The numbers can assume different types and the type of the result depends on both of them, for example, the result between two vectors of integers is a vector of integers while that between a vector of integers and a vector of floats is a vector of floats. Of course, one can explicitly define every elemental addition function between any two different types of vectors of numbers, like this:
# wrong design pattern
function elementaladdition(v₁::Vector{Int64}, v₂::Vector{Int64})
result = Int[]
...
end
function elementaladdition(v₁::Vector{Int64}, v₂::Vector{Float64})
result = Float64[]
...
end
...
...
Writing down all such methods is already a heavy repetition. What's worse, you will quickly find that a lot more functions, such as the elemental subtraction, elemental multiplication and elemental division, are waiting for you to implement. This is a total disaster.
The correct strategy is to define the promotion rule of any two types of numbers and use it to define the type of the result:
# correct design pattern
promotion(::Type{Int64}, ::Type{Int64}) = Int64
promotion(::Type{Int64}, ::Type{Float64}) = Float64
...
...
function elementaladdition(v₁::Vector{T₁}, v₂::Vector{T₂}) where {T₁<:Number, T₂<:Number}
result = promotion(T₁, T₂)[]
...
end
function elementalsubtraction(v₁::Vector{T₁}, v₂::Vector{T₂}) where {T₁<:Number, T₂<:Number}
result = promotion(T₁, T₂)[]
...
end
...
...
The promotion rule applies equally to all the arithmetic operations on numbers. Therefore, tedious code repetition could be avoided with it. In fact, similar promotion rules have already been defined in Julia base, and the default implementations of arithmetic operations in Julia are indeed based on them (see Base.promote_rule
and Base.promote_type
). When new user-defined numeric types are introduced, the only things you need to do is to add new promotion rules and implement a few basic arithmetic functions for these new types. Then quite a lot of generic codes could apply to them without any modification.
Type helpers with type parameters
The input and output types of a promotion rule are known at compile time, thus, the promotion rule is a trait function aiming at the computation of compile-time information of types. Trait functions dealing with the inquiries of compile-time information of types are also widely used in Julia, such as the eltype
function of Vector
:
julia> eltype(Vector{String})
String
For a user-defined parametric type, it is also useful to provide an inquiry function to access to the type parameters:
julia> struct Hi{T<:Number}
content::T
end
contenttype(::Type{Hi{T}}) where T<:Number = T
contenttype(Hi{Int64})
Int64
However, the above defined function contenttype
could not apply to a UnionAll
type, such as Hi{<:Real}
:
julia> contenttype(Hi{<:Real})
ERROR: MethodError: no method matching contenttype(::Type{Hi{<:Real}})
[...]
In fact in Julia base, all such inquiry functions, e.g., the eltype
function, work poor for the UnionAll
types:
julia> eltype(Vector{<:Real})
Any
In concept, eltype(Vector{<:Real}
should return Real
instead of Any
as every element in Vector{<:Real}
is indeed a real number. Similarly, we expect that contenttype(Hi{<:Real})
should also give us Real
. Unfortunately, functions defined in the similar form like this could never achieve such goals. Julia base doesn't provide generic functions to access or change the information of the parameters of a type. In this module, we try to fill this gap with a set of generic trait functions.
Access or change the type parameters by their position orders
The most direct information of the parameters of a type is their position orders. We provide parametertype
to access to them by such information:
julia> parametertype(Hi{<:Real}, 1)
Real
julia> parametertype(Vector{<:Real}, 1)
Real
julia> parametertype(Vector{<:Real}, 2)
1
You can use parametercount
to inquire the total number of the parameters of a type:
julia> parametercount(Hi)
1
julia> parametercount(Vector)
2
It is noted that Vector
has 2 type parameters because it is just a type alias for Array{T, 1} where T
.
To change the parameters of a type, reparameter
can be used:
julia> reparameter(Hi{Int64}, 1, Real)
Hi{<:Real}
julia> reparameter(Vector{Int64}, 1, Real)
Vector{<:Real} (alias for Array{<:Real, 1})
julia> reparameter(Vector{<:Real}, 2, 3)
Array{<:Real, 3}
julia> reparameter(Hi{Int64}, 1, Real, false)
Hi{Real}
julia> reparameter(Hi{Int64}, 1, Real, true)
Hi{<:Real}
We want to remark that by providing the fourth positional argument with the true
value, a UnionAll
type could be generated. When the fourth positional argument is omitted, it is actually determined by another trait function, i.e., isparameterbound
. This function judges whether an input type should be considered as the upper bound of the new parameter of a type. By default, it is defined to be
isparameterbound(::Type{}, ::Val{}, ::Type{D}) where D = !isconcretetype(D)
isparameterbound(::Type{}, ::Val{}, ::Any) = false
This function can be overloaded to change the behavior for a certain type:
julia> isparameterbound(::Type{<:Vector}, ::Val{1}, ::Type{D}) where D = false;
julia> reparameter(Vector, 1, Real)
Vector{Real} (alias for Array{Real, 1})
The second positional argument of isparameterbound
must be of type Val
because in principle you should be able to assign different behaviors for different parameters of a type separately. If it is of type Integer
, a single overloading would change the behaviors for all.
Besides, you can inquire all the parameters of a type by parametertypes
:
julia> parametertypes(Hi{<:Real})
Tuple{Real}
julia> parametertypes(Vector{Int64})
Tuple{Int64, 1}
The obtained type parameters are stored as those of a Tuple
.
At the same time, you can change all the parameters of a type by fulltype
:
julia> fulltype(Hi{Int64}, Tuple{Real})
Hi{<:Real}
julia> fulltype(Hi{Int64}, Tuple{Real}, (false,))
Hi{Real}
julia> fulltype(Vector{Int64}, Tuple{Real, 2})
Matrix{Real} (alias for Array{Real, 2})
julia> fulltype(Vector{Int64}, Tuple{Real, 2}, (true, false))
Matrix{<:Real} (alias for Array{<:Real, 2})
Like reparameter
, the last positional argument of fulltype
could determine whether the corresponding types specified by the type parameters of the input Tuple
should be considered as the upper bounds of the new parameters of a type. When this argument is omitted, it is determined by another trait function isparameterbounds
, which successively calls the isparameterbound
function to determine the behaviors for all the parameters of a type as the literal indicates.
Associate type parameters with names
Sometimes, it is more convenient to associate names with the parameters of a type, and then access or change them by their names. This can be done by overloading the parameternames
trait function for a certain type:
julia> parameternames(::Type{<:Hi}) = (:content,)
parameternames (generic function with 3 methods)
Now, you can inquire the name of a type parameter by parametername
with the given position order or vice versa by parameterorder
:
julia> parametername(Hi, 1)
:content
julia> parameterorder(Hi, :content)
1
You can also inquire whether a type has a parameter with the given name by hasparameter
:
julia> hasparameter(Hi, :content)
true
julia> hasparameter(Hi, :others)
false
And parametertype
and reparameter
can be applied by the name of a type parameter instead of its position order:
julia> parametertype(Hi{<:Real}, :content)
Real
julia> reparameter(Hi{Int}, :content, Real)
Hi{<:Real}
julia> reparameter(Hi{Int}, :content, Real, false)
Hi{Real}
To change the reparameter
behavior when its last positional argument is omitted, you should overload the isparameterbound
function accordingly, e.g.:
julia> isparameterbound(::Type{<:Hi}, ::Val{:content}, ::Type{D}) where D = false;
julia> reparameter(Hi{Int}, :content, Real)
Hi{Real}
Accessing or altering a parameter of a type by its name is independent from that by its position order. Thus, even the following method
isparameterbound(::Type{<:Hi}, ::Val{1}, D)
has been overloaded, it doesn't affect the result of the function call like
reparameter(Hi{Int}, :content, Real)
Rather, it only affect the result of the function call like
reparameter(Hi{Int}, 1, Real)
To change the default behavior of the former function call, you must overload the following method manually as well
isparameterbound(::Type{<:Hi}, ::Val{:content}, D)
A new trait function parameterpair
is provided to inquire the name-type pair of a parameter of a type:
julia> parameterpair(Hi{<:Real}, 1)
Pair{:content, Real}
julia> parameterpair(Hi{<:Real}, :content)
Pair{:content, Real}
And a new trait function parameterpairs
can be used to inquire all the name-type pairs of the parameters of a type:
julia> parameterpairs(Hi{<:Real})
@NamedTuple{content::Real}
The parameters of a type can be altered all at once by giving the name-type pairs to fulltype
:
julia> fulltype(Hi{Int}, NamedTuple{(:content,), Tuple{Real}}, (false,))
Hi{Real}
julia> fulltype(Hi{Int}, NamedTuple{(:content,), Tuple{Real}}, (true,))
Hi{<:Real}
julia> fulltype(Hi{Int}, NamedTuple{(:content,), Tuple{Real}})
Hi{Real}
Here, the last positional argument can be omitted whose default value would be determined by the isparameterbounds
function which successively calls the isparameterbound
function on each of the named parameter. Note that similar to the situation of the reparameter
function in this subsubsection, the isparameterbound
function called here is also the version that takes the parameter name as the input rather than that of the position order.
Type helpers with predefined contents
Julia abstract types don't have any field or attribute. They are only tags on the type tree. However, we may expect sometimes an abstract type to possess some kind of predefined content so that the design of some methods could be highly simplified. For example, we may need an abstract type that describes a composite vector. Apparently, it should have a field that is the vector contained in it. Of course, we can appoint a fixed field name with it and force every concrete subtype must contain such a field. In such a design pattern, the name of this field in every concrete subtype must be kept unchanged, which may be annoying when it conflicts with that of another field. What's worse, a predefined content of an abstract type is not always limited to a certain field. Maybe we need more than one fields to construct such a content. The just mentioned design pattern cannot deal with such situations.
Here, we provide a set of trait functions to help the design of abstract types with predefined contents. We take the case of composite vector for illustration, and the generalization to other situations is straightforward. First, the trait function contentnames
should be overloaded to define the names of the predefined contents:
julia> abstract type CompositeVector{T} end
contentnames(::Type{<:CompositeVector}) = (:content,);
Then you can inquire the total number of predefined contents by contentcount
, inquire the name of a predefined content with its position order by contentname
, and judge whether a type has a predefined content with a given name by hascontent
:
julia> contentcount(CompositeVector)
1
julia> contentname(CompositeVector, 1)
:content
julia> hascontent(CompositeVector, :content)
true
julia> hascontent(CompositeVector, :value)
false
The key is the interface getcontent
, which defines how to get the value of the predefined content. For the simple case when the predefined content just corresponds to a field, and also the field name of the predefined content coincides with the content name, the overloading of getcontent
can be omitted, e.g.:
julia> struct AnotherCompositeVector{T} <: CompositeVector{T}
content::Vector{T}
end;
julia> v = AnotherCompositeVector([1, 2, 3])
getcontent(v, :content)
3-element Vector{Int64}:
1
2
3
For the cases when a predefined contents does not share the same name with a certain field, or even it is not limited to only one certain field, you must implement your own getcontent
manually. Let's see two typical examples:
julia> struct DifferentFieldName{T} <: CompositeVector{T}
data::Vector{T}
end
getcontent(v::DifferentFieldName, ::Val{:content}) = v.data;
julia> struct BeyondSingleField{T} <: CompositeVector{T}
firsthalf::Vector{T}
secondhalf::Vector{T}
end
getcontent(v::BeyondSingleField, ::Val{:content}) = [v.firsthalf; v.secondhalf];
julia> v = DifferentFieldName([1, 2, 3])
getcontent(v, :content)
3-element Vector{Int64}:
1
2
3
julia> v = BeyondSingleField([1, 2, 3], [4, 5, 6])
getcontent(v, :content)
6-element Vector{Int64}:
1
2
3
4
5
6
Note that for the method overloading of getcontent
, the second argument is of type Val{:content}
. This is convenient because in principle an abstract type could have more than only one predefined content, thus, the behaviors of the getcontent
function could be defined separately for different predefined contents in this way. In fact, the function call getcontent(m, contentname)
is just an alias for getcontent(m, contentname|>Val)
.
Holy traits
As an emergent feature of Julia, basically speaking, a Holy trait is a Julia type that could direct the generic function of a user-defined type to a certain implementation based on the Julia multi-dispatch mechanism. For different user-defined types, they could be assigned with different Holy traits, leading to different implementations of the same generic interface. Since the information of Holy traits are known at compile time, such design pattern doesn't affect the runtime efficiency as long as type stability is ensured.
Alternative of multi-inheritance
Maybe the most common application of Holy traits is to serve as the alternative of multi-inheritance. Let's see a simple scenario. You have defined an abstract type. It is natural to demand that for every concrete subtype of it, a pair of instances could be compared and judge whether they are equivalent to each other by the value. Unfortunately, for a new user-defined type, the default ==
function in Julia actually judges whether they are the same object, but not equal to each other by the value. Therefore, you need to define your own ==
function for this abstract type. However, you may need define a lot of abstract types when you are developing a Julia package. It is annoying if such simple functions must be written for each of them. In other languages like Python, this could be solved with the help of multi-inheritance. But Julia does not support multi-inheritance. The common way is to use Holy traits. For example, the above issue could be solved like this:
struct Equivalence end
const equivalence = Equivalence()
function Base.:(==)(::Equivalence, o₁, o₂)
n₁, n₂ = fieldcount(typeof(o₁)), fieldcount(typeof(o₂))
n₁≠n₂ && return false
for i = 1:n₁
getfield(o₁, i)≠getfield(o₂, i) && return false
end
return true
end
abstract type TypeWithEquivalence end
Base.:(==)(o₁::TypeWithEquivalence, o₂::TypeWithEquivalence) = ==(equivalence, o₁, o₂);
struct ConcreteTypeWithEquivalence{F₁, F₂} <: TypeWithEquivalence
f₁::F₁
f₂::F₂
end;
a₁ = ConcreteTypeWithEquivalence(("a", "b", "c"), [1, 2, 3])
a₂ = ConcreteTypeWithEquivalence(("a", "b", "c"), [1.0, 2.0, 3.0])
a₁ == a₂
true
Here, the type Equivalence
is the Holy trait that helps the abstract type TypeWithEquivalence
to implement the ==
function, which applies equally to any other types.
Type stability and the generated function trick
However, the story does not end up here. If you are concerned about the code efficiency, you may find that the above implementation is not type stable:
using BenchmarkTools
@benchmark $a₁ == $a₂
BenchmarkTools.Trial: 10000 samples with 954 evaluations.
Range (min … max): 91.230 ns … 13.354 μs ┊ GC (min … max): 0.00% … 98.50%
Time (median): 99.380 ns ┊ GC (median): 0.00%
Time (mean ± σ): 121.513 ns ± 266.547 ns ┊ GC (mean ± σ): 13.81% ± 7.24%
▁▁▆███▇▆▂▁▁▁▁▁ ▂▂▁ ▁▁▁ ▂
███████████████▇▆▆▇▅▄▅▅▇▆▅▄▃▃▄▅▅▄▄▅▃▁▄▄▅▆▆▅▅▆▅▆▆▇█████████▇▆▇ █
91.2 ns Histogram: log(frequency) by time 189 ns <
Memory estimate: 256 bytes, allocs estimate: 6.
The memory allocation occurs when the ==
function tries to compare the values of getfield(o₁, i)
and getfield(o₂, i)
because in principle the types of these values depend on the runtime value of the variable i
. To ensure type stability, the generated function trick can be utilized:
struct EfficientEquivalence end
const efficientequivalence = EfficientEquivalence()
@generated function Base.:(==)(::EfficientEquivalence, o₁, o₂)
n₁, n₂ = fieldcount(o₁), fieldcount(o₂)
n₁≠n₂ && return false
expr = :(getfield(o₁, 1) == getfield(o₂, 1))
for i = 2:n₁
expr = Expr(:&&, expr, :(getfield(o₁, $i) == getfield(o₂, $i)))
end
return expr
end
abstract type TypeWithEfficientEquivalence end
function Base.:(==)(o₁::TypeWithEfficientEquivalence, o₂::TypeWithEfficientEquivalence)
return ==(efficientequivalence, o₁, o₂)
end
struct ConcreteTypeWithEfficientEquivalence{F₁, F₂} <: TypeWithEfficientEquivalence
f₁::F₁
f₂::F₂
end
a₁ = ConcreteTypeWithEfficientEquivalence(("a", "b", "c"), [1, 2, 3])
a₂ = ConcreteTypeWithEfficientEquivalence(("a", "b", "c"), [1.0, 2.0, 3.0])
a₁ == a₂
true
@benchmark $a₁ == $a₂
BenchmarkTools.Trial: 10000 samples with 997 evaluations.
Range (min … max): 21.616 ns … 43.261 ns ┊ GC (min … max): 0.00% … 0.00%
Time (median): 22.771 ns ┊ GC (median): 0.00%
Time (mean ± σ): 22.980 ns ± 1.312 ns ┊ GC (mean ± σ): 0.00% ± 0.00%
▂▃▆▇▄█▄▁
▃▃▆██████████▆▅▅▅▄▄▃▃▃▃▂▂▂▂▂▂▂▂▂▂▂▂▂▁▂▁▁▁▂▂▂▂▂▂▁▂▁▁▁▂▁▁▁▁▂▂ ▃
21.6 ns Histogram: frequency by time 29.2 ns <
Memory estimate: 0 bytes, allocs estimate: 0.
At runtime of the generated ==
function, it compares the values of getfield(o₁, 1)
and getfield(o₂, 1)
, getfield(o₁, 2)
and getfield(o₂, 2)
, etc., whose types are known at compile time. Therefore, type stability could be ensured.
EfficientOperations
EfficientOperations
is a Holy trait defined in this module that packs several common operations, such as ==/isequal
, </isless
, isapprox
and replace
, to help other (abstract) types to implement such functions by passing efficientoperations
as the first argument, just as illustrated above. See the manual for more detailed information.
Manual
For traits with types themselves:
QuantumLattices.Toolkit.SubTypeTree
— TypeSubTypeTree(root::Type)
Construct the complete depth-first subtype tree of a root Type
.
QuantumLattices.Toolkit.commontype
— Functioncommontype(f::Function, types, ::Type{T}=Any) where T
Find the common return type of a function.
QuantumLattices.Toolkit.fulltype
— Functionfulltype(::Type{T}, ::Type{PS}, ubs::Tuple{Vararg{Bool}}=isparameterbounds(T, PS)) where {T, PS<:Tuple}
fulltype(::Type{T}, ::Type{PS}, ubs::Tuple{Vararg{Bool}}=isparameterbounds(T, PS)) where {T, PS<:NamedTuple}
Get the full type of type T
with the type parameters replaced by those of PS
.
Here, ubs
determines whether the new type parameter should be considered as the upper bound accordingly.
QuantumLattices.Toolkit.rawtype
— Functionrawtype(::Type{T}) where T -> DataType/UnionAll
Get the "raw part" of a type. That is, the type without all its type parameters.
Core.DataType
— TypeDataType <: Type{T}
DataType
represents explicitly declared types that have names, explicitly declared supertypes, and, optionally, parameters. Every concrete value in the system is an instance of some DataType
.
Examples
julia> typeof(Real)
DataType
julia> typeof(Int)
DataType
julia> struct Point
x::Int
y
end
julia> typeof(Point)
DataType
Base.supertype
— Functionsupertype(T::DataType)
Return the supertype of DataType T
.
Examples
julia> supertype(Int32)
Signed
supertype(T, termination::Symbol) -> DataType
Get the supertype of T
till termination.
For traits with type parameters:
QuantumLattices.Toolkit.hasparameter
— Functionhasparameter(::Type{T}, name::Symbol) where T -> Bool
For type T
, judge whether it has a type parameter specified by name
.
QuantumLattices.Toolkit.isparameterbound
— Functionisparameterbound(::Type{T}, i::Integer, D) where T -> Bool
isparameterbound(::Type{T}, name::Symbol, D) where T -> Bool
For a type T
, judge whether a type D
should be considered as the upper bound of one of its type parameters.
The default implementations of this function is
isparameterbound(::Type{}, ::Val{}, ::Type{D}) where D = !isconcretetype(D)
isparameterbound(::Type{}, ::Val{}, ::Any) = false
QuantumLattices.Toolkit.isparameterbounds
— Functionisparameterbounds(::Type{T}, ::Type{PS}) where {T, PS<:Tuple} -> Tuple{Vararg{Bool}}
isparameterbounds(::Type{T}, ::Type{PS}) where {T, PS<:NamedTuple} -> Tuple{Vararg{Bool}}
For a type T
, judge whether the types specified by PS
should be considered as the upper bounds of its corresponding type parameters.
QuantumLattices.Toolkit.parametercount
— Functionparametercount(::Type{T}) where T -> Int
For a type T
, get the number of its type parameters.
QuantumLattices.Toolkit.parametername
— Functionparametername(::Type{T}, i::Integer) where T -> Symbol
For a type T
, get the name of its ith type parameter.
QuantumLattices.Toolkit.parameternames
— Functionparameternames(::Type{T}) where T -> Tuple{Vararg{Symbol}}
For a type T
, get the names of all its type parameters.
QuantumLattices.Toolkit.parameterorder
— Functionparameterorder(::Type{T}, name::Symbol) where T -> Int
For a type T
, get the order of one of its type parameters.
QuantumLattices.Toolkit.parameterpair
— Functionparameterpair(::Type{T}, name::Symbol) where T
parameterpair(::Type{T}, i::Integer) where T
For type T
, get the name-type pair of one of its type parameters.
The result is stored in the type parameters of a Pair
.
QuantumLattices.Toolkit.parameterpairs
— Functionparameterpairs(::Type{T}) where T
For a type T
, get the name-type pairs of all its type parameters.
The return types are stored in the type parameters of a NamedTuple
.
QuantumLattices.Toolkit.parametertype
— Functionparametertype(::Type{T}, name::Symbol) where T
parametertype(::Type{T}, i::Integer) where T
For a type T
, get the type of one of its type parameters.
QuantumLattices.Toolkit.parametertypes
— Functionparametertypes(::Type{T}) where T
For a type T
, get the types of all its type parameters.
The returned types are stored in the type parameters of a Tuple
.
QuantumLattices.Toolkit.promoteparameters
— Functionpromoteparameters(::Type{T₁}, ::Type{T₂}) where {T₁<:NamedTuple, T₂<:NamedTuple}
Promote the types specified by two named tuples with the same names accordingly.
The result is stored in the type parameters of a NamedTuple
.
QuantumLattices.Toolkit.reparameter
— Functionreparameter(::Type{T}, i::Integer, P, ub::Bool=isparameterbound(T, i, P)) where T
reparameter(::Type{T}, name::Symbol, P, ub::Bool=isparameterbound(T, name, P)) where T
For a type T
, replace the type of its ith type parameter with P
. Here, ub
determines whether P
should be considered as the upper bound.
For traits with type contents:
QuantumLattices.Toolkit.contentcount
— Functioncontentcount(::Type{T}) where T -> Int
For a type T
, get the number of its predefined contents.
QuantumLattices.Toolkit.contentname
— Functioncontentname(::Type{T}, i::Integer) where T -> Symbol
For a type T
, get the name of its ith predefined content.
QuantumLattices.Toolkit.contentnames
— Functioncontentnames(::Type{T}) where T -> Tuple{Vararg{Symbol}}
For a type T
, define the names of its predefined contents.
QuantumLattices.Toolkit.contentorder
— Functioncontentorder(::Type{T}, name::Symbol) where T -> Int
For a type T
, get the position order of a predefined content by the name.
QuantumLattices.Toolkit.contenttype
— Functioncontenttype(::Type{T}, name::Symbol) where T
contenttype(::Type{T}, ::Val{name}) where {T, name}
For a type T
, get the type of a predefined content by the name.
QuantumLattices.Toolkit.contenttypes
— Functioncontenttypes(::Type{T}) where T
For a type T
, get the types of its predefined contents.
QuantumLattices.Toolkit.getcontent
— Functiongetcontent(m, i::Integer)
getcontent(m, name::Symbol)
getcontent(m, ::Val{name}) where name
Get the value of the predefined content of m
.
QuantumLattices.Toolkit.hascontent
— Functionhascontent(::Type{T}, name::Symbol) where T -> Bool
For a type T
, judge whether it has a predefined content specified by name
.
QuantumLattices.Toolkit.dissolve
— Functiondissolve(m, f::Function=identity, args...; kwargs...) -> Tuple
Convert m
to a tuple by the function f
applied elementally to its contents with the extra positional arguments (args
) and keyword arguments (kwargs
).
To each content of m
, the underlying interface of the dissolve
function when f
is applied is as follows:
dissolve(m, Val(name), f, args...; kwargs...)
Here, name
is the name of the corresponding content of m
.
Basically, the rule of how f
operates on each field of m
can be overridden by redefining the above dissolve
function.
The default dissolve
function ignores the operation of function f
and just return the content value of m
.
dissolve(m, ::Val{name}, f::Function, args...; kwargs...) where name
Dissolve the content specified by name
of m
by the function f
applied with the extra positional arguments (args
) and keyword arguments (kwargs
).
For traits with type operations:
QuantumLattices.Toolkit.efficientoperations
— Constantefficientoperations
Indicate that the efficient operations, i.e. "=="/"isequal", "<"/"isless" or "replace", will be used.
Composite structures
In principle, Julia is not an object-oriented programming language. For example, only abstract types can be inherited so that subtype cannot inherit fields from their parents. Therefore, Julia prefers composition over inheritance. However, to make a new concrete type behaves much alike another one, tedious repetitions of redefining the generic interfaces are usually not avoidable, especially for the basic types in Julia base. In this module, we implement to such composited types, CompositeVector
and CompositeDict
, for the sake of future usages.
CompositeVector
A composite vector can be considered as a vector that is implemented by including a concrete subtype of AbstractVector
as its data attribute, and it itself is a subtype of AbstractVector
.
To take full advantages of the Julia base, the following interfaces are redefined:
- inquiry of info:
size
,length
- comparison between objects:
==
,isequal
- obtainment of old elements:
getindex
- operation of old elements:
setindex!
- addition of new elements:
push!
,pushfirst!
,insert!
,append!
,prepend!
- removal of old elements:
splice!
,deleteat!
,pop!
,popfirst!
,empty!
- construction of new objects:
empty
,reverse
,similar
- iteration:
iterate
,keys
,values
,pairs
Note that arithmetic operations and logical operations excluding ==
and isequal
are not supported.
CompositeDict
A composite dict can be considered as a dict that is implemented by including a concrete subtype of AbstractDict
as its data attribute and it itself is a subtype of AbstractDict
.
To take full advantages of the Julia base, the following interfaces are redefined:
- inquiry of info:
isempty
,length
,haskey
,in
- comparison between objects:
==
,isequal
- obtainment of old elements:
get
,getkey
,getindex
- operation and addition of elements:
push!
,get!
,setindex!
- removal of old elements:
pop!
,delete!
,empty!
- construction of new objects:
merge
,empty
- iteration:
iterate
,keys
,values
,pairs
Manual
QuantumLattices.Toolkit.CompositeDict
— TypeCompositeDict{K, V}
A composite dict can be considered as a dict that is implemented by including a concrete subtype of AbstractDict
as its data attribute.
QuantumLattices.Toolkit.CompositeVector
— TypeCompositeVector{T}
A composite vector can be considered as a vector that is implemented by including a concrete subtype of AbstractVector
as its data attribute.
Vector spaces
A vector space is a linear space, in which the addition of vectors and multiplication of a vector by a scalar are defined.
Vector spaces are frequently encountered in physics, e.g. the Hilbert space in quantum mechanics. In this submodule, we only implement those with finite dimensions. We want to remark that in our implementation, a vector space is a subtype of an abstract vector, therefore, the bases always possess a order, which means, two vector spaces are not considered to be equal to each other even if their corresponding actual mathematical spaces are the same but the orders of the bases are different.
VectorSpace
VectorSpace{B}
is the abstraction of a vector space, which has only one type parameter:
B<:Any
: the type of the bases of the vector space
Basically, a subtype should implement the following 2 methods:
Get the dimension of a vector spaceBase.length(vs::VectorSpace) -> Int
Get the ith basis of a vector spaceBase.getindex(vs::VectorSpace{B}, i::Int) where B -> B
Other features include
- comparison:
==
andisequal
- iteration:
iterate
- inquiry:
size
andin
- search:
searchsortedfirst
Manual
Predefined types of vector spaces:
QuantumLattices.Toolkit.VectorSpace
— TypeVectorSpace{B} <: AbstractVector{B}
Abstract vector space.
QuantumLattices.Toolkit.NamedVectorSpace
— TypeNamedVectorSpace{B} <: VectorSpace{B}
Abstract named vector space.
QuantumLattices.Toolkit.SimpleNamedVectorSpace
— TypeSimpleNamedVectorSpace{N, B} <: NamedVectorSpace{B}
Abstract simple named vector space.
QuantumLattices.Toolkit.ParameterSpace
— TypeParameterSpace{N, T, B} <: SimpleNamedVectorSpace{N, B}
Parameter space.
QuantumLattices.Toolkit.NamedVectorSpaceProd
— TypeNamedVectorSpaceProd{Order, T<:Tuple{Vararg{SimpleNamedVectorSpace}}, B<:Tuple} <: CompositeNamedVectorSpace{T, B}
Direct producted named vector space.
QuantumLattices.Toolkit.NamedVectorSpaceZip
— TypeNamedVectorSpaceZip{T<:Tuple{Vararg{SimpleNamedVectorSpace}}, B<:Tuple} <: CompositeNamedVectorSpace{T, B}
Zipped named vector space.
Predefined types of vector space style:
QuantumLattices.Toolkit.VectorSpaceStyle
— TypeVectorSpaceStyle
Style of a concrete type of vector space.
QuantumLattices.Toolkit.VectorSpaceGeneral
— TypeVectorSpaceGeneral <: VectorSpaceStyle
Default vector space style.
QuantumLattices.Toolkit.VectorSpaceEnumerative
— TypeVectorSpaceEnumerative <: VectorSpaceStyle
Enumerative vector space style, which indicates that the vector space has a predefined content named contents
that contains all its bases.
QuantumLattices.Toolkit.VectorSpaceCartesian
— TypeVectorSpaceCartesian <: VectorSpaceStyle
Cartesian vector space style, which indicates that every basis in it could be represented by a Cartesian index.
QuantumLattices.Toolkit.VectorSpaceDirectProducted
— TypeVectorSpaceDirectProducted{Order} <: VectorSpaceStyle
Vector space style which indicates that a vector space is the direct product of its sub-components.
The type parameter Order
must be either :forward
or :backward
:
:forward
: the direct product iterates over the first sub-component first like a Julia array;:backward
: the direct product iterates over the last sub-component first like a C/C++ array.
QuantumLattices.Toolkit.VectorSpaceDirectSummed
— TypeVectorSpaceDirectSummed <: VectorSpaceStyle
Vector space style which indicates that a vector space is the direct sum of its sub-components.
QuantumLattices.Toolkit.VectorSpaceZipped
— TypeVectorSpaceZipped <: VectorSpaceStyle
Vector space style which indicates that a vector space is the zip of its sub-components.