MultiScaleTreeGraph.jl

Read and write MTG files, compute with DataFrames.jl's alike syntax, and convert into a DataFrame or a MetaGraph.
Author VEZY
Popularity
6 Stars
Updated Last
1 Year Ago
Started In
October 2020

MultiScaleTreeGraph

Stable Dev Build Status Coverage Code Style: Blue ColPrac: Contributor's Guide on Collaborative Practices for Community Packages DOI

The goal of MultiScaleTreeGraph.jl is to read, write, analyse and plot MTG (Multi-scale Tree Graph) files. These files describe the plant topology (i.e. structure) along with some attributes for each node (e.g. geometry, colors, state...).

The package is under intensive development and is in a very early version. The functions may heavily change from one version to another until a more stable version is released.

1. Installation

You can install the latest stable version of MultiScaleTreeGraph.jl using this command:

] add MultiScaleTreeGraph

Or if you prefer the development version:

using Pkg
Pkg.add(url="https://github.com/VEZY/MultiScaleTreeGraph.jl", rev="master")

2. Example

Read a simple MTG file:

using MultiScaleTreeGraph

file = joinpath(dirname(dirname(pathof(MultiScaleTreeGraph))),"test","files","simple_plant.mtg")

mtg = read_mtg(file)

Then you can compute new variables in the MTG using a DataFrame.jl's alike syntax:

transform!(mtg, :Length => (x -> x * 1000.) => :length_mm)

And then write the MTG back to disk:

write_mtg("test.mtg",mtg)

3. Conversion

You can convert an MTG into a DataFrame and select the variables you need:

DataFrame(mtg, [:length_mm, :XX])

Or convert it to a MetaGraph:

MetaGraph(mtg)

4. Compatibility

We can plot the MTG using the companion package PlantGeom.jl.

MultiScaleTreeGraph.jl trees are compatible with the AbstractTrees.jl package, which means you can use all functions from that package, e.g.:

using AbstractTrees

node = get_node(mtg, 4)

nodevalue(node)
parent(node)
nextsibling(node)
prevsibling(nextsibling(node))
childrentype(node)
childtype(node)
childstatetype(node)
getdescendant(mtg, (1, 1, 1, 2))
collect(PreOrderDFS(mtg))
collect(PostOrderDFS(mtg))
collect(Leaves(mtg))
collect(nodevalues(PreOrderDFS(mtg)))
print_tree(mtg)

You can learn more about MultiScaleTreeGraph.jl in the documentation of the package.

3. Roadmap

To do before v1:

  • Functions to read the MTG (read_mtg())
  • Helpers to mutate the MTG:
    • traverse!()
    • descendants()
    • ancestors()
    • @mutate_mtg!()
    • traverse!() for a more julian way
    • delete_nodes!() to delete nodes in the tree based on filters
    • insert_nodes!() to add new nodes in the tree (e.g. a new scale). Use new_id() for id them.
    • Use DataFrame-like API?
      • select!
      • transform!
      • filter! -> cannot implement this one, we cannot predict before-hand how to link the nodes of other scales when deleting all nodes of a given scale. It really depends on the MTG itself.
      • names (return feature names)
  • Use sizehint! in descendants, etc...
  • Make Node compatible with AbstractTrees.jl
  • Make Node indexable for:
    • children using Int
    • attributes using Symbols or anything else
    • node fields using the dot notation
  • iterable
  • Use MutableNamedTuple for node.children by default -> rolled back to Dict instead
  • Tree printing:
    • Tree printing
    • Link + symbol + unique ID
    • Color for scales
  • Functions to plot the MTG
  • Easy handling of the scales in tree traversal
  • Get stats for scales:
    • nb scales
    • min/max scale
    • nb nodes in total / for a given scale
  • Add documentation
    • Add tutorials
    • Add documentation on helper functions, e.g. get_features, get_node...
  • Add tests
    • Add test on the row at which the columns are declared (at ENTITY-CODE!)
    • Add test when there's a missing link at a given line
    • Add test for when the scale of the element is not found in the classes (see line 59 and 141 of parse_mtg.jl, i.e. classes.SCALE[node_element[2] .== classes.SYMBOL][1]
    • Add test in parse_section! for empty lines in section (such as a while loop to ignore it).
  • Add tests for insert_parent!, insert_generation!, insert_child!, insert_sibling!
  • Add tests for insert_parents!, insert_generations!, insert_children!, insert_siblings!
  • Add conversion from DataFrame and from MetaGraph
  • Make the children field a vector of children by default instead of a Dict
  • Add OPF parser (moved to PlantGeom.jl)
  • Add possibility to prune from a node: add it to the docs
  • Add tests for delete_node! and delete_nodes!
  • Add prune!, delete_node! and delete_nodes! to the docs
  • Add possibility to insert a sub_tree
  • Export plotting to PlantGeom.jl so we remove one more dependency away.
  • Make transform! parallel. Look into https://github.com/JuliaFolds/FLoops.jl.
  • Delete siblings field from Node
  • Add option to visit only some scales without the need to visit all nodes in-between
    • Add complex + components in Node.
    • Update names: children are nodes of the same scale, components of a scale with higher number
    • Update traverse and traverse! to visit children (same scale) if e.g. only the first or second scale is needed, avoid visiting scale 3. For that we need to visit only the components of the first node of scale 1, and then it will visit scale 1 + scale 2 and never scale 3 that is a component of scale 2. To implement this, we can remove the scale arg from the filter, and pass it to an equivalent to children that would test if:
      • the scale we want include a scale that is above the scale of the node, return the component,
      • the scale we want is equal, it would return the children
      • the scale is below, return an error because we shouldn't visit this node We have 2 ideas at the time:
      • check that a scale is connected to all nodes of that scale (e.g. a leaf in a tree is not connected to others, but all axes are). If a scale is connected we can safely visit all nodes by visiting their children (same scale, all connected). If a scale is not connected we cannot do the same because we would miss some nodes by just visiting the children. So we need to visit all nodes of its complex to make sure we visited every node with our chosen scale. Iteratively if the complex is not connected we have to do the same for this one too and its complex etc until finding a connected complex. A scale is connected if all nodes with a lower scale decompose to the node scale. So to keep track of if a scale is connected, we can put a counter for each scale on the root node, and increment it each time we add a new node of a lower scale, and decrement it each time it is decomposed. Then if a scale has a value of 0, it is connected, and if it has a value > 0, it may not (we dont know). If it is, we can visit just using the children, if it is not, we have to visit all nodes of the upper scale to be sure to visit all.
      • we could also add a function e.g. cache_scale() that would allow a user to cache a dictionary into the root node with keys being the node name and the values the nodes at that scale. So if users regularly visit a scale they can traverse the dictionary instead of the full MTG. It would work for non-connected scales too. But this idea is not concurrent to the previous one because it does not deal with descendants and ancestors alone (need to avoid visiting all nodes in the tree).
    • Update ancestors and descendants accordingly. See if we can re-use traverse or some functions for descendants to avoid a maintenance nightmare. For ancestors, we need a function that checks if we want the same scale (= parent) or a scale with a smaller value (= complex).
    • Add Tables.jl interface ? So we can iterate over the MTG as rows of a Table.
    • Add possibility to add a node "type" as a parametric type so we can dispatch on this ? E.g. Internode, Leaf... It would be a field of node with default value of e.g. AnyNode

4. Acknowledgments

Several tree-related functions in use are adapted from DataTrees.jl.

This package is heavily inspired by OpenAlea's MTG implementation in Python.

5. Similar packages