MTG implementation

Introduction

In this package, the MTG is represented as a tree data structure.

The tree is built from a series of nodes with different fields that describe the topology (i.e. how nodes are connected together) and the attributes of the node.

Note

The package use terms from computer science rather than plant biology. So we use words such as "root" in an MTG, which is not the plant root, but the first node in the tree, i.e. the one without any parent. Similarly a leaf node is not a leaf from a plant but a node without any children.

Data types

The nodes have their own data type called Node. A Node has several fields:

fieldnames(Node)
(:id, :parent, :children, :MTG, :attributes, :traversal_cache)

Here is a little description of each field:

  • id: The unique integer identifier of the node. It can be set by the user but is usually set automatically.
  • parent: The parent node of the curent node. If the curent node is the root node, it will return nothing. You can test whether a node is a root node sing the isroot function.
  • children: a dictionary of the children nodes with their id as key, or nothing if none;
  • MTG: The MTG encoding of the node (see below, or NodeMTG)
  • attributes: the node attributes. Usually a NamedTuple, a MutableNamedTuple or a Dict or similar (e.g. OrderedDict), but the type is optional. The choice of the data structure depends mainly on how much you plan to change the attributes and their values. Attributes include for example the length or diameter of a node, its colour, 3d position...
  • traversal_cache: a cache for the traversal, used by e.g. traverse to traverse more efficiently particular nodes in the MTG

The value of tee fields are accessed using accessor functions: node_id, parent, children, node_mtg, node_attributes, and the last one get_traversal_cache which is not exported because users shouldn't use it directly.

The MTG field of a node describes the topology encoding of the node: its type of link with its parent (decompose: /, follow: <, and branch: +), its symbol, index, and scale (see Node MTG and attributes and The MTG section for more details). The MTG field must be encoded in a data structure called NodeMTG or in a MutableNodeMTG. They have four fields corresponding to the topology encoding:

fieldnames(NodeMTG)
(:link, :symbol, :index, :scale)

Creating a NodeMTG is very simple, just pass the arguments by position. For example if we have an Axis that decomposes its parent node ("/"), with an index 0 and a scale of 1, we would declare it as follows:

axis_mtg_encoding = NodeMTG("/", "Axis", 0, 1)
NodeMTG("/", "Axis", 0, 1)

The we can access the data using the dot syntax:

axis_mtg_encoding.symbol
"Axis"
Note

NodeMTG is the immutable data type, meaning that information cannot be changed once read. By default the package the mutable equivalent called MutableNodeMTG. Accessing the information of a mutable data structure is slower, but it is more convenient if we need to change its values.

Learning by example

Let's print again the example MTG from the previous section:

file = joinpath(dirname(dirname(pathof(MultiScaleTreeGraph))),"test","files","simple_plant.mtg")
println(read(file, String))
CODE:	FORM-A

CLASSES:
SYMBOL	SCALE	DECOMPOSITION	INDEXATION	DEFINITION
$ 	0	FREE	FREE	IMPLICIT
Individual	1	FREE	FREE	IMPLICIT
Axis	2	FREE	FREE	IMPLICIT # you can add comments anywhere
Internode	3	FREE	FREE	IMPLICIT
Leaf	3	FREE	FREE	IMPLICIT

DESCRIPTION:
LEFT	RIGHT	RELTYPE	MAX
Internode	Internode,Leaf	+	?
Internode	Internode,Leaf	<	?

FEATURES:
NAME	TYPE
Length	ALPHA
Width	ALPHA
XEuler	REAL
isAlive	BOOLEAN
dateDeath	DD/MM/YY

MTG:
ENTITY-CODE		Length	Width	XEuler	isAlive	dateDeath
/Scene0
^/Individual0
^/Axis0
^/Internode0		0.1	0.02	1
	+Leaf0	0.2	0.1		0	24/08/2022
^<Internode1		0.1	0.02	180.0	1
	+Leaf0	0.2	0.1		1

We can use read_mtg from MultiScaleTreeGraph.jl to read this MTG:

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

read_mtg returns the first node of the MTG, of type Node:

typeof(mtg)
Node{MutableNodeMTG, Dict{Symbol, Any}}
Note

The Node is a parametric type, that's why typeof(mtg) also returns the type used for the MTG data in the node (MutableNodeMTG) and the type used for the attributes (Dict{Symbol, Any}). But this is not important here.

We can access the fields of the node using the accessor functions:

node_id(mtg)
1
parent(mtg)

This one returns nothing because the node is the root node, it has no parent, but we could use it on its child, and it would return the root again:

mtg_child = mtg[1]
parent(mtg_child) == mtg
true
children(mtg)
1-element Vector{Node{MutableNodeMTG, Dict{Symbol, Any}}}:
 / 2: Individual
└─ / 3: Axis
   └─ / 4: Internode
      ├─ + 5: Leaf
      └─ < 6: Internode
         └─ + 7: Leaf
node_mtg(mtg)
MutableNodeMTG("/", "Scene", 0, 0)
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",…

The package also provide helper functions to access the MTG encoding of the node directly:

symbol(mtg)
"Scene"
index(mtg)
0
scale(mtg)
0