Node attributes

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)
/ 1: Scene
└─ / 2: Individual
   └─ / 3: Axis
      └─ / 4: Internode
         ├─ + 5: Leaf
         └─ < 6: Internode
            └─ + 7: Leaf

You can get all the attributes of a node using the node_attributes function:

node_attributes(mtg)
Dict{Symbol, Any} with 3 entries:
  :scales      => [0, 1, 2, 3, 3]
  :description => 2×4 DataFrame…
  :symbols     => SubString{String}["Scene", "Individual", "Axis", "Internode",…
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 both equivalent to:

node_attributes(node_5)[:Length]
0.2

But we strongly recommend to avoid this last notation. In our case the attributes are stored in a Dictionary (Dict, the default), so we access their values using the Dict notation: node_attributes(node_5)[:Length]. But if the attributes are stored as a NamedTuple-alike structure, we must use the dot notation instead: node_attributes(node_5).Length (see Attributes type for more details). That is why the package implements the more generic node_5[:Length] notation that works with any structure used for the attributes, which helps develop more generic code.

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

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

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

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

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

A more simple way to get all nodes and their attributes is to convert the MTG into a DataFrame like so:

DataFrame(mtg)
7×15 DataFrame
Rowtreeidsymbolscaleindexparent_idlinkdescriptionsymbolsscalesXEulerLengthWidthdateDeathisAlive
String?Int64?String?Int64?Int64?Int64?String?DataFrame?Array…?Array…?Float64?Float64?Float64?DateBool?
1/ 1: Scene1Scene00missing/2×4 DataFrameSubString{String}["Scene", "Individual", "Axis", "Internode", "Leaf"][0, 1, 2, 3, 3]missingmissingmissingmissingmissing
2└─ / 2: Individual2Individual101/missingmissingmissingmissingmissingmissingmissingmissing
3 └─ / 3: Axis3Axis202/missingmissingmissingmissingmissingmissingmissingmissing
4 └─ / 4: Internode4Internode303/missingmissingmissing1.00.10.02missingmissing
5 ├─ + 5: Leaf5Leaf304+missingmissingmissingmissing0.20.12022-08-24false
6 └─ < 6: Internode6Internode314<missingmissingmissing180.00.10.02missingtrue
7 └─ + 7: Leaf7Leaf306+missingmissingmissingmissing0.20.1missingtrue

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{Any}:
  nothing
  nothing
 0.1
 0.2
 0.1
 0.2

The descendants function visits every children nodes recursively until finding a leaf node. It returns the values in the same order than the visited nodes.

The function can also help get the nodes directly if we don't pass any attribute:

descendants(mtg)
6-element Vector{Node{MutableNodeMTG, Dict{Symbol, Any}}}:
 / 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. Of course you can still get their attributes using the node_attributes function, e.g.:

node_attributes.(descendants(mtg))
6-element Vector{Dict{Symbol, Any}}:
 Dict()
 Dict()
 Dict(:XEuler => 1.0, :Length => 0.1, :Width => 0.02)
 Dict(:Length => 0.2, :Width => 0.1, :dateDeath => Dates.Date("2022-08-24"), :isAlive => false)
 Dict(:XEuler => 180.0, :Length => 0.1, :Width => 0.02, :isAlive => true)
 Dict(:Length => 0.2, :Width => 0.1, :isAlive => true)

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{Any}:
 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{Any}:
 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{Any}:
 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)
Any[]
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{Any}:
 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.