Skip to content

Commit

Permalink
Environment Directed Graph Documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
Robadob committed Oct 16, 2023
1 parent 9ef8afb commit 38e529a
Show file tree
Hide file tree
Showing 5 changed files with 311 additions and 7 deletions.
139 changes: 133 additions & 6 deletions src/guide/agent-functions/interacting-with-environment.rst
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
.. _device environment:

Accessing the Environment
^^^^^^^^^^^^^^^^^^^^^^^^^
=========================

As detailed in the earlier chapter detailing the :ref:`defining of environmental properties<defining environmental properties>`, there are two types of environment property which can be interacted with in agent functions. The :class:`DeviceEnvironment<flamegpu::DeviceEnvironment>` instance can be accessed, to interact with both of these, via ``FLAMEGPU->environment`` (C++) or ``pyflamegpu.environment`` (Python).

Environment Properties
----------------------
^^^^^^^^^^^^^^^^^^^^^^

Agent functions can only read environmental properties. If you wish to modify an environmental property, this must be done
via :ref:`host functions<host environment>`.
Expand Down Expand Up @@ -37,7 +37,7 @@ Environmental properties are accessed, using :class:`DeviceEnvironment<flamegpu:


Environment Macro Properties
----------------------------
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Agent functions have much greater access to environmental macroscopic properties, however they still cannot be directly written to, or both updated and read in the same layer.

Expand Down Expand Up @@ -140,14 +140,141 @@ Example usage is shown below:

# Other behaviour code
...
}

.. warning::
Be careful when using :class:`DeviceMacroProperty<flamegpu::DeviceMacroProperty>`. When you retrieve an element e.g. ``location[0][0][0]`` (from the example above), it is of type :class:`DeviceMacroProperty<flamegpu::DeviceMacroProperty>` not ``unsigned int``. Therefore you cannot pass it directly to functions which take generic arguments such as ``printf()``, as it will be interpreted incorrectly. You must either store it in a variable of the correct type which you instead pass, or explicitly cast it to the correct type when passing it e.g. ``(unsigned int)location[0][0][0]`` or ``static_cast<unsigned int>(location[0][0][0])`` (or ``numpy.uint(location[0][0][0])`` in Python).


Environment Directed Graph
^^^^^^^^^^^^^^^^^^^^^^^^^^

To access the graph on the device, vertex indexes are used rather than IDs, to minimise ID->index conversion for efficient access, methods are available to convert between ID and index.

If executing a model without :ref:`FLAMEGPU_SEATBELTS?<FLAMEGPU_SEATBELTS>` enabled, :func:`getVertexIndex()<flamegpu::DeviceEnvironmentDirectedGraph::getVertexIndex>` and :func:`getEdgeIndex()<flamegpu::DeviceEnvironmentDirectedGraph::getEdgeIndex>` will return zero if the specified vertex or edge does not exist.

.. tabs::

.. code-tab:: cuda Agent C++

FLAMEGPU_AGENT_FUNCTION(GraphTestID, MessageNone, MessageNone) {
DeviceEnvironmentDirectedGraph fgraph = FLAMEGPU->environment.getDirectedGraph("fgraph");

// Fetch the ID of the vertex at index 0
flamegpu::id_t vertex_id = fgraph.getVertexID(0);
// Fetch the index of the vertex with ID 1
unsigned int vertex_index = fgraph.getVertexIndex(1);

// Access a property of vertex with ID 1
float bar_0 = fgraph.getVertexProperty<float, 2>("bar", 0);

// Fetch the source and destination indexes from the edge at index 0
unsigned int source_index = fgraph.getEdgeSource(0);
unsigned int destination_index = fgraph.getEdgeDestination(0);

// Fetch the index of the edge from vertex ID 1 to vertex ID 2
unsigned int edge_index = fgraph.getEdgeIndex(1, 2);

// Access a property of edge with source ID 1, destination ID 2
int foo = fgraph.getEdgeProperty<int>("foo", edge_index);

return flamegpu::ALIVE;
}

.. code-tab:: py Agent Python

@pyflamegpu.agent_function
def ExampleFn(message_in: pyflamegpu.MessageNone, message_out: pyflamegpu.MessageNone):
fgraph = pyflamegpu.environment.getDirectedGraph("fgraph")

# Fetch the ID of the vertex at index 0
vertex_id = fgraph.getVertexID(0)
# Fetch the index of the vertex with ID 1
vertex_index = fgraph.getVertexIndex(1)

# Access a property of vertex with ID 1
bar_0 = fgraph.getVertexPropertyFloatArray2("bar", 0)

# Fetch the source and destination indexes from the edge at index 0
source_index = fgraph.getEdgeSource(0)
destination_index = fgraph.getEdgeDestination(0)

# Fetch the index of the edge from vertex ID 1 to vertex ID 2
edge_index = fgraph.getEdgeIndex(1, 2)

# Access a property of edge with source ID 1, destination ID 2
foo = fgraph.getEdgePropertyInt("foo", edge_index)

return pyflamegpu.ALIVE

.. note::

Edge indices should only be stored within agents if edges will not have their source or destinations updated on the host. Updating edge connectivity triggers a graph rebuild, this causes edges to be sorted (hence invalidating indexes).

Traversing Graphs
-----------------

Agents are able to traverse the graph by iterating edges joining and leaving a specified vertex using the iterators provided by :func:`inEdges()<flamegpu::DeviceEnvironmentDirectedGraph::inEdges>` and `outEdges()<flamegpu::DeviceEnvironmentDirectedGraph::outEdges>` respectively.

.. tabs::

.. code-tab:: cuda Agent C++

FLAMEGPU_AGENT_FUNCTION(GraphTestID, MessageNone, MessageNone) {
DeviceEnvironmentDirectedGraph fgraph = FLAMEGPU->environment.getDirectedGraph("fgraph");

// Fetch the index of the vertex with ID 1
unsigned int vertex_index = fgraph.getVertexIndex(1);

// Iterate the edges leaving the vertex with ID 1
for (auto &edge : fgraph.outEdges(vertex_index)) {
// Read the current edges' destination vertex index
unsigned int dest_vertex_index = edge.getEdgeDestination();
// Read a property from the edge
int foo = edge.getProperty<int>("foo");
}

// Iterate the edges joining the vertex with ID 1
for (auto &edge : fgraph.inEdges(vertex_index)) {
// Read the current edges' source vertex index
unsigned int src_vertex_index = edge.getEdgeSource();
// Read a property from the edge
int foo = edge.getProperty<int>("foo");
}

return flamegpu::ALIVE;
}

.. code-tab:: py Agent Python

@pyflamegpu.agent_function
def ExampleFn(message_in: pyflamegpu.MessageNone, message_out: pyflamegpu.MessageNone):
fgraph = pyflamegpu.environment.getDirectedGraph("fgraph")

# Fetch the index of the vertex with ID 1
vertex_index = fgraph.getVertexIndex(1)

# Iterate the edges leaving the vertex with ID 1
for edge in fgraph.outEdges(vertex_index):
# Read the current edges' destination vertex index
dest_vertex_index = edge.getEdgeDestination()
# Read a property from the edge
foo = edge.getPropertyInt("foo")

# Iterate the edges joining the vertex with ID 1
for edge in fgraph.inEdges(vertex_index):
# Read the current edges' source vertex index
src_vertex_index = edge.getEdgeSource()
# Read a property from the edge
foo = edge.getPropertyInt("foo")

return pyflamegpu.ALIVE


.. note::

The implementation of :func:`outEdges()()<flamegpu::DeviceEnvironmentDirectedGraph::outEdges>` is more efficient than that of :func:`inEdges()()<flamegpu::DeviceEnvironmentDirectedGraph::inEdges>`.

Related Links
-------------
^^^^^^^^^^^^^

* User Guide Page: :ref:`Defining Environmental Properties<defining environmental properties>`
* User Guide Page: :ref:`Host Functions: Accessing the Environment<host environment>`
Expand Down
33 changes: 33 additions & 0 deletions src/guide/environment/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,39 @@ The type, dimensions and name of the macro property are all specified. The macro
# Declare an int macro property named 'foobar', with array dimensions [5, 5, 5, 3]
env.newMacroPropertyInt("foobar", 5, 5, 5, 3)


Defining a Directed Graph
^^^^^^^^^^^^^^^^^^^^^^^^^
FLAME GPU 2 introduces static directed graphs as a structure for storing organised data within the environment. The graph's structure can be defined within a host function, with properties attached to vertices and/or edges.

Directed graphs can then be traversed by agents which can iterate either input or output edges to a given vertex.

Environment directed graphs are currently static, therefore resizing the number of vertices or edges requires all properties to be reinitialised.

.. tabs::

.. code-tab:: cpp C++

// Fetch the model's environment
flamegpu::EnvironmentDescription env = model.Environment();
// Declare a new directed graph named 'fgraph'
EnvironmentDirectedGraphDescription fgraph = model.Environment().newDirectedGraph("fgraph");
// Attach an float[2] property 'bar' to vertices
fgraph.newVertexProperty<float, 2>("bar");
// Attach an int property 'foo' to edges
fgraph.newEdgeProperty<int>("foo");

.. code-tab:: py Python

# Fetch the model's environment
env = model.Environment()
# Declare a new directed graph named 'fgraph'
EnvironmentDirectedGraphDescription fgraph = model.Environment().newDirectedGraph("fgraph")
# Attach an float[2] property 'bar' to vertices
fgraph.newVertexPropertyArrayFloat("bar", 2)
# Attach an int property 'foo' to edges
fgraph.newEdgePropertyInt("foo")

Related Links
^^^^^^^^^^^^^

Expand Down
138 changes: 137 additions & 1 deletion src/guide/host-functions/interacting-with-environment.rst
Original file line number Diff line number Diff line change
Expand Up @@ -159,14 +159,150 @@ Environment macro properties are best suited for large datasets. For this reason

.. code-tab:: python

# Define an host function called write_env_hostfn
# Define an host function called macro_prop_io_hostfn
class macro_prop_io_hostfn(pyflamegpu.HostFunction):
def run(self,FLAMEGPU):
# Export the macro property
FLAMEGPU.environment.exportMacroProperty("macro_float_3_3_3", "out.bin");
# Import a macro property
FLAMEGPU.environment.importMacroProperty("macro_float_3_3_3", "in.json");

Environment Directed Graph
^^^^^^^^^^^^^^^^^^^^^^^^^^

The environment directed graph can be initialised within host functions, defining the connectivity and initialising any properties stored within.

The host API allows vertices and edges to be managed via a map/dictionary interface, where the ID is used to access a vertex, or source and destination vertex IDs to access an edge.

Vertex IDs are unsigned integers, however the value `0` is reserved so cannot be assigned. Vertex IDs are not required to be contiguous, however they are stored sparsely such that two vertices with IDs `1` and `1000001` will require an index of length `1000000`. It may be possible to run out of memory if IDs are too sparsely distributed.

.. tabs::

.. code-tab:: cuda CUDA C++

// Define an host function called directed_graph_hostfn
FLAMEGPU_HOST_FUNCTION(directed_graph_hostfn) {
// Fetch a handle to the directed graph
HostEnvironmentDirectedGraph fgraph = FLAMEGPU->environment.getDirectedGraph("fgraph");
// Declare the number of vertices and edges
fgraph.setVertexCount(5);
fgraph.setEdgeCount(5);
// Initialise the vertices
HostEnvironmentDirectedGraph::VertexMap vertices = graph.vertices();
for (int i = 1; i <= 5; ++i) {
// Create (or fetch) vertex with ID i
HostEnvironmentDirectedGraph::VertexMap::Vertex vertex = vertices[i];
vertex.setProperty<float, 2>("bar", {0.0f, 10.0f});
}
// Initialise the edges
HostEnvironmentDirectedGraph::EdgeMap edges = graph.edges();
for (int i = 1; i <= 5; ++i) {
// Create (or fetch) edge with specified source/dest vertex IDs
HostEnvironmentDirectedGraph::EdgeMap::Edge edge = edges[{i, ((i + 1)%5) + 1}];
edge.setProperty<int>("foo", 12);
}
}

.. code-tab:: python

# Define an host function called directed_graph_hostfn
class directed_graph_hostfn(pyflamegpu.HostFunction):
def run(self,FLAMEGPU):
# Fetch a handle to the directed graph
fgraph = FLAMEGPU->environment.getDirectedGraph("fgraph")
# Declare the number of vertices and edges
fgraph.setVertexCount(5)
fgraph.setEdgeCount(5)
# Initialise the vertices
vertices = graph.vertices()
for i in range(1, 6):
# Create (or fetch) vertex with ID i
vertex = vertices[i]
vertex.setPropertyPropertyArrayFloat("bar", [0, 10])
# Initialise the edges
edges = graph.edges()
for i in range(1, 6):
# Create (or fetch) edge with specified source/dest vertex IDs
edge = edges[i, ((i + 1)%5) + 1]
edge.setPropertyInt("foo", 12)

.. note:
If :func:`setVertexCount()<flamegpu::HostEnvironmentDirectedGraph::setVertexCount>` or :func:`setEdgeCount()<flamegpu::HostEnvironmentDirectedGraph::setEdgeCount>` is called, all data currently in the associated vertex/edge buffers will be lost.
.. _directed graph io:

Directed Graph File Input/Output
--------------------------------
:class:`HostEnvironmentDirectedGraph<flamegpu::HostEnvironmentDirectedGraph>` provides :func:`importGraph()<flamegpu::HostEnvironmentDirectedGraph::importGraph>` and :func:`exportGraph()<flamegpu::HostEnvironmentDirectedGraph::exportGraph>` to import and export the graph respectively using a common JSON format.

.. tabs::

.. code-tab:: cuda CUDA C++

// Define an host function called directed_graph_hostfn
FLAMEGPU_HOST_FUNCTION(directed_graph_hostfn) {
// Fetch a handle to the directed graph
HostEnvironmentDirectedGraph fgraph = FLAMEGPU->environment.getDirectedGraph("fgraph");
// Export the graph
fgraph.exportGraph("out.json");
// Import a different graph
fgraph.importGraph("in.json");
}

.. code-tab:: python

# Define an host function called directed_graph_hostfn
class directed_graph_hostfn(pyflamegpu.HostFunction):
def run(self,FLAMEGPU):
# Fetch a handle to the directed graph
fgraph = FLAMEGPU->environment.getDirectedGraph("fgraph")
# Export the graph
fgraph.exportGraph("out.json");
# Import a different graph
fgraph.importGraph("in.json");

An example of this format is shown below:

.. tabs::

.. code-tab:: json

{
"nodes": [
{
"id": "1",
"bar": [
12.0,
22.0
]
},
{
"id": "2",
"bar": [
13.0,
23.0
]
}
],
"links": [
{
"source": "1",
"target": "2",
"foo": 21
},
{
"source": "2",
"target": "1",
"foo": 22
}
]
}

.. note:
When importing a graph, if string IDs do not map directly to integers they will automatically be replaced and remapped.
Related Links
^^^^^^^^^^^^^

Expand Down
4 changes: 4 additions & 0 deletions src/guide/running-a-simulation/collecting-data.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ Collecting Data

After your simulation has completed, you probably want to get some data back. FLAME GPU provides two methods of achieving this: Logging which can reduce a large simulation state down to the most important data points, and export of the entire simulation state.

.. note::

It is not currently possible to export environment directed graphs via logging or JSON/XML state files. Environment directed graphs can only be imported and exported during host functions using :func:`importGraph()<flamegpu::HostEnvironmentDirectedGraph::importGraph>` and :func:`exportGraph()<flamegpu::HostEnvironmentDirectedGraph::exportGraph>`, which utilises a :ref:`common JSON graph storage format<directed graph io>`.

Logging
-------

Expand Down
4 changes: 4 additions & 0 deletions src/guide/running-a-simulation/initial-state.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ Overriding the Initial State

When executing a FLAME GPU model its common to wish to override parts of the initial environment or provide a predefined agent population.

.. note::

It is not currently possible to initialise environment directed graphs via :class:`RunPlan<flamegpu::RunPlan>` or JSON/XML state files. Environment directed graphs can only be imported and exported during host functions using :func:`importGraph()<flamegpu::HostEnvironmentDirectedGraph::importGraph>` and :func:`exportGraph()<flamegpu::HostEnvironmentDirectedGraph::exportGraph>`, which utilises a :ref:`common JSON graph storage format<directed graph io>`.

With Code
---------

Expand Down

0 comments on commit 38e529a

Please sign in to comment.