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
Note

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))
5×11 DataFrame
Rowtreeidsymbolscaleindexparent_idlinkspeciesdiameterarealength
String?Int64?String?Int64?Int64?Int64?String?String?Float64?Float64?Float64?
1/ 1: Plant1Plant10missing/Grassy-plantmissingmissingmissing
2└─ / 2: Internode2Internode201/missing0.1missing0.5
3 ├─ + 3: Leaf3Leaf202+missingmissing0.2missing
4 └─ < 4: Internode4Internode212<missing0.15missing0.3
5 └─ + 5: Leaf5Leaf214+missingmissing0.2missing

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:

Warning

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.

Note

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
Note

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 nodes
  • insert_children!: add a new child node to the filtered nodes
  • insert_siblings!: add a new sibling node to the filtered nodes
  • insert_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).
Warning

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
Danger

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)
15×8 DataFrame
Rowtreeidsymbolscaleindexparent_idlinkarea
String?Int64?String?Int64?Int64?Int64?String?Float64?
1/ 1: Plant1Plant10missing/missing
2└─ / 2: Internode2Internode201/missing
3 ├─ + 3: Leaf3Leaf202+0.2
4 │ └─ / 8: Spear8Spear303/missing
5 ├─ < 4: Internode4Internode212<missing
6 │ ├─ + 5: Leaf5Leaf214+0.2
7 │ │ └─ / 9: Leaflet9Leaflet305/missing
8 │ ├─ + 7: Flower7Flower204+missing
9 │ ├─ + 11: Leaf11Leaf204+0.1
10 │ ├─ + 13: Leaf13Leaf204+0.4
11 │ └─ + 14: Leaf14Leaf204+0.2
12 ├─ + 6: Flower6Flower202+missing
13 ├─ + 10: Leaf10Leaf202+0.1
14 ├─ + 12: Leaf12Leaf202+0.4
15 └─ + 15: Leaf15Leaf202+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)