{ "cells": [ { "cell_type": "markdown", "metadata": { "id": "PdDwYHc1iDBB" }, "source": [ "# Welcome to the FLAME GPU 2 Tutorial!\n", "\n", "For the purpose of this tutorial, the Jupyter notebook has been configured to work with FLAME GPU 2. The Jupyter notebook is a web based environment for interactive computing. It is capable of running code in a wide variety of programming languages. The notebook consists of cells which can be of type code or markdown (by default, new cells are created as code cells). You can execute the content of code cells by clicking the Run button. Cells which are actively running code will show a 'Play' icon and cells which have completed will show a green tick.\n", "\n", "The notebook will guide you through the various commands required to run and edit a FLAME GPU 2 model using the python interface.\n", "\n", "## Before Beginning\n", "If using Google Colab, please ensure you have this notebook enabled with GPU before running (Runtime->Change runtime type). If you are not using Google Colab you can ignore this instruction.\n", "\n", "## Running your first code cell\n", "The cell below is a simple example of a code cell. Clicking on the play button which appears when hovering over the cell with the mouse will run the code and you should see \"Hello!\" displayed beneath the code cell." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "H_KmmXfRcRun" }, "outputs": [], "source": [ "print(\"Hello!\")" ] }, { "cell_type": "markdown", "metadata": { "id": "eOznL4ZFcbj5" }, "source": [ "# Installing FLAME GPU 2\n", "\n", "The python library can be installed through pip. This installs a binary release of the FLAME GPU 2 library. Run the cell below to install the FLAME GPU 2 library." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "xd8iL5XniTnb" }, "outputs": [], "source": [ "import importlib.util\n", "if importlib.util.find_spec('pyflamegpu') is None:\n", " import sys\n", " !{sys.executable} -m pip install --extra-index-url https://whl.flamegpu.com/whl/cuda112/ pyflamegpu==2.0.0rc0 # type: ignore\n", "\n", "# Import pyflamegpu and some other libraries we will use in the tutorial\n", "import pyflamegpu\n", "import sys, random, math\n", "import matplotlib.pyplot as plt\n" ] }, { "cell_type": "markdown", "metadata": { "id": "qa0JQvHTcxNV" }, "source": [ "With the library installed, we need to configure the CUDA_PATH environment variable so the library can find the CUDA installation." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "QLHTuacuQupp" }, "outputs": [], "source": [ "%env CUDA_PATH=/usr/local/cuda" ] }, { "cell_type": "markdown", "metadata": { "id": "vIXVvyz1dCQK" }, "source": [ "\n", "# The Predator-Prey Model\n", "## Model Description\n", "\n", "Predator-prey models capture the dynamic behaviours of competing species within an environment to demonstrate population dynamics as species try to survive. The model is important as it captures emergent macro level phenomenon which can be observed within other domains such as economics.\n", "\n", "In its most basic form, a two species system consists of both prey and predators. Prey reproduce but are eaten by predators and predators reproduce and eat prey. Typically such systems can be represented using non linear differential equations known as Lotka-Volterra equations. In a more advanced form of the model, microscopic behaviours of the species can be considered by using agent based representation. E.g. The predators can have energy which is depleted as they move. Prey can form cohesive groups (flocking) and dynamically avoid predators, while predators can move towards prey. If a predator and prey are close enough (i.e. within a fixed kill radius), the predator is able to eat the prey and increase the amount of energy it has. Predators die when they run out of energy.\n", "\n", "The model has been implemented in various Agent Based Modelling frameworks (e.g:NetLogo and FLAME). The tutorial model implements the following behaviours:\n", "\n", "* Predator and Prey agents move and act in a sequence of iterations (time steps).\n", "* Predator and Prey agents have real valued (x, y) positions and velocities which are integrated over time.\n", "* Predator agents have an initial velocity. A Predator adjusts its velocity to follow the nearest prey within a synthetic limited vision.\n", "* A predator’s energy is reduced by 1 unit of energy each time it moves.\n", "* Prey are eaten by the closest predator agent within a fixed (kill) radius.\n", "* A predator’s energy increases each time it catches a prey.\n", "* A predator dies if it has run out of energy.\n", "\n", "More specific details of the model features are described below.\n", "\n", "**Prey catching** There is no limit on the amount of prey a predator can eat per iteration. If a predator is close to multiple prey within the specific (kill) radius, the predator will kill/eat them all. A prey cannot be shared between multiple predators. If there are multiple predators within the kill radius then only the closest will eat the prey.\n", "\n", "**Reproduction** Each species has a rate of reproduction. When an agent reproduces, the parent’s energy is shared evenly between parent and child, so both will have half of the parent’s energy for the next iteration.\n", "\n", "\n", "# FLAME GPU 2 Implementation\n", "\n", "The FLAME GPU 2 implementation of the predator prey model is based on the assumptions made in the previous section. All agents have a real valued position (``x``, ``y``) within a continuous environment. The environment bounds are wrapped to form a continuous toroidal space in which agents can move. Agents have a velocity (``vx`` and ``vy``) in which they will move and a steering velocity (``steer_x`` and ``steer_y``) which dictates how they should adjust their velocity according to local interactions.Predator agents have a variable (``life``) which contains their level of energy. An agent variable, ``type``, is used for visualisations of the simulation where unique type values correspond to different colours within the visualisation. The type is set by the initial values of the agent and does not change over time.\n", "\n", "Each agent has a unique identifier variable (``id``). When a predator kills a prey, it is necessary to communicate identifiers to ensure correct behaviour as will be explained later in this section.\n", "\n", "All communication between agents in FLAME GPU 2 is performed indirectly via messages. In this model we have three message types which are required to perform the mechanism for killing. E.g,\n", "\n", "* Both prey and predators output their (``x``, ``y``) coordinates to a prey or predator location message respectively.\n", "* Prey read predator locations and perform evasive behaviour to avoid them.\n", "* Predators read prey locations and move towards the closest visible prey.\n", "* Prey read each others location and move towards each other to form cohesive groups.\n", "* Both predators and prey read their own species' locations to avoid each other when in close proximity.\n", "* Prey read the locations of predators and calculate the nearest predator within the kill distance. If a predator is within the kill distance then it notifies this predator by sending a ``prey_eaten_message`` message with the predator's id and the eaten prey dies (is removed for the simulation).\n", "* Predators read the ``prey_eaten_message`` messages and check the to see if their id has been communicated by any dead prey. If the message indicates that the predator ate some prey, then the predator increases its energy/life accordingly.\n", "\n", "# The FLAME GPU 2 Model\n", "\n", "A FLAME GPU 2 model specification consists of: an **environment** which holds global information related to the simulation such as constant variables and behaviour script file names, **agents** which describe use variables and functions to describe the properties and behaviours of individuals in the simulation, and **messages** which represent information communicated between agents. Each aspect is introduced in the following subsections with examples. If you are confident of the model's behaviour and the FLAMEGPU syntax then feel free to move onto the hands on material.\n", "\n", "In the python build of FLAME GPU 2, all of these model elements are described through the python interface. A `ModelDescription` object acts as a container for the various components which make up the model. The following sections will describe how to create each of the above components and attach them to the `ModelDescription`. Each code cell defines a particular function. By defining functions rather than running the code directly, they can be updated individually and defined out of order. These functions are then all called in the correct order in the run simulation cell later in the document.\n", "\n", "\n" ] }, { "cell_type": "markdown", "metadata": { "id": "PShc0LdDTOQW" }, "source": [ "# Create a Model\n", "\n", "\n", "\n" ] }, { "cell_type": "markdown", "metadata": { "id": "NI9DGZnHP7by" }, "source": [ "The [`ModelDescription`](https://docs.flamegpu.com/guide/2-model-definition/1-model.html#creating-a-modeldescription-object) is created with the name \"python-tutorial\". Individual functions are used to wrap the creation of each of the various elements of the model." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "vGjWrGnZTQUK" }, "outputs": [], "source": [ "def create_model():\n", " model = pyflamegpu.ModelDescription(\"python-tutorial\")\n", " return model" ] }, { "cell_type": "markdown", "metadata": { "id": "vJFHyKx9ToNa" }, "source": [ "# Define Environmental Properties\n", "\n", "For the Predator-Prey model a number of self explanatory model parameters are described within the environment section. The first line of the code below queries the model to allow us to modify its environment through the [`EnvironmentDescription`](https://docs.flamegpu.com/guide/2-model-definition/2-environment.html) object, env. Environmental parameters can be added by name and type and are given a default value. These default values may be overwritten by an optional XML or JSON input file describing the initial state of the simulation. This allows modification of model parameters without recompiling the code." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "EYlVTM42Tqwq" }, "outputs": [], "source": [ "def define_environment(model):\n", " \"\"\"\n", " Environment\n", " \"\"\"\n", " env = model.Environment()\n", "\n", " # Reproduction\n", " env.newPropertyFloat(\"REPRODUCE_PREY_PROB\", 0.05)\n", " env.newPropertyFloat(\"REPRODUCE_PRED_PROB\", 0.03)\n", "\n", " # Cohesion/Avoidance\n", " env.newPropertyFloat(\"SAME_SPECIES_AVOIDANCE_RADIUS\", 0.035)\n", " env.newPropertyFloat(\"PREY_GROUP_COHESION_RADIUS\", 0.2)\n", "\n", " # Predator/Prey/Grass interaction\n", " env.newPropertyFloat(\"PRED_PREY_INTERACTION_RADIUS\", 0.3)\n", " env.newPropertyFloat(\"PRED_SPEED_ADVANTAGE\", 3.0)\n", " env.newPropertyFloat(\"PRED_KILL_DISTANCE\", 0.05)\n", " env.newPropertyFloat(\"GRASS_EAT_DISTANCE\", 0.02)\n", " env.newPropertyUInt(\"GAIN_FROM_FOOD_PREY\", 80)\n", " env.newPropertyUInt(\"GAIN_FROM_FOOD_PREDATOR\", 100)\n", " env.newPropertyUInt(\"GRASS_REGROW_CYCLES\", 100)\n", "\n", " # Simulation properties\n", " env.newPropertyFloat(\"DELTA_TIME\", 0.001)\n", " env.newPropertyFloat(\"BOUNDS_WIDTH\", 2.0)\n", " env.newPropertyFloat(\"MIN_POSITION\", -1.0)\n", " env.newPropertyFloat(\"MAX_POSITION\", 1.0)" ] }, { "cell_type": "markdown", "metadata": { "id": "DaW5LRKnTUoM" }, "source": [ "# Define Messages" ] }, { "cell_type": "markdown", "metadata": { "id": "idPjmsiZQgq4" }, "source": [ "[Messages](https://docs.flamegpu.com/guide/3-behaviour-definition/4-agent-communication.html) are simple collections of variables which are used to communicate data between agents. Messages are defined in the same way as the environment variables and agents, however, messages also require a choice of spatial partitioning scheme. These schemes are typically used to improve computation time by limiting the messages each agent sees to those which are nearby. Spatial partitioning is simple to use in FLAME GPU 2, although we will use the simplest messaging type, MessageBruteForce which features no partitioning. This indicates that the message reading functions will iterate all messages in the list. The code below shows the definition of a brute force message which can be used to communicate the location of a prey agent:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "FiBghMU9TXKd" }, "outputs": [], "source": [ "def define_messages(model):\n", " \"\"\"\n", " Location messages\n", " \"\"\"\n", " message = model.newMessageBruteForce(\"predator_location_message\")\n", " message.newVariableID(\"id\")\n", " message.newVariableFloat(\"x\")\n", " message.newVariableFloat(\"y\")\n", "\n", " message = model.newMessageBruteForce(\"prey_location_message\")\n", " message.newVariableID(\"id\")\n", " message.newVariableFloat(\"x\")\n", " message.newVariableFloat(\"y\")\n", "\n", "\n", " \"\"\"\n", " Agent eaten messages\n", " \"\"\"\n", "\n", " message = model.newMessageBruteForce(\"prey_eaten_message\")\n", " message.newVariableID(\"id\")\n", " message.newVariableInt(\"pred_id\")\n", "\n", " message = model.newMessageBruteForce(\"grass_eaten_message\")\n", " message.newVariableID(\"id\")\n", " message.newVariableInt(\"prey_id\")" ] }, { "cell_type": "markdown", "metadata": { "id": "wJZB5mNYTdqp" }, "source": [ "# Define Agents" ] }, { "cell_type": "markdown", "metadata": { "id": "fDbOHOsaRQNi" }, "source": [ "**Agent Variables**\n", "\n", "[Agents](https://docs.flamegpu.com/guide/2-model-definition/3-agent.html) represent individuals in the simulation and are defined using a `AgentDescription`. Much like the environment, we ask the model to provide us with a reference to the new agent which we can then add variables to. As with the environment, each variable is given a type and a name.\n", "\n", "**Specifying Agent Functions**\n", "\n", "Agent functions are used to implement agent behaviours. The tables below give the functions of the predators and prey and their roles.\n", "\n", "| Prey Function | Description |\n", "|:---|:---|\n", "|prey_output_location | each prey agent outputs information to be read by other agents|\n", "|prey_avoid_pred |\tprey agents avoiding predator agents|\n", "|prey_flock | flocking between prey agents|\n", "|prey_move | movement of prey agents|\n", "|prey_eaten | prey agents get killed by predator agents|\n", "|prey_reproduction | regeneration of prey|\n", "\n", "\n", "| Predator Function | Description |\n", "|:---|:---|\n", "|pred_output_location | each predator agent outputs information to be read by other agents|\n", "|pred_follow_prey | predator agents follow prey agents|\n", "|pred_avoid | predator agents avoid each other|\n", "|pred_move | movement of predator agents|\n", "|pred_eat_or_starve | predator agents gain energy or starve|\n", "|prey_reproduction | regeneration of predators|\n", "\n", "Functions can be applied to agents in a specified *initial_state* and can optionally cause agents to move to other *states*. As our model is relatively simple, we only use a single default state. Furthermore, functions can have *conditions*. For example, a function which simulates agent death might check a variable incremented each iteration called life-cycles to see if it has reached a maximum number. Only agents meeting this condition would perform the behaviour and move into a dead state. The predator-prey model is a stateless model (each agent has only a single state) as functions do not require any function conditions.\n", "\n", "Functions can have at most one input and one output. Inputs and outputs are in the form of messages which, are a collection of variables which are persistent until the end of the simulation iteration (at which point they are destroyed). A function description within a model requires that any inputs or outputs are fully specified.\n", "\n", "The descriptions of the predator, prey and grass agents' variables and functions in FLAME GPU 2 are given below:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "46ccjW3DTfZH" }, "outputs": [], "source": [ "def define_agents(model):\n", " \"\"\"\n", " Prey agent\n", " \"\"\"\n", " # Create the agent\n", " agent = model.newAgent(\"prey\")\n", "\n", " # Assign its variables\n", " agent.newVariableFloat(\"x\")\n", " agent.newVariableFloat(\"y\")\n", " agent.newVariableFloat(\"vx\")\n", " agent.newVariableFloat(\"vy\")\n", " agent.newVariableFloat(\"steer_x\")\n", " agent.newVariableFloat(\"steer_y\")\n", " agent.newVariableInt(\"life\")\n", " agent.newVariableFloat(\"type\")\n", "\n", " # Assign its functions\n", " fn = agent.newRTCFunction(\"prey_output_location\", prey_output_location)\n", " fn.setMessageOutput(\"prey_location_message\")\n", "\n", " fn = agent.newRTCFunction(\"prey_avoid_pred\", prey_avoid_pred)\n", " fn.setMessageInput(\"predator_location_message\")\n", "\n", " fn = agent.newRTCFunction(\"prey_flock\", prey_flock)\n", " fn.setMessageInput(\"prey_location_message\")\n", "\n", " fn = agent.newRTCFunction(\"prey_move\", prey_move)\n", "\n", " fn = agent.newRTCFunction(\"prey_eaten\", prey_eaten)\n", " fn.setMessageInput(\"predator_location_message\")\n", " fn.setMessageOutput(\"prey_eaten_message\")\n", " fn.setMessageOutputOptional(True)\n", " fn.setAllowAgentDeath(True)\n", "\n", " #fn = agent.newRTCFunction(\"prey_eat_or_starve\", prey_eat_or_starve)\n", " #fn.setMessageInput(\"grass_eaten_message\")\n", " #fn.setAllowAgentDeath(True)\n", "\n", " fn = agent.newRTCFunction(\"prey_reproduction\", prey_reproduction)\n", " fn.setAgentOutput(\"prey\", \"default\")\n", "\n", " \"\"\"\n", " Predator agent\n", " \"\"\"\n", " # Create the agent\n", " agent = model.newAgent(\"predator\")\n", "\n", " # Assign its variables\n", " agent.newVariableFloat(\"x\")\n", " agent.newVariableFloat(\"y\")\n", " agent.newVariableFloat(\"vx\")\n", " agent.newVariableFloat(\"vy\")\n", " agent.newVariableFloat(\"steer_x\")\n", " agent.newVariableFloat(\"steer_y\")\n", " agent.newVariableInt(\"life\")\n", " agent.newVariableFloat(\"type\")\n", "\n", " # Assign its functions\n", " fn = agent.newRTCFunction(\"pred_output_location\", pred_output_location)\n", " fn.setMessageOutput(\"predator_location_message\")\n", " fn = agent.newRTCFunction(\"pred_follow_prey\", pred_follow_prey)\n", " fn.setMessageInput(\"prey_location_message\")\n", " fn = agent.newRTCFunction(\"pred_avoid\", pred_avoid)\n", " fn.setMessageInput(\"predator_location_message\")\n", " fn = agent.newRTCFunction(\"pred_move\", pred_move)\n", " fn = agent.newRTCFunction(\"pred_eat_or_starve\", pred_eat_or_starve)\n", " fn.setMessageInput(\"prey_eaten_message\")\n", " fn.setAllowAgentDeath(True)\n", " fn = agent.newRTCFunction(\"pred_reproduction\", pred_reproduction)\n", " fn.setAgentOutput(\"predator\", \"default\")\n", "\n", "" ] }, { "cell_type": "markdown", "metadata": { "id": "Evq-lX-QTFVT" }, "source": [ "# Define Agent Function Behaviours" ] }, { "cell_type": "markdown", "metadata": { "id": "a7xqiPy_b8Cz" }, "source": [ "The agent function behaviours must be defined using a small subset of the CUDA C++ programming language. A brief summary of the language is given below:\n", "\n", "| Action | Code format |\n", "| :--- | :--- |\n", "| Write a comment | `// This will be a comment` |\n", "| Declare a variable | `type variable_name = value;` |\n", "| Declare a constant | `const type variable_name = value;` |\n", "| Get the value of an environment property| `FLAMEGPU->environment.getProperty(\"property_name\");` |\n", "| Iterate & access input messages | `for (const auto& msg : FLAMEGPU->message_in) { `
   `// Each message is now accessible inside this loop via msg`
`}`\n", "| Read an input message variable value | `msg.getVariable(\"variable_name\");` |\n", "| Set an output message variable value | `FLAMEGPU->message_out.setVariable(\"variable_name\", value_to_set_variable_to);` |\n", "| Generate a random float in the range [0,1) | `FLAMEGPU->random.uniform()` |\n", "| Function leaves agent alive | `return flamegpu::ALIVE` |\n", "| Function kills agent | `return flamegpu::DEAD` |\n", "\n", "We recognise that this is a python tutorial, so if you are having any trouble at all with the agent function syntax please do feel free to ask for help!\n", "\n", "It is not important to read every single agent function definition below, just try to familiarise yourself with their structure a bit as we will be writing some of these later in the tutorial!" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "UX7EG8mXSz1b" }, "outputs": [], "source": [ "\"\"\"\n", " outputdata agent function for Boid agents, which outputs publicly visible properties to a message list\n", "\"\"\"\n", "pred_output_location = r\"\"\"\n", "FLAMEGPU_AGENT_FUNCTION(pred_output_location, flamegpu::MessageNone, flamegpu::MessageBruteForce) {\n", " const flamegpu::id_t id = FLAMEGPU->getID();\n", " const float x = FLAMEGPU->getVariable(\"x\");\n", " const float y = FLAMEGPU->getVariable(\"y\");\n", " FLAMEGPU->message_out.setVariable(\"id\", id);\n", " FLAMEGPU->message_out.setVariable(\"x\", x);\n", " FLAMEGPU->message_out.setVariable(\"y\", y);\n", "\n", " return flamegpu::ALIVE;\n", "}\n", "\"\"\"\n", "\n", "\"\"\"\n", " inputdata agent function for Boid agents, which reads data from neighbouring Boid agents, to perform the boid flocking model.\n", "\"\"\"\n", "pred_follow_prey = r\"\"\"\n", "FLAMEGPU_AGENT_FUNCTION(pred_follow_prey, flamegpu::MessageBruteForce, flamegpu::MessageNone) {\n", " const float PRED_PREY_INTERACTION_RADIUS = FLAMEGPU->environment.getProperty(\"PRED_PREY_INTERACTION_RADIUS\");\n", " // Fetch the predator's position\n", " const float predator_x = FLAMEGPU->getVariable(\"x\");\n", " const float predator_y = FLAMEGPU->getVariable(\"y\");\n", "\n", " // Find the closest prey by iterating the prey_location messages\n", " float closest_prey_x = 0.0f;\n", " float closest_prey_y = 0.0f;\n", " float closest_prey_distance = PRED_PREY_INTERACTION_RADIUS;\n", " int is_a_prey_in_range = 0;\n", "\n", " for (const auto& msg : FLAMEGPU->message_in) {\n", " // Fetch prey location\n", " const float prey_x = msg.getVariable(\"x\");\n", " const float prey_y = msg.getVariable(\"y\");\n", "\n", " // Check if prey is within sight range of predator\n", " const float dx = predator_x - prey_x;\n", " const float dy = predator_y - prey_y;\n", " const float separation = sqrt(dx * dx + dy * dy);\n", "\n", " if (separation < closest_prey_distance) {\n", " closest_prey_x = prey_x;\n", " closest_prey_y = prey_y;\n", " closest_prey_distance = separation;\n", " is_a_prey_in_range = 1;\n", " }\n", " }\n", "\n", " // If there was a prey in range, steer the predator towards it\n", " if (is_a_prey_in_range) {\n", " const float steer_x = closest_prey_x - predator_x;\n", " const float steer_y = closest_prey_y - predator_y;\n", " FLAMEGPU->setVariable(\"steer_x\", steer_x);\n", " FLAMEGPU->setVariable(\"steer_y\", steer_y);\n", " }\n", "\n", " return flamegpu::ALIVE;\n", "}\n", "\"\"\"\n", "\n", "pred_avoid = r\"\"\"\n", "FLAMEGPU_AGENT_FUNCTION(pred_avoid, flamegpu::MessageBruteForce, flamegpu::MessageNone) {\n", " const float SAME_SPECIES_AVOIDANCE_RADIUS = FLAMEGPU->environment.getProperty(\"SAME_SPECIES_AVOIDANCE_RADIUS\");\n", " // Fetch this predator's position\n", " const float predator_x = FLAMEGPU->getVariable(\"x\");\n", " const float predator_y = FLAMEGPU->getVariable(\"y\");\n", " float avoid_velocity_x = 0.0f;\n", " float avoid_velocity_y = 0.0f;\n", "\n", " // Add a steering factor away from each other predator. Strength increases with closeness.\n", " for (const auto& msg : FLAMEGPU->message_in) {\n", " // Fetch location of other predator\n", " const float other_predator_x = msg.getVariable(\"x\");\n", " const float other_predator_y = msg.getVariable(\"y\");\n", "\n", " // Check if the two predators are within interaction radius\n", " const float dx = predator_x - other_predator_x;\n", " const float dy = predator_y - other_predator_y;\n", " const float separation = sqrt(dx * dx + dy * dy);\n", "\n", " if (separation < SAME_SPECIES_AVOIDANCE_RADIUS && separation > 0.0f) {\n", " avoid_velocity_x += SAME_SPECIES_AVOIDANCE_RADIUS / separation * dx;\n", " avoid_velocity_y += SAME_SPECIES_AVOIDANCE_RADIUS / separation * dy;\n", " }\n", " }\n", "\n", " float steer_x = FLAMEGPU->getVariable(\"steer_x\");\n", " float steer_y = FLAMEGPU->getVariable(\"steer_y\");\n", " steer_x += avoid_velocity_x;\n", " steer_y += avoid_velocity_y;\n", " FLAMEGPU->setVariable(\"steer_x\", steer_x);\n", " FLAMEGPU->setVariable(\"steer_y\", steer_y);\n", "\n", " return flamegpu::ALIVE;\n", "}\n", "\"\"\"\n", "\n", "pred_move = r\"\"\"\n", "FLAMEGPU_AGENT_FUNCTION(pred_move, flamegpu::MessageNone, flamegpu::MessageNone) {\n", " const float MIN_POSITION = FLAMEGPU->environment.getProperty(\"MIN_POSITION\");\n", " const float MAX_POSITION = FLAMEGPU->environment.getProperty(\"MAX_POSITION\");\n", " const float DELTA_TIME = FLAMEGPU->environment.getProperty(\"DELTA_TIME\");\n", " const float PRED_SPEED_ADVANTAGE = FLAMEGPU->environment.getProperty(\"PRED_SPEED_ADVANTAGE\");\n", " float predator_x = FLAMEGPU->getVariable(\"x\");\n", " float predator_y = FLAMEGPU->getVariable(\"y\");\n", " float predator_vx = FLAMEGPU->getVariable(\"vx\");\n", " float predator_vy = FLAMEGPU->getVariable(\"vy\");\n", " const float predator_steer_x = FLAMEGPU->getVariable(\"steer_x\");\n", " const float predator_steer_y = FLAMEGPU->getVariable(\"steer_y\");\n", " const float predator_life = FLAMEGPU->getVariable(\"life\");\n", "\n", " // Integrate steering forces and cap velocity\n", " predator_vx += predator_steer_x;\n", " predator_vy += predator_steer_y;\n", "\n", " float speed = sqrt(predator_vx * predator_vx + predator_vy * predator_vy);\n", " if (speed > 1.0f) {\n", " predator_vx /= speed;\n", " predator_vy /= speed;\n", " }\n", "\n", " // Integrate velocity\n", " predator_x += predator_vx * DELTA_TIME * PRED_SPEED_ADVANTAGE;\n", " predator_y += predator_vy * DELTA_TIME * PRED_SPEED_ADVANTAGE;\n", "\n", " // Bound the position within the environment\n", " predator_x = predator_x < MIN_POSITION ? MIN_POSITION : predator_x;\n", " predator_x = predator_x > MAX_POSITION ? MAX_POSITION : predator_x;\n", " predator_y = predator_y < MIN_POSITION ? MIN_POSITION : predator_y;\n", " predator_y = predator_y > MAX_POSITION ? MAX_POSITION : predator_y;\n", "\n", " // Update agent state\n", " FLAMEGPU->setVariable(\"x\", predator_x);\n", " FLAMEGPU->setVariable(\"y\", predator_y);\n", " FLAMEGPU->setVariable(\"vx\", predator_vx);\n", " FLAMEGPU->setVariable(\"vy\", predator_vy);\n", "\n", " // Reduce life by one unit of energy\n", " FLAMEGPU->setVariable(\"life\", predator_life - 1);\n", "\n", " return flamegpu::ALIVE;\n", "}\n", "\"\"\"\n", "\n", "pred_eat_or_starve = r\"\"\"\n", "FLAMEGPU_AGENT_FUNCTION(pred_eat_or_starve, flamegpu::MessageBruteForce, flamegpu::MessageNone) {\n", " const flamegpu::id_t predator_id = FLAMEGPU->getID();\n", " int predator_life = FLAMEGPU->getVariable(\"life\");\n", " int isDead = 0;\n", "\n", " // Iterate prey_eaten messages to see if this predator ate a prey\n", " for (const auto& msg : FLAMEGPU->message_in) {\n", " if (msg.getVariable(\"pred_id\") == predator_id) {\n", " predator_life += FLAMEGPU->environment.getProperty(\"GAIN_FROM_FOOD_PREDATOR\");\n", " }\n", " }\n", "\n", " // Update agent state\n", " FLAMEGPU->setVariable(\"life\", predator_life);\n", "\n", " // Did the predator starve?\n", " if (predator_life < 1) {\n", " isDead = 1;\n", " }\n", "\n", " return isDead ? flamegpu::DEAD : flamegpu::ALIVE;\n", "}\n", "\"\"\"\n", "\n", "pred_reproduction = r\"\"\"\n", "FLAMEGPU_AGENT_FUNCTION(pred_reproduction, flamegpu::MessageNone, flamegpu::MessageNone) {\n", " const float BOUNDS_WIDTH = FLAMEGPU->environment.getProperty(\"BOUNDS_WIDTH\");\n", " float random = FLAMEGPU->random.uniform();\n", " const int currentLife = FLAMEGPU->getVariable(\"life\");\n", " if (random < FLAMEGPU->environment.getProperty(\"REPRODUCE_PRED_PROB\")) {\n", " int id = FLAMEGPU->random.uniform() * (float)INT_MAX;\n", " float x = FLAMEGPU->random.uniform() * BOUNDS_WIDTH - BOUNDS_WIDTH / 2.0f;\n", " float y = FLAMEGPU->random.uniform() * BOUNDS_WIDTH - BOUNDS_WIDTH / 2.0f;\n", " float vx = FLAMEGPU->random.uniform() * 2 - 1;\n", " float vy = FLAMEGPU->random.uniform() * 2 - 1;\n", "\n", " FLAMEGPU->setVariable(\"life\", currentLife / 2);\n", "\n", " FLAMEGPU->agent_out.setVariable(\"x\", x);\n", " FLAMEGPU->agent_out.setVariable(\"y\", y);\n", " FLAMEGPU->agent_out.setVariable(\"type\", 0.0f);\n", " FLAMEGPU->agent_out.setVariable(\"vx\", vx);\n", " FLAMEGPU->agent_out.setVariable(\"vy\", vy);\n", " FLAMEGPU->agent_out.setVariable(\"steer_x\", 0.0f);\n", " FLAMEGPU->agent_out.setVariable(\"steer_y\", 0.0f);\n", " FLAMEGPU->agent_out.setVariable(\"life\", currentLife / 2);\n", "\n", " }\n", " return flamegpu::ALIVE;\n", "}\n", "\"\"\"\n", "\n", "prey_output_location = r\"\"\"\n", "FLAMEGPU_AGENT_FUNCTION(prey_output_location, flamegpu::MessageNone, flamegpu::MessageBruteForce) {\n", " const flamegpu::id_t id = FLAMEGPU->getID();\n", " const float x = FLAMEGPU->getVariable(\"x\");\n", " const float y = FLAMEGPU->getVariable(\"y\");\n", " FLAMEGPU->message_out.setVariable(\"id\", id);\n", " FLAMEGPU->message_out.setVariable(\"x\", x);\n", " FLAMEGPU->message_out.setVariable(\"y\", y);\n", " return flamegpu::ALIVE;\n", "}\n", "\"\"\"\n", "\n", "prey_avoid_pred = r\"\"\"\n", "FLAMEGPU_AGENT_FUNCTION(prey_avoid_pred, flamegpu::MessageBruteForce, flamegpu::MessageNone) {\n", " const float PRED_PREY_INTERACTION_RADIUS = FLAMEGPU->environment.getProperty(\"PRED_PREY_INTERACTION_RADIUS\");\n", " // Fetch this prey's position\n", " const float prey_x = FLAMEGPU->getVariable(\"x\");\n", " const float prey_y = FLAMEGPU->getVariable(\"y\");\n", " float avoid_velocity_x = 0.0f;\n", " float avoid_velocity_y = 0.0f;\n", "\n", " // Add a steering factor away from each predator. Strength increases with closeness.\n", " for (const auto& msg : FLAMEGPU->message_in) {\n", " // Fetch location of predator\n", " const float predator_x = msg.getVariable(\"x\");\n", " const float predator_y = msg.getVariable(\"y\");\n", "\n", " // Check if the two predators are within interaction radius\n", " const float dx = prey_x - predator_x;\n", " const float dy = prey_y - predator_y;\n", " const float distance = sqrt(dx * dx + dy * dy);\n", "\n", " if (distance < PRED_PREY_INTERACTION_RADIUS) {\n", " // Steer the prey away from the predator\n", " avoid_velocity_x += (PRED_PREY_INTERACTION_RADIUS / distance) * dx;\n", " avoid_velocity_y += (PRED_PREY_INTERACTION_RADIUS / distance) * dy;\n", " }\n", " }\n", "\n", " // Update agent state\n", " FLAMEGPU->setVariable(\"steer_x\", avoid_velocity_x);\n", " FLAMEGPU->setVariable(\"steer_y\", avoid_velocity_y);\n", "\n", " return flamegpu::ALIVE;\n", "}\n", "\"\"\"\n", "\n", "prey_flock = r\"\"\"\n", "FLAMEGPU_AGENT_FUNCTION(prey_flock, flamegpu::MessageBruteForce, flamegpu::MessageNone) {\n", " const float PREY_GROUP_COHESION_RADIUS = FLAMEGPU->environment.getProperty(\"PREY_GROUP_COHESION_RADIUS\");\n", " const float SAME_SPECIES_AVOIDANCE_RADIUS = FLAMEGPU->environment.getProperty(\"SAME_SPECIES_AVOIDANCE_RADIUS\");\n", " const flamegpu::id_t prey_id = FLAMEGPU->getID();\n", " const float prey_x = FLAMEGPU->getVariable(\"x\");\n", " const float prey_y = FLAMEGPU->getVariable(\"y\");\n", "\n", " float group_centre_x = 0.0f;\n", " float group_centre_y = 0.0f;\n", " float group_velocity_x = 0.0f;\n", " float group_velocity_y = 0.0f;\n", " float avoid_velocity_x = 0.0f;\n", " float avoid_velocity_y = 0.0f;\n", " int group_centre_count = 0;\n", "\n", " for (const auto& msg : FLAMEGPU->message_in) {\n", " const int other_prey_id = msg.getVariable(\"id\");\n", " const float other_prey_x = msg.getVariable(\"x\");\n", " const float other_prey_y = msg.getVariable(\"y\");\n", " const float dx = prey_x - other_prey_x;\n", " const float dy = prey_y - other_prey_y;\n", " const float separation = sqrt(dx * dx + dy * dy);\n", "\n", " if (separation < PREY_GROUP_COHESION_RADIUS && prey_id != other_prey_id) {\n", " group_centre_x += other_prey_x;\n", " group_centre_y += other_prey_y;\n", " group_centre_count += 1;\n", "\n", " // Avoidance behaviour\n", " if (separation < SAME_SPECIES_AVOIDANCE_RADIUS) {\n", " // Was a check for separation > 0 in original - redundant?\n", " avoid_velocity_x += SAME_SPECIES_AVOIDANCE_RADIUS / separation * dx;\n", " avoid_velocity_y += SAME_SPECIES_AVOIDANCE_RADIUS / separation * dy;\n", " }\n", " }\n", " }\n", "\n", " // Compute group centre as the average of the nearby prey positions and a velocity to move towards the group centre\n", " if (group_centre_count > 0) {\n", " group_centre_x /= group_centre_count;\n", " group_centre_y /= group_centre_count;\n", " group_velocity_x = group_centre_x - prey_x;\n", " group_velocity_y = group_centre_y - prey_y;\n", " }\n", "\n", " float prey_steer_x = FLAMEGPU->getVariable(\"steer_x\");\n", " float prey_steer_y = FLAMEGPU->getVariable(\"steer_y\");\n", " prey_steer_x += group_velocity_x + avoid_velocity_x;\n", " prey_steer_y += group_velocity_y + avoid_velocity_y;\n", " FLAMEGPU->setVariable(\"steer_x\", prey_steer_x);\n", " FLAMEGPU->setVariable(\"steer_y\", prey_steer_y);\n", "\n", " return flamegpu::ALIVE;\n", "}\n", "\"\"\"\n", "\n", "prey_move = r\"\"\"\n", "FLAMEGPU_AGENT_FUNCTION(prey_move, flamegpu::MessageNone, flamegpu::MessageNone) {\n", " const float MIN_POSITION = FLAMEGPU->environment.getProperty(\"MIN_POSITION\");\n", " const float MAX_POSITION = FLAMEGPU->environment.getProperty(\"MAX_POSITION\");\n", " const float DELTA_TIME = FLAMEGPU->environment.getProperty(\"DELTA_TIME\");\n", " float prey_x = FLAMEGPU->getVariable(\"x\");\n", " float prey_y = FLAMEGPU->getVariable(\"y\");\n", " float prey_vx = FLAMEGPU->getVariable(\"vx\");\n", " float prey_vy = FLAMEGPU->getVariable(\"vy\");\n", " const float prey_steer_x = FLAMEGPU->getVariable(\"steer_x\");\n", " const float prey_steer_y = FLAMEGPU->getVariable(\"steer_y\");\n", " const float prey_life = FLAMEGPU->getVariable(\"life\");\n", "\n", " // Integrate steering forces and cap velocity\n", " prey_vx += prey_steer_x;\n", " prey_vy += prey_steer_y;\n", "\n", " float speed = sqrt(prey_vx * prey_vx + prey_vy * prey_vy);\n", " if (speed > 1.0f) {\n", " prey_vx /= speed;\n", " prey_vy /= speed;\n", " }\n", "\n", " // Integrate velocity\n", " prey_x += prey_vx * DELTA_TIME;\n", " prey_y += prey_vy * DELTA_TIME;\n", "\n", " // Bound the position within the environment - can this be moved\n", " prey_x = prey_x < MIN_POSITION ? MIN_POSITION : prey_x;\n", " prey_x = prey_x > MAX_POSITION ? MAX_POSITION : prey_x;\n", " prey_y = prey_y < MIN_POSITION ? MIN_POSITION : prey_y;\n", " prey_y = prey_y > MAX_POSITION ? MAX_POSITION : prey_y;\n", "\n", "\n", " // Update agent state\n", " FLAMEGPU->setVariable(\"x\", prey_x);\n", " FLAMEGPU->setVariable(\"y\", prey_y);\n", " FLAMEGPU->setVariable(\"vx\", prey_vx);\n", " FLAMEGPU->setVariable(\"vy\", prey_vy);\n", "\n", " // Reduce life by one unit of energy\n", " FLAMEGPU->setVariable(\"life\", prey_life - 1);\n", "\n", " return flamegpu::ALIVE;\n", "}\n", "\"\"\"\n", "\n", "prey_eaten = r\"\"\"\n", "FLAMEGPU_AGENT_FUNCTION(prey_eaten, flamegpu::MessageBruteForce, flamegpu::MessageBruteForce) {\n", " const float PRED_KILL_DISTANCE = FLAMEGPU->environment.getProperty(\"PRED_KILL_DISTANCE\");\n", " const flamegpu::id_t id = FLAMEGPU->getID();\n", " int eaten = 0;\n", " int predator_id = -1;\n", " float closest_pred = PRED_KILL_DISTANCE;\n", " const float prey_x = FLAMEGPU->getVariable(\"x\");\n", " const float prey_y = FLAMEGPU->getVariable(\"y\");\n", "\n", " // Iterate predator_location messages to find the closest predator\n", " for (const auto& msg : FLAMEGPU->message_in) {\n", " // Fetch location of predator\n", " const float predator_x = msg.getVariable(\"x\");\n", " const float predator_y = msg.getVariable(\"y\");\n", "\n", " // Check if the two predators are within interaction radius\n", " const float dx = prey_x - predator_x;\n", " const float dy = prey_y - predator_y;\n", " const float distance = sqrt(dx * dx + dy * dy);\n", "\n", " if (distance < closest_pred) {\n", " predator_id = msg.getVariable(\"id\");\n", " closest_pred = distance;\n", " eaten = 1;\n", " }\n", " }\n", "\n", " if (eaten) {\n", " FLAMEGPU->message_out.setVariable(\"id\", id);\n", " FLAMEGPU->message_out.setVariable(\"pred_id\", predator_id);\n", " }\n", "\n", " return eaten ? flamegpu::DEAD : flamegpu::ALIVE;\n", "}\n", "\"\"\"\n", "\n", "prey_reproduction = r\"\"\"\n", "FLAMEGPU_AGENT_FUNCTION(prey_reproduction, flamegpu::MessageNone, flamegpu::MessageNone) {\n", " const float REPRODUCE_PREY_PROB = FLAMEGPU->environment.getProperty(\"REPRODUCE_PREY_PROB\");\n", " const float BOUNDS_WIDTH = FLAMEGPU->environment.getProperty(\"BOUNDS_WIDTH\");\n", " float random = FLAMEGPU->random.uniform();\n", " const int currentLife = FLAMEGPU->getVariable(\"life\");\n", " if (random < FLAMEGPU->environment.getProperty(\"REPRODUCE_PREY_PROB\")) {\n", " int id = FLAMEGPU->random.uniform() * (float)INT_MAX;\n", " float x = FLAMEGPU->random.uniform() * BOUNDS_WIDTH - BOUNDS_WIDTH / 2.0f;\n", " float y = FLAMEGPU->random.uniform() * BOUNDS_WIDTH - BOUNDS_WIDTH / 2.0f;\n", " float vx = FLAMEGPU->random.uniform() * 2 - 1;\n", " float vy = FLAMEGPU->random.uniform() * 2 - 1;\n", "\n", " FLAMEGPU->setVariable(\"life\", currentLife / 2);\n", "\n", " FLAMEGPU->agent_out.setVariable(\"x\", x);\n", " FLAMEGPU->agent_out.setVariable(\"y\", y);\n", " FLAMEGPU->agent_out.setVariable(\"type\", 1.0f);\n", " FLAMEGPU->agent_out.setVariable(\"vx\", vx);\n", " FLAMEGPU->agent_out.setVariable(\"vy\", vy);\n", " FLAMEGPU->agent_out.setVariable(\"steer_x\", 0.0f);\n", " FLAMEGPU->agent_out.setVariable(\"steer_y\", 0.0f);\n", " FLAMEGPU->agent_out.setVariable(\"life\", currentLife / 2);\n", "\n", " }\n", " return flamegpu::ALIVE;\n", "}\n", "\"\"\"\n", "\n", "grass_output_location = r\"\"\"\n", "FLAMEGPU_AGENT_FUNCTION(grass_output_location, flamegpu::MessageNone, flamegpu::MessageBruteForce) {\n", " // Exercise 3.1 : Set the variables for the grass_location message\n", " const flamegpu::id_t id = FLAMEGPU->getID();\n", " const float x = FLAMEGPU->getVariable(\"x\");\n", " const float y = FLAMEGPU->getVariable(\"y\");\n", " FLAMEGPU->message_out.setVariable(\"id\", id);\n", " FLAMEGPU->message_out.setVariable(\"x\", x);\n", " FLAMEGPU->message_out.setVariable(\"y\", y);\n", " return flamegpu::ALIVE;\n", "}\n", "\n", "\n", "\"\"\"\n", "\n", "grass_eaten = r\"\"\"\n", "FLAMEGPU_AGENT_FUNCTION(grass_eaten, flamegpu::MessageBruteForce, flamegpu::MessageBruteForce) {\n", " const float grass_x = FLAMEGPU->getVariable(\"x\");\n", " const float grass_y = FLAMEGPU->getVariable(\"y\");\n", " int available = FLAMEGPU->getVariable(\"available\");\n", " if (available) {\n", "\n", " int prey_id = -1;\n", " float closest_prey = FLAMEGPU->environment.getProperty(\"GRASS_EAT_DISTANCE\");\n", " int eaten = 0;\n", "\n", " // Iterate predator_location messages to find the closest predator\n", " for (const auto& msg : FLAMEGPU->message_in) {\n", " // Fetch location of prey\n", " const float prey_x = msg.getVariable(\"x\");\n", " const float prey_y = msg.getVariable(\"y\");\n", "\n", " // Check if the two preys are within interaction radius\n", " const float dx = grass_x - prey_x;\n", " const float dy = grass_y - prey_y;\n", " const float distance = sqrt(dx*dx + dy*dy);\n", "\n", " if (distance < closest_prey) {\n", " prey_id = msg.getVariable(\"id\");\n", " closest_prey= distance;\n", " eaten = 1;\n", " }\n", " }\n", "\n", " if (eaten) {\n", " // Add grass eaten message\n", " FLAMEGPU->message_out.setVariable(\"id\", FLAMEGPU->getID());\n", " FLAMEGPU->message_out.setVariable(\"prey_id\", prey_id);\n", "\n", " // Update grass agent variables\n", " FLAMEGPU->setVariable(\"dead_cycles\", 0);\n", " FLAMEGPU->setVariable(\"available\", 0);\n", " FLAMEGPU->setVariable(\"type\", 3.0f);\n", " }\n", " }\n", " return flamegpu::ALIVE;\n", "}\n", "\"\"\"\n", "\n", "prey_eat_or_starve = r\"\"\"\n", "FLAMEGPU_AGENT_FUNCTION(prey_eat_or_starve, flamegpu::MessageBruteForce, flamegpu::MessageNone) {\n", " int isDead = 0;\n", " const flamegpu::id_t id = FLAMEGPU->getID();\n", " const int life = FLAMEGPU->getVariable(\"life\");\n", "\n", " // Iterate the grass eaten messages\n", " for (const auto& msg : FLAMEGPU->message_in)\n", " {\n", " // If the grass eaten message indicates that this prey ate some grass then increase the preys life by adding energy\n", " if (id == msg.getVariable(\"prey_id\")) {\n", " FLAMEGPU->setVariable(\"life\", life + FLAMEGPU->environment.getProperty(\"GAIN_FROM_FOOD_PREY\"));\n", " }\n", " }\n", "\n", " // If the life has reduced to 0 then the prey should die or starvation\n", " if (FLAMEGPU->getVariable(\"life\") < 1)\n", " isDead = 1;\n", "\n", " return isDead ? flamegpu::DEAD : flamegpu::ALIVE;\n", "}\n", "\"\"\"\n", "\n", "grass_growth = r\"\"\"\n", "FLAMEGPU_AGENT_FUNCTION(grass_growth, flamegpu::MessageNone, flamegpu::MessageNone) {\n", " const int dead_cycles = FLAMEGPU->getVariable(\"dead_cycles\");\n", " int new_dead_cycles = dead_cycles + 1;\n", " if (dead_cycles == FLAMEGPU->environment.getProperty(\"GRASS_REGROW_CYCLES\")) {\n", " FLAMEGPU->setVariable(\"dead_cycles\", 0);\n", " FLAMEGPU->setVariable(\"available\", 1);\n", " FLAMEGPU->setVariable(\"type\", 2.0f);\n", " }\n", "\n", " const int available = FLAMEGPU->getVariable(\"available\");\n", " if (available == 0) {\n", " FLAMEGPU->setVariable(\"dead_cycles\", new_dead_cycles);\n", " }\n", "\n", " return flamegpu::ALIVE;\n", "}\n", "\"\"\"\n", "\n", "class population_tracker(pyflamegpu.HostFunction):\n", " def __init__(self, has_grass):\n", " super().__init__(); # Mandatory if we are defining __init__ ourselves\n", " # Local, so value is maintained between calls to calculate_convergence::run\n", " self.pred_count = []\n", " self.prey_count = []\n", " self.grass_count = []\n", " self.has_grass = has_grass\n", "\n", " def run(self, FLAMEGPU):\n", " # Reduce force and overlap\n", " self.pred_count.append(FLAMEGPU.agent(\"predator\").count())\n", " self.prey_count.append(FLAMEGPU.agent(\"prey\").count())\n", " if (self.has_grass):\n", " self.grass_count.append(FLAMEGPU.agent(\"grass\").countInt(\"available\", 1))\n", " else:\n", " self.grass_count.append(0)\n", "\n", " def plot(self):\n", " plt.figure(figsize=(16,10))\n", " plt.rcParams.update({'font.size': 18})\n", " plt.xlabel(\"Step\")\n", " plt.ylabel(\"Population\")\n", " plt.plot(range(0, len(self.pred_count)), self.pred_count, 'r', label=\"Predators\")\n", " plt.plot(range(0, len(self.prey_count)), self.prey_count, 'b', label=\"Prey\")\n", " plt.plot(range(0, len(self.grass_count)), self.grass_count, 'g', label=\"Grass\")\n", " plt.legend()\n", " plt.show()\n", "\n", " def reset(self):\n", " self.pred_count = []\n", " self.prey_count = []\n", " self.grass_count = []" ] }, { "cell_type": "markdown", "metadata": { "id": "tHuPbOUhaaPa" }, "source": [ "# Keeping Track of Model Outputs\n", "\n", "It is possible for FLAME GPU 2 to log the variables of each agent at every time step. This can be useful for post processing of simulation data but is usually more information than is required. For the predator prey model we are interested only in the population counts and as such we can use FLAME GPUs init, step and exit functions to run host code for logging or tracking information about the state of the model. Init functions are executed once at the start of a simulation, [step functions](https://docs.flamegpu.com/guide/3-behaviour-definition/6-host-functions.html) are run after each simulation step and exit functions are run at the end of the simulation. In this case, we have used FLAME GPU 2's `HostFunction` to track the populations at each step and plot them. This is shown at the end of the previous code segment." ] }, { "cell_type": "markdown", "metadata": { "id": "9zB8ncuhTzI9" }, "source": [ "# Define Execution Order" ] }, { "cell_type": "markdown", "metadata": { "id": "_WbR0UTuZu6e" }, "source": [ "The final piece of the puzzle is function layers which define the order functions should be executed in. Functions in the same layer may be executed concurrently. It is also possible to automatically generate the execution layers to optimise performance using concurrent execution. This is achieved using specification of the dependencies between the functions. We will not cover this in this tutorial, but if you are interested, the documentation is available [here](https://docs.flamegpu.com/guide/5-running-a-sim/1-defining-execution-order.html). The function layers from this model are shown below:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "tlx2xfqRT0-V" }, "outputs": [], "source": [ "def define_execution_order(model):\n", " \"\"\"\n", " Control flow\n", " \"\"\"\n", " layer = model.newLayer()\n", " layer.addAgentFunction(\"prey\", \"prey_output_location\")\n", " layer.addAgentFunction(\"predator\", \"pred_output_location\")\n", "\n", " layer = model.newLayer()\n", " layer.addAgentFunction(\"predator\", \"pred_follow_prey\")\n", " layer.addAgentFunction(\"prey\", \"prey_avoid_pred\")\n", "\n", " layer = model.newLayer()\n", " layer.addAgentFunction(\"prey\", \"prey_flock\")\n", " layer.addAgentFunction(\"predator\", \"pred_avoid\")\n", "\n", " layer = model.newLayer()\n", " layer.addAgentFunction(\"prey\", \"prey_move\")\n", " layer.addAgentFunction(\"predator\", \"pred_move\")\n", "\n", " layer = model.newLayer()\n", " layer.addAgentFunction(\"prey\", \"prey_eaten\")\n", "\n", " layer = model.newLayer()\n", " #layer.addAgentFunction(\"prey\", \"prey_eat_or_starve\")\n", " layer.addAgentFunction(\"predator\", \"pred_eat_or_starve\")\n", "\n", " layer = model.newLayer()\n", " layer.addAgentFunction(\"predator\", \"pred_reproduction\")\n", " layer.addAgentFunction(\"prey\", \"prey_reproduction\")" ] }, { "cell_type": "markdown", "metadata": { "id": "FpJ3rnDqT41o" }, "source": [ "# Create Populations" ] }, { "cell_type": "markdown", "metadata": { "id": "CJ3WYh0JaCq9" }, "source": [ "The initial agent data for the model can either be specified within the main python file or in an XML file. In this tutorial, we demonstrate the agents being generated programmatically. The program generates the specified number of predator and prey agents in random positions (``x``,``y``) between [-1,1] with random velocities (``vx``,``vy``) between [-1,1]. Each predator is given an amount of energy (life) which is randomly selected from the interval of [0,40]. Prey are initialised with random positions between [-1, 1], a velocity of 0 and energy randomly selected in the interval [0, 50]." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "NrfF6_XhUBOv" }, "outputs": [], "source": [ "def initialise_simulation(num_prey, num_predators, num_grass, seed):\n", " model = create_model()\n", " define_messages(model)\n", " define_agents(model)\n", " define_environment(model)\n", " define_execution_order(model)\n", "\n", " # Set up a population tracker for logging/plotting\n", " pop_tracker = population_tracker(num_grass > 0)\n", " model.addStepFunction(pop_tracker)\n", "\n", " \"\"\"\n", " Create Model Runner\n", " \"\"\"\n", " cudaSimulation = pyflamegpu.CUDASimulation(model)\n", "\n", " # Apply a simulation seed\n", " if seed is not None:\n", " cudaSimulation.SimulationConfig().random_seed = seed\n", " cudaSimulation.applyConfig()\n", "\n", " \"\"\"\n", " Initialise Model\n", " \"\"\"\n", " # If no xml model file was is provided, generate a population programmatically.\n", " if not cudaSimulation.SimulationConfig().input_file:\n", " # Uniformly distribute agents within space, with uniformly distributed initial velocity.\n", " # Using random seed\n", " random.seed(cudaSimulation.SimulationConfig().random_seed)\n", "\n", " # Initialise prey agents\n", " preyPopulation = pyflamegpu.AgentVector(model.Agent(\"prey\"), num_prey)\n", " for i in range(0, num_prey):\n", " prey = preyPopulation[i]\n", " prey.setVariableFloat(\"x\", random.uniform(-1.0, 1.0))\n", " prey.setVariableFloat(\"y\", random.uniform(-1.0, 1.0))\n", " prey.setVariableFloat(\"vx\", random.uniform(-1.0, 1.0))\n", " prey.setVariableFloat(\"vy\", random.uniform(-1.0, 1.0))\n", " prey.setVariableFloat(\"steer_x\", 0.0)\n", " prey.setVariableFloat(\"steer_y\", 0.0)\n", " prey.setVariableFloat(\"type\", 1.0)\n", " prey.setVariableInt(\"life\", random.randint(0, 50))\n", "\n", " # Initialise predator agents\n", " predatorPopulation = pyflamegpu.AgentVector(model.Agent(\"predator\"), num_predators)\n", " for i in range(0, num_predators):\n", " predator = predatorPopulation[i]\n", " predator.setVariableFloat(\"x\", random.uniform(-1.0, 1.0))\n", " predator.setVariableFloat(\"y\", random.uniform(-1.0, 1.0))\n", " predator.setVariableFloat(\"vx\", random.uniform(-1.0, 1.0))\n", " predator.setVariableFloat(\"vy\", random.uniform(-1.0, 1.0))\n", " predator.setVariableFloat(\"steer_x\", 0.0)\n", " predator.setVariableFloat(\"steer_y\", 0.0)\n", " predator.setVariableFloat(\"type\", 0.0)\n", " predator.setVariableInt(\"life\", random.randint(0, 5))\n", "\n", " cudaSimulation.setPopulationData(predatorPopulation)\n", " cudaSimulation.setPopulationData(preyPopulation)\n", "\n", " return [cudaSimulation, pop_tracker]" ] }, { "cell_type": "markdown", "metadata": { "id": "bmwDyP79WA2i" }, "source": [ "# Set the number of steps and run!\n", "\n", "Depending on the parameters this could take anywhere from a few seconds to a few minutes." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "-SrapeI6WD5h" }, "outputs": [], "source": [ "def run_simulation():\n", " \"\"\"\n", " Execution\n", " \"\"\"\n", " # Initialise the simulation\n", " [cudaSimulation, pop_tracker] = initialise_simulation(num_prey = 200, num_predators = 50, num_grass = 0, seed = 64)\n", " cudaSimulation.SimulationConfig().steps = 1600\n", "\n", " # Run the simulation\n", " pop_tracker.reset()\n", " cudaSimulation.simulate()\n", " pop_tracker.plot()\n", "\n", "run_simulation()" ] }, { "cell_type": "markdown", "metadata": { "id": "Wbe-ZqglQ_2O" }, "source": [ "# Exercise 01: Running FLAME GPU 2\n", "\n", "To run the simulation, you can either run each code cell sequentially from the top of the document, or you can click **Runtime** then **Run All** in the top menu. If you update a code segment, you will need to run that segment again to save the changes to the model, or use the **Run All** option again. The first time you run the simulation it might take a little while as the library is installed and the agent functions are compiled.\n", "\n", "Try using **Run All** now to run the simulation. You should see a graph appear showing the population changes over time." ] }, { "cell_type": "markdown", "metadata": { "id": "YvaTU6pmR1Jw" }, "source": [ "# Exercise 02: Changing the Model's Behaviour\n", "\n", "Let's change the parameters of the simulation and see how this affects the results. To start, try changing the `GAIN_FROM_FOOD_PREDATOR` environment variable to ``50``.\n", "\n", "(**Hint:** You can collapse the individual sections of code so you don't have to scroll so far up and down)\n", "\n", "Now try modifying the initial populations which are passed to the `initialise_simulation` function in the section \"Set the number of steps and run!\"\n", "\n", "See if you can produce oscillating predator and prey populations.\n", "\n", "\n" ] }, { "cell_type": "markdown", "metadata": { "id": "rhKgPTeITH-a" }, "source": [ "# Exercise 03: Extending the Model\n", "\n", "In this exercise, we are going to extend our model to include grass to familiarise you with how agents and their behaviours are implemented within FLAMEGPU2. The grass will provide a source of food for the prey which should behave according to the following rules:\n", "\n", "- A prey’s energy is reduced by 1 unit each time it moves.\n", "- A prey’s energy increases each time it eats grass.\n", "- A prey dies if it has run out of energy.\n", "- Once eaten, grass regrows after a fixed number of iterations.\n", "\n", "With respect to the implementation this will require , a fourth and fifth message type as grass should only be eaten by prey within a certain certain radius. The mechanism for \"grazing\" is as follows:\n", "\n", "1. Grass agents which currently are currently available read the positions of the prey from the location messages to locate the nearest prey.\n", "2. If a prey was in eating range, a grass_eaten_message message is output. Note that if there is more than 1 prey on within the minimum distance, the grass agent should be eaten by the closest prey.\n", "3. Grass agents then modify the active agent variable to indicate they are in a regrowth period and cannot be eaten at this time.\n", "4. Prey agents read the grass_eaten_message messages and check to see if any match their ID. They then increase their energy accordingly.\n", "5. Prey agents die if they do not have enough life/energy.\n", "\n", "In the following exercises you will add the grass agent to the model:\n", "\n", "
\n", "
\n", "\n", "## Exercise 3.1\n", "Exercise 3.1 will add the grass agent definition.\n", "\n", "    **a)** Add a new agent type called ``grass``\n", "\n", "    **b)** Add the following variables to the ``grass`` agent:\n", "\n", "\n", "| Type | Name |\n", "| :--- | :--- |\n", "| float | x |\n", "| float | y |\n", "| int | dead_cycles |\n", "| int | available |\n", "| float | type |\n", "\n", "
\n", "
\n", "\n", "\n", "## Exercise 3.2\n", "Exercise 3.2 will create the message types used by the grass agent.\n", "\n", "    **a)** Add a message type called ``grass_location_message`` with the following properties:\n", "\n", "| Type | Name |\n", "| :--- | :--- |\n", "| ID | id |\n", "| float | x |\n", "| float | y |\n", "\n", "
\n", "
\n", "\n", "## Exercise 3.3\n", "Exercise 3.3 will add functions to the ``grass`` agent. Add the following functions to the ``grass`` agent with the properties given in the table below:\n", "\n", "| Name | Message Input | Message Output | Message Output Optional | Allow Agent Death |\n", "| :--- | :--- | :--- | :--- | :--- |\n", "| grass_output_location | n/a | grass_location_message | True | n/a |\n", "| grass_eaten | prey_location_message | grass_eaten_message | True | True |\n", "| grass_growth | n/a | n/a | n/a | n/a |\n", "\n", "
\n", "
\n", "\n", "## Exercise 3.4\n", "Exercise 3.4 will add a grass population to the simulation. Using the ``predatorPopulation`` and ``preyPopulation`` as examples, create a ``grassPopulation`` with variables initialised with the following properties:\n", "\n", "| Variable | Initial Value |\n", "| :--- | :--- |\n", "| ``x`` | ``random.uniform(-1.0, 1.0)`` |\n", "| ``y`` | ``random.uniform(-1.0, 1.0)`` |\n", "| ``dead_cycles`` | ``0`` |\n", "| ``available`` | ``1`` |\n", "| ``type`` | ``2.0`` |\n", "\n", "
\n", "
\n", "\n", "## Exercise 3.5\n", "Exercise 3.5 will include the newly created grass agent functions in the execution layers.\n", "\n", "    **a)** Add the ``grass_output_location`` function to the layer with ``prey_output_location`` and ``predator_output_location``.\n", "\n", "    **b)** Add the ``grass_eaten`` function to the layer with ``prey_eaten``.\n", "\n", "    **c)** Add the ``grass_growth`` function to the layer with ``predator_reproduction`` and ``prey_reproduction``.\n", "\n", "    **d)** Uncomment the line ``#layer.addAgentFunction(\"prey\", \"prey_eat_or_starve\")`` in ``define_execution_order`` and the lines\n", " ```\n", " #fn = agent.newRTCFunction(\"prey_eat_or_starve\", prey_eat_or_starve)\n", " #fn.setMessageInput(\"grass_eaten_message\")\n", " #fn.setAllowAgentDeath(True)\n", " ```\n", " in ``define_agents``.\n", "\n", "
\n", "
\n", "\n", "## Exercise 3.6\n", "Exercise 3.6 will add the behaviour definition for the ``grass_output_location`` function. The function should get the ``id``, ``x`` and ``y`` variables of the agent and use them to set the ``id``, ``x`` and ``y`` variables of the output message. The other grass agent function behaviour definitions have been provided for you, so if you change the initial number of grass and run the model, you should now see the grass population changing and affecting the dynamics of the simulation!\n", "\n", "\n", "\n", "\n" ] }, { "cell_type": "markdown", "metadata": { "id": "Z2MqMqx1gECn" }, "source": [ "# Exercise 3.1 Solution\n" ] }, { "cell_type": "markdown", "metadata": { "id": "mEUIR0RWlTBO" }, "source": [ "```python\n", " \"\"\"\n", " Exercise 3.1 - Grass agent\n", " \"\"\"\n", " # Create the agent\n", " agent = model.newAgent(\"grass\")\n", "\n", " # Assign its variables\n", " agent.newVariableFloat(\"x\")\n", " agent.newVariableFloat(\"y\")\n", " agent.newVariableInt(\"dead_cycles\")\n", " agent.newVariableInt(\"available\")\n", " agent.newVariableFloat(\"type\")\n", "```" ] }, { "cell_type": "markdown", "metadata": { "id": "Tsx2PoCRlVX_" }, "source": [ "# Exercise 3.2 Solution" ] }, { "cell_type": "markdown", "metadata": { "id": "NCfIviwOlX1_" }, "source": [ "```python \n", " # Assign its functions\n", " fn = agent.newRTCFunction(\"grass_output_location\", grass_output_location)\n", " fn.setMessageOutput(\"grass_location_message\")\n", " fn.setMessageOutputOptional(True)\n", "\n", " fn = agent.newRTCFunction(\"grass_eaten\", grass_eaten)\n", " fn.setMessageInput(\"prey_location_message\")\n", " fn.setMessageOutput(\"grass_eaten_message\")\n", " fn.setMessageOutputOptional(True)\n", " fn.setAllowAgentDeath(True)\n", " \n", " fn = agent.newRTCFunction(\"grass_growth\", grass_growth)\n", "```" ] }, { "cell_type": "markdown", "metadata": { "id": "VOC3uQjRliYO" }, "source": [ "# Exercise 3.3 Solution" ] }, { "cell_type": "markdown", "metadata": { "id": "oG29oHBulkad" }, "source": [ "```python \n", " message = model.newMessageBruteForce(\"grass_location_message\")\n", " message.newVariableID(\"id\")\n", " message.newVariableFloat(\"x\")\n", " message.newVariableFloat(\"y\")\n", "```" ] }, { "cell_type": "markdown", "metadata": { "id": "ENmsyzJGl_fZ" }, "source": [ "# Exercise 3.4 Solution" ] }, { "cell_type": "markdown", "metadata": { "id": "1gz8pmdzmBTd" }, "source": [ "```python\n", " # Initialise grass agents\n", " grassPopulation = pyflamegpu.AgentVector(model.Agent(\"grass\"), num_grass)\n", " for i in range(0, num_grass):\n", " grass = grassPopulation[i]\n", " grass.setVariableFloat(\"x\", random.uniform(-1.0, 1.0))\n", " grass.setVariableFloat(\"y\", random.uniform(-1.0, 1.0))\n", " grass.setVariableInt(\"dead_cycles\", 0)\n", " grass.setVariableInt(\"available\", 1)\n", " grass.setVariableFloat(\"type\", 2.0)\n", " \n", " cudaSimulation.setPopulationData(predatorPopulation)\n", " cudaSimulation.setPopulationData(preyPopulation)\n", " cudaSimulation.setPopulationData(grassPopulation)\n", "```" ] }, { "cell_type": "markdown", "metadata": { "id": "2c8zlCLQmL5Z" }, "source": [ "# Exercise 3.5 Solution" ] }, { "cell_type": "markdown", "metadata": { "id": "g7nNq3znmLuB" }, "source": [ "```python\n", "def define_execution_order(model):\n", " \"\"\"\n", " Control flow\n", " \"\"\" \n", " layer = model.newLayer()\n", " layer.addAgentFunction(\"prey\", \"prey_output_location\")\n", " layer.addAgentFunction(\"predator\", \"pred_output_location\")\n", " layer.addAgentFunction(\"grass\", \"grass_output_location\")\n", " \n", " layer = model.newLayer()\n", " layer.addAgentFunction(\"predator\", \"pred_follow_prey\")\n", " layer.addAgentFunction(\"prey\", \"prey_avoid_pred\")\n", " \n", " layer = model.newLayer()\n", " layer.addAgentFunction(\"prey\", \"prey_flock\")\n", " layer.addAgentFunction(\"predator\", \"pred_avoid\")\n", " \n", " layer = model.newLayer()\n", " layer.addAgentFunction(\"prey\", \"prey_move\")\n", " layer.addAgentFunction(\"predator\", \"pred_move\")\n", " \n", " layer = model.newLayer()\n", " layer.addAgentFunction(\"grass\", \"grass_eaten\")\n", " layer.addAgentFunction(\"prey\", \"prey_eaten\")\n", " \n", " layer = model.newLayer()\n", " layer.addAgentFunction(\"prey\", \"prey_eat_or_starve\")\n", " layer.addAgentFunction(\"predator\", \"pred_eat_or_starve\")\n", " \n", " layer = model.newLayer()\n", " layer.addAgentFunction(\"predator\", \"pred_reproduction\")\n", " layer.addAgentFunction(\"prey\", \"prey_reproduction\")\n", " layer.addAgentFunction(\"grass\", \"grass_growth\")\n", "```" ] }, { "cell_type": "markdown", "metadata": { "id": "fS2r-ouumIB9" }, "source": [ "# Exercise 3.6 Solution" ] }, { "cell_type": "markdown", "metadata": { "id": "TwrYwbnnnMHZ" }, "source": [ "```c++\n", "FLAMEGPU_AGENT_FUNCTION(grass_output_location, flamegpu::MessageNone, flamegpu::MessageBruteForce) {\n", " // Exercise 3.1 : Set the variables for the grass_location message\n", " const flamegpu::id_t id = FLAMEGPU->getID();\n", " const float x = FLAMEGPU->getVariable(\"x\");\n", " const float y = FLAMEGPU->getVariable(\"y\");\n", " FLAMEGPU->message_out.setVariable(\"id\", id);\n", " FLAMEGPU->message_out.setVariable(\"x\", x);\n", " FLAMEGPU->message_out.setVariable(\"y\", y);\n", " return flamegpu::ALIVE;\n", "}\n", "```" ] }, { "cell_type": "markdown", "metadata": { "id": "CmNnjxEZfyWc" }, "source": [ "# Thanks for Attending!\n", "\n", "If you have enjoyed today's tutorial and wish to continue experimenting with FLAME GPU 2, the following resources may be useful to you:\n", "\n", "- [Software Website](https://flamegpu.com/)\n", "- [FLAME GPU 2 on Github](https://github.com/FLAMEGPU/FLAMEGPU2/)\n", "- [Documentation](https://docs.flamegpu.com/guide/index.html#)\n", "- [C++ Tutorial](https://github.com/FLAMEGPU/FLAMEGPU2-tutorial-cudacpp)\n", "\n", "If you think FLAME GPU 2 could be a good fit for your research project, please also feel free to get in touch with us via [rse@sheffield.ac.uk](rse@sheffield.ac.uk). The RSE team is also capable of providing expertise in reproducible research software, high-performance computing and training. For more information, please visit [rse.shef.ac.uk](https://rse.shef.ac.uk/)" ] } ], "metadata": { "accelerator": "GPU", "colab": { "collapsed_sections": [ "Z2MqMqx1gECn", "Tsx2PoCRlVX_", "VOC3uQjRliYO", "ENmsyzJGl_fZ", "2c8zlCLQmL5Z", "fS2r-ouumIB9" ], "name": "FLAME-GPU-2-python-tutorial.ipynb", "provenance": [] }, "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.10" } }, "nbformat": 4, "nbformat_minor": 0 }