|
| 1 | + |
| 2 | + |
| 3 | +# Traversing a graph |
| 4 | + |
| 5 | +In this guide we will see how to address one possible situation where there is need to |
| 6 | +traverse a graph to accumulate information in a relational manner. The key idea in this |
| 7 | +example is that a given node of a graph requires accumulated information from all the |
| 8 | +descendent nodes of a particular type. This pattern shows up in many different contexts |
| 9 | +such as (i) calculating the probability of bud break as affected by apical dominance (without |
| 10 | +explict simulation of hormone transport) or (ii) the pipe model (where the cross-sectional |
| 11 | +area of a stem at a given point is proportional to the leaf area above that point). |
| 12 | + |
| 13 | +This problem can be solved in a number of ways, which differ in terms of complexity and |
| 14 | +performance: |
| 15 | + |
| 16 | +1. Relational query: For every target node, query the graph for all the descendent nodes of |
| 17 | +a particular type and accumulate the information. This does not require any change to the |
| 18 | +rewriting rules and allows for complex relationships (see tutorials for examples). |
| 19 | + |
| 20 | +2. Annotated topology: Assign each target node and descendents with a level tag such that |
| 21 | +we can reconstruct later all the descendents of a particular node (i.e., all nodes of the |
| 22 | +same level or higher). This requires a change to the rewriting rules to add the level tag |
| 23 | +but the traversal is simple as we simply extract all the relevant nodes from the graph and |
| 24 | +perform the logic outside of the graph by filtering for different tag levels. |
| 25 | + |
| 26 | +3. Ad-hoc traversal: Use the `traverse_dfs()` or `traverse_bfs()` functions to write an |
| 27 | +ad-hoc traversal algorithm. Since the accumulation must happen in reverse (from the leaf |
| 28 | +nodes towards the root node), the ad-hoc function needs to be written in a recursive manner, |
| 29 | +most likely keeping its own stack of nodes. This is the most complex solution but also |
| 30 | +(potentially) the most efficient one as no nodes need to be extracted and each graph is |
| 31 | +visited exactly once. |
| 32 | + |
| 33 | +To illustrate all these approaches, let's create a simple graph that represents the essence |
| 34 | +of the problem. We are going to assume to types of nodes (`A` and `B`), where the target |
| 35 | +nodes are of type `A` and the descendents of interest include both types. The information to |
| 36 | +be accumulated is a simple integer that we will call `state`. The result of accumulating the |
| 37 | +states will be stored in the `value`. We also add the `level` tag for second approach. The |
| 38 | +types are defined in a module as usual: |
| 39 | + |
| 40 | +```julia |
| 41 | +using VirtualPlantLab |
| 42 | +import GLMakie |
| 43 | +module TravTypes |
| 44 | + import PlantGraphs: Node |
| 45 | + export A, B |
| 46 | + @kwdef mutable struct A <: Node |
| 47 | + value::Float64 = 0.0 |
| 48 | + state::Float64 = 0.0 |
| 49 | + level::Int = 0 |
| 50 | + end |
| 51 | + @kwdef struct B <: Node |
| 52 | + state::Float64 = 0.0 |
| 53 | + level::Int = 0 |
| 54 | + end |
| 55 | +end |
| 56 | + |
| 57 | +using .TravTypes |
| 58 | + |
| 59 | +# Function that generates the arguments to create a node |
| 60 | +fill(level) = (level = level, state = rand()) |
| 61 | +# Motif of the graph (`;fill(l)...` is equivalent to typing `level = l, state = rand()`) |
| 62 | +motif(l) = A(;fill(l)...) + (B(;fill(l)...), B(;fill(l)...) + (B(;fill(l)...), B(;fill(l)...))) |
| 63 | +# Create the graph by repeating the motif 10 times |
| 64 | +axiom = motif(1) |
| 65 | +for i in 2:10 |
| 66 | + axiom += motif(i) |
| 67 | +end |
| 68 | +``` |
| 69 | + |
| 70 | +We specialize `darw()` to include the level tag and the state of each node when we draw the |
| 71 | +graph. Remember that this can be achieved by defining methods for `node_label()` for each |
| 72 | +type of node. |
| 73 | + |
| 74 | +```julia |
| 75 | +PlantGraphs.node_label(node::A, id) = "A: $(node.level) - $(round(node.value, digits = 2))" |
| 76 | +PlantGraphs.node_label(node::B, id) = "B: $(node.level)" |
| 77 | +g = Graph(axiom = axiom) |
| 78 | +draw(g) |
| 79 | +``` |
| 80 | + |
| 81 | +## Relational queries |
| 82 | + |
| 83 | +The first approach is to use relational queries to accumulate the information as a |
| 84 | +side-effect. Note that we do not actually return the nodes as that is not required, we |
| 85 | +simple use the query mechanism to access the context of each node so that we can traverse |
| 86 | +the graph in a relational manner. |
| 87 | + |
| 88 | +We could also implement this logic inside a rule if the computation performed for each node |
| 89 | +would affect whether a particular rule needs to be triggered or not (for example, if we |
| 90 | +were calculating the probability of a lateral bud breaking). |
| 91 | + |
| 92 | +First, we construct the function that will be used to accumulate the information and modify |
| 93 | +the node in-place as a side-effect. We then return the nodes. |
| 94 | + |
| 95 | +```julia |
| 96 | +function accumulate(node) |
| 97 | + # Extract the first child and put it into a stack |
| 98 | + stack = [children(node)...] |
| 99 | + state = 0.0 |
| 100 | + while !isempty(stack) |
| 101 | + # Pop the last child from the stack |
| 102 | + child = pop!(stack) |
| 103 | + # Accumulate the state |
| 104 | + state += data(child).state |
| 105 | + # Add the children of the child to the stack |
| 106 | + if has_children(child) |
| 107 | + for child in children(child) |
| 108 | + push!(stack, child) |
| 109 | + end |
| 110 | + end |
| 111 | + end |
| 112 | + data(node).value = state |
| 113 | + return true |
| 114 | +end |
| 115 | + |
| 116 | +# We not construct the query object and apply it |
| 117 | +q = Query(A, condition = accumulate) |
| 118 | +apply(g, q) |
| 119 | +draw(g) |
| 120 | +``` |
| 121 | + |
| 122 | +## Annotated topology |
| 123 | + |
| 124 | +In the second approach, we assign a level tag to each node so that we can reconstruct the |
| 125 | +descendents of a particular node. We already incorporated this when constructing the axiom, |
| 126 | +so here we simply extract the nodes and perform the accumulation based on on those level |
| 127 | +tags. The queries are very simply, as we just extract all nodes of a given type. |
| 128 | + |
| 129 | +```julia |
| 130 | +qA = Query(A) |
| 131 | +qB = Query(B) |
| 132 | +nodesA = apply(g, qA) |
| 133 | +nodesB = apply(g, qB) |
| 134 | +``` |
| 135 | + |
| 136 | +Now we construct a table that will store the accumulated information for each level as a |
| 137 | +dictionary. We know that the total number of levels is equal to the number of `A` nodes. |
| 138 | + |
| 139 | +```julia |
| 140 | +accum = zeros(length(nodesA)) |
| 141 | +``` |
| 142 | + |
| 143 | +The logic now is to add the `state` of each `A` or `B` node to all the levels that are equal |
| 144 | +or lower (we treat the positions in the array `accum` as the levels). We can then assign |
| 145 | +these values to the `value` field of the `A` nodes. Note how for `A` nodes we need to avoid |
| 146 | +adding the `state` of the node that defines the level (hence the `1:level-1`) since only |
| 147 | +descendents should be used. |
| 148 | + |
| 149 | +```julia |
| 150 | +for node in nodesB |
| 151 | + level = node.level |
| 152 | + accum[1:level] .+= node.state ## Add node.state to elements 1:level |
| 153 | +end |
| 154 | +for node in nodesA |
| 155 | + level = node.level |
| 156 | + level > 1 && (accum[1:level-1] .+= node.state) |
| 157 | +end |
| 158 | +for node in nodesA |
| 159 | + node.value = accum[node.level] |
| 160 | +end |
| 161 | +draw(g) |
| 162 | +``` |
| 163 | + |
| 164 | +## Ad-hoc traversal |
| 165 | + |
| 166 | +*Work in progress* |
| 167 | + |
| 168 | +--- |
| 169 | + |
| 170 | +*This page was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl).* |
| 171 | + |
0 commit comments