Skip to content

Commit 9f204b1

Browse files
committed
Add how-to guide on more advanced traversal
1 parent 83a1ee8 commit 9f204b1

2 files changed

Lines changed: 173 additions & 1 deletion

File tree

docs/make.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ makedocs(;
4444
"How-to guides" => [
4545
"Setting up a grid cloner" => "howto/GridCloner.md",
4646
"Messages in scenes" => "howto/Message.md",
47-
"Multiple materials/colors" => "howto/Materials.md"
47+
"Multiple materials/colors" => "howto/Materials.md",
48+
"Advanced traversal" => "howto/Traversal.md"
4849
],
4950
"API" => [
5051
"Graphs" => "api/graphs.md",

docs/src/howto/Traversal.md

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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

Comments
 (0)