Add/remove nodes
Make an MTG manually
It is very easy to add or remove nodes in an MTG. Actually, we can even construct an MTG completely manually.
Root node
Create the root node:
mtg = Node(MutableNodeMTG("/", "Plant", 0, 1), Dict{Symbol,Any}(:species => "Grassy-plant"))
/ 1: Plant
The first argument to Node
is its MTG encoding, which describes the topology of the node: what kind of link it has with its parent, its scale, its index and its symbol. It is given as a MutableNodeMTG
(or a NodeMTG
). The second argument is used to add attributes to the MTG.
Node id
The ids of the nodes should be unique. They are automatically computed using new_id
starting by 1 at the root node:
new_id(mtg)
2
Adding new nodes
To create a child node, we use a different method of Node
. This method is close to the one we used for the root, except there is a new argument at the first position: the parent node. Here we use the root node as the parent (mtg
):
IN1 = Node(mtg, MutableNodeMTG("/", "Internode", 0, 2), Dict{Symbol,Any}(:diameter => 0.1, :length => 0.5))
/ 2: Internode
Now the MTG has two nodes:
mtg
/ 1: Plant
└─ / 2: Internode
We can continue like this indefinitely. For example we can add a leaf to the first internode:
Node(IN1, MutableNodeMTG("+", "Leaf", 0, 2), Dict{Symbol,Any}(:area => 0.2))
+ 3: Leaf
If a node has no children, there is no need to keep track of it in an object.
And an internode following the first internode:
IN2 = Node(IN1, MutableNodeMTG("<", "Internode", 1, 2), Dict{Symbol,Any}(:diameter => 0.15, :length => 0.3))
< 4: Internode
And a leaf to it:
Node(IN2, MutableNodeMTG("+", "Leaf", 1, 2), Dict{Symbol,Any}(:area => 0.2))
+ 5: Leaf
And here is our resulting MTG:
mtg
/ 1: Plant
└─ / 2: Internode
├─ + 3: Leaf
└─ < 4: Internode
└─ + 5: Leaf
And the attributes:
DataFrame(mtg, get_attributes(mtg))
Row | tree | id | symbol | scale | index | parent_id | link | species | diameter | area | length |
---|---|---|---|---|---|---|---|---|---|---|---|
String? | Int64? | String? | Int64? | Int64? | Int64? | String? | String? | Float64? | Float64? | Float64? | |
1 | / 1: Plant | 1 | Plant | 1 | 0 | missing | / | Grassy-plant | missing | missing | missing |
2 | └─ / 2: Internode | 2 | Internode | 2 | 0 | 1 | / | missing | 0.1 | missing | 0.5 |
3 | ├─ + 3: Leaf | 3 | Leaf | 2 | 0 | 2 | + | missing | missing | 0.2 | missing |
4 | └─ < 4: Internode | 4 | Internode | 2 | 1 | 2 | < | missing | 0.15 | missing | 0.3 |
5 | └─ + 5: Leaf | 5 | Leaf | 2 | 1 | 4 | + | missing | missing | 0.2 | missing |
Inserting nodes
Insertion functions
Adding nodes recursively is easy, but sometimes we want to insert nodes in-between other nodes. We can still use Node
to do so, but it becomes a bit cumbersome because you'll have to handle manually the changes in parents, children and siblings.
We provide some helper functions that does it for you instead:
insert_parent!
: add a new parent nodeinsert_child!
: add a new child nodeinsert_sibling!
: add a new sibling nodeinsert_generation!
: add a new child node, but this new child is considered a whole new generation, meaning the previous children of the node become the children of the new node.
Note the singular form for the name of the functions. The plural form does the job on the whole MTG for selected nodes (see Insert nodes at position).
The NodeMTG
Those functions use a NodeMTG
(or MutableNodeMTG
), and automatically:
- find a unique id for the node
- add its children, parents and siblings
- update the links of the parents / siblings / children
mtg_2 = deepcopy(mtg)
insert_parent!(mtg_2, NodeMTG("/", "Scene", 0, 0))
mtg_2 = get_root(mtg_2)
/ 6: Scene
└─ / 1: Plant
└─ / 2: Internode
├─ + 3: Leaf
└─ < 4: Internode
└─ + 5: Leaf
The NodeMTG
can also be computed based on the node on which we insert the new node. In this case we can pass a function that take the node as input and returns the template for us:
mtg_2 = deepcopy(mtg)
insert_parent!(
mtg_2,
node -> (
link = link(node),
symbol = "Scene",
index = index(node),
scale = scale(node) - 1
)
)
mtg_2 = get_root(mtg_2)
node_mtg(mtg_2)
MutableNodeMTG("/", "Scene", 0, 0)
The MTG encoding field of the newly-created root node (node_mtg(mtg_2)
) used some of the information from the MTG to compute its values.
We use get_root
to recompute mtg_2
because insert_parent!
always return the input node, which is not the root node of the MTG anymore.
Node attributes
We can also provide attributes for the new node using the attr_fun
argument. attr_fun
expects a function that computes new attributes based on the input node. This function must return attribute values of the same type as the one used for other nodes attributes in the MTG (e.g. Dict
or NamedTuple
).
To know what is the type used for the attributes of your MTG, you can use typeof
as follows:
typeof(node_attributes(mtg))
Dict{Symbol, Any}
If you just need to pass attributes values to a node, you can do as follows:
mtg_2 = deepcopy(mtg)
insert_child!(
mtg_2,
NodeMTG("/", "Axis", 0, 2),
node -> Dict{Symbol, Any}(:length => 2, :area => 0.1)
)
node_attributes(mtg_2[1])
Dict{Symbol, Any} with 2 entries:
:diameter => 0.1
:length => 0.5
But we can also compute our attributes based on other nodes data:
mtg_2 = deepcopy(mtg)
insert_child!(
mtg_2,
NodeMTG("/", "Axis", 0, 2),
node -> Dict{Symbol, Any}(:total_length => sum(descendants(node, :length, ignore_nothing = true)))
)
node_attributes(mtg_2[1])
Dict{Symbol, Any} with 2 entries:
:diameter => 0.1
:length => 0.5
We use mtg_2[1]
here to get the first child of the root node.
Delete a node
It is possible to remove a single node in an MTG using delete_node!
. For example if we want to delete the second internode (node 4):
mtg_del = deepcopy(mtg)
delete_node!(get_node(mtg_del, 4))
mtg_del
/ 1: Plant
└─ / 2: Internode
├─ + 3: Leaf
└─ + 5: Leaf
As we can see the new MTG has only one internode now, and the children of the second internode are now the children of its parents, the first internode.
But what if we deleted the first internode?
mtg_del = deepcopy(mtg)
delete_node!(get_node(mtg_del, 2))
mtg_del
/ 1: Plant
├─ + 3: Leaf
└─ / 4: Internode
└─ + 5: Leaf
We don't see it here in the documentation but this expressions returns a warning now. It says:
Warning: Scale of the child node branched but its deleted parent was decomposing. Keep branching, please check if the decomposition is still correct.
This is because we don't really know what should be the new link for a branching child replacing a decomposing node. So by default we don't make any assumption and keep the scale of the child as it is, in the hope the user will look into it.
In our example the first leaf is now branching from the plant, while it should decompose it because it is not of the same scale. But a leaf decomposing a Plant has no meaning botanically. The best approach would be to keep an intermediary node, as it was before.
The user can define its own rules for the new links using the child_link_fun
keyword argument of delete_node!
(click to see an example usage). It expect a function that takes the child node as input and return its new link.
For example one could decide to never replace the children link and manage them afterward. In this case we can use the identity function like this:
mtg_del = deepcopy(mtg)
delete_node!(get_node(mtg_del, 2), child_link_fun = link)
mtg_del
/ 1: Plant
├─ + 3: Leaf
└─ < 4: Internode
└─ + 5: Leaf
It didn't change anything here because the child already kept its own link. But it will differ for other types of parent / children links.
Insert/remove nodes programmatically
Sometimes we want to remove or add a lot of nodes in an MTG. This is possible to do it programmatically using dedicated functions.
Delete nodes
We can remove all nodes that meet specific conditions given by the usual node filters (see Filters for more details). For example one could remove all nodes of scale 2 in an MTG, i.e. all nodes except the Plant in our example:
mtg_2 = deepcopy(mtg)
delete_nodes!(mtg_2, scale = 2)
/ 1: Plant
We can also remove nodes with more complex filters, for example all nodes with an index greater or equal to 1:
mtg_3 = deepcopy(mtg)
delete_nodes!(mtg_3, filter_fun = node -> node_mtg(node).index >= 1)
/ 1: Plant
└─ / 2: Internode
└─ + 3: Leaf
delete_nodes!
always return the root node of the MTG. If the root node of the original MTG is deleted, its child becomes the new root node. If the root had several children, it returns an error. The function always return the root node of the new MTG, so if the root has not been modified, it remains the same, but if it has been deleted, the new root is returned. That is why it is preferable to use delete_nodes!
has a non-mutating function and re-assign the results to an object if it is planned to remove the root node.
As for delete_node!
(singular), by default delete_nodes!
(plural) uses new_child_link
to re-link the children of the deleted nodes, but the user can provide a custom function. See the function details to learn more about it.
Insert nodes
Insert nodes at position
We can add new nodes in an MTG programmatically using:
insert_parents!
: add a new parent node to the filtered nodesinsert_children!
: add a new child node to the filtered nodesinsert_siblings!
: add a new sibling node to the filtered nodesinsert_generations!
: add a new child node to the filtered nodes, but this new child is considered a whole new generation, meaning the previous children of the targeted nodes become the children of the new child node (i.e. a new generation).
Note the plural form for the name of the functions. The singular form does the same thing but only on the node we provide as input. The plural forms do the job for every filtered node in the MTG.
The functions insert new nodes based either on a template NodeMTG or a function that computes it. The attributes of the nodes are empty by default, be can also be provided by the user either as is, or as a function that computes them.
The id of the inserted node is automatically computed using new_id
.
For example if we need to insert new Flower nodes as parents of each Leaf, we would do:
mtg_4 = deepcopy(mtg)
template = MutableNodeMTG("+", "Flower", 0, 2)
insert_parents!(mtg_4, template, symbol = "Leaf")
/ 1: Plant
└─ / 2: Internode
├─ < 4: Internode
│ └─ + 7: Flower
│ └─ + 5: Leaf
└─ + 6: Flower
└─ + 3: Leaf
Similarly, we can add a new child to leaves using insert_children!
:
template = MutableNodeMTG("/", "Leaflet", 0, 3)
insert_children!(mtg_4, template, symbol = "Leaf")
/ 1: Plant
└─ / 2: Internode
├─ < 4: Internode
│ └─ + 7: Flower
│ └─ + 5: Leaf
│ └─ / 8: Leaflet
└─ + 6: Flower
└─ + 3: Leaf
└─ / 9: Leaflet
Usually, the flower is positioned as a sibling of the leaf though. To do so, we can use insert_siblings!
:
mtg_5 = deepcopy(mtg)
template = MutableNodeMTG("+", "Flower", 0, 2)
insert_siblings!(mtg_5, template, symbol = "Leaf")
/ 1: Plant
└─ / 2: Internode
├─ + 3: Leaf
├─ < 4: Internode
│ ├─ + 5: Leaf
│ └─ + 7: Flower
└─ + 6: Flower
Compute the template on the fly
The template for the NodeMTG
can also be computed on the fly for more complex designs:
insert_children!(
mtg_5,
node -> if node_id(node) == 3 MutableNodeMTG("/", "Spear", 0, 3) else MutableNodeMTG("/", "Leaflet", 0, 3) end,
symbol = "Leaf"
)
/ 1: Plant
└─ / 2: Internode
├─ + 3: Leaf
│ └─ / 8: Spear
├─ < 4: Internode
│ ├─ + 5: Leaf
│ │ └─ / 9: Leaflet
│ └─ + 7: Flower
└─ + 6: Flower
Compute attributes on the fly
The same is true for the attributes. We can provide them as is:
insert_siblings!(
mtg_5,
MutableNodeMTG("+", "Leaf", 0, 2),
Dict{Symbol, Any}(:area => 0.1),
symbol = "Leaf"
)
/ 1: Plant
└─ / 2: Internode
├─ + 3: Leaf
│ └─ / 8: Spear
├─ < 4: Internode
│ ├─ + 5: Leaf
│ │ └─ / 9: Leaflet
│ ├─ + 7: Flower
│ └─ + 11: Leaf
├─ + 6: Flower
└─ + 10: Leaf
Or compute them based on the node on which we insert the new nodes. For example if we want the new node to take twice the values of the area of the node it is inserted on, we would do:
insert_siblings!(
mtg_5,
MutableNodeMTG("+", "Leaf", 0, 2),
node -> node[:area] === nothing ? nothing : Dict{Symbol, Any}(:area => node[:area] * 2),
symbol = "Leaf"
)
/ 1: Plant
└─ / 2: Internode
├─ + 3: Leaf
│ └─ / 8: Spear
├─ < 4: Internode
│ ├─ + 5: Leaf
│ │ └─ / 9: Leaflet
│ ├─ + 7: Flower
│ ├─ + 11: Leaf
│ ├─ + 13: Leaf
│ └─ + 14: Leaf
├─ + 6: Flower
├─ + 10: Leaf
├─ + 12: Leaf
└─ + 15: Leaf
The function used to compute the attributes must return data using the same structure as the one used for the other nodes attributes. In our example it returns a Dict{Symbol, Any}
, but it can be different depending on your MTG. To know which structure you should use, use this command:
typeof(node_attributes(mtg))
Let's see the results for the area of our leaves:
DataFrame(mtg_5, :area)
Row | tree | id | symbol | scale | index | parent_id | link | area |
---|---|---|---|---|---|---|---|---|
String? | Int64? | String? | Int64? | Int64? | Int64? | String? | Float64? | |
1 | / 1: Plant | 1 | Plant | 1 | 0 | missing | / | missing |
2 | └─ / 2: Internode | 2 | Internode | 2 | 0 | 1 | / | missing |
3 | ├─ + 3: Leaf | 3 | Leaf | 2 | 0 | 2 | + | 0.2 |
4 | │ └─ / 8: Spear | 8 | Spear | 3 | 0 | 3 | / | missing |
5 | ├─ < 4: Internode | 4 | Internode | 2 | 1 | 2 | < | missing |
6 | │ ├─ + 5: Leaf | 5 | Leaf | 2 | 1 | 4 | + | 0.2 |
7 | │ │ └─ / 9: Leaflet | 9 | Leaflet | 3 | 0 | 5 | / | missing |
8 | │ ├─ + 7: Flower | 7 | Flower | 2 | 0 | 4 | + | missing |
9 | │ ├─ + 11: Leaf | 11 | Leaf | 2 | 0 | 4 | + | 0.1 |
10 | │ ├─ + 13: Leaf | 13 | Leaf | 2 | 0 | 4 | + | 0.4 |
11 | │ └─ + 14: Leaf | 14 | Leaf | 2 | 0 | 4 | + | 0.2 |
12 | ├─ + 6: Flower | 6 | Flower | 2 | 0 | 2 | + | missing |
13 | ├─ + 10: Leaf | 10 | Leaf | 2 | 0 | 2 | + | 0.1 |
14 | ├─ + 12: Leaf | 12 | Leaf | 2 | 0 | 2 | + | 0.4 |
15 | └─ + 15: Leaf | 15 | Leaf | 2 | 0 | 2 | + | 0.2 |
Write the MTG
Finally, we can write our newly created MTG to disk using write_mtg
. The header of the MTG file will be computed automatically based on the information in the MTG. If you want to pass your own header information you can use the corresponding arguments in the function, e.g. classes
.
write_mtg("myMTG.mtg",mtg)