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 descendants to move downward in the MTG.
  • Use ancestors to 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.2
Tip

If 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"]
Note

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.2

And even with the dot notation:

node_5.Length
0.2

This 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.2

Use 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
 :dateDeath

We also define an alias for a more DataFrame.jl-alike experience (names):

names(node_5)
4-element Vector{Symbol}:
 :Length
 :Width
 :isAlive
 :dateDeath

Note 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
 :dateDeath

If 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
 :dateDeath

A 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.2

The 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
  nothing

Filters

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.2

Filter 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.2

Filter 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}[]
Warning

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.

Note

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.2

In 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.6000000000000001

Compute 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.