Author
Alejandro Morales Sierra

Centre for Crop Systems Analysis - Wageningen University

Published

March 19, 2023

# Context sensitive rules

This examples goes back to a very simple situation: a linear sequence of 3 cells. The point of this example is to introduce relational growth rules and context capturing.

A relational rules matches nodes based on properties of neighbouring nodes in the graph. This requires traversing the graph, which can be done with the methods `parent` and `children` on the `Context` object of the current node, which return a list of `Context` objects for the parent or children nodes.

In some cases, it is not only sufficient to query the neighbours of a node but also to use properties of those neighbours in the right hand side component of the rule. This is know as “capturing the context” of the node being updated. This can be done by returning the additional nodes from the `lhs` component (in addition to `true` or `false`) and by accepting these additional nodes in the `rhs` component. In addition, we tell VPL that this rule is capturing the context with `captures = true`.

In the example below, each `Cell` keeps track of a `state` variable (which is either 0 or 1). Only the first cell has a state of 1 at the beginning. In the growth rule, we check the father of each `Cell`. When a `Cell` does not have a parent, the rule does not match, otherwise, we pass capture the parent node. In the right hand side, we replace the cell with a new cell with the state of the parent node that was captured. Note that that now, the rhs component gets a new argument, which corresponds to the context of the father node captured in the lhs.

``````using VPL
struct Cell <: Node
state::Int64
end
function transfer(context)
if hasParent(context)
return (true, (parent(context), ))
else
return (false, ())
end
end
rule = Rule(Cell, lhs = transfer, rhs = (context, father) -> Cell(data(father).state), captures = true)
axiom = Cell(1) + Cell(0) + Cell(0)
pop = Graph(axiom = axiom, rules = rule)``````

Rewrite with queries to access the values

In the original state defined by the axiom, only the first node contains a state of 1. We can retrieve the state of each node with a query. A `Query` object is a like a `Rule` but without a right-hand side (i.e., its purpose is to return the nodes that match a particular condition). In this case, we just want to return all the `Cell` nodes. A `Query` object is created by passing the type of the node to be queried as an argument to the `Query` function. Then, to actually execute the query we need to use the `apply` function on the graph.

``````getCell = Query(Cell)
apply(pop, getCell)``````

If we rewrite the graph one we will see that a second cell now has a state of 1.

``````rewrite!(pop)
apply(pop, getCell)``````

And a second iteration results in all cells have a state of 1

``````rewrite!(pop)
apply(pop, getCell)``````

Note that queries may not return nodes in the same order as they were created because of how they are internally stored (and because queries are meant to return collection of nodes rather than reconstruct the topology of a graph). If we need to process nodes in a particular order, then it is best to use a traversal algorithm on the graph that follows a particular order (for example depth-first traversal with `traverseDFS()`). This algorithm requires a function that applies to each node in the graph. In this simple example we can just store the `state` of each node in a vector (unlike Rules and Queries, this function takes the actual node as argument rather than a `Context` object, see the documentation for more details):

``````pop  = Graph(axiom = axiom, rules = rule)
states = Int64[]
traverseDFS(pop, fun = node -> push!(states, node.state))
states``````

Now the states of the nodes are in the same order as they were created:

``````rewrite!(pop)
states = Int64[]
traverseDFS(pop, fun = node -> push!(states, node.state))
states``````