# Module Core

## Types

# ** VPL.Core.Graph** —

*Type*.

`Graph(;axiom, rules = nothing, vars = nothing)`

Create a dynamic graph from an axiom, one or more rules and, optionally, graph-level variables.

**Arguments**

`axiom`

: A single object inheriting from`Node`

or a subgraph generated with

the graph construction DSL. It should represent the initial state of the dynamic graph.

`rules`

: A single`Rule`

object or a tuple of`Rule`

objects (optional). It

should include all graph-rewriting rules of the graph.

`vars`

: A single object of any user-defined type (optional). This will be the

graph-level variable accessible from any rule or query applied to the graph.

`FT`

: Floating-point precision to be used when generating the 3D geometry

associated to a graph.

**Details**

All arguments are assigned by keyword. The axiom and rules are deep-copied when creating the graph but the graph-level variables (if a copy is needed due to mutability, the user needs to care of that).

**Return**

An object of type `Graph`

representing a dynamic graph. Printing this object results in a human-readable description of the type of data stored in the graph.

**Examples**

```
let
struct A0 <: Node end
struct B0 <: Node end
= A0() + B0()
axiom = Graph(axiom = axiom)
no_rules_graph = Rule(A, rhs = x -> A0() + B0())
rule = Graph(axiom = axiom, rules = rule)
rules_graph end
```

# ** VPL.Core.Rule** —

*Type*.

`Rule(nodetype; lhs = x -> true, rhs = x -> nothing, captures = false)`

Create a replacement rule for nodes of type `nodetype`

.

**Arguments**

`nodetype`

: Type of node to be matched.`lhs`

: Function or function-like object that takes a`Context`

object and

returns whether the node should be replaced or not (with `true`

or `false`

).

`rhs`

: Function or function-like object that takes one or more`Context`

objects and returns a replacement graph or `nothing`

. If it takes several inputs, the first one will correspond to the node being replaced.

`captures`

: Either`false`

or`true`

to indicate whether the left-hand side

of the rule is capturing nodes in the context of the replacement node to be used for the construction of the replace graph.

**Details**

See VPL documentation for details on rule-based graph rewriting.

**Return**

An object of type `Rule`

.

**Examples**

```
let
struct A <: Node end
struct B <: Node end
= A() + B()
axiom = Rule(A, rhs = x -> A() + B())
rule = Graph(axiom = axiom, rules = rule)
rules_graph rewrite!(rules_graph)
end
```

# ** VPL.Core.Query** —

*Type*.

`Query(nodetype::DataType; condition = x -> true)`

Create a query that matches nodes of type `nodetype`

and a `condition`

.

**Arguments**

`nodetype::DataType`

: Type of node to be matched.`condition`

: Function or function-like object that checks if a node should be

selected. It is assigned as a keyword argument.

**Details**

If the `nodetype`

should refer to a concrete type and match one of the types stored inside the graph. Abstract types or types that are not contained in the graph are allowed but the query will never return anything.

The `condition`

must be a function or function-like object that takes a `Context`

as input and returns `true`

or `false`

. The default `condition`

always return `true`

such that the query will

**Return**

It returns an object of type `Query`

. Use `apply()`

to execute the query on a dynamic graph.

**Example**

```
struct A <: Node end
struct B <: Node end
= A() + B()
axiom = Graph(axiom)
graph = Query(A)
query apply(graph, query)
```

# ** VPL.Core.Node** —

*Type*.

` Node`

Abstract type from which every node in a graph should inherit. This allows using the graph construction DSL.

**Example**

```
let
struct bar <: Node
::Int
xend
= bar(1)
b1 = bar(2)
b2 + b2
b1 end
```

# ** VPL.Core.Context** —

*Type*.

` Context`

Data structure than links a node to the rest of the graph.

**Fields**

`graph`

: Dynamic graph that contains the node.`node`

: Node inside the graph.

**Details**

A `Context`

object wraps references to a node and its associated graph. The purpose of this structure is to be able to test relationships among nodes within a graph (from with a query or rule), as well as access the data stored in a node (with `data()`

) or the graph (with `vars()`

).

Users do not build `Context`

objects directly but they are provided by VPL as inputs to the user-defined functions inside rules and queries.

## Graph DSL

# ** Base.:+** —

*Method*.

`+(n1::Node, n2::Node)`

Creates a graph with two nodes where `n1`

is the root and `n2`

is the insertion point.

**Examples**

```
let
struct A1 <: Node val::Int end
struct B1 <: Node val::Int end
= A1(1) + B1(1)
axiom draw(axiom)
end
```

# ** Base.:+** —

*Method*.

`+(g::StaticGraph, n::Node)`

Creates a graph as the result of appending the node `n`

to the insertion point of graph `g`

.

**Examples**

```
let
struct A1 <: Node val::Int end
struct B1 <: Node val::Int end
= A1(1) + B1(1)
axiom = axiom + A1(2)
axiom draw(axiom)
end
```

# ** Base.:+** —

*Method*.

`+(n::Node, g::StaticGraph)`

Creates a graph as the result of appending the static graph `g`

to the node `n`

.

**Examples**

```
let
struct A1 <: Node val::Int end
struct B1 <: Node val::Int end
= A1(1) + B1(1)
axiom = A1(2) + axiom
axiom draw(axiom)
end
```

# ** Base.:+** —

*Method*.

`+(g1::StaticGraph, g2::StaticGraph)`

Creates a graph as the result of appending `g2`

to the insertion point of `g1`

. The insertion point of the final graph corresponds to the insertion point of `g2`

.

**Examples**

```
let
struct A1 <: Node val::Int end
struct B1 <: Node val::Int end
= A1(1) + B1(1)
axiom1 = A1(2) + B1(2)
axiom2 = axiom1 + axiom2
axiom draw(axiom)
end
```

# ** Base.:+** —

*Method*.

```
+(g::StaticGraph, T::Tuple)
+(n::Node, T::Tuple)
```

Creates a graph as the result of appending a tuple of graphs/nodes `T`

to the insertion point of the graph `g`

or node `n`

. Each graph/node in `L`

becomes a branch.

**Examples**

```
let
struct A1 <: Node val::Int end
struct B1 <: Node val::Int end
= A1(1) + (B1(1) + A1(3), B1(4))
axiom draw(axiom)
end
```

## Applying rules and queries

# ** VPL.Core.apply** —

*Method*.

`apply(g::Graph, query::Query)`

Return an array with all the nodes in the graph that match the query supplied by the user.

**Example**

```
struct A <: Node end
struct B <: Node end
= A() + B()
axiom = Graph(axiom)
graph = Query(A)
query apply(graph, query)
```

# ** VPL.Core.rewrite!** —

*Method*.

`rewrite!(g::Graph)`

Apply the graph-rewriting rules stored in the graph.

**Arguments**

`g::Graph`

: The graph to be rewritten. It will be modified in-place.

**Details**

This function will match the left-hand sides of all the rules in a graph. If any node is matched by more than one rule this will result in an error. The rules are then applied in order to replaced the matched nodes with the result of executing the right hand side of the rules. The rules are applied in the order in which they are stored in the graph but the order in which the nodes are processed is not defined. Since graph rewriting is semantically a parallel process, the rules should not be rely on any particular order for their functioning.

**Returns**

This function returns `nothing`

, but the graph passed as input will be modified by the execution of the rules.

**Example**

```
let
struct A <: Node end
struct B <: Node end
= A() + B()
axiom = Rule(A, rhs = x -> A() + B())
rule = Graph(axiom = axiom, rules = rule)
g rewrite!(g)
end
```

## Extracting information

# ** VPL.Core.vars** —

*Method*.

vars(g::Graph)

Returns the graph-level variables.

**Example**

```
struct A <: Node end
= A()
axiom = Graph(axiom, vars = 2)
graph vars(graph)
```

# ** VPL.Core.rules** —

*Method*.

`rules(g::Graph)`

Returns a tuple with all the graph-rewriting rules stored in a dynamic graph

**Examples**

```
struct A <: Node end
struct B <: Node end
= A() + B()
axiom = Rule(A, rhs = x -> A() + B())
rule = Graph(axiom, rules = rule)
rules_graph rules(rules_graph)
```

# ** VPL.Core.vars** —

*Method*.

`vars(c::Context)`

Returns the graph-level variables. Intended to be used within a rule or query.

# ** VPL.Core.data** —

*Method*.

`data(c::Context)`

Returns the data stored in a node. Intended to be used within a rule or query.

## Node relations

# ** VPL.Core.hasParent** —

*Method*.

`hasParent(c::Context)`

Check if a node has a parent and return `true`

or `false`

. Intended to be used within a rule or query.

# ** VPL.Core.isRoot** —

*Method*.

`isRoot(c::Context)`

Check if a node is the root of the graph (i.e., has no parent) and return `true`

or `false`

. Intended to be used within a rule or query.

# ** VPL.Core.hasAncestor** —

*Method*.

`hasAncestor(c::Context; condition = x -> true, maxlevel::Int = typemax(Int))`

Check if a node has an ancestor that matches the condition. Intended to be used within a rule or query.

**Arguments**

`c::Context`

: Context associated to a node in a dynamic graph.`condition`

: An user-defined function that takes a`Context`

object as input

and returns `true`

or `false`

. It is assigned by the user by keyword.

`maxlevel::Int`

: Maximum number of steps that the algorithm may take when

traversing the graph.

**Details**

This function traverses the graph from the node associated to `c`

towards the root of the graph until a node is found for which `condition`

returns `true`

. If no node meets the condition, then it will return `false`

. The defaults values for this function are such that the algorithm always returns `true`

after one step (unless it is applied to the root node) in which case it is equivalent to calling `hasParent`

on the node.

The number of levels that the algorithm is allowed to traverse is capped by `maxlevel`

(mostly to avoid excessive computation, though the user may want to specify a meaningful limit based on the topology of the graphs being used).

The function `condition`

should take an object of type `Context`

as input and return `true`

or `false`

.

**Return**

Return a tuple with two values a `Bool`

and an `Int`

, the boolean indicating whether the node has an ancestor meeting the condition, the integer indicating the number of levels in the graph separating the node an its ancestor.

**Examples**

```
let
struct A1 <: Node val::Int end
struct B1 <: Node val::Int end
= A1(2) + (B1(1) + A1(3), B1(4))
axiom = Graph(axiom = axiom)
g function qfun(n)
hasAncestor(n, condition = x -> data(x).val == 1)[1]
end
= Query(A1, query = qfun)
Q1 = apply(g, Q1)
R1 = Query(B1, query = qfun)
Q2 = apply(g, Q2)
R2
(R1,R2)end
```

# ** Base.parent** —

*Method*.

`parent(c::Context; nsteps::Int)`

Returns the parent of a node that is `nsteps`

away towards the root of the graph. Intended to be used within a rule or query.

**Details**

If `hasParent()`

returns `false`

for the same node or the algorithm has reached the root node but `nsteps`

have not been reached, then `parent()`

will return `missing`

, otherwise it returns the `Context`

associated to the matching node.

**Return**

Return a `Context`

object or `missing`

.

**Examples**

```
let
struct A1 <: Node val::Int end
struct B1 <: Node val::Int end
= A1(2) + (B1(1) + A1(3), B1(4))
axiom = Graph(axiom = axiom)
g function qfun(n)
= parent(n, nsteps = 2)
np ismissing(np) && data(np).val == 2
!end
= Query(A1, query = qfun)
Q1 = apply(g, Q1)
R1 = Query(B1, query = qfun)
Q2 = apply(g, Q2)
R2
(R1,R2)end
```

# ** VPL.Core.ancestor** —

*Method*.

`ancestor(c::Context; condition = x -> true, maxlevel::Int = typemax(Int))`

Returns the first ancestor of a node that matches the `condition`

. Intended to be used within a rule or query.

**Details**

If `hasAncestor()`

returns `false`

for the same node and `condition`

, `ancestor()`

will return `missing`

, otherwise it returns the `Context`

associated to the matching node

**Return**

Return a `Context`

object or `missing`

.

**Examples**

```
let
struct A1 <: Node val::Int end
struct B1 <: Node val::Int end
= A1(1) + (B1(1) + A1(3), B1(4))
axiom = Graph(axiom = axiom)
g function qfun(n)
= ancestor(n, condition = x -> (data(x).val == 1))
na if !ismissing(na)
data(na) isa B1
else
false
end
end
= Query(A1, query = qfun)
Q1 = apply(g, Q1)
R1 = Query(B1, query = qfun)
Q2 = apply(g, Q2)
R2
(R1,R2)end
```

# ** VPL.Core.hasChildren** —

*Method*.

`hasChildren(c::Context)`

Check if a node has at least one child and return `true`

or `false`

. Intended to be used within a rule or query.

# ** VPL.Core.isLeaf** —

*Method*.

`isLeaf(c::Context)`

Check if a node is a leaf in the graph (i.e., has no children) and return `true`

or `false`

. Intended to be used within a rule or query.

# ** VPL.Core.hasDescendent** —

*Method*.

`hasDescendent(c::Context; condition = x -> true, maxlevel::Int = typemax(Int))`

Check if a node has a descendent that matches the optional condition. Intended to be used within a rule or query.

**Arguments**

`c::Context`

: Context associated to a node in a dynamic graph.`condition`

: An user-defined function that takes a`Context`

object as input

and returns `true`

or `false`

. It is assigned by the user by keyword.

`maxlevel::Int`

: Maximum number of steps that the algorithm may take when

traversing the graph.

**Details**

This function traverses the graph from the node associated to `c`

towards the leaves of the graph until a node is found for which `condition`

returns `true`

. If no node meets the condition, then it will return `false`

. The defaults values for this function are such that the algorithm always returns `true`

after one step (unless it is applied to a leaf node) in which case it is equivalent to calling `hasChildren`

on the node.

The number of levels that the algorithm is allowed to traverse is capped by `maxlevel`

(mostly to avoid excessive computation, though the user may want to specify a meaningful limit based on the topology of the graphs being used).

The function `condition`

should take an object of type `Context`

as input and return `true`

or `false`

.

**Return**

Return a tuple with two values a `Bool`

and an `Int`

, the boolean indicating whether the node has an ancestor meeting the condition, the integer indicating the number of levels in the graph separating the node an its ancestor.

**Examples**

```
let
struct A1 <: Node val::Int end
struct B1 <: Node val::Int end
= A1(2) + (B1(1) + A1(3), B1(4))
axiom = Graph(axiom = axiom)
g function qfun(n)
hasDescendent(n, condition = x -> data(x).val == 1)[1]
end
= Query(A1, query = qfun)
Q1 = apply(g, Q1)
R1 = Query(B1, query = qfun)
Q2 = apply(g, Q2)
R2
(R1,R2)end
```

# ** VPL.Core.children** —

*Method*.

`children(c::Context)`

Returns all the children of a node as `Context`

objects.

# ** VPL.Core.descendent** —

*Method*.

`descendent(c::Context; condition = x -> true, maxlevel::Int = typemax(Int))`

Returns the first descendent of a node that matches the `condition`

. Intended to be used within a rule or query.

**Details**

If `hasDescendent()`

returns `false`

for the same node and `condition`

, `descendent()`

will return `missing`

, otherwise it returns the `Context`

associated to the matching node.

**Return**

Return a `Context`

object or `missing`

.

**Examples**

```
let
struct A1 <: Node val::Int end
struct B1 <: Node val::Int end
= A1(1) + (B1(1) + A1(3), B1(4))
axiom = Graph(axiom = axiom)
g function qfun(n)
= descendent(n, condition = x -> (data(x).val == 1))
na if !ismissing(na)
data(na) isa B1
else
false
end
end
= Query(A1, query = qfun)
Q1 = apply(g, Q1)
R1 = Query(B1, query = qfun)
Q2 = apply(g, Q2)
R2
(R1,R2)end
```

# Traversal algorithms

# ** VPL.Core.traverse** —

*Method*.

`traverse(g::Graph; fun = () -> nothing)`

Iterates over all the nodes in the graph and execute for the function `fun`

on each node

**Arguments**

`g::Graph`

: The graph object that will be traversed.`fun`

: A function or function-like object defined by the user that will be

applied to each node. This argument is assigned by keyword.

**Details**

This traveral happens in the order in which the nodes are stored in the graph. This order is arbitrary and may vary across executions of the code (it does not correspond to the order in which nodes are created). For algorithms that require a particular traveral order of the graph, see `traverseDFS`

and `traverseBFS`

.

This function does not store any results generated by `fun`

. Hence, if the user wants to keep track of such results, they should be stored indirectly (e.g., via a global variable or internally by creating a functor).

The function or function-like object provided by the user should take only one argument that corresponds to applying `data()`

to each node in the graph. Several methods of such function may be defined for different types of nodes in the graph. Since the function will use the data stored in the nodes, relations among nodes may not be used as input. For algorithms where relations among nodes are important, the user should be using queries instead (see `Query`

and general VPL documentation).

**Return**

This function returns nothing but `fun`

may have side-effects.

**Examples**

```
let
struct A1 <: Node val::Int end
struct B1 <: Node val::Int end
struct Foo
::Vector{Int}
valsend
function (f::Foo)(x)
push!(f.vals, x.val)
end
= Foo(Int[])
f = A1(2) + (B1(1) + A1(3), B1(4))
axiom = Graph(axiom = axiom)
g traverse(g, fun = f)
f.valsend
```

# ** VPL.Core.traverseDFS** —

*Method*.

`traverseDFS(g::Graph; fun = () -> nothing, ID = root(g))`

Iterates over all the nodes in the graph (depth-first order, starting at a any node) and execute for the function `fun`

on each node

**Arguments**

`g::Graph`

: The graph object that will be traversed.`fun`

: A function or function-like object defined by the user that will be

applied to each node. This argument is assigned by keyword.

`ID`

: The ID of the node where the traveral should start. This argument is

assigned by keyword and is, by default, the root of the graph.

**Details**

This traveral happens in a depth-first order. That is, all nodes in a branch of the graph are visited until reach a leaf node, then moving to the next branch. Hence, this algorithm should always generate the same result when applied to the same graph (assuming the user-defined function is not stochastic). For a version of this function that us breadth-first order see `traverseBFS`

.

This function does not store any results generated by `fun`

. Hence, if the user wants to keep track of such results, they should be stored indirectly (e.g., via a global variable or internally by creating a functor).

The function or function-like object provided by the user should take only one argument that corresponds to applying `data()`

to each node in the graph. Several methods of such function may be defined for different types of nodes in the graph. Since the function will use the data stored in the nodes, relations among nodes may not be used as input. For algorithms where relations among nodes are important, the user should be using queries instead (see `Query`

and general VPL documentation).

**Return**

This function returns nothing but `fun`

may have side-effects.

**Examples**

```
let
struct A1 <: Node val::Int end
struct B1 <: Node val::Int end
struct Foo
::Vector{Int}
valsend
function (f::Foo)(x)
push!(f.vals, x.val)
end
= Foo(Int[])
f = A1(2) + (B1(1) + A1(3), B1(4))
axiom = Graph(axiom = axiom)
g traverseDFS(g, fun = f)
f.valsend
```

# ** VPL.Core.traverseBFS** —

*Method*.

`traverseBFS(g::Graph; fun = () -> nothing, ID = root(g))`

Iterates over all the nodes in the graph (breadth-first order, starting at a any node) and execute for the function `fun`

on each node

**Arguments**

`g::Graph`

: The graph object that will be traversed.`fun`

: A function or function-like object defined by the user that will be

applied to each node. This argument is assigned by keyword.

`ID`

: The ID of the node where the traveral should start. This argument is

assigned by keyword and is, by default, the root of the graph.

**Details**

This traveral happens in a breadth-first order. That is, all nodes at a given depth of the the graph are visited first, then moving on to the next level. Hence, this algorithm should always generate the same result when applied to the same graph (assuming the user-defined function is not stochastic). For a version of this function that us depth-first order see `traverseDFS`

.

This function does not store any results generated by `fun`

. Hence, if the user wants to keep track of such results, they should be stored indirectly (e.g., via a global variable or internally by creating a functor).

The function or function-like object provided by the user should take only one argument that corresponds to applying `data()`

to each node in the graph. Several methods of such function may be defined for different types of nodes in the graph. Since the function will use the data stored in the nodes, relations among nodes may not be used as input. For algorithms where relations among nodes are important, the user should be using queries instead (see `Query`

and general VPL documentation).

**Return**

This function returns nothing but `fun`

may have side-effects.

**Examples**

```
let
struct A1 <: Node val::Int end
struct B1 <: Node val::Int end
struct Foo
::Vector{Int}
valsend
function (f::Foo)(x)
push!(f.vals, x.val)
end
= Foo(Int[])
f = A1(2) + (B1(1) + A1(3), B1(4))
axiom = Graph(axiom = axiom)
g traverseBFS(g, fun = f)
f.valsend
```

## Graph visualization

# ** VPL.Core.draw** —

*Method*.

```
draw(g::Graph; force = false, backend = "native", inline = false,
= (1920, 1080), nlabels_textsize = 15, arrow_size = 15,
resolution = 5) node_size
```

Visualize a graph as network diagram.

**Arguments**

All arguments are assigned by keywords except the graph `g`

.

`g::Graph`

: The graph to be visualized.`force = false`

: Force the creation of a new window to store the network

diagram.

`backend = "native"`

: The graphics backend to render the network diagram. It

can have the values `"native"`

, `"web"`

and `"vector"`

. See VPL documentation for details.

`inline = false`

: Currently this argument does not do anything (will change in

future versions of VPL).

`resolution = (1920, 1080)`

: The resolution of the image to be rendered, in

pixels (online relevant for native and web backends). Default resolution is HD.

`nlabels_textsize = 15`

: Customize the size of the labels in the diagram.`arrow_size = 15`

: Customize the size of the arrows representing edges in the

diagram.

`node_size = 5`

: Customize the size of the nodes in the diagram.

**Details**

By default, nodes are labelled with the type of data stored and their unique ID. See function `node_label()`

to customize the label for different types of data.

See `export_graph()`

to export the network diagram as a raster or vector image (depending on the backend). The function `calculate_resolution()`

can be useful to ensure a particular dpi of the exported image (assuming some physical size).

The graphics backend will interact with the environment where the Julia code is being executed (i.e., terminal, IDE such as VS Code, interactive notebook such as Jupyter or Pluto). These interactions are all controlled by the graphics package Makie that VPL relies on. Some details on the expected behavior specific to `draw()`

can be found in the general VPL documentation as www.virtualplantlab.com

**Return**

This function returns a Makie `Figure`

object, while producing the visualization as a side effect.

**Examples**

```
let
struct A1 <: Node val::Int end
struct B1 <: Node val::Int end
= A1(1) + (B1(1) + A1(3), B1(4))
axiom = Graph(axiom = axiom)
g draw(g)
end
```

# ** VPL.Core.draw** —

*Method*.

```
draw(g::StaticGraph; force = false, backend = "native", inline = false,
= (1920, 1080), nlabels_textsize = 15, arrow_size = 15,
resolution = 5) node_size
```

Equivalent to the method `draw(g::Graph; kwargs...)`

but to visualize static graphs (e.g., the axiom of a graph).

# ** VPL.Core.node_label** —

*Method*.

`node_label(n::Node, id)`

Function to construct a label for a node to be used by `draw()`

when visualizing. The user can specialize this method for user-defined data types to customize the labels. By default, the type of data stored in the node and the unique ID of the node are used as labels.

# ** VPL.Core.export_graph** —

*Method*.

`export_graph(f; filename, kwargs...)`

Save a network diagram generated by `draw()`

to an external file.

**Arguments**

`f`

: Object of type`Figure`

return by`draw()`

.`filename`

: Name of the file where the diagram will be stored. The extension

will be used to determined the format of the image (see example below).

**Details**

Internally, `export_graph()`

calls the `save()`

method from the ImageIO package and its dependencies. Any keyword argument supported by the relevant save method will be passed along by `export_graph()`

. For example, exporting diagrams as PNG allows defining the compression level as `compression_level`

(see PNGFiles package for details).

**Return**

The function returns nothing but, if successful, it will generate a new file containing the network diagram in the appropiate format.

**Examples**

**Examples**

```
let
struct A1 <: Node val::Int end
struct B1 <: Node val::Int end
= A1(1) + (B1(1) + A1(3), B1(4))
axiom = Graph(axiom = axiom)
g = draw(g);
f export_graph(f, filename = "test.png")
end
```

# ** VPL.Core.calculate_resolution** —

*Method*.

```
calculate_resolution(;width = 1024/300*2.54, height = 768/300*2.54,
= "raster", dpi = 300) format
```

Calculate the resolution required to achieve a specific `width`

and `height`

(in cm) of the exported image, with a particular `dpi`

(for raster formats).