Traversal, descendants, ancestors and filters
Get attributes of a node
Let's first read our example MTG:
using MultiScaleTreeGraph
file = joinpath(dirname(dirname(pathof(MultiScaleTreeGraph))),"test","files","simple_plant.mtg")
mtg = read_mtg(file)Symbols: Scene Individual Axis Internode Leaf
Scales: 0 1 2 3 3
/ 1: Scene
└─ / 2: Individual
└─ / 3: Axis
└─ / 4: Internode
├─ + 5: Leaf
└─ < 6: Internode
└─ + 7: Leaf
If you are new to these words
If you are not from computer science, the vocabulary can be confusing. In this package:
- Traversal means "walking through nodes one by one".
- A parent is the node directly above another node.
- A child is a node directly below another node.
- Descendants are children, children of children, etc.
- Ancestors are parent, parent of parent, etc.
You can think of this like a family tree.
For the common cases:
- Use
descendantsto move downward in the MTG. - Use
ancestorsto move upward in the MTG. - Add filters (
symbol,scale,filter_fun) when you only need part of the MTG.
Traversal quick recipes
These examples cover the most common requests:
node_5 = get_node(mtg, 5)
# 1) Get one attribute for the whole subtree
descendants(mtg, :Length)
# 2) Get nodes instead of values
descendants(mtg)
# 3) Get one attribute from parents and grandparents
ancestors(node_5, :Length)
# 4) Filter by symbol and ignore nodes without the attribute
descendants(mtg, :Length, symbol=:Leaf, ignore_nothing=true)2-element Vector{Float64}:
0.2
0.2If you call the same traversal many times in a simulation loop, look at Performance Considerations, especially in-place methods (descendants!, ancestors!).
You can get all the attributes of a node as a dictionary snapshot using attributes:
attributes(mtg, format=:dict)Dict{Symbol, Any} with 3 entries:
:scales => [0, 1, 2, 3, 3]
:description => ColumnTable([:LEFT, :RIGHT, :RELTYPE, :MAX], Dict(:LEFT=>1, :…
:symbols => ["Scene", "Individual", "Axis", "Internode", "Leaf"]The attributes of the root node always include the data from the header sections of an MTG file: the scales of the MTG, the description and the symbols. You can learn more in The MTG sections.
We can also access particular attribute values by indexing into the node with a Symbol:
node_5 = get_node(mtg, 5) # Get the 5th node of the MTG
node_5[:Length]0.2... or a String:
node_5["Length"]0.2And even with the dot notation:
node_5.Length0.2This one even has autocompletion! It means that you can type node_5. and then press TAB to see all the available attributes, and when you start typing the name of an attribute, it will suggest the completion of the name.
The previous notations are equivalent to:
attribute(node_5, :Length)0.2Use attributes when you want all values from one node at once.
For day-to-day use, the simpler APIs are usually clearer:
node[:attr]attribute(node, :attr)attribute!(node, :attr, value)
To get the names of all attributes available in the node subtree, you can use get_attributes:
get_attributes(node_5)4-element Vector{Symbol}:
:Length
:Width
:isAlive
:dateDeathWe also define an alias for a more DataFrame.jl-alike experience (names):
names(node_5)4-element Vector{Symbol}:
:Length
:Width
:isAlive
:dateDeathNote that it returns only two attributes here because "node_5" is a leaf (a node without children), and get_attributes and names only return the attributes present in the node's subtree. To be sure to get all the attributes available in the whole MTG, it is better to call get_attributes on the root node like so:
get_attributes(mtg)8-element Vector{Symbol}:
:description
:symbols
:scales
:XEuler
:Length
:Width
:isAlive
:dateDeathIf you start from another node you can retrieve the root node using get_root:
get_attributes(get_root(node_5))8-element Vector{Symbol}:
:description
:symbols
:scales
:XEuler
:Length
:Width
:isAlive
:dateDeathA simple way to get all nodes and their attributes is to use the unified table view:
to_table(mtg)Symbols: Scene Individual Axis Internode Leaf
Scales: 0 1 2 3 3
Attributes Table (7 x 11)
╭───┬─────────┬────────────┬───────┬───────┬────────┬───────────┬──────────┬────
│ │ node_id │ symbol │ scale │ index │ link │ parent_id │ XEuler │ ⋯
│ │ Int64 │ Symbol │ Int64 │ Int64 │ Symbol │ Int64? │ Float64? │ F ⋯
├───┼─────────┼────────────┼───────┼───────┼────────┼───────────┼──────────┼────
│ 1 │ 1 │ Scene │ 0 │ 0 │ / │ missing │ missing │ ⋯
│ 2 │ 2 │ Individual │ 1 │ 0 │ / │ 1 │ missing │ ⋯
│ 3 │ 3 │ Axis │ 2 │ 0 │ / │ 2 │ missing │ ⋯
│ 4 │ 4 │ Internode │ 3 │ 0 │ / │ 3 │ 1.0 │ ⋯
│ 5 │ 5 │ Leaf │ 3 │ 0 │ + │ 4 │ missing │ ⋯
│ 6 │ 6 │ Internode │ 3 │ 1 │ < │ 4 │ 180.0 │ ⋯
│ 7 │ 7 │ Leaf │ 3 │ 0 │ + │ 6 │ missing │ ⋯
╰───┴─────────┴────────────┴───────┴───────┴────────┴───────────┴──────────┴────
4 columns omitted
Descendants
An MTG can hold a lot of information, usually measured locally at one given scale. It is often interesting to compute new attributes based on the topological environment of the nodes.
For example one could be interested in computing the total length of all nodes in a plant. To do so we must get the attributes of all descendants of a node. This is quite easy to do using MultiScaleTreeGraph.jl. For example to get the length attributes we would do:
descendants(mtg, :Length)6-element Vector{Union{Nothing, Float64}}:
nothing
nothing
0.1
0.2
0.1
0.2The descendants function visits each child, then each child of each child, and so on until leaves. It returns values in the same order as nodes are visited.
The function can also help get the nodes directly if we don't pass any attribute:
descendants(mtg)6-element Vector{Node{MutableNodeMTG, MultiScaleTreeGraph.ColumnarAttrs}}:
/ 2: Individual
└─ / 3: Axis
└─ / 4: Internode
├─ + 5: Leaf
└─ < 6: Internode
└─ + 7: Leaf
/ 3: Axis
└─ / 4: Internode
├─ + 5: Leaf
└─ < 6: Internode
└─ + 7: Leaf
/ 4: Internode
├─ + 5: Leaf
└─ < 6: Internode
└─ + 7: Leaf
+ 5: Leaf
< 6: Internode
└─ + 7: Leaf
+ 7: Leaf
This is useful to get more information about the nodes, like their scale, symbol, index, or link to their parent. You can still get attributes for each returned node, for example:
[attributes(node, format=:dict) for node in descendants(mtg)]6-element Vector{Dict{Symbol, Any}}:
Dict()
Dict()
Dict(:XEuler => 1.0, :Length => 0.1, :Width => 0.02, :isAlive => nothing)
Dict(:Length => 0.2, :Width => 0.1, :isAlive => false, :dateDeath => Dates.Date("2022-08-24"))
Dict(:XEuler => 180.0, :Length => 0.1, :Width => 0.02, :isAlive => true)
Dict(:Length => 0.2, :Width => 0.1, :isAlive => true, :dateDeath => nothing)Ancestors
To get the values of an attribute from the ancestors of a node, we would similarly do:
node_5 = get_node(mtg, 5)
ancestors(node_5, :Length)4-element Vector{Union{Nothing, Float64}}:
0.1
nothing
nothing
nothingFilters
Sometimes we only want the values of descendants or ancestors based on a given information. It is possible to filter out nodes based on their scale, symbol, link, or really anything by using the keyword arguments.
Filter by scale
For example if we want the length of all descendants of the root node of our MTG that are of scale 3 (leaves & internodes), we would simply do:
descendants(mtg, :Length, scale = 3)4-element Vector{Union{Nothing, Float64}}:
0.1
0.2
0.1
0.2Filter by symbol
If we need only the leaves, we would filter by their symbol (i.e. :Leaf):
descendants(mtg, :Length, symbol = :Leaf)2-element Vector{Union{Nothing, Float64}}:
0.2
0.2Filter by anything
And if we want to filter depending on an arbitrary value, we can use the filter_fun argument. For example if we want the length of the nodes, but only the ones with a width greater than 1, will would do like so:
descendants(mtg, :Length, filter_fun = x -> x[:Width] === nothing ? false : x[:Width] > 1)Union{Nothing, Float64}[]By default if a node does not have an attribute, trying to get its value returns nothing. So if one uses attributes in the function passed to filter_fun, the function must handle missing values. This is what we do here by first testing if x[:Width] is nothing (in which case we return false to filter out the node), and then apply our test on the value of the node width.
The function passed to filter_fun must take a node as input, not attributes directly. This is because we want to be able to access any information the user could need.
Because filter_fun takes a node as input, we can even filter on the node's parent. Let's say for example we want the values for the :Length, but only for the nodes that are children of a an Internode that follows another node:
descendants(mtg, :Length, filter_fun = node -> !isroot(node) && symbol(parent(node)) == :Internode && link(parent(node)) == :<)1-element Vector{Union{Nothing, Float64}}:
0.2In this example it returns only one value, because there is only one node that corresponds to this criteria: The Leaf with id 7.
We could apply the same kind of filtering on the node's children, or any combination of topological information and attributes.
Note that we first test if the node is not the root node, because the root node does not have a parent. We then test if the parent's symbol is :Internode and if the link is :<.
Filter helpers
There are three other arguments to help filtering nodes.
The first one is all. It is used to stop the search for new nodes as soon as one node does not correspond to the filters the user asked for.
It is generally used to get all nodes that have a "follow" link ("<") with their parents for example. You can find an example usage here, where we compute the index of the segment nodes ("S") along an axis ("A"), except for branching nodes, i.e. only the nodes that either decompose ("/") or follow ("<").
The second one is the self argument. It is used to return the value of the node on which we call the function if its true, and only the ancestors / descendants if false (the default).
The third one is the recursivity_level, that is used to control the depth of the search for the ancestors / descendants. It is set to -1 by default, which does not apply any filter on the depth. It is generally used to get e.g. only the children values of a node (recursivity_level = 1).
The fourth one is ignore_nothing. It is used to not return the values of a node if it is nothing. Note that it is applied after the filter, so filter_fun still has to handle nothing values.
Transform values
Assign attributes to a node
It is possible to change the values of attributes in a node. For example one could be interested to compute the total length of all nodes for the scene in our example MTG. In this case we can do:
mtg[:Length] = sum(descendants(mtg, :Length, ignore_nothing = true))0.6000000000000001Compute attributes in an MTG
Now MTGs can be very large, and it quickly becomes cumbersome to manually visit each node to change its value.
Instead, you can compute new attributes for all nodes in an MTG using transform. Head to the next tutorial for more information: Transform an MTG.
Helpers
Some helper functions can be useful when filtering nodes. For example you can use isroot to test if a node is the root node of the MTG. This is particularly useful when searching for ancestor values, but need a special treatment for the root node.
Similarly, you can use isleaf to filter the leaf nodes of an MTG.
You also have nleaves to compute the number of leaf nodes on the sub-tree of a given node.