API
MultiScaleArrays.AbstractMultiScaleArray — Type
AbstractMultiScaleArray{B}
AbstractMultiScaleArray{B} <: AbstractVector{B}
Abstract supertype for the nodes of a hierarchical array. A concrete leaf subtype stores a values field, while non-leaf subtypes store nodes, values, and end_idxs fields in that order. The head node should additionally subtype AbstractMultiScaleArrayHead, so it can be passed to add_node!, remove_node!, and getindices.
Interface
- A leaf subtype must have
values::AbstractVectoras its first field. - A non-leaf subtype must have
nodes,values, andend_idxs::AbstractVector{Int}as its first three fields.end_idxsstores the cumulative linear lengths ofnodes, followed by the length includingvalueswhenvaluesis nonempty. - Extra fields may follow the required fields and may be mutable or immutable.
Examples
struct Cell <: AbstractMultiScaleArrayLeaf{Float64}
values::Vector{Float64}
end
struct Tissue <: AbstractMultiScaleArray{Float64}
nodes::Vector{Cell}
values::Vector{Float64}
end_idxs::Vector{Int}
endDefining A MultiScaleModel: The Interface
The required interface is as follows. Leaf types must extend AbstractMultiScaleArrayLeaf, the highest level of the model or the head extends MultiScaleModelHead, and all intermediate types extend AbstractMultiScaleArray. The leaf has an array values::Vector{B}. Each type above then contains three fields:
nodes::Vector{T}values::Vector{B}end_idxs::Vector{Int}
Note that the ordering of the fields matters. B is the BottomType, which has to be the same as the eltype for the array in the leaf types. T is another AbstractMultiScaleArray. Thus, at each level, an AbstractMultiScaleArray contains some information of its own (values), the next level down in the hierarchy (nodes), and caching for indices (end_idxs). You can add and use extra fields as you please, and you can even make the types immutable.
The MultiScaleModel API
The resulting type acts as an array. A leaf type l acts exactly as an array with l[i] == l.values[i]. Higher nodes also act as a linear array. If ln is level n in the hierarchy, then ln.nodes is the vector of level n-1 objects, and ln.values are its “intrinsic values”. There is an indexing scheme on ln, where:
ln[i,j,k]gets thekthn-3object in thejthn-2object in theith leveln-1object. Of course, this recurses for the whole hierarchy.ln[i]provides a linear index through all.nodesand.valuesvalues in every lower level andln.valuesitself.
Thus, ln isa AbstractVector{B}, where B is the eltype of its leaves and all .values's.
In addition, iterators are provided to make it easy to iterate through levels. For h being the head node, level_iter(h,n) iterates through all level objects n levels down from the top, while level_iter_idx(h,n) is an enumeration (node,y,z) where node are the nth from the head objects, with h[y:z] being the values it holds in the linear indexing.
Indexing and Iteration
The head node then acts as the king. It is designed to have functionality which mimics a vector in order for usage in DifferentialEquations or Optim. So for example
embryo[12]returns the “12th protein”, counting by Embryo > Tissue > Population > Cell in order of the vectors. The linear indexing exists for every AbstractMultiScaleArray. These types act as full linear vectors, so standard operations do the sensical operations:
embryo[10] = 4.0 # changes protein concentration 10
embryo[2, 3, 1] # Gives the 1st cell in the 3rd population of the second tissue
embryo[:] # generates a vector of all of the protein concentrations
eachindex(embryo) # generates an iterator for the indicesContinuous models can thus be written at the protein level and will work seamlessly with DifferentialEquations or Optim which will treat it like a vector of protein concentrations. Using the iterators, note that we can get each cell population by looping through 2 levels below the top, so
for cell in level_iter(embryo, 3)
# Do something with the cells!
endor the multiple-level iter, which is the one generally used in DifferentialEquations.jl functions:
for (cell, dcell) in LevelIter(3, embryo, dembryo)
# If these are similar structures, `cell` and `dcell` are the similar parts
cell_ode(dcell, cell, p, t)
endLevelIterIdx can give the indices along with iteration:
for (cell, y, z) in LevelIterIdx(embryo, 3)
# cell = embryo[y:z]
endHowever, the interesting behavior comes from event handling. Since embryo will be the “vector” for the differential equation or optimization problem, it will be the value passed to the event handling. MultiScaleArrays includes behavior for changing the structure. For example:
tissue3 = construct(Tissue, deepcopy([population, population2]))
add_node!(embryo, tissue3) # Adds a new tissue to the embryo
remove_node!(embryo, 2, 1) # Removes population 1 from tissue 2 of the embryoCombined with event handling, this allows for dynamic structures to be derived from low-level behaviors.
Heterogeneous Nodes via Tuples
Note that tuples can be used as well. This allows for type-stable broadcasting with heterogeneous nodes. This could be useful for mixing types inside of the nodes. For example:
struct PlantSettings{T}
x::T
end
struct OrganParams{T}
y::T
end
struct Organ{B <: Number, P} <: AbstractMultiScaleArrayLeaf{B}
values::Vector{B}
name::Symbol
params::P
end
struct Plant{B, S, N <: Tuple{Vararg{Organ{<:Number}}}} <: AbstractMultiScaleArray{B}
nodes::N
values::Vector{B}
end_idxs::Vector{Int}
settings::S
end
struct Community{B, N <: Tuple{Vararg{Plant{<:Number}}}} <: AbstractMultiScaleArray{B}
nodes::N
values::Vector{B}
end_idxs::Vector{Int}
end
mutable struct Scenario{B, N <: Tuple{Vararg{Community{<:Number}}}} <:
AbstractMultiScaleArrayHead{B}
nodes::N
values::Vector{B}
end_idxs::Vector{Int}
end
organ1 = Organ([1.1, 2.1, 3.1], :Shoot, OrganParams(:grows_up))
organ2 = Organ([4.1, 5.1, 6.1], :Root, OrganParams("grows down"))
organ3 = Organ([1.2, 2.2, 3.2], :Shoot, OrganParams(true))
organ4 = Organ([4.2, 5.2, 6.2], :Root, OrganParams(1 // 3))
plant1 = construct(Plant, (deepcopy(organ1), deepcopy(organ2)), Float64[], PlantSettings(1))
plant2 = construct(
Plant, (deepcopy(organ3), deepcopy(organ4)), Float64[],
PlantSettings(1.0)
)
community = construct(Community, (deepcopy(plant1), deepcopy(plant2)))
scenario = construct(Scenario, (deepcopy(community),))(of course at the cost of mutability).
MultiScaleArrays.AbstractMultiScaleArrayLeaf — Type
AbstractMultiScaleArrayLeaf{B}Abstract supertype for leaf nodes in a multiscale array hierarchy.
Interface
A concrete leaf must define values as its first field. Its elements provide the leaf's linear array entries. Use construct directly for a leaf only when its ordinary constructor accepts the same arguments.
Examples
struct Cell <: AbstractMultiScaleArrayLeaf{Float64}
values::Vector{Float64}
end
Cell([1.0, 2.0])MultiScaleArrays.AbstractMultiScaleArrayHead — Type
AbstractMultiScaleArrayHead{B}Abstract supertype for the top node of a multiscale array hierarchy.
Interface
A head has the same required fields as AbstractMultiScaleArray. It is the entry point for structural changes: add_node!, remove_node!, and getindices accept a head and update or inspect the whole hierarchy.
Examples
mutable struct ModelHead <: AbstractMultiScaleArrayHead{Float64}
nodes::Vector{Tissue}
values::Vector{Float64}
end_idxs::Vector{Int}
endMultiScaleArrays.construct — Function
construct(::Type{T}, args...) where {T <: AbstractMultiScaleArray}Construct a multiscale array node of type T and populate its cached end indices.
Arguments
T: Concrete subtype ofAbstractMultiScaleArrayto construct.nodes: Child nodes for a non-leaf node. It may be a vector or tuple of compatible multiscale arrays.values: Optional intrinsic values stored at the current node. It defaults to an empty vector with element typeeltype(T).args...: Extra fields ofT, in declaration order after the required fields.
Examples
cell = Cell([1.0, 2.0])
tissue = construct(Tissue, [cell])For a leaf subtype, construct(T, args...) calls the ordinary constructor T(args...).
MultiScaleArrays.add_node! — Function
add_node!(m::AbstractMultiScaleArrayHead, node::AbstractMultiScaleArray, I...)Insert node at the location identified by I and update cached linear indices.
Arguments
m: AnAbstractMultiScaleArrayHeadto mutate.node: The multiscale array node to append or insert.I...: Optional indices that select the parent node. With no indices,nodeis appended tom; with one or more indices, it is inserted into the selected descendant.
Examples
add_node!(model, Cell([0.0]))
add_node!(model, Cell([0.0]), 2)MultiScaleArrays.remove_node! — Function
remove_node!(m::AbstractMultiScaleArrayHead, I...)Remove the node at the location identified by I and update cached linear indices.
Arguments
m: AnAbstractMultiScaleArrayHeadto mutate.I...: Indices selecting the node to remove. A single index removes a direct child; additional indices descend through the hierarchy.
Examples
remove_node!(model, 2)
remove_node!(model, 2, 1)MultiScaleArrays.num_nodes — Function
num_nodes(m::AbstractMultiScaleArray)Return the number of immediate child nodes in m.
Arguments
m: A multiscale array node.
Examples
num_nodes(tissue)Leaf nodes have no children and return 0.
MultiScaleArrays.getindices — Function
getindices(m::AbstractMultiScaleArrayHead, I...)Return the linear index range corresponding to the node selected by I.
Arguments
m: AnAbstractMultiScaleArrayHead.I...: Optional child indices. With no indices, the returned range covers the whole hierarchy.
Examples
getindices(model)
getindices(model, 2, 1)The result is a UnitRange suitable for indexing the head's linear representation.
MultiScaleArrays.level_iter — Function
level_iter(S::AbstractMultiScaleArray, n::Int)Iterate over nodes n levels below the top of S.
Arguments
S: A multiscale array node whose descendants are traversed.n: Number of levels belowS.n = 1iterates over direct children.
Examples
for cell in level_iter(model, 2)
println(length(cell))
endMultiScaleArrays.LevelIter — Function
LevelIter(n::Int, S::AbstractMultiScaleArray...)Zip level_iter(s, n) across one or more multiscale arrays.
Arguments
n: Number of levels below each input to visit.S...: One or more multiscale arrays with matching hierarchy structure.
Examples
for (state_node, derivative_node) in LevelIter(2, state, derivative)
derivative_node .= state_node
endMultiScaleArrays.LevelIterIdx — Type
LevelIterIdx(S::AbstractMultiScaleArray, n::Int)Iterator over nodes n levels below S, yielding each node and its linear index range.
Arguments
S: A multiscale array node whose descendants are traversed.n: Number of levels belowSto visit.
Examples
for node, first_index, last_index in LevelIterIdx(model, 2)
@view model[first_index:last_index]
endMultiScaleArrays.print_human_readable — Function
print_human_readable(X::AbstractMultiScaleArray; kwargs...)Print a compact tree view of a multiscale array hierarchy.
Arguments
X: The multiscale array hierarchy to display.
Keywords
n_char_per_name = 6: Maximum number of characters shown for a node type name.fields = nothing: Optional iterable of leaf-field names to print instead of the leaf type.levelmax = Inf: Maximum hierarchy depth to display.n_item_max_per_levels = Inf: Maximum number of entries shown at each level.
Examples
print_human_readable(embryo)
# +|Tissue; |Tissue
# +|Popula; |Popula; |Popula; +|Popula; |Popula; |Popula
# +Cell; Cell; Cell; +Cell; Cell; Cell; +Cell; Cell; Cell; +Cell; Cell; Cell; +Cell; Cell; Cell; +Cell; Cell; Cell
print_human_readable(embryo; n_char_per_name = 2)
# +|Ti; |Ti
# +|Po; |Po; |Po; +|Po; |Po; |Po
# +Ce; Ce; Ce; +Ce; Ce; Ce; +Ce; Ce; Ce; +Ce; Ce; Ce; +Ce; Ce; Ce; +Ce; Ce; CeHere, if the 'AbstractMultiScaleArrayLeaf's contain several fields, you can specify them with fields = [field1,field2,...]
print_human_readable(embryo; n_char_per_name = 2, fields = [:values])
# +|Ti; |Ti
# +|Po; |Po; |Po; +|Po; |Po; |Po
# +va: [1.0, 2.0, 3.0]; va: [3.0, 2.0, 5.0]; va: [4.0, 6.0]; +va: [1.0, 2.0, 3.0]; va: [3.0, 2.0, 5.0]; va: [4.0, 6.0]; +va: [1.0, 2.0, 3.0]; va: [3.0, 2.0, 5.0]; va: [4.0, 6.0]; +va: [1.0, 2.0, 3.0]; va: [3.0, 2.0, 5.0]; va: [4.0, 6.0]; +va: [1.0, 2.0, 3.0]; va: [3.0, 2.0, 5.0]; va: [4.0, 6.0]; +va: [1.0, 2.0, 3.0]; va: [3.0, 2.0, 5.0]; va: [4.0, 6.0]if your screen is small, then print a sub-part of the AbstractMultiScaleArray:
print_human_readable(embryo.nodes[1].nodes[1]; fields = [:values])
# +values: [1.0, 2.0, 3.0]; values: [3.0, 2.0, 5.0]; values: [4.0, 6.0]Extensions
Note that this only showed the most basic MultiScaleArray. These types can be extended as one pleases. For example, we can change the definition of the cell to have:
struct Cell{B} <: AbstractMultiScaleArrayLeaf{B}
values::Vector{B}
celltype::Symbol
endNote that the ordering of the fields matters here: the extra fields must come after the standard fields (so for a leaf it comes after values, for a standard multiscale array it would come after nodes,values,end_idxs). Then we'd construct cells with cell3 = Cell([3.0; 2.0; 5.0], :BCell), and can give it a cell type. This information is part of the call, so
for (cell, y, z) in level_iter_idx(embryo, 2)
f(t, cell, @view embryo[y:z])
endcan allow one to check the cell.celltype in f an apply a different ODE depending on the cell type. You can add fields however you want, so you can use them to name cells and track lineages.
Showing the use of values, you just pass it to the constructor. Let's pass it an array of 3 values:
tissue = construct(Tissue, deepcopy([population; population2]), [0.0; 0.0; 0.0])We can selectively apply some function on these values via:
for (tissue, y, z) in level_iter_idx(embryo, 1)
f(t, tissue, @view embryo[y:z])
endand mutate tis.values in f. For example, we could have
function f(du, tissue::Tissue, p, t)
du .+= randn(3)
endapplies normal random numbers to the three values. We could use this to add to the model the fact that tissue.values[1:3] are the tissue's position, and f would then be adding Brownian motion.
Of course, you can keep going and kind of do whatever you want. The power is yours!