diff --git a/.gitignore b/.gitignore index 989ac6a0f..03b00314e 100644 --- a/.gitignore +++ b/.gitignore @@ -500,3 +500,13 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ + + +#Temporary for this branch +GEMINI.md +executionsteps + +# All log and image files +*.png +*.gif +*.csv diff --git a/CMakeLists.txt b/CMakeLists.txt index f6c3173f5..eb7353524 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,9 +30,9 @@ set(DOCUMENTATION_ONLY_BUILD OFF) # Check compiler functionailty, as there are known issues in some cases, but version checks are not always sufficient. include(./cmake/CheckCompilerFunctionality.cmake) -# If this returned a negative result, set the docs only build. +# If this returned a negative result, set the docs only build. if(NOT FLAMEGPU_CheckCompilerFunctionality_RESULT) - set(DOCUMENTATION_ONLY_BUILD ON) + set(DOCUMENTATION_ONLY_BUILD ON) message(STATUS "Documentation-only build: due to compiler compatability version. See prior warnings.") endif() @@ -60,7 +60,7 @@ endif() # If CUDA is not available, or the minimum version is too low only build the docs. if(DOCUMENTATION_ONLY_BUILD) - # Not able to build code, so just make docs + # Not able to build code, so just make docs include(./cmake/dependencies/doxygen.cmake) if(${FLAMEGPU_BUILD_API_DOCUMENTATION}) flamegpu_create_doxygen_target("${FLAMEGPU_ROOT}" "${CMAKE_CURRENT_BINARY_DIR}" "") @@ -74,7 +74,7 @@ include(CMakeDependentOption) # Option to enable building all examples, defaults to ON if FLAMEGPU is the top level cmake, else OFF cmake_dependent_option(FLAMEGPU_BUILD_ALL_EXAMPLES "Enable building all FLAMEGPU examples" ON "FLAMEGPU_PROJECT_IS_TOP_LEVEL" OFF) -# Options to enable building individual examples, if FLAMEGPU_BUILD_ALL_EXAMPLES is off. +# Options to enable building individual examples, if FLAMEGPU_BUILD_ALL_EXAMPLES is off. # Dependent options hide these from the CMake GUI if FLAMEGPU_BUILD_ALL_EXAMPLES is on, or if it is not the top level project cmake_dependent_option(FLAMEGPU_BUILD_EXAMPLE_BOIDS_BRUTEFORCE "Enable building examples/cpp/boids_bruteforce" OFF "FLAMEGPU_PROJECT_IS_TOP_LEVEL; NOT FLAMEGPU_BUILD_ALL_EXAMPLES" OFF) cmake_dependent_option(FLAMEGPU_BUILD_EXAMPLE_BOIDS_SPATIAL3D "Enable building examples/cpp/boids_spatial3D" OFF "FLAMEGPU_PROJECT_IS_TOP_LEVEL; NOT FLAMEGPU_BUILD_ALL_EXAMPLES" OFF) @@ -87,6 +87,7 @@ cmake_dependent_option(FLAMEGPU_BUILD_EXAMPLE_HOST_FUNCTIONS "Enable building ex cmake_dependent_option(FLAMEGPU_BUILD_EXAMPLE_ENSEMBLE "Enable building examples/cpp/ensemble" OFF "FLAMEGPU_PROJECT_IS_TOP_LEVEL; NOT FLAMEGPU_BUILD_ALL_EXAMPLES" OFF) cmake_dependent_option(FLAMEGPU_BUILD_EXAMPLE_SUGARSCAPE "Enable building examples/cpp/sugarscape" OFF "FLAMEGPU_PROJECT_IS_TOP_LEVEL; NOT FLAMEGPU_BUILD_ALL_EXAMPLES" OFF) cmake_dependent_option(FLAMEGPU_BUILD_EXAMPLE_DIFFUSION "Enable building examples/cpp/diffusion" OFF "FLAMEGPU_PROJECT_IS_TOP_LEVEL; NOT FLAMEGPU_BUILD_ALL_EXAMPLES" OFF) +cmake_dependent_option(FLAMEGPU_BUILD_EXAMPLE_BEESANDBEES "Enable building examples/cpp/beesandflowers" OFF "FLAMEGPU_PROJECT_IS_TOP_LEVEL; NOT FLAMEGPU_BUILD_ALL_EXAMPLES" OFF) option(FLAMEGPU_BUILD_PYTHON "Enable python bindings via SWIG" OFF) @@ -144,10 +145,14 @@ if(FLAMEGPU_BUILD_ALL_EXAMPLES OR FLAMEGPU_BUILD_EXAMPLE_ENSEMBLE) endif() if(FLAMEGPU_BUILD_ALL_EXAMPLES OR FLAMEGPU_BUILD_EXAMPLE_SUGARSCAPE) add_subdirectory(examples/cpp/sugarscape) + add_subdirectory(examples/cpp/sugarscapeSubModelAPI) endif() if(FLAMEGPU_BUILD_ALL_EXAMPLES OR FLAMEGPU_BUILD_EXAMPLE_DIFFUSION) add_subdirectory(examples/cpp/diffusion) endif() +if(FLAMEGPU_BUILD_ALL_EXAMPLES OR FLAMEGPU_BUILD_EXAMPLE_BEESANDBEES) + add_subdirectory(examples/cpp/beesandflowers) +endif() # Add the tests directory (if required) if(FLAMEGPU_BUILD_TESTS OR FLAMEGPU_BUILD_TESTS_DEV) # Enable Ctest diff --git a/examples/cpp/beesandflowers/CMakeLists.txt b/examples/cpp/beesandflowers/CMakeLists.txt new file mode 100644 index 000000000..a1e24e607 --- /dev/null +++ b/examples/cpp/beesandflowers/CMakeLists.txt @@ -0,0 +1,34 @@ +# Minimum CMake version 3.25.2 for CUDA --std=c++20 +cmake_minimum_required(VERSION 3.25.2...4.1.1 FATAL_ERROR) + +# Set the location of the ROOT flame gpu project relative to this CMakeList.txt +get_filename_component(FLAMEGPU_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../../.. REALPATH) + +# Handle CMAKE_CUDA_ARCHITECTURES gracefully +include(${FLAMEGPU_ROOT}/cmake/CUDAArchitectures.cmake) +flamegpu_init_cuda_architectures(PROJECT beesandflowers) + +# Name the project and enable required languages +project(beesandflowers CXX CUDA) + +# Include common rules. +include(${FLAMEGPU_ROOT}/cmake/common.cmake) + +# Define output location of binary files +SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/${CMAKE_BUILD_TYPE}/) + +# Prepare list of source files +# Can't do this automatically, as CMake wouldn't know when to regen (as CMakeLists.txt would be unchanged) +SET(ALL_SRC + ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cu +) + +# Add the executable and set required flags for the target +flamegpu_add_executable("${PROJECT_NAME}" "${ALL_SRC}" "${FLAMEGPU_ROOT}" "${PROJECT_BINARY_DIR}" TRUE) + +# Also set as startup project (if top level project) +set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" PROPERTY VS_STARTUP_PROJECT "${PROJECT_NAME}") + +# Set the default (visual studio) debug working directory and args +set_target_properties("${PROJECT_NAME}" PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + VS_DEBUGGER_COMMAND_ARGUMENTS "-s 10") \ No newline at end of file diff --git a/examples/cpp/beesandflowers/src/main.cu b/examples/cpp/beesandflowers/src/main.cu new file mode 100644 index 000000000..eb90534c1 --- /dev/null +++ b/examples/cpp/beesandflowers/src/main.cu @@ -0,0 +1,275 @@ +#include +#include +#include +#include +#include +#include +#include "flamegpu/flamegpu.h" +#include "flamegpu/stockAgent/subModels/SingleAgentDiscreteMovement.h" + +#define ENV_DIM 100 +#define SIMULATION_STEPS 100 + +using flamegpu::ModelDescription; +using flamegpu::AgentDescription; +using flamegpu::AgentFunctionDescription; +using flamegpu::LayerDescription; +using flamegpu::CUDASimulation; +using flamegpu::MessageNone; +using flamegpu::ALIVE; +using flamegpu::EnvironmentDescription; + +// Global log file +std::ofstream agents_log; + +FLAMEGPU_AGENT_FUNCTION(calculate_priority, MessageNone, MessageNone) { + float current_nectar = FLAMEGPU->getVariable("current_cell_score"); + float hunger_level = FLAMEGPU->getVariable("hunger_level"); + + // If at a flower and still hungry, stay put (priority 0) + if (current_nectar > 0.01f && hunger_level > 0.0f) { + FLAMEGPU->setVariable("priority", 0.0f); + // Ensure submodel doesn't move us if we want to stay + FLAMEGPU->setVariable("current_cell_score", 1000.0f); + return ALIVE; + } + + int wait = FLAMEGPU->getVariable("wait"); + float wh = FLAMEGPU->environment.getProperty("WH"); + float ww = FLAMEGPU->environment.getProperty("WW"); + + // Priority for movement (higher = more likely to win a cell) + float priority = hunger_level * wh + (float)wait * ww + FLAMEGPU->random.uniform(0.0f, 1.0f); + FLAMEGPU->setVariable("priority", priority); + + // Force movement by setting current_cell_score to a low value. + // This ensures any neighbor with score >= 0.0 will be considered a valid move. + FLAMEGPU->setVariable("current_cell_score", -1.0f); + + return ALIVE; +} + +FLAMEGPU_AGENT_FUNCTION(update_hunger_wait, MessageNone, MessageNone) { + float current_nectar = FLAMEGPU->getVariable("current_cell_score"); + float hunger_level = FLAMEGPU->getVariable("hunger_level"); + int wait = FLAMEGPU->getVariable("wait"); + + if (current_nectar > 0.01f && hunger_level > 0.0f) { + // Feed: decrease hunger_level + hunger_level -= 5.0f; + if (hunger_level <= 0.0f) { + hunger_level = 0.0f; + } + wait = 0; + } else { + // Hunger increases over time + hunger_level += 2.0f; + wait += 1; + } + + FLAMEGPU->setVariable("hunger_level", hunger_level); + FLAMEGPU->setVariable("wait", wait); + + return ALIVE; +} + +FLAMEGPU_INIT_FUNCTION(createAgent) { + const int GRID_DIM = 100; + const int FLOWER_SPACING = 5; + + // Create bees at random unique positions first + const int NUM_BEES = 100; + auto bee_api = FLAMEGPU->agent("bee"); + + std::vector available_indices(GRID_DIM * GRID_DIM); + std::iota(available_indices.begin(), available_indices.end(), 0); + + std::mt19937 g(std::random_device {}()); + std::shuffle(available_indices.begin(), available_indices.end(), g); + + std::vector is_bee_at(GRID_DIM * GRID_DIM, false); + + for (int i = 0; i < NUM_BEES; ++i) { + int index = available_indices[i]; + int x = index / GRID_DIM; + int y = index % GRID_DIM; + is_bee_at[index] = true; + + auto bee = bee_api.newAgent(); + bee.setVariable("x", x); + bee.setVariable("y", y); + bee.setVariable("last_x", -1); + bee.setVariable("last_y", -1); + bee.setVariable("last_resources_x", -1); + bee.setVariable("last_resources_y", -1); + bee.setVariable("hunger_level", FLAMEGPU->random.uniform(0.0f, 100.0f)); + bee.setVariable("wait", 0); + bee.setVariable("priority", 0.0f); + bee.setVariable("current_cell_score", 0.0f); + } + + // Create a 100x100 grid of cells and set occupancy + auto cell_api = FLAMEGPU->agent("flower_cell"); + for (int i = 0; i < GRID_DIM; ++i) { + for (int j = 0; j < GRID_DIM; ++j) { + int index = i * GRID_DIM + j; + auto cell = cell_api.newAgent(); + cell.setVariable("x", i); + cell.setVariable("y", j); + cell.setVariable("is_occupied", is_bee_at[index] ? 1 : 0); + + float nectar = 0.0f; + if (i % FLOWER_SPACING == 0 && j % FLOWER_SPACING == 0) { + nectar = FLAMEGPU->random.uniform(10.0f, 50.0f); + } + cell.setVariable("nectar", nectar); + } + } +} + + +FLAMEGPU_INIT_FUNCTION(initLog) { + agents_log.open("bees_log.csv"); + agents_log << "step,id,x,y,hunger_level,wait" << std::endl; +} + +FLAMEGPU_STEP_FUNCTION(stepLogger) { + auto bees = FLAMEGPU->agent("bee"); + auto& bee_pop = bees.getPopulationData(); + unsigned int step = FLAMEGPU->getStepCounter(); + + int count = 0; + for (const auto& bee : bee_pop) { + if (count < 5) { + std::cout << "Bee " << bee.getID() << " at (" << bee.getVariable("x") << ", " << bee.getVariable("y") << ")" << std::endl; + count++; + } + agents_log << step << "," + << bee.getID() << "," + << bee.getVariable("x") << "," + << bee.getVariable("y") << "," + << bee.getVariable("hunger_level") << "," + << bee.getVariable("wait") << "\n"; + } + + // Log cells with nectar once at the start + if (step == 0) { + std::ofstream flower_log("flowers_log.csv"); + flower_log << "id,x,y,nectar" << std::endl; + auto cells = FLAMEGPU->agent("flower_cell"); + auto& cell_pop = cells.getPopulationData(); + for (const auto& cell : cell_pop) { + float nectar = cell.getVariable("nectar"); + if (nectar > 0.0f) { + flower_log << cell.getID() << "," + << cell.getVariable("x") << "," + << cell.getVariable("y") << "," + << nectar << "\n"; + } + } + flower_log.close(); + } + + float avg_hunger = bees.sum("hunger_level") / (float)bees.count(); + std::cout << "Step: " << step + << " | Bee count: " << bees.count() + << " | Avg Hunger: " << avg_hunger << std::endl; +} + +FLAMEGPU_EXIT_FUNCTION(exitLog) { + if (agents_log.is_open()) { + agents_log.close(); + } +} + +void define_model(ModelDescription &model) { + // Environment variables + EnvironmentDescription env = model.Environment(); + env.newProperty("WH", 0.6f); + env.newProperty("WW", 0.4f); + + // Cell Agent + AgentDescription cell = model.newAgent("flower_cell"); + cell.newVariable("x"); + cell.newVariable("y"); + cell.newVariable("is_occupied", 0); + cell.newVariable("nectar", 0.0f); + + // Bee Agent + AgentDescription bee = model.newAgent("bee"); + bee.newVariable("x"); + bee.newVariable("y"); + bee.newVariable("last_x", -1); + bee.newVariable("last_y", -1); + bee.newVariable("last_resources_x", -1); + bee.newVariable("last_resources_y", -1); + bee.newVariable("hunger_level"); + bee.newVariable("wait", 0); + bee.newVariable("priority", 0.0f); + bee.newVariable("current_cell_score", 0.0f); + + + // Declared on the stack - uses empty constructor + flamegpu::stockAgent::submodels::SingleAgentDiscreteMovement move_sub_logic; + + // Initialize the submodel + move_sub_logic.addSingleAgentDiscreteMovementSubmodel(model, ENV_DIM, ENV_DIM); + + // Bind parent agents to submodel + move_sub_logic.setMovingAgent("bee", + { + {"x", "x"}, + {"y", "y"}, + {"last_x", "last_x"}, + {"last_y", "last_y"}, + {"last_resources_x", "last_resources_x"}, + {"last_resources_y", "last_resources_y"}, + {"priority", "priority"}, + {"current_cell_score", "current_cell_score"} + }, + { + {"active", flamegpu::ModelData::DEFAULT_STATE} + }); + + move_sub_logic.setEnvironmentAgent("flower_cell", + { + {"x", "x"}, + {"y", "y"}, + {"is_occupied", "is_occupied"}, + {"cell_score", "nectar"} + }, + { + {"active", flamegpu::ModelData::DEFAULT_STATE} + }); + + + bee.newFunction("calculate_priority", calculate_priority); + bee.newFunction("update_hunger_wait", update_hunger_wait); + + LayerDescription l0 = model.newLayer(); + l0.addAgentFunction(calculate_priority); + + LayerDescription l1 = model.newLayer(); + l1.addSubModel(move_sub_logic.getSubModelDescription()); + + LayerDescription l2 = model.newLayer(); + l2.addAgentFunction(update_hunger_wait); + + model.addInitFunction(createAgent); + model.addInitFunction(initLog); + model.addStepFunction(stepLogger); + model.addExitFunction(exitLog); +} + +int main(int argc, const char ** argv) { + ModelDescription model("OneAgentMovingModel"); + + define_model(model); + + CUDASimulation simulation(model); + simulation.SimulationConfig().steps = SIMULATION_STEPS; + + simulation.simulate(); + + return EXIT_SUCCESS; +} diff --git a/examples/cpp/beesandflowers/visualizesimulation.py b/examples/cpp/beesandflowers/visualizesimulation.py new file mode 100644 index 000000000..cfbcc890e --- /dev/null +++ b/examples/cpp/beesandflowers/visualizesimulation.py @@ -0,0 +1,182 @@ +import matplotlib.pyplot as plt +import numpy as np +import matplotlib.animation as animation +import os + +def visualize(): + # Load data + try: + # Load bees_log.csv using numpy + # Header: step,id,x,y,hunger_level,wait + if not os.path.exists('bees_log.csv'): + print("bees_log.csv not found.") + return + bees_data = np.genfromtxt('bees_log.csv', delimiter=',', skip_header=1) + + # Load flowers_log.csv using numpy + # Header: id,x,y,nectar + if not os.path.exists('flowers_log.csv'): + print("flowers_log.csv not found.") + return + flowers_data = np.genfromtxt('flowers_log.csv', delimiter=',', skip_header=1) + except Exception as e: + print(f"Error loading logs: {e}") + return + + # Extract columns + step = bees_data[:, 0] + bee_id = bees_data[:, 1] + bee_x = bees_data[:, 2] + bee_y = bees_data[:, 3] + hunger_level = bees_data[:, 4] + wait = bees_data[:, 5] + + flower_x = flowers_data[:, 1] + flower_y = flowers_data[:, 2] + # flower_nectar = flowers_data[:, 3] # unused for now + + unique_steps = np.sort(np.unique(step)).astype(int) + grid_dim = 100 + + # 1. Plot Average Hunger Level and Wait over time + print("Generating statistics plot...") + avg_hunger = [] + avg_wait = [] + for s in unique_steps: + mask = (step == s) + avg_hunger.append(np.mean(hunger_level[mask])) + avg_wait.append(np.mean(wait[mask])) + + fig_stats, ax1 = plt.subplots(figsize=(10, 6)) + + ax1.set_xlabel('Step') + ax1.set_ylabel('Avg Hunger Level', color='tab:red') + ax1.plot(unique_steps, avg_hunger, color='tab:red', linewidth=2, label='Avg Hunger Level') + ax1.tick_params(axis='y', labelcolor='tab:red') + ax1.grid(True, which='both', linestyle='--', alpha=0.5) + + ax2 = ax1.twinx() + ax2.set_ylabel('Avg Wait', color='tab:blue') + ax2.plot(unique_steps, avg_wait, color='tab:blue', linewidth=2, label='Avg Wait') + ax2.tick_params(axis='y', labelcolor='tab:blue') + + plt.title('Average Bee Hunger Level and Wait over Time') + fig_stats.tight_layout() + plt.savefig('hunger_wait_plot.png') + print("Saved hunger_wait_plot.png") + + # 2. Animation + print("Creating animation (this might take a moment)...") + fig_anim, ax_anim = plt.subplots(figsize=(8, 8)) + + # Plot static flowers + ax_anim.scatter(flower_x, flower_y, c='green', marker='*', s=60, alpha=0.4, label='Flowers') + + # Initial bee plot + mask0 = (step == unique_steps[0]) + # Color bees by hunger level: yellow (full) to red (starving) + scat = ax_anim.scatter(bee_x[mask0], bee_y[mask0], c=hunger_level[mask0], + cmap='YlOrRd', vmin=0, vmax=100, + marker='o', s=30, edgecolors='k', linewidths=0.5, label='Bees') + + cbar = plt.colorbar(scat, ax=ax_anim) + cbar.set_label('Hunger Level') + + ax_anim.set_xlim(-1, grid_dim) + ax_anim.set_ylim(-1, grid_dim) + ax_anim.set_title(f'Bee Simulation - Step {unique_steps[0]}') + ax_anim.legend(loc='upper right') + + def update(frame): + s = unique_steps[frame] + mask = (step == s) + # Update bee positions + scat.set_offsets(np.c_[bee_x[mask], bee_y[mask]]) + # Update colors based on hunger level + scat.set_array(hunger_level[mask]) + ax_anim.set_title(f'Bee Simulation - Step {s}') + return scat, + + ani = animation.FuncAnimation(fig_anim, update, frames=len(unique_steps), interval=100, blit=True) + + # Save animation (requires ffmpeg or pillow) + try: + import PIL + ani.save('bee_simulation.gif', writer='pillow', fps=10) + print("Saved bee_simulation.gif") + except ImportError: + print("Pillow not found, skipping GIF save.") + except Exception as e: + print(f"Could not save animation: {e}") + + # 3. Static snapshots + print("Generating movement snapshots...") + steps_to_plot = [0, 25, 50, 75, 99] + steps_to_plot = [s for s in steps_to_plot if s in unique_steps] + + fig_snap, axes = plt.subplots(1, len(steps_to_plot), figsize=(20, 4)) + if len(steps_to_plot) == 1: + axes = [axes] + + for i, s in enumerate(steps_to_plot): + ax = axes[i] + mask = (step == s) + + ax.scatter(flower_x, flower_y, c='green', marker='*', s=30, alpha=0.3) + ax.scatter(bee_x[mask], bee_y[mask], c=hunger_level[mask], + cmap='YlOrRd', vmin=0, vmax=100, marker='o', s=10) + + ax.set_title(f'Step {s}') + ax.set_xlim(0, grid_dim) + ax.set_ylim(0, grid_dim) + ax.set_aspect('equal') + + plt.suptitle('Bee and Flower Positions over Time') + plt.tight_layout() + plt.savefig('movement_snapshots.png') + print("Saved movement_snapshots.png") + + # 4. Individual bee trajectories (sample 15 bees) + print("Generating trajectories plot...") + plt.figure(figsize=(10, 10)) + unique_ids = np.unique(bee_id) + np.random.seed(42) + sample_ids = np.random.choice(unique_ids, min(15, len(unique_ids)), replace=False) + + plt.scatter(flower_x, flower_y, c='green', marker='*', s=100, alpha=0.2, label='Flowers') + + for bid in sample_ids: + mask = (bee_id == bid) + idx = np.argsort(step[mask]) + plt.plot(bee_x[mask][idx], bee_y[mask][idx], marker='.', alpha=0.6, linewidth=1) + # Mark start + plt.scatter(bee_x[mask][idx][0], bee_y[mask][idx][0], marker='o', c='blue', s=30, zorder=5) + # Mark end + plt.scatter(bee_x[mask][idx][-1], bee_y[mask][idx][-1], marker='x', c='red', s=40, zorder=5) + + plt.title('Sample Bee Trajectories (15 Bees)') + plt.xlabel('X') + plt.ylabel('Y') + plt.xlim(0, grid_dim) + plt.ylim(0, grid_dim) + plt.gca().set_aspect('equal') + # Custom legend + from matplotlib.lines import Line2D + custom_lines = [Line2D([0], [0], color='green', marker='*', linestyle='None', markersize=10, alpha=0.3), + Line2D([0], [0], color='gray', marker='.', linestyle='-', alpha=0.6), + Line2D([0], [0], color='blue', marker='o', linestyle='None', markersize=8), + Line2D([0], [0], color='red', marker='x', linestyle='None', markersize=8)] + plt.legend(custom_lines, ['Flowers', 'Bee Paths', 'Start', 'End'], loc='upper right') + + plt.savefig('bee_trajectories.png') + print("Saved bee_trajectories.png") + + print("Visualization complete.") + # plt.show() + +if __name__ == "__main__": + # Change to the directory of the script to ensure logs are found + script_dir = os.path.dirname(os.path.abspath(__file__)) + os.chdir(script_dir) + + visualize() diff --git a/examples/cpp/sugarscapeSubModelAPI/CMakeLists.txt b/examples/cpp/sugarscapeSubModelAPI/CMakeLists.txt new file mode 100644 index 000000000..cc50b2dda --- /dev/null +++ b/examples/cpp/sugarscapeSubModelAPI/CMakeLists.txt @@ -0,0 +1,34 @@ +# Minimum CMake version 3.25.2 for CUDA --std=c++20 +cmake_minimum_required(VERSION 3.25.2...4.1.1 FATAL_ERROR) + +# Set the location of the ROOT flame gpu project relative to this CMakeList.txt +get_filename_component(FLAMEGPU_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../../.. REALPATH) + +# Handle CMAKE_CUDA_ARCHITECTURES gracefully +include(${FLAMEGPU_ROOT}/cmake/CUDAArchitectures.cmake) +flamegpu_init_cuda_architectures(PROJECT sugarscapeAI) + +# Name the project and enable required languages +project(sugarscapeAI CXX CUDA) + +# Include common rules. +include(${FLAMEGPU_ROOT}/cmake/common.cmake) + +# Define output location of binary files +SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/${CMAKE_BUILD_TYPE}/) + +# Prepare list of source files +# Can't do this automatically, as CMake wouldn't know when to regen (as CMakeLists.txt would be unchanged) +SET(ALL_SRC + ${CMAKE_CURRENT_SOURCE_DIR}/src/main.cu +) + +# Add the executable and set required flags for the target +flamegpu_add_executable("${PROJECT_NAME}" "${ALL_SRC}" "${FLAMEGPU_ROOT}" "${PROJECT_BINARY_DIR}" TRUE) + +# Also set as startup project (if top level project) +set_property(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" PROPERTY VS_STARTUP_PROJECT "${PROJECT_NAME}") + +# Set the default (visual studio) debug working directory and args +set_target_properties("${PROJECT_NAME}" PROPERTIES VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + VS_DEBUGGER_COMMAND_ARGUMENTS "-s 10") \ No newline at end of file diff --git a/examples/cpp/sugarscapeSubModelAPI/src/main.cu b/examples/cpp/sugarscapeSubModelAPI/src/main.cu new file mode 100644 index 000000000..47e84eb01 --- /dev/null +++ b/examples/cpp/sugarscapeSubModelAPI/src/main.cu @@ -0,0 +1,260 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "flamegpu/flamegpu.h" +#include "flamegpu/stockAgent/subModels/SingleAgentDiscreteMovement.h" + +// Grid Size +#define GRID_WIDTH 256 +#define GRID_HEIGHT 256 + +// Growback variables +#define SUGAR_GROWBACK_RATE 1.0f +#define SUGAR_MAX_CAPACITY 7.0f + +/** + * Agent Functions + */ + +// 1. Metabolise & Harvest: Bug eats the sugar at its new location and consumes energy +// This runs AFTER movement, so current_cell_score is already updated by the submodel. +FLAMEGPU_AGENT_FUNCTION(metabolise, flamegpu::MessageNone, flamegpu::MessageNone) { + float sugar = FLAMEGPU->getVariable("sugar"); + float metabolism = FLAMEGPU->getVariable("metabolism"); + float harvested = FLAMEGPU->getVariable("current_cell_score"); + + // Add what we found and subtract what we used + sugar += harvested; + sugar -= metabolism; + + // Death check + if (sugar <= 0.0f) { + return flamegpu::DEAD; + } + + FLAMEGPU->setVariable("sugar", sugar); + return flamegpu::ALIVE; +} + +// 2. Growback: SugarCell grows sugar or is emptied if a bug is currently standing on it +FLAMEGPU_AGENT_FUNCTION(growback, flamegpu::MessageNone, flamegpu::MessageNone) { + float sugar = FLAMEGPU->getVariable("sugar"); + float max_sugar = FLAMEGPU->getVariable("max_sugar"); + int is_occupied = FLAMEGPU->getVariable("is_occupied"); + + if (is_occupied) { + // A bug is here, so it has eaten the sugar + sugar = 0.0f; + } else { + // Grow back + sugar += SUGAR_GROWBACK_RATE; + if (sugar > max_sugar) { + sugar = max_sugar; + } + } + + FLAMEGPU->setVariable("sugar", sugar); + return flamegpu::ALIVE; +} + +/** + * Step function to log simulation state to CSV + */ +FLAMEGPU_STEP_FUNCTION(step_logger) { + unsigned int step = FLAMEGPU->getStepCounter(); + unsigned int bug_count = FLAMEGPU->agent("bug").count(); + printf("Step %u: bugs=%u\n", step, bug_count); + + // Log bugs every step + static std::ofstream bug_log; + if (step == 0) { + bug_log.open("bugs_log.csv"); + bug_log << "step,x,y,sugar,metabolism" << std::endl; + } + // getPopulationData returns a reference to a DeviceAgentVector_impl, so we must use a reference + auto& bug_pop = FLAMEGPU->agent("bug").getPopulationData(); + for (const auto& bug : bug_pop) { + bug_log << step << "," << bug.getVariable("x") << "," << bug.getVariable("y") << "," << bug.getVariable("sugar") << "," << bug.getVariable("metabolism") << std::endl; + } + + // Log cells every 10 steps to save space (and step 0) + static std::ofstream cell_log; + if (step == 0) { + cell_log.open("cells_log.csv"); + cell_log << "step,x,y,sugar,max_sugar" << std::endl; + } + if (step % 10 == 0) { + auto& cell_pop = FLAMEGPU->agent("sugar_cell").getPopulationData(); + for (const auto& cell : cell_pop) { + cell_log << step << "," << cell.getVariable("x") << "," << cell.getVariable("y") << "," << cell.getVariable("sugar") << "," << cell.getVariable("max_sugar") << std::endl; + } + } +} + +/** + * Main + */ +int main(int argc, const char ** argv) { + flamegpu::ModelDescription model("Sugarscape"); + + /** + * Agents + */ + // Bug Agent (The moving agent) + flamegpu::AgentDescription bug = model.newAgent("bug"); + bug.newVariable("sugar"); + bug.newVariable("metabolism"); + bug.newVariable("x"); + bug.newVariable("y"); + bug.newVariable("last_x", -1); + bug.newVariable("last_y", -1); + bug.newVariable("last_resources_x", -1); + bug.newVariable("last_resources_y", -1); + bug.newVariable("current_cell_score", 0.0f); + + // SugarCell Agent (The environment agent) + flamegpu::AgentDescription sugar_cell = model.newAgent("sugar_cell"); + sugar_cell.newVariable("x"); + sugar_cell.newVariable("y"); + sugar_cell.newVariable("sugar"); + sugar_cell.newVariable("max_sugar"); + sugar_cell.newVariable("is_occupied", 0); + + /** + * Submodel Configuration + */ + flamegpu::stockAgent::submodels::SingleAgentDiscreteMovement move_sub_logic; + move_sub_logic.addSingleAgentDiscreteMovementSubmodel(model, GRID_WIDTH, GRID_HEIGHT); + + // Bind Bug to the submodel's moving agent + move_sub_logic.setMovingAgent("bug", + { + {"x", "x"}, + {"y", "y"}, + {"last_x", "last_x"}, + {"last_y", "last_y"}, + {"last_resources_x", "last_resources_x"}, + {"last_resources_y", "last_resources_y"}, + {"current_cell_score", "current_cell_score"} + }, + { + {"active", flamegpu::ModelData::DEFAULT_STATE} + }); + + // Bind SugarCell to the submodel's environment agent + move_sub_logic.setEnvironmentAgent("sugar_cell", + { + {"x", "x"}, + {"y", "y"}, + {"is_occupied", "is_occupied"}, + {"cell_score", "sugar"} + }, + { + {"active", flamegpu::ModelData::DEFAULT_STATE} + }); + + /** + * Functions and Layers + */ + bug.newFunction("metabolise", metabolise).setAllowAgentDeath(true); + sugar_cell.newFunction("growback", growback); + + // Layer 1: Movement + model.newLayer().addSubModel(move_sub_logic.getSubModelDescription()); + + // Layer 2: Life logic (Metabolism and Growback can happen in parallel) + { + auto l = model.newLayer(); + l.addAgentFunction(metabolise); + l.addAgentFunction(growback); + } + + model.addStepFunction(step_logger); + + /** + * Simulation Setup + */ + flamegpu::CUDASimulation cudaSimulation(model); + cudaSimulation.initialise(argc, argv); + cudaSimulation.SimulationConfig().steps = 100; + + // If no input file, generate a random starting state + if (cudaSimulation.getSimulationConfig().input_file.empty()) { + std::mt19937_64 rng(42); + // Define sugar hotspots (spatial distribution of max_sugar) + std::vector> sugar_hotspots; + { + std::uniform_int_distribution width_dist(0, GRID_WIDTH - 1); + std::uniform_int_distribution height_dist(0, GRID_HEIGHT - 1); + std::uniform_int_distribution radius_dist(15, 45); + float hotspot_area = 0; + while (hotspot_area < (GRID_WIDTH * GRID_HEIGHT) * 0.6f) { + unsigned int rad = radius_dist(rng); + std::array hs = {width_dist(rng), height_dist(rng), rad, (unsigned int)SUGAR_MAX_CAPACITY}; + sugar_hotspots.push_back(hs); + hotspot_area += 3.141f * rad * rad; + } + } + + // Generate a shuffled list of all grid indices to place bugs uniquely + std::vector indices(GRID_WIDTH * GRID_HEIGHT); + std::iota(indices.begin(), indices.end(), 0); + std::shuffle(indices.begin(), indices.end(), rng); + + const float bug_density = 0.05f; + const unsigned int BUG_COUNT = (unsigned int)((GRID_WIDTH * GRID_HEIGHT) * bug_density); + + std::vector bug_at(GRID_WIDTH * GRID_HEIGHT, false); + flamegpu::AgentVector bug_pop(bug, BUG_COUNT); + std::uniform_real_distribution bug_sugar_dist(10.0f, 30.0f); + std::uniform_real_distribution bug_metabolism_dist(1.0f, 2.5f); + + for (unsigned int i = 0; i < BUG_COUNT; ++i) { + int idx = indices[i]; + bug_at[idx] = true; + auto instance = bug_pop[i]; + instance.setVariable("x", idx / GRID_HEIGHT); + instance.setVariable("y", idx % GRID_HEIGHT); + instance.setVariable("sugar", bug_sugar_dist(rng)); + instance.setVariable("metabolism", bug_metabolism_dist(rng)); + } + + flamegpu::AgentVector cell_pop(sugar_cell, GRID_WIDTH * GRID_HEIGHT); + for (unsigned int x = 0; x < GRID_WIDTH; ++x) { + for (unsigned int y = 0; y < GRID_HEIGHT; ++y) { + unsigned int idx = x * GRID_HEIGHT + y; + auto instance = cell_pop[idx]; + instance.setVariable("x", (int)x); + instance.setVariable("y", (int)y); + instance.setVariable("is_occupied", bug_at[idx] ? 1 : 0); + + float max_val = 0; + for (auto &hs : sugar_hotspots) { + float dx = (float)hs[0] - (float)x; + float dy = (float)hs[1] - (float)y; + float dist = sqrtf(dx*dx + dy*dy); + if (dist < (float)hs[2]) { + float v = (float)hs[3] * (1.0f - (dist / (float)hs[2])); + if (v > max_val) max_val = v; + } + } + instance.setVariable("max_sugar", max_val); + instance.setVariable("sugar", max_val); + } + } + + cudaSimulation.setPopulationData(bug_pop); + cudaSimulation.setPopulationData(cell_pop); + } + + cudaSimulation.simulate(); + + return 0; +} diff --git a/examples/cpp/sugarscapeSubModelAPI/visualizesimulationsugar.py b/examples/cpp/sugarscapeSubModelAPI/visualizesimulationsugar.py new file mode 100644 index 000000000..6e5458d21 --- /dev/null +++ b/examples/cpp/sugarscapeSubModelAPI/visualizesimulationsugar.py @@ -0,0 +1,156 @@ +import matplotlib.pyplot as plt +import numpy as np +import matplotlib.animation as animation +import os + +def visualize(): + # Load data + print("Loading logs...") + try: + if not os.path.exists('bugs_log.csv'): + print("bugs_log.csv not found.") + return + bugs_data = np.genfromtxt('bugs_log.csv', delimiter=',', skip_header=1) + + if not os.path.exists('cells_log.csv'): + print("cells_log.csv not found.") + return + cells_data = np.genfromtxt('cells_log.csv', delimiter=',', skip_header=1) + except Exception as e: + print(f"Error loading logs: {e}") + return + + # Extract columns + # bugs: step,x,y,sugar,metabolism + bug_steps = bugs_data[:, 0] + bug_x = bugs_data[:, 1] + bug_y = bugs_data[:, 2] + bug_sugar = bugs_data[:, 3] + + # cells: step,x,y,sugar,max_sugar + cell_steps = cells_data[:, 0] + cell_x = cells_data[:, 1] + cell_y = cells_data[:, 2] + cell_sugar = cells_data[:, 3] + + unique_bug_steps = np.sort(np.unique(bug_steps)).astype(int) + unique_cell_steps = np.sort(np.unique(cell_steps)).astype(int) + grid_dim = 256 + + # 1. Statistics Plot + print("Generating statistics plot...") + bug_count = [] + avg_bug_sugar = [] + avg_cell_sugar = [] + + for s in unique_bug_steps: + mask = (bug_steps == s) + bug_count.append(np.sum(mask)) + avg_bug_sugar.append(np.mean(bug_sugar[mask])) + + for s in unique_cell_steps: + mask = (cell_steps == s) + avg_cell_sugar.append(np.mean(cell_sugar[mask])) + + fig_stats, ax1 = plt.subplots(figsize=(10, 6)) + ax1.set_xlabel('Step') + ax1.set_ylabel('Bug Count / Avg Sugar', color='tab:red') + ax1.plot(unique_bug_steps, bug_count, color='tab:red', label='Bug Count') + ax1.plot(unique_bug_steps, avg_bug_sugar, color='tab:orange', label='Avg Bug Sugar') + ax1.tick_params(axis='y', labelcolor='tab:red') + ax1.legend(loc='upper left') + + ax2 = ax1.twinx() + ax2.set_ylabel('Avg Cell Sugar', color='tab:blue') + # Resample unique_cell_steps to match unique_bug_steps for plotting if needed, + # but here we just plot them as they are. + ax2.plot(unique_cell_steps, avg_cell_sugar, color='tab:blue', marker='o', label='Avg Cell Sugar') + ax2.tick_params(axis='y', labelcolor='tab:blue') + ax2.legend(loc='upper right') + + plt.title('Sugarscape Simulation Statistics') + fig_stats.tight_layout() + plt.savefig('sugarscape_stats.png') + print("Saved sugarscape_stats.png") + + # 2. Animation + print("Creating animation (this might take a moment)...") + fig_anim, ax_anim = plt.subplots(figsize=(8, 8)) + + # Helper to get grid at a specific step + def get_grid(step): + # find closest available cell step (rounding down) + available_steps = unique_cell_steps[unique_cell_steps <= step] + if len(available_steps) == 0: + s = unique_cell_steps[0] + else: + s = available_steps[-1] + + mask = (cell_steps == s) + grid = np.zeros((grid_dim, grid_dim)) + grid[cell_x[mask].astype(int), cell_y[mask].astype(int)] = cell_sugar[mask] + return grid + + grid_img = ax_anim.imshow(get_grid(unique_bug_steps[0]).T, origin='lower', cmap='YlOrBr', + extent=[0, grid_dim, 0, grid_dim], vmin=0, vmax=7) + + # Plot bugs + mask0 = (bug_steps == unique_bug_steps[0]) + scat = ax_anim.scatter(bug_x[mask0], bug_y[mask0], c='red', s=2, alpha=0.6, label='Bugs') + + plt.colorbar(grid_img, ax=ax_anim, label='Sugar Level') + ax_anim.set_title(f'Sugarscape - Step {unique_bug_steps[0]}') + ax_anim.set_xlim(0, grid_dim) + ax_anim.set_ylim(0, grid_dim) + + def update(frame): + s = unique_bug_steps[frame] + # Update grid if this step has cell data + if s in unique_cell_steps: + grid_img.set_data(get_grid(s).T) + + mask = (bug_steps == s) + scat.set_offsets(np.c_[bug_x[mask], bug_y[mask]]) + ax_anim.set_title(f'Sugarscape - Step {s} (Bugs: {int(np.sum(mask))})') + return grid_img, scat + + # Reduce number of frames for GIF if too many + frames = unique_bug_steps + if len(frames) > 100: + frames = frames[::len(frames)//100] + + ani = animation.FuncAnimation(fig_anim, update, frames=len(frames), interval=100, blit=True) + + try: + ani.save('sugarscape_simulation.gif', writer='pillow', fps=10) + print("Saved sugarscape_simulation.gif") + except Exception as e: + print(f"Could not save animation: {e}") + + # 3. Static snapshots (Start, Mid, End) + print("Generating snapshots...") + steps_to_plot = [unique_bug_steps[0], unique_bug_steps[len(unique_bug_steps)//2], unique_bug_steps[-1]] + fig_snap, axes = plt.subplots(1, 3, figsize=(18, 6)) + for i, s in enumerate(steps_to_plot): + ax = axes[i] + grid = get_grid(s) + ax.imshow(grid.T, origin='lower', cmap='YlOrBr', extent=[0, grid_dim, 0, grid_dim], vmin=0, vmax=7) + mask = (bug_steps == s) + ax.scatter(bug_x[mask], bug_y[mask], c='red', s=1, alpha=0.5) + ax.set_title(f'Step {s} (Bugs: {int(np.sum(mask))})') + ax.set_xlim(0, grid_dim) + ax.set_ylim(0, grid_dim) + + plt.tight_layout() + plt.savefig('sugarscape_snapshots.png') + print("Saved sugarscape_snapshots.png") + + print("Visualization complete.") + +if __name__ == "__main__": + # Change to the directory of the script if logs aren't in CWD + if not os.path.exists('bugs_log.csv'): + script_dir = os.path.dirname(os.path.abspath(__file__)) + if script_dir: + os.chdir(script_dir) + visualize() diff --git a/examples/python_rtc/sugarscapeModel/sugarscape.py b/examples/python_rtc/sugarscapeModel/sugarscape.py new file mode 100644 index 000000000..900558377 --- /dev/null +++ b/examples/python_rtc/sugarscapeModel/sugarscape.py @@ -0,0 +1,246 @@ +import os +import pyflamegpu +import pyflamegpu.codegen +import csv +import random, math + +# Ensure CUDA_PATH is set for RTC (Jitify) to find headers +if "CUDA_PATH" not in os.environ: + os.environ["CUDA_PATH"] = "/usr/local/cuda" + +GRID_WIDTH: pyflamegpu.constant = 256 +GRID_HEIGHT: pyflamegpu.constant = 256 + +# Growback variables +SUGAR_GROWBACK_RATE: pyflamegpu.constant = 1.0 +SUGAR_MAX_CAPACITY: pyflamegpu.constant = 7.0 + +@pyflamegpu.agent_function +def metabolise(message_in: pyflamegpu.MessageNone, message_out: pyflamegpu.MessageNone): + sugar = pyflamegpu.getVariableFloat("sugar") + metabolism = pyflamegpu.getVariableFloat("metabolism") + harvested = pyflamegpu.getVariableFloat("current_cell_score") + + # Add what we found and subtract what we used + sugar += harvested; + sugar -= metabolism; + + # Death check + if (sugar <= 0.0 ): + return pyflamegpu.DEAD + + pyflamegpu.setVariableFloat("sugar", sugar) + return pyflamegpu.ALIVE + +@pyflamegpu.agent_function +def growback(message_in: pyflamegpu.MessageNone, message_out: pyflamegpu.MessageNone): + sugar = pyflamegpu.getVariableFloat("sugar") + max_sugar = pyflamegpu.getVariableFloat("max_sugar") + is_occupied = pyflamegpu.getVariableInt("is_occupied") + + + if (is_occupied): + # A bug is here, so it has eaten the sugar + sugar = 0.0 + else: + # Grow back + sugar += SUGAR_GROWBACK_RATE + if (sugar > max_sugar): + sugar = max_sugar + + pyflamegpu.setVariableFloat("sugar", sugar) + return pyflamegpu.ALIVE + + +class step_logger(pyflamegpu.HostFunction): + def __init__(self): + super().__init__() + self.bug_log_file = None + self.cell_log_file = None + self.bug_writer = None + self.cell_writer = None + + def run(self, FLAMEGPU): + step = FLAMEGPU.getStepCounter() + bug_count = FLAMEGPU.agent("bug").count() + print(f"Step {step}: bugs={bug_count}") + + # 1. Log bugs every step + if step == 0: + self.bug_log_file = open("bugs_log.csv", "w", newline='') + self.bug_writer = csv.writer(self.bug_log_file) + self.bug_writer.writerow(["step", "x", "y", "sugar", "metabolism"]) + + bug_pop = FLAMEGPU.agent("bug").getPopulationData() + for bug in bug_pop: + self.bug_writer.writerow([ + step, + bug.getVariableInt("x"), + bug.getVariableInt("y"), + bug.getVariableFloat("sugar"), + bug.getVariableFloat("metabolism") + ]) + self.bug_log_file.flush() + + # 2. Log cells every 10 steps + if step == 0: + self.cell_log_file = open("cells_log.csv", "w", newline='') + self.cell_writer = csv.writer(self.cell_log_file) + self.cell_writer.writerow(["step", "x", "y", "sugar", "max_sugar"]) + + if step % 10 == 0: + cell_pop = FLAMEGPU.agent("sugar_cell").getPopulationData() + for cell in cell_pop: + self.cell_writer.writerow([ + step, + cell.getVariableInt("x"), + cell.getVariableInt("y"), + cell.getVariableFloat("sugar"), + cell.getVariableFloat("max_sugar") + ]) + self.cell_log_file.flush() + + +class random_initialisation(pyflamegpu.HostFunction): + def __init__(self): + super().__init__() + + def run(self, FLAMEGPU): + # 1. Generate Sugar Hotspots + sugar_hotspots = [] + hotspot_area = 0 + target_area = (GRID_WIDTH * GRID_HEIGHT) * 0.6 + + while hotspot_area < target_area: + rad = random.randint(15, 45) + hs = [random.randint(0, GRID_WIDTH - 1), + random.randint(0, GRID_HEIGHT - 1), + rad, + SUGAR_MAX_CAPACITY] + sugar_hotspots.append(hs) + hotspot_area += math.pi * rad * rad + + # 2. Place Bugs randomly + bug_density = 0.05 + bug_count = int((GRID_WIDTH * GRID_HEIGHT) * bug_density) + + indices = list(range(GRID_WIDTH * GRID_HEIGHT)) + random.shuffle(indices) + + bug_at = [False] * (GRID_WIDTH * GRID_HEIGHT) + for i in range(bug_count): + idx = indices[i] + bug_at[idx] = True + + b = FLAMEGPU.agent("bug").newAgent() + b.setVariableInt("x", idx // GRID_HEIGHT) + b.setVariableInt("y", idx % GRID_HEIGHT) + b.setVariableFloat("sugar", random.uniform(10.0, 30.0)) + b.setVariableFloat("metabolism", random.uniform(1.0, 2.5)) + + # 3. Create Sugar Cells + for x in range(GRID_WIDTH): + for y in range(GRID_HEIGHT): + idx = x * GRID_HEIGHT + y + cell = FLAMEGPU.agent("sugar_cell").newAgent() + cell.setVariableInt("x", x) + cell.setVariableInt("y", y) + cell.setVariableInt("is_occupied", 1 if bug_at[idx] else 0) + + max_val = 0.0 + for hs in sugar_hotspots: + dx = hs[0] - x + dy = hs[1] - y + dist = math.sqrt(dx*dx + dy*dy) + if dist < hs[2]: + v = hs[3] * (1.0 - (dist / hs[2])) + if v > max_val: + max_val = v + + cell.setVariableFloat("max_sugar", max_val) + cell.setVariableFloat("sugar", max_val) + +if __name__ == "__main__": + + # create the model and define the environmet, agents and dependancies + model = pyflamegpu.ModelDescription("sugarscape") + + bug = model.newAgent("bug") + + bug.newVariableInt("x") + bug.newVariableInt("y") + bug.newVariableFloat("sugar") + bug.newVariableFloat("metabolism") + bug.newVariableInt("last_x") + bug.newVariableInt("last_y") + bug.newVariableInt("last_resources_x") + bug.newVariableInt("last_resources_y") + bug.newVariableFloat("current_cell_score") + + sugar_cell = model.newAgent("sugar_cell") + + sugar_cell.newVariableInt("x") + sugar_cell.newVariableInt("y") + sugar_cell.newVariableFloat("sugar") + sugar_cell.newVariableFloat("max_sugar") + sugar_cell.newVariableInt("is_occupied") + + submodel = pyflamegpu.SingleAgentDiscreteMovement() + + submodel.addSingleAgentDiscreteMovementSubmodel(model, GRID_HEIGHT, GRID_WIDTH) + + bug_vars = pyflamegpu.map_string_string() + bug_vars["x"] = "x" + bug_vars["y"] = "y" + bug_vars["last_x"] = "last_x" + bug_vars["last_y"] = "last_y" + bug_vars["last_resources_x"] = "last_resources_x" + bug_vars["last_resources_y"] = "last_resources_y" + bug_vars["current_cell_score"] = "current_cell_score" + + bug_states = pyflamegpu.map_string_string() + bug_states["active"] = "default" # Ecco di nuovo il nostro "default" + + submodel.setMovingAgent("bug", + bug_vars, + bug_states + ) + + env_vars = pyflamegpu.map_string_string() + env_vars["x"] = "x" + env_vars["y"] = "y" + env_vars["is_occupied"] = "is_occupied" + env_vars["cell_score"] = "sugar" + + # 4. Definisci la mappatura degli stati per l'ambiente + env_states = pyflamegpu.map_string_string() + env_states["active"] = "default" + + submodel.setEnvironmentAgent("sugar_cell", + env_vars, + env_states + ) + + metabolise_fn = bug.newRTCFunction("metabolise", pyflamegpu.codegen.translate(metabolise)) + metabolise_fn.setAllowAgentDeath(True) + growback_fn = sugar_cell.newRTCFunction("growback", pyflamegpu.codegen.translate(growback)) + + layer1 = model.newLayer() + layer1.addAgentFunction(growback_fn) + layer2 = model.newLayer() + layer2.addAgentFunction(metabolise_fn) + layer3 = model.newLayer() + layer3.addSubModel(submodel.getSubModelDescription()) + + my_step = step_logger() + model.addStepFunction(my_step) + + # Register the random initialisation + model.addInitFunction(random_initialisation()) + + # Set up and run the simulation + cudaSimulation = pyflamegpu.CUDASimulation(model) + cudaSimulation.SimulationConfig().steps = 100 + cudaSimulation.simulate() + + print("Starting pyflamegpu example: Sugarscape model") \ No newline at end of file diff --git a/examples/python_rtc/sugarscapeModel/visualizesimulationsugar.py b/examples/python_rtc/sugarscapeModel/visualizesimulationsugar.py new file mode 100644 index 000000000..6e5458d21 --- /dev/null +++ b/examples/python_rtc/sugarscapeModel/visualizesimulationsugar.py @@ -0,0 +1,156 @@ +import matplotlib.pyplot as plt +import numpy as np +import matplotlib.animation as animation +import os + +def visualize(): + # Load data + print("Loading logs...") + try: + if not os.path.exists('bugs_log.csv'): + print("bugs_log.csv not found.") + return + bugs_data = np.genfromtxt('bugs_log.csv', delimiter=',', skip_header=1) + + if not os.path.exists('cells_log.csv'): + print("cells_log.csv not found.") + return + cells_data = np.genfromtxt('cells_log.csv', delimiter=',', skip_header=1) + except Exception as e: + print(f"Error loading logs: {e}") + return + + # Extract columns + # bugs: step,x,y,sugar,metabolism + bug_steps = bugs_data[:, 0] + bug_x = bugs_data[:, 1] + bug_y = bugs_data[:, 2] + bug_sugar = bugs_data[:, 3] + + # cells: step,x,y,sugar,max_sugar + cell_steps = cells_data[:, 0] + cell_x = cells_data[:, 1] + cell_y = cells_data[:, 2] + cell_sugar = cells_data[:, 3] + + unique_bug_steps = np.sort(np.unique(bug_steps)).astype(int) + unique_cell_steps = np.sort(np.unique(cell_steps)).astype(int) + grid_dim = 256 + + # 1. Statistics Plot + print("Generating statistics plot...") + bug_count = [] + avg_bug_sugar = [] + avg_cell_sugar = [] + + for s in unique_bug_steps: + mask = (bug_steps == s) + bug_count.append(np.sum(mask)) + avg_bug_sugar.append(np.mean(bug_sugar[mask])) + + for s in unique_cell_steps: + mask = (cell_steps == s) + avg_cell_sugar.append(np.mean(cell_sugar[mask])) + + fig_stats, ax1 = plt.subplots(figsize=(10, 6)) + ax1.set_xlabel('Step') + ax1.set_ylabel('Bug Count / Avg Sugar', color='tab:red') + ax1.plot(unique_bug_steps, bug_count, color='tab:red', label='Bug Count') + ax1.plot(unique_bug_steps, avg_bug_sugar, color='tab:orange', label='Avg Bug Sugar') + ax1.tick_params(axis='y', labelcolor='tab:red') + ax1.legend(loc='upper left') + + ax2 = ax1.twinx() + ax2.set_ylabel('Avg Cell Sugar', color='tab:blue') + # Resample unique_cell_steps to match unique_bug_steps for plotting if needed, + # but here we just plot them as they are. + ax2.plot(unique_cell_steps, avg_cell_sugar, color='tab:blue', marker='o', label='Avg Cell Sugar') + ax2.tick_params(axis='y', labelcolor='tab:blue') + ax2.legend(loc='upper right') + + plt.title('Sugarscape Simulation Statistics') + fig_stats.tight_layout() + plt.savefig('sugarscape_stats.png') + print("Saved sugarscape_stats.png") + + # 2. Animation + print("Creating animation (this might take a moment)...") + fig_anim, ax_anim = plt.subplots(figsize=(8, 8)) + + # Helper to get grid at a specific step + def get_grid(step): + # find closest available cell step (rounding down) + available_steps = unique_cell_steps[unique_cell_steps <= step] + if len(available_steps) == 0: + s = unique_cell_steps[0] + else: + s = available_steps[-1] + + mask = (cell_steps == s) + grid = np.zeros((grid_dim, grid_dim)) + grid[cell_x[mask].astype(int), cell_y[mask].astype(int)] = cell_sugar[mask] + return grid + + grid_img = ax_anim.imshow(get_grid(unique_bug_steps[0]).T, origin='lower', cmap='YlOrBr', + extent=[0, grid_dim, 0, grid_dim], vmin=0, vmax=7) + + # Plot bugs + mask0 = (bug_steps == unique_bug_steps[0]) + scat = ax_anim.scatter(bug_x[mask0], bug_y[mask0], c='red', s=2, alpha=0.6, label='Bugs') + + plt.colorbar(grid_img, ax=ax_anim, label='Sugar Level') + ax_anim.set_title(f'Sugarscape - Step {unique_bug_steps[0]}') + ax_anim.set_xlim(0, grid_dim) + ax_anim.set_ylim(0, grid_dim) + + def update(frame): + s = unique_bug_steps[frame] + # Update grid if this step has cell data + if s in unique_cell_steps: + grid_img.set_data(get_grid(s).T) + + mask = (bug_steps == s) + scat.set_offsets(np.c_[bug_x[mask], bug_y[mask]]) + ax_anim.set_title(f'Sugarscape - Step {s} (Bugs: {int(np.sum(mask))})') + return grid_img, scat + + # Reduce number of frames for GIF if too many + frames = unique_bug_steps + if len(frames) > 100: + frames = frames[::len(frames)//100] + + ani = animation.FuncAnimation(fig_anim, update, frames=len(frames), interval=100, blit=True) + + try: + ani.save('sugarscape_simulation.gif', writer='pillow', fps=10) + print("Saved sugarscape_simulation.gif") + except Exception as e: + print(f"Could not save animation: {e}") + + # 3. Static snapshots (Start, Mid, End) + print("Generating snapshots...") + steps_to_plot = [unique_bug_steps[0], unique_bug_steps[len(unique_bug_steps)//2], unique_bug_steps[-1]] + fig_snap, axes = plt.subplots(1, 3, figsize=(18, 6)) + for i, s in enumerate(steps_to_plot): + ax = axes[i] + grid = get_grid(s) + ax.imshow(grid.T, origin='lower', cmap='YlOrBr', extent=[0, grid_dim, 0, grid_dim], vmin=0, vmax=7) + mask = (bug_steps == s) + ax.scatter(bug_x[mask], bug_y[mask], c='red', s=1, alpha=0.5) + ax.set_title(f'Step {s} (Bugs: {int(np.sum(mask))})') + ax.set_xlim(0, grid_dim) + ax.set_ylim(0, grid_dim) + + plt.tight_layout() + plt.savefig('sugarscape_snapshots.png') + print("Saved sugarscape_snapshots.png") + + print("Visualization complete.") + +if __name__ == "__main__": + # Change to the directory of the script if logs aren't in CWD + if not os.path.exists('bugs_log.csv'): + script_dir = os.path.dirname(os.path.abspath(__file__)) + if script_dir: + os.chdir(script_dir) + visualize() diff --git a/include/flamegpu/stockAgent/subModels/AbstractSubmodels.h b/include/flamegpu/stockAgent/subModels/AbstractSubmodels.h new file mode 100644 index 000000000..9fed9f9d9 --- /dev/null +++ b/include/flamegpu/stockAgent/subModels/AbstractSubmodels.h @@ -0,0 +1,41 @@ +#ifndef INCLUDE_FLAMEGPU_STOCKAGENT_SUBMODELS_ABSTRACTSUBMODELS_H_ +#define INCLUDE_FLAMEGPU_STOCKAGENT_SUBMODELS_ABSTRACTSUBMODELS_H_ + +#include +#include "flamegpu/flamegpu.h" + +namespace flamegpu { +namespace stockAgent { +namespace submodels { + /** + * Abstract base class for submodels. + * Submodels are used to group together related agent functions and variables, and to allow for modularity and reusability of code. + * Submodels can be nested within other submodels, allowing for hierarchical organization of code. + */ + class AbstractSubmodel { + public: + virtual ~AbstractSubmodel() = default; + + /** + * Returns the underlying FLAME GPU SubModelDescription. + * Throws if the submodel hasn't been initialized/added to a model yet. + */ + virtual flamegpu::SubModelDescription getSubModelDescription() const = 0; + + /** + * Validates that all required agents and variables have been mapped. + */ + virtual void validate() = 0; + + /** + * Returns the name of the submodel instance. + */ + virtual std::string getName() const = 0; + }; + + +} // namespace submodels +} // namespace stockAgent +} // namespace flamegpu + +#endif // INCLUDE_FLAMEGPU_STOCKAGENT_SUBMODELS_ABSTRACTSUBMODELS_H_ diff --git a/include/flamegpu/stockAgent/subModels/SingleAgentDiscreteMovement.h b/include/flamegpu/stockAgent/subModels/SingleAgentDiscreteMovement.h new file mode 100644 index 000000000..70fbb492a --- /dev/null +++ b/include/flamegpu/stockAgent/subModels/SingleAgentDiscreteMovement.h @@ -0,0 +1,74 @@ +#ifndef INCLUDE_FLAMEGPU_STOCKAGENT_SUBMODELS_SINGLEAGENTDISCRETEMOVEMENT_H_ +#define INCLUDE_FLAMEGPU_STOCKAGENT_SUBMODELS_SINGLEAGENTDISCRETEMOVEMENT_H_ + +#include +#include +#include +#include + +#include "AbstractSubmodels.h" + + +namespace flamegpu { +namespace stockAgent { +namespace submodels { + +/** + * Submodel for handling agent movement logic on a discrete 2D grid. + * This submodel now supports two distinct agent types: + * 1. A moving agent (e.g., Bee, Person) + * 2. An environment agent (e.g., Flower, Grid Cell) + */ +class SingleAgentDiscreteMovement : public AbstractSubmodel { + public: + /** + * Empty constructor. Object must be initialized via addSingleAgentDiscreteMovementSubmodel(). + */ + SingleAgentDiscreteMovement() = default; + + /** + * Defines the movement submodel and adds it to the provided parent model. + * @param model The parent ModelDescription + * @param ENV_SIZE_X Width of the environment + * @param ENV_SIZE_Y Height of the environment + * @return The created SubModelDescription + */ + flamegpu::SubModelDescription addSingleAgentDiscreteMovementSubmodel(flamegpu::ModelDescription &model, int ENV_SIZE_X, int ENV_SIZE_Y); + + /** + * Binds a parent agent to the submodel's internal moving agent. + */ + void setMovingAgent(const std::string& parent_name, + const std::map& var_map = {}, + const std::map& state_map = {}, + bool auto_map = false); + + /** + * Binds a parent agent to the submodel's internal environment agent (grid cell). + */ + void setEnvironmentAgent(const std::string& parent_name, + const std::map& var_map = {}, + const std::map& state_map = {}, + bool auto_map = false); + + flamegpu::SubModelDescription getSubModelDescription() const override; + + void validate() override; + + std::string getName() const override; + + private: + // SubModelDescription is a proxy. We use optional to allow late initialization without unique_ptr. + std::optional smm; + + // Internal setup methods + void setMessages(flamegpu::AgentDescription &movingAgent, flamegpu::AgentDescription &envAgent, int ENV_SIZE_X, int ENV_SIZE_Y); + void defineMessageSubmodule(flamegpu::ModelDescription &smm, int ENV_SIZE_X, int ENV_SIZE_Y); + void defineLayer(flamegpu::ModelDescription &smm); +}; + +} // namespace submodels +} // namespace stockAgent +} // namespace flamegpu + +#endif // INCLUDE_FLAMEGPU_STOCKAGENT_SUBMODELS_SINGLEAGENTDISCRETEMOVEMENT_H_ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f71d21797..232525a66 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -299,6 +299,8 @@ SET(SRC_INCLUDE ${FLAMEGPU_ROOT}/include/flamegpu/detail/Timer.h ${FLAMEGPU_ROOT}/include/flamegpu/detail/TestSuiteTelemetry.h ${FLAMEGPU_ROOT}/include/flamegpu/detail/JitifyCache.h + ${FLAMEGPU_ROOT}/include/flamegpu/stockAgent/subModels/AbstractSubmodels.h + ${FLAMEGPU_ROOT}/include/flamegpu/stockAgent/subModels/SingleAgentDiscreteMovement.h ) SET(SRC_FLAMEGPU ${FLAMEGPU_ROOT}/src/flamegpu/exception/FLAMEGPUException.cpp @@ -385,6 +387,7 @@ SET(SRC_FLAMEGPU ${FLAMEGPU_ROOT}/src/flamegpu/detail/wddm.cu ${FLAMEGPU_ROOT}/src/flamegpu/detail/JitifyCache.cu ${FLAMEGPU_ROOT}/src/flamegpu/detail/TestSuiteTelemetry.cpp + ${FLAMEGPU_ROOT}/src/flamegpu/stockAgent/subModels/SingleAgentDiscreteMovement.cu ) SET(SRC_DYNAMIC ${DYNAMIC_VERSION_SRC_DEST} diff --git a/src/flamegpu/stockAgent/subModels/SingleAgentDiscreteMovement.cu b/src/flamegpu/stockAgent/subModels/SingleAgentDiscreteMovement.cu new file mode 100644 index 000000000..c29efc503 --- /dev/null +++ b/src/flamegpu/stockAgent/subModels/SingleAgentDiscreteMovement.cu @@ -0,0 +1,427 @@ +#include "flamegpu/stockAgent/subModels/SingleAgentDiscreteMovement.h" + +#include +#include +#include +#include + +#include "flamegpu/flamegpu.h" + +using std::string; +using std::map; + +namespace flamegpu { +namespace stockAgent { +namespace submodels { + +const char* INTERNAL_MOVING_AGENT_NAME = "MovingAgent"; +const char* INTERNAL_ENV_AGENT_NAME = "GridCell"; + +namespace { + + /** + * 1. Environment agent broadcasts its status (score/nectar and current occupancy) + */ + FLAMEGPU_AGENT_FUNCTION(envAgent_broadcast_status, MessageNone, MessageArray2D) { + int x = FLAMEGPU->getVariable("x"); + int y = FLAMEGPU->getVariable("y"); + int is_occupied = FLAMEGPU->getVariable("is_occupied"); + float score = FLAMEGPU->getVariable("cell_score"); + + FLAMEGPU->message_out.setIndex(x, y); + FLAMEGPU->message_out.setVariable("is_occupied", is_occupied); + FLAMEGPU->message_out.setVariable("cell_score", score); + return ALIVE; + } + + /** + * 2. Moving agent looks at neighbors and requests a move to the best UNOCCUPIED one. + */ + FLAMEGPU_AGENT_FUNCTION(movingAgent_request_move, MessageArray2D, MessageArray2D) { + // Reset movement status ONLY on the first internal iteration of the submodel call. + if (FLAMEGPU->getStepCounter() == 0) { + FLAMEGPU->setVariable("moved_this_step", 0); + FLAMEGPU->setVariable("target_x", -1); + FLAMEGPU->setVariable("target_y", -1); + } + + if (FLAMEGPU->getVariable("moved_this_step") == 1) { + // Always output a message to keep the MessageArray2D dense/occupied at current location + FLAMEGPU->message_out.setIndex(FLAMEGPU->getVariable("x"), FLAMEGPU->getVariable("y")); + FLAMEGPU->message_out.setVariable("requester_id", FLAMEGPU->getID()); + FLAMEGPU->message_out.setVariable("target_x", -1); + FLAMEGPU->message_out.setVariable("target_y", -1); + FLAMEGPU->message_out.setVariable("priority", -1.0f); + return ALIVE; + } + + int x = FLAMEGPU->getVariable("x"); + int y = FLAMEGPU->getVariable("y"); + int lx = FLAMEGPU->getVariable("last_x"); + int ly = FLAMEGPU->getVariable("last_y"); + int lfx = FLAMEGPU->getVariable("last_resources_x"); + int lfy = FLAMEGPU->getVariable("last_resources_y"); + float current_cell_score = FLAMEGPU->getVariable("current_cell_score"); + + float best_neighbor_score = -1e10f; + float max_tie_breaker = -1.0f; + int target_x = -1; + int target_y = -1; + + for (auto &msg : FLAMEGPU->message_in(x, y, 1)) { + int mx = msg.getX(); + int my = msg.getY(); + + // Consider only unoccupied cells + // AND exclude the cell we just came from (lx, ly) + // AND exclude the last resource we visited (lfx, lfy) + if (msg.getVariable("is_occupied") == 0 && !(mx == lx && my == ly) && !(mx == lfx && my == lfy)) { + float n = msg.getVariable("cell_score"); + float tie_breaker = FLAMEGPU->random.uniform(); + + if (n > best_neighbor_score || (n == best_neighbor_score && tie_breaker > max_tie_breaker)) { + best_neighbor_score = n; + max_tie_breaker = tie_breaker; + target_x = mx; + target_y = my; + } + } + } + + // Only request a move if the best neighbor is at least as good as current spot + if (target_x != -1 && best_neighbor_score >= current_cell_score) { + FLAMEGPU->setVariable("target_x", target_x); + FLAMEGPU->setVariable("target_y", target_y); + } else { + FLAMEGPU->setVariable("target_x", -1); + FLAMEGPU->setVariable("target_y", -1); + } + + FLAMEGPU->message_out.setIndex(x, y); + FLAMEGPU->message_out.setVariable("requester_id", FLAMEGPU->getID()); + FLAMEGPU->message_out.setVariable("target_x", FLAMEGPU->getVariable("target_x")); + FLAMEGPU->message_out.setVariable("target_y", FLAMEGPU->getVariable("target_y")); + FLAMEGPU->message_out.setVariable("priority", FLAMEGPU->getVariable("priority")); + + return ALIVE; + } + + /** + * 3. Environment agent resolves conflicts. + */ + FLAMEGPU_AGENT_FUNCTION(envAgent_resolve_conflict, MessageArray2D, MessageArray2D) { + int x = FLAMEGPU->getVariable("x"); + int y = FLAMEGPU->getVariable("y"); + + id_t winner_id = ID_NOT_SET; + float best_priority = -1e10f; + float max_tie_breaker = -1.0f; + + // Only resolve if currently unoccupied + if (FLAMEGPU->getVariable("is_occupied") == 0) { + for (auto &msg : FLAMEGPU->message_in(x, y, 1)) { + if (msg.getVariable("target_x") == x && msg.getVariable("target_y") == y) { + float p = msg.getVariable("priority"); + float tie_breaker = FLAMEGPU->random.uniform(); + + if (p > best_priority || (p == best_priority && tie_breaker > max_tie_breaker)) { + best_priority = p; + max_tie_breaker = tie_breaker; + winner_id = msg.getVariable("requester_id"); + } + } + } + } + + FLAMEGPU->message_out.setIndex(x, y); + FLAMEGPU->message_out.setVariable("winner_id", winner_id); + FLAMEGPU->message_out.setVariable("cell_score", FLAMEGPU->getVariable("cell_score")); + return ALIVE; + } + + /** + * 4. Moving agent checks if it won the cell it requested. + */ + FLAMEGPU_AGENT_FUNCTION(movingAgent_execute_move, MessageArray2D, MessageArray2D) { + int tx = FLAMEGPU->getVariable("target_x"); + int ty = FLAMEGPU->getVariable("target_y"); + int x = FLAMEGPU->getVariable("x"); + int y = FLAMEGPU->getVariable("y"); + id_t my_id = FLAMEGPU->getID(); + + if (tx != -1 && ty != -1) { + auto msg = FLAMEGPU->message_in.at(tx, ty); + if (msg.getVariable("winner_id") == my_id) { + // Update "last" position before moving + FLAMEGPU->setVariable("last_x", x); + FLAMEGPU->setVariable("last_y", y); + + x = tx; + y = ty; + FLAMEGPU->setVariable("x", x); + FLAMEGPU->setVariable("y", y); + FLAMEGPU->setVariable("moved_this_step", 1); + + // If the new cell has resources, update last_resources + float score = msg.getVariable("cell_score"); + if (score > 0.01f) { + FLAMEGPU->setVariable("last_resources_x", x); + FLAMEGPU->setVariable("last_resources_y", y); + } + } + } + + // Always sample current cell score + auto msg = FLAMEGPU->message_in.at(x, y); + FLAMEGPU->setVariable("current_cell_score", msg.getVariable("cell_score")); + + // Notify current location of presence + FLAMEGPU->message_out.setIndex(x, y); + FLAMEGPU->message_out.setVariable("moving_agent_id", my_id); + + return ALIVE; + } + + /** + * 5. Environment agent updates its occupancy status. + */ + FLAMEGPU_AGENT_FUNCTION(envAgent_update_occupancy, MessageArray2D, MessageNone) { + int x = FLAMEGPU->getVariable("x"); + int y = FLAMEGPU->getVariable("y"); + auto msg = FLAMEGPU->message_in.at(x, y); + FLAMEGPU->setVariable("is_occupied", (msg.getVariable("moving_agent_id") != ID_NOT_SET) ? 1 : 0); + return ALIVE; + } + + FLAMEGPU_HOST_CONDITION(move_exit_condition) { + static int iterations = 0; + iterations++; + bool unresolved = FLAMEGPU->agent(INTERNAL_MOVING_AGENT_NAME, "active").count("moved_this_step", 0) > 0; + if (unresolved && iterations < 5) { + return CONTINUE; + } + iterations = 0; + return EXIT; + } + + FLAMEGPU_INIT_FUNCTION(reset_variables) { + auto movingAgent = FLAMEGPU->agent(INTERNAL_MOVING_AGENT_NAME, "active"); + auto &movingPopulation = movingAgent.getPopulationData(); + for (auto agent : movingPopulation) { + agent.setVariable("moved_this_step", 0); + agent.setVariable("target_x", -1); + agent.setVariable("target_y", -1); + } + } + + /** + * Automatically synchronize the is_occupied status of the grid with the starting positions of the agents. + */ + FLAMEGPU_INIT_FUNCTION(initial_occupancy_sync) { + int width = FLAMEGPU->environment.getProperty("submodel_env_width"); + int height = FLAMEGPU->environment.getProperty("submodel_env_height"); + + auto envAgent = FLAMEGPU->agent(INTERNAL_ENV_AGENT_NAME, "active"); + auto &envPop = envAgent.getPopulationData(); + + // 1. Reset all cells to unoccupied + for (auto cell : envPop) { + cell.setVariable("is_occupied", 0); + } + + // 2. Map x,y to cell index for efficiency (assuming standard grid layout) + // If the population size matches width*height, we assume standard indexing. + bool is_standard_grid = static_cast(envPop.size()) == (width * height); + + auto movingAgent = FLAMEGPU->agent(INTERNAL_MOVING_AGENT_NAME, "active"); + auto &movingPop = movingAgent.getPopulationData(); + + for (auto agent : movingPop) { + int ax = agent.getVariable("x"); + int ay = agent.getVariable("y"); + + if (is_standard_grid) { + int index = ax * height + ay; + if (index >= 0 && index < static_cast(envPop.size())) { + envPop[index].setVariable("is_occupied", 1); + } + } else { + // Fallback: Search for the matching cell (O(N) search per agent) + for (auto cell : envPop) { + if (cell.getVariable("x") == ax && cell.getVariable("y") == ay) { + cell.setVariable("is_occupied", 1); + break; + } + } + } + } + } +} // namespace + +flamegpu::SubModelDescription SingleAgentDiscreteMovement::addSingleAgentDiscreteMovementSubmodel(ModelDescription &model, int ENV_SIZE_X, int ENV_SIZE_Y) { + ModelDescription sub_model_move("movement_submodel"); + + sub_model_move.Environment().newProperty("submodel_env_width", ENV_SIZE_X); + sub_model_move.Environment().newProperty("submodel_env_height", ENV_SIZE_Y); + + + AgentDescription movingAgent = sub_model_move.newAgent(INTERNAL_MOVING_AGENT_NAME); + movingAgent.newState("active"); + movingAgent.newVariable("x"); + movingAgent.newVariable("y"); + movingAgent.newVariable("priority", 0.0f); + movingAgent.newVariable("current_cell_score", 0.0f); + movingAgent.newVariable("target_x", -1); + movingAgent.newVariable("target_y", -1); + movingAgent.newVariable("moved_this_step", 0); + movingAgent.newVariable("last_x", -1); + movingAgent.newVariable("last_y", -1); + movingAgent.newVariable("last_resources_x", -1); + movingAgent.newVariable("last_resources_y", -1); + + AgentDescription envAgent = sub_model_move.newAgent(INTERNAL_ENV_AGENT_NAME); + envAgent.newState("active"); + envAgent.newVariable("x"); + envAgent.newVariable("y"); + envAgent.newVariable("is_occupied", 0); + envAgent.newVariable("cell_score", 0.0f); + + defineMessageSubmodule(sub_model_move, ENV_SIZE_X, ENV_SIZE_Y); + setMessages(movingAgent, envAgent, ENV_SIZE_X, ENV_SIZE_Y); + defineLayer(sub_model_move); + sub_model_move.addInitFunction(reset_variables); + sub_model_move.addInitFunction(initial_occupancy_sync); + sub_model_move.addExitCondition(move_exit_condition); + + this->smm = model.newSubModel("MovementInstance", sub_model_move); + this->smm->setMaxSteps(5); + + return *(this->smm); +} + +void SingleAgentDiscreteMovement::setMovingAgent(const string& parent_name, + const map& var_map, + const map& state_map, + bool auto_map) { + if (!smm.has_value()) { + throw exception::InvalidSubModel("SingleAgentDiscreteMovement submodel was not initialized. Call addSingleAgentDiscreteMovementSubmodel() first."); + } + auto agent_map = this->smm->bindAgent(INTERNAL_MOVING_AGENT_NAME, parent_name, auto_map, auto_map); + for (auto const& [internal_var, parent_var] : var_map) { agent_map.mapVariable(internal_var, parent_var); } + for (auto const& [internal_state, parent_state] : state_map) { agent_map.mapState(internal_state, parent_state); } +} + +void SingleAgentDiscreteMovement::setEnvironmentAgent(const string& parent_name, + const map& var_map, + const map& state_map, + bool auto_map) { + if (!smm.has_value()) { + throw exception::InvalidSubModel("SingleAgentDiscreteMovement submodel was not initialized. Call addSingleAgentDiscreteMovementSubmodel() first."); + } + auto agent_map = this->smm->bindAgent(INTERNAL_ENV_AGENT_NAME, parent_name, auto_map, auto_map); + for (auto const& [internal_var, parent_var] : var_map) { agent_map.mapVariable(internal_var, parent_var); } + for (auto const& [internal_state, parent_state] : state_map) { agent_map.mapState(internal_state, parent_state); } +} + +flamegpu::SubModelDescription SingleAgentDiscreteMovement::getSubModelDescription() const { + if (!smm.has_value()) { + throw exception::InvalidSubModel("SingleAgentDiscreteMovement submodel was not initialized."); + } + return *smm; +} + +std::string SingleAgentDiscreteMovement::getName() const { + return "SingleAgentDiscreteMovement"; +} + +void SingleAgentDiscreteMovement::defineMessageSubmodule(ModelDescription &smm_desc, int ENV_SIZE_X, int ENV_SIZE_Y) { + auto m1 = smm_desc.newMessage("cell_status"); + m1.newVariable("cell_score"); + m1.newVariable("is_occupied"); + m1.setDimensions(ENV_SIZE_X, ENV_SIZE_Y); + + auto m2 = smm_desc.newMessage("move_requests"); + m2.newVariable("requester_id"); + m2.newVariable("target_x"); + m2.newVariable("target_y"); + m2.newVariable("priority"); + m2.setDimensions(ENV_SIZE_X, ENV_SIZE_Y); + + auto m3 = smm_desc.newMessage("move_responses"); + m3.newVariable("winner_id"); + m3.newVariable("cell_score"); + m3.setDimensions(ENV_SIZE_X, ENV_SIZE_Y); + + auto m4 = smm_desc.newMessage("new_locations"); + m4.newVariable("moving_agent_id"); + m4.setDimensions(ENV_SIZE_X, ENV_SIZE_Y); +} + +void SingleAgentDiscreteMovement::setMessages(AgentDescription &movingAgent, AgentDescription &envAgent, int ENV_SIZE_X, int ENV_SIZE_Y) { + envAgent.newFunction("envAgent_broadcast_status", envAgent_broadcast_status).setMessageOutput("cell_status"); + + auto movingAgent_request_move_fn = movingAgent.newFunction("movingAgent_request_move", movingAgent_request_move); + movingAgent_request_move_fn.setMessageInput("cell_status"); + movingAgent_request_move_fn.setMessageOutput("move_requests"); + + auto envAgent_resolve_conflict_fn = envAgent.newFunction("envAgent_resolve_conflict", envAgent_resolve_conflict); + envAgent_resolve_conflict_fn.setMessageInput("move_requests"); + envAgent_resolve_conflict_fn.setMessageOutput("move_responses"); + + auto movingAgent_execute_move_fn = movingAgent.newFunction("movingAgent_execute_move", movingAgent_execute_move); + movingAgent_execute_move_fn.setMessageInput("move_responses"); + movingAgent_execute_move_fn.setMessageOutput("new_locations"); + + auto envAgent_update_occupancy_fn = envAgent.newFunction("envAgent_update_occupancy", envAgent_update_occupancy); + envAgent_update_occupancy_fn.setMessageInput("new_locations"); +} + +void SingleAgentDiscreteMovement::defineLayer(ModelDescription &smm_desc) { + smm_desc.newLayer().addAgentFunction(envAgent_broadcast_status); + smm_desc.newLayer().addAgentFunction(movingAgent_request_move); + smm_desc.newLayer().addAgentFunction(envAgent_resolve_conflict); + smm_desc.newLayer().addAgentFunction(movingAgent_execute_move); + smm_desc.newLayer().addAgentFunction(envAgent_update_occupancy); +} + +void SingleAgentDiscreteMovement::validate() { + if (!this->smm.has_value()) { + throw exception::InvalidSubModel("SingleAgentDiscreteMovement submodel was not initialized. Call addSingleAgentDiscreteMovementSubmodel() first."); + } + + try { + auto moving_agent = this->smm->getSubAgent(INTERNAL_MOVING_AGENT_NAME); + auto env_agent = this->smm->getSubAgent(INTERNAL_ENV_AGENT_NAME); + + // Mandatory mappings check + moving_agent.getVariableMapping("x"); + moving_agent.getVariableMapping("y"); + moving_agent.getVariableMapping("last_x"); + moving_agent.getVariableMapping("last_y"); + moving_agent.getVariableMapping("last_resources_x"); + moving_agent.getVariableMapping("last_resources_y"); + moving_agent.getVariableMapping("current_cell_score"); + + env_agent.getVariableMapping("x"); + env_agent.getVariableMapping("y"); + env_agent.getVariableMapping("is_occupied"); + env_agent.getVariableMapping("cell_score"); + + // Mandatory state mapping check + env_agent.getStateMapping("active"); + moving_agent.getStateMapping("active"); + } catch (const exception::InvalidAgentState& e) { + string msg = "SingleAgentDiscreteMovement submodel missing required state binding: " + string(e.what()); + throw exception::InvalidAgentState(msg.c_str()); + } catch (const exception::InvalidSubAgentName& e) { + string msg = "SingleAgentDiscreteMovement submodel missing required agent binding: " + string(e.what()); + throw exception::InvalidSubAgentName(msg.c_str()); + } catch (const exception::InvalidAgentVar& e) { + string msg = "SingleAgentDiscreteMovement submodel missing required variable mapping: " + string(e.what()); + throw exception::InvalidAgentVar(msg.c_str()); + } +} + +} // namespace submodels +} // namespace stockAgent +} // namespace flamegpu diff --git a/swig/python/flamegpu.i b/swig/python/flamegpu.i index 0fb1f2374..3455ce22b 100644 --- a/swig/python/flamegpu.i +++ b/swig/python/flamegpu.i @@ -42,10 +42,13 @@ %{ // Include the main library header, that should subsequently make all other required (public) headers available. #include "flamegpu/flamegpu.h" -// Also include TestSuiteTelemetyr header, which is not intended to be public. +// Also include TestSuiteTelemetyr header, which is not intended to be public. #include "flamegpu/detail/TestSuiteTelemetry.h" +#include "flamegpu/stockAgent/subModels/AbstractSubmodels.h" +#include "flamegpu/stockAgent/subModels/SingleAgentDiscreteMovement.h" // #include "flamegpu/runtime/HostFunctionCallback.h" -using namespace flamegpu; // @todo - is this required? Ideally it shouldn't be, but swig just dumps stuff into the global namespace. +using namespace flamegpu; // @todo - is this required? Ideally it shouldn't be, but swig just dumps stuff into the global namespace. +using namespace flamegpu::stockAgent::submodels; %} // Expand SWIG support for the standard library @@ -61,7 +64,7 @@ using namespace flamegpu; // @todo - is this required? Ideally it shouldn't be, %include // argc/argv support -%include +%include // Swig exception support %include "exception.i" @@ -98,14 +101,14 @@ using namespace flamegpu; // @todo - is this required? Ideally it shouldn't be, * TEMPLATE_VARIABLE_INSTANTIATE_FLOATS macro * Expands for floating point types */ -%define TEMPLATE_VARIABLE_INSTANTIATE_FLOATS(function, classfunction) +%define TEMPLATE_VARIABLE_INSTANTIATE_FLOATS(function, classfunction) // float and double %template(function ## Float) classfunction; %template(function ## Double) classfunction; %enddef // Array version, passing default 2nd template arg 0 -%define TEMPLATE_VARIABLE_ARRAY_INSTANTIATE_FLOATS(function, classfunction) +%define TEMPLATE_VARIABLE_ARRAY_INSTANTIATE_FLOATS(function, classfunction) // float and double %template(function ## Float) classfunction; %template(function ## Double) classfunction; @@ -115,7 +118,7 @@ using namespace flamegpu; // @todo - is this required? Ideally it shouldn't be, * TEMPLATE_VARIABLE_INSTANTIATE macro * Expands for int types */ -%define TEMPLATE_VARIABLE_INSTANTIATE_INTS(function, classfunction) +%define TEMPLATE_VARIABLE_INSTANTIATE_INTS(function, classfunction) // signed ints %template(function ## Int16) classfunction; %template(function ## Int32) classfunction; @@ -130,7 +133,7 @@ using namespace flamegpu; // @todo - is this required? Ideally it shouldn't be, %enddef // Array version, passing default 2nd template arg 0 -%define TEMPLATE_VARIABLE_ARRAY_INSTANTIATE_INTS(function, classfunction) +%define TEMPLATE_VARIABLE_ARRAY_INSTANTIATE_INTS(function, classfunction) // signed ints %template(function ## Int16) classfunction; %template(function ## Int32) classfunction; @@ -146,13 +149,13 @@ using namespace flamegpu; // @todo - is this required? Ideally it shouldn't be, /** * TEMPLATE_VARIABLE_INSTANTIATE macro - * Given a function name and a class::function specifier, this macro instanciates a typed version of the function for a set of basic types. + * Given a function name and a class::function specifier, this macro instanciates a typed version of the function for a set of basic types. * E.g. TEMPLATE_VARIABLE_INSTANTIATE(function, SomeClass:function) will generate swig typed versions of the function like the following * typedef SomeClass:function functionInt; * typedef SomeClass:function functionFloat; * ... */ -%define TEMPLATE_VARIABLE_INSTANTIATE(function, classfunction) +%define TEMPLATE_VARIABLE_INSTANTIATE(function, classfunction) TEMPLATE_VARIABLE_INSTANTIATE_FLOATS(function, classfunction) TEMPLATE_VARIABLE_INSTANTIATE_INTS(function, classfunction) // char types @@ -165,7 +168,7 @@ TEMPLATE_VARIABLE_INSTANTIATE_INTS(function, classfunction) %enddef // Array version, passing default 2nd template arg 0 -%define TEMPLATE_VARIABLE_ARRAY_INSTANTIATE(function, classfunction) +%define TEMPLATE_VARIABLE_ARRAY_INSTANTIATE(function, classfunction) TEMPLATE_VARIABLE_ARRAY_INSTANTIATE_FLOATS(function, classfunction) TEMPLATE_VARIABLE_ARRAY_INSTANTIATE_INTS(function, classfunction) // char types @@ -183,22 +186,22 @@ TEMPLATE_VARIABLE_ARRAY_INSTANTIATE_INTS(function, classfunction) */ %define TEMPLATE_VARIABLE_INSTANTIATE_ID(function, classfunction) %template(function ## ID) classfunction; -TEMPLATE_VARIABLE_INSTANTIATE(function, classfunction) +TEMPLATE_VARIABLE_INSTANTIATE(function, classfunction) %enddef // Array version, passing default 2nd template arg 0 %define TEMPLATE_VARIABLE_ARRAY_INSTANTIATE_ID(function, classfunction) %template(function ## ID) classfunction; -TEMPLATE_VARIABLE_ARRAY_INSTANTIATE(function, classfunction) +TEMPLATE_VARIABLE_ARRAY_INSTANTIATE(function, classfunction) %enddef /** * TEMPLATE_SUM_INSTANTIATE macro - * Specific template expansion for sum which allows different return types to avoid range issues + * Specific template expansion for sum which allows different return types to avoid range issues * Return with same type is not Instantiated. I.e. All int type use 64 bit signed returned types */ -%define TEMPLATE_SUM_INSTANTIATE(sum_class) +%define TEMPLATE_SUM_INSTANTIATE(sum_class) // float and double %template(sumFloat) sum_class ## ::sum; %template(sumDouble) sum_class ## ::sum; @@ -222,7 +225,7 @@ TEMPLATE_VARIABLE_ARRAY_INSTANTIATE(function, classfunction) %template(UIntArray3) std::array; /* Create some type objects to obtain sizes and type info of flamegpu2 basic types. - * It is not required to demangle type names. These can be used to compare directly with the type_index + * It is not required to demangle type names. These can be used to compare directly with the type_index * returned by the flamegpu2 library */ %nodefaultctor std::type_index; @@ -233,7 +236,7 @@ TEMPLATE_VARIABLE_ARRAY_INSTANTIATE(function, classfunction) static unsigned int size(){ return sizeof(T); } - + static const char* typeName() { return std::type_index(typeid(T)).name(); } @@ -338,7 +341,7 @@ class FLAMEGPURuntimeException : public std::exception { PyObject* type_obj_name = PyObject_GetAttrString(type, "__name__"); PyObject *type_str = PyObject_Str(type_obj_name); const char *pTypeStr = PyUnicode_AsUTF8(type_str); - // Director Obj Type + // Director Obj Type PyObject* hostfn_type = PyObject_Type(hostfn); PyObject* hostfn_type_name = PyObject_GetAttrString(hostfn_type, "__name__"); PyObject *hostfn_str = PyObject_Str(hostfn_type_name); @@ -376,22 +379,22 @@ class FLAMEGPURuntimeException : public std::exception { catch (flamegpu::exception::FLAMEGPUException& e) { FLAMEGPURuntimeException *except = new FLAMEGPURuntimeException(std::string(e.what()), std::string(e.exception_type())); PyObject *err = SWIG_NewPointerObj(except, SWIGTYPE_p_FLAMEGPURuntimeException, 1); - SWIG_Python_Raise(err, except.type(), SWIGTYPE_p_FLAMEGPURuntimeException); + SWIG_Python_Raise(err, except.type(), SWIGTYPE_p_FLAMEGPURuntimeException); SWIG_fail; } - catch (Swig::DirectorException&) { - SWIG_fail; + catch (Swig::DirectorException&) { + SWIG_fail; } catch(const std::exception& e) { SWIG_exception(SWIG_RuntimeError, const_cast(e.what()) ); } catch (...) { SWIG_exception(SWIG_RuntimeError, "Unknown Exception"); - } + } } -// Ignore directives. These go before any %includes. +// Ignore directives. These go before any %includes. // ----------------- // Disable non RTC function and function condition set methods @@ -474,7 +477,7 @@ class FLAMEGPURuntimeException : public std::exception { // Do not provide the FLAMEGPU_VERSION macro, instead just the pyflamegpu.VERSION* variants. %ignore FLAMEGPU_VERSION; -// Ignores for nested classes, where flatnested is enabled. +// Ignores for nested classes, where flatnested is enabled. %feature("flatnested"); // flat nested on // Ignore some of the internal host classes defined for messaging // In the future should these be in the detail namespace which could globally be ignored? // @todo @@ -487,11 +490,11 @@ class FLAMEGPURuntimeException : public std::exception { // Rename directives. These go before any %includes // ----------------- -%rename(insert) flamegpu::AgentVector::py_insert; -%rename(erase) flamegpu::AgentVector::py_erase; +%rename(insert) flamegpu::AgentVector::py_insert; +%rename(erase) flamegpu::AgentVector::py_erase; -%rename(insert) flamegpu::DeviceAgentVector_impl::py_insert; -%rename(erase) flamegpu::DeviceAgentVector_impl::py_erase; +%rename(insert) flamegpu::DeviceAgentVector_impl::py_insert; +%rename(erase) flamegpu::DeviceAgentVector_impl::py_erase; // Renames which require flatnested, as swig/python does not support nested classes. %feature("flatnested"); // flat nested on to ensure Config is included @@ -531,7 +534,7 @@ class FLAMEGPURuntimeException : public std::exception { // Director features. These go before the %includes. // ----------------- /* Enable callback functions for step, exit and init through the use of "director" which allows Python -> C and C-> Python in callback. - * FLAMEGPU2 supports callback or function pointers so no special tricks are needed. + * FLAMEGPU2 supports callback or function pointers so no special tricks are needed. * To prevent raw pointer functions being exposed in Python these are ignored so only the callback versions are accessible. */ %feature("director") flamegpu::HostFunctionCallback; @@ -582,7 +585,7 @@ class FLAMEGPURuntimeException : public std::exception { // Value wrappers also go before includes. // ----------------- // %feature("valuewrapper") flamegpu::DeviceAgentVector; // @todo - this doesn't appear to be required. - + // Enums / type definitions. // ----------------- @@ -601,11 +604,11 @@ namespace EnvironmentManager{ // ----------------- // This is required where there are circular dependencies and how swig doesn't #include things. Instead, forward declare the class within the namespace that is otherwise #included. -namespace flamegpu { -class ModelDescription; // For DependencyGraph circular dependency. +namespace flamegpu { +class ModelDescription; // For DependencyGraph circular dependency. } -// If visualisation is enabled, then CUDASimulation provides access to the visualisation class. This requires a forward declaraiton to place it in the correct namespace. +// If visualisation is enabled, then CUDASimulation provides access to the visualisation class. This requires a forward declaraiton to place it in the correct namespace. #ifdef FLAMEGPU_VISUALISATION namespace flamegpu { namespace visualiser { @@ -614,10 +617,10 @@ class ModelVis; } // namespace flamegpu #endif -// %includes for classes to wrap. +// %includes for classes to wrap. // ----------------- -// A number of typedefs are not placed in the namespace, but they are currently unused anyway. -// SWIGTYPE_p_FLAMEGPURuntimeException - swig only, doesn't need to be namespaced? +// A number of typedefs are not placed in the namespace, but they are currently unused anyway. +// SWIGTYPE_p_FLAMEGPURuntimeException - swig only, doesn't need to be namespaced? %include "flamegpu/defines.h" // Provides flamegpu::id_t amongst others. %include "flamegpu/version.h" // provides FLAMEGPU_VERSION etc @@ -695,7 +698,7 @@ class ModelVis; %include "flamegpu/runtime/AgentFunction_shim.cuh" %include "flamegpu/runtime/AgentFunctionCondition_shim.cuh" -// These are essentially nested classes that have been split out. +// These are essentially nested classes that have been split out. %include "flamegpu/simulation/AgentVector_Agent.h" %include "flamegpu/simulation/AgentVector.h" %include "flamegpu/runtime/agent/AgentInstance.h" @@ -713,18 +716,22 @@ class ModelVis; %include "flamegpu/runtime/agent/HostNewAgentAPI.h" %include "flamegpu/runtime/agent/HostAgentAPI.cuh" -%include "flamegpu/runtime/HostAPI.h" +%include "flamegpu/runtime/HostAPI.h" // Include logging implementations %include "flamegpu/simulation/LoggingConfig.h" %include "flamegpu/simulation/AgentLoggingConfig.h" %include "flamegpu/simulation/AgentLoggingConfig_SumReturn.h" -%include "flamegpu/simulation/LogFrame.h" // Includes RunLog. +%include "flamegpu/simulation/LogFrame.h" // Includes RunLog. // Include ensemble implementations %include "flamegpu/simulation/RunPlan.h" %include "flamegpu/simulation/RunPlanVector.h" +// Include submodel implementations +%include "flamegpu/stockAgent/subModels/AbstractSubmodels.h" +%include "flamegpu/stockAgent/subModels/SingleAgentDiscreteMovement.h" + // Include public utility headers %include "flamegpu/util/cleanup.h" // Don't flatnest this, range is explicitly not included incase of GC related issues. @@ -735,7 +742,7 @@ class ModelVis; // unignore detail %rename("%s") flamegpu::detail; -// Rename test suite telemetry detail/private api function, prior to ignoring detail. +// Rename test suite telemetry detail/private api function, prior to ignoring detail. // Because of how _ prefixes are handeled, this must be called via pyflamegpu._pyflamegpu.__TestSuiteTelemetry_sendResults() %rename (__TestSuiteTelemetry) flamegpu::detail::TestSuiteTelemetry; %include "flamegpu/detail/TestSuiteTelemetry.h" @@ -743,7 +750,7 @@ class ModelVis; %ignore flamegpu::detail::JitifyCache::loadKernel; %rename(JitifyCache) flamegpu::detail::JitifyCache; %include "flamegpu/detail/JitifyCache.h" -// Ignore detail agian? +// Ignore detail agian? %ignore flamegpu::detail; @@ -779,7 +786,7 @@ namespace std { $self->operator[](index).setData(value); } } -/* Extend HostRandom to add a templated version of the uniform function with a different name so this can be instantiated +/* Extend HostRandom to add a templated version of the uniform function with a different name so this can be instantiated * It is required to ingore the original defintion of uniform and separate the two functions to have a distinct name */ %extend flamegpu::HostRandom{ @@ -897,7 +904,7 @@ class FLAMEGPUGraphMapIterator(object): %template(StepLogFrameList) std::list; %template(RunLogMap) std::map; - + // Instantiate template versions of agent functions from the API TEMPLATE_VARIABLE_INSTANTIATE_ID(newVariable, flamegpu::AgentDescription::newVariable) TEMPLATE_VARIABLE_INSTANTIATE_ID(newVariableArray, flamegpu::AgentDescription::newVariableArray) @@ -1134,7 +1141,7 @@ TEMPLATE_VARIABLE_INSTANTIATE_INTS(poisson, flamegpu::HostRandom::poisson) # do not allow passthrough (host exection of this function) pass return wrapper - + def agent_function_condition(func): @wraps(func) def wrapper(): @@ -1148,7 +1155,7 @@ TEMPLATE_VARIABLE_INSTANTIATE_INTS(poisson, flamegpu::HostRandom::poisson) # essenitally a passthrough in case the function is also used in python host code # passthrough obviously does not support pyflamegpu device functions return func(*args, **kwargs) - + # create an attribute to identify the wrapped function as having this decorator (without having to parse) wrapper.__is_pyflamegpu_device_function = True return wrapper @@ -1167,7 +1174,7 @@ TEMPLATE_VARIABLE_INSTANTIATE_INTS(poisson, flamegpu::HostRandom::poisson) using namespace flamegpu::visualiser; %} - // Ignore directives. These go before any %includes. + // Ignore directives. These go before any %includes. // ----------------- // Disable nvtx::range class, we don't trust swig+GC to dtor at the right time for it to be reliable %ignore flamegpu::util::nvtx::range; @@ -1196,7 +1203,7 @@ TEMPLATE_VARIABLE_INSTANTIATE_INTS(poisson, flamegpu::HostRandom::poisson) // ----------------- // Enums / type definitions. // ----------------- - // %includes for classes to wrap. + // %includes for classes to wrap. // ----------------- %include "flamegpu/visualiser/config/Stock.h" %include "flamegpu/visualiser/StaticModelVis.h" @@ -1259,22 +1266,22 @@ TEMPLATE_VARIABLE_INSTANTIATE_INTS(poisson, flamegpu::HostRandom::poisson) TEMPLATE_VARIABLE_INSTANTIATE_ID(newEnvironmentPropertyDrag, flamegpu::visualiser::PanelVis::newEnvironmentPropertyDrag) TEMPLATE_VARIABLE_INSTANTIATE_ID(newEnvironmentPropertyInput, flamegpu::visualiser::PanelVis::newEnvironmentPropertyInput) TEMPLATE_VARIABLE_INSTANTIATE_INTS(newEnvironmentPropertyToggle, flamegpu::visualiser::PanelVis::newEnvironmentPropertyToggle) - + TEMPLATE_VARIABLE_ARRAY_INSTANTIATE_ID(newEnvironmentPropertySlider, flamegpu::visualiser::PanelVis::newEnvironmentPropertySlider) TEMPLATE_VARIABLE_ARRAY_INSTANTIATE_ID(newEnvironmentPropertyDrag, flamegpu::visualiser::PanelVis::newEnvironmentPropertyDrag) TEMPLATE_VARIABLE_ARRAY_INSTANTIATE_ID(newEnvironmentPropertyInput, flamegpu::visualiser::PanelVis::newEnvironmentPropertyInput) TEMPLATE_VARIABLE_ARRAY_INSTANTIATE_INTS(newEnvironmentPropertyToggle, flamegpu::visualiser::PanelVis::newEnvironmentPropertyToggle) - + // Redefine the value to ensure it makes it into the python modules (without the FLAMEGPU_ prefix) #undef FLAMEGPU_VISUALISATION #define VISUALISATION true -#else +#else // Define in the python module as false. #define VISUALISATION false #endif -// Define pyflamegpu.SEATBELTS as true or false as appropriate, so tests can be disabled / enabled +// Define pyflamegpu.SEATBELTS as true or false as appropriate, so tests can be disabled / enabled #if defined(FLAMEGPU_SEATBELTS) && FLAMEGPU_SEATBELTS #undef FLAMEGPU_SEATBELTS #define SEATBELTS true diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 18193f127..c939c6233 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -88,6 +88,7 @@ SET(TESTS_SRC ${CMAKE_CURRENT_SOURCE_DIR}/test_cases/runtime/messaging/test_append_truncate.cu ${CMAKE_CURRENT_SOURCE_DIR}/test_cases/util/test_cleanup.cu ${CMAKE_CURRENT_SOURCE_DIR}/test_cases/util/test_nvtx.cu + ${CMAKE_CURRENT_SOURCE_DIR}/test_cases/stockAgent/test_single_agent_discrete_movement.cu ${CMAKE_CURRENT_SOURCE_DIR}/test_cases/test_namespaces/test_namespaces.cu ${CMAKE_CURRENT_SOURCE_DIR}/test_cases/test_namespaces/test_rtc_namespaces.cu ${CMAKE_CURRENT_SOURCE_DIR}/test_cases/test_version.cpp diff --git a/tests/python/stockAgent/test_single_agent_discrete_movement.py b/tests/python/stockAgent/test_single_agent_discrete_movement.py new file mode 100644 index 000000000..534f544eb --- /dev/null +++ b/tests/python/stockAgent/test_single_agent_discrete_movement.py @@ -0,0 +1,367 @@ +import pytest +from unittest import TestCase +from pyflamegpu import * + +class SingleAgentDiscreteMovementTest(TestCase): + """ + Test 1: Initialization & Validation + Verifies that the submodel correctly validates its agent and variable bindings. + """ + def test_initialization(self): + model = pyflamegpu.ModelDescription("parent_model") + move_submodel = pyflamegpu.SingleAgentDiscreteMovement() + + # Should throw if we try to bind before calling addSingleAgentDiscreteMovementSubmodel + with pytest.raises(pyflamegpu.FLAMEGPURuntimeException) as e: + move_submodel.setMovingAgent("agent") + assert e.value.type() == "InvalidSubModel" + + # Initialize the submodel + move_submodel.addSingleAgentDiscreteMovementSubmodel(model, 10, 10) + + # Setup a valid parent agent for moving + agent = model.newAgent("agent") + agent.newVariableInt("x") + agent.newVariableInt("y") + agent.newVariableInt("last_x") + agent.newVariableInt("last_y") + agent.newVariableInt("last_resources_x") + agent.newVariableInt("last_resources_y") + agent.newVariableFloat("current_cell_score") + agent.newState("default") + + # Setup a valid parent agent for the environment grid + cell = model.newAgent("cell") + cell.newVariableInt("x") + cell.newVariableInt("y") + cell.newVariableInt("is_occupied") + cell.newVariableFloat("cell_score") + cell.newState("default") + + # Bind agents: auto_map=true handles variables with matching names, + # but we must explicitly map internal "active" state to parent "default" state. + bug_vars = pyflamegpu.map_string_string() + bug_states = pyflamegpu.map_string_string() + bug_states["active"] = "default" + move_submodel.setMovingAgent("agent", bug_vars, bug_states, True) + + env_vars = pyflamegpu.map_string_string() + env_states = pyflamegpu.map_string_string() + env_states["active"] = "default" + move_submodel.setEnvironmentAgent("cell", env_vars, env_states, True) + + # Validation should pass now + try: + move_submodel.validate() + except pyflamegpu.FLAMEGPURuntimeException as e: + pytest.fail(f"validate() threw {e.type()} unexpectedly: {e.what()}") + + # Check getName and getSubModelDescription + assert move_submodel.getName() == "SingleAgentDiscreteMovement" + assert move_submodel.getSubModelDescription() is not None + + """ + Test 2: Basic Greedy Movement + Verifies that an agent at (0,0) moves to (1,1) if that cell has a higher score. + """ + def test_simple_move(self): + model = pyflamegpu.ModelDescription("parent_model") + WIDTH = 3 + HEIGHT = 3 + + move_submodel = pyflamegpu.SingleAgentDiscreteMovement() + move_submodel.addSingleAgentDiscreteMovementSubmodel(model, WIDTH, HEIGHT) + + # Submodels must be added to a layer to be executed during the simulation step + model.newLayer().addSubModel(move_submodel.getSubModelDescription()) + + # Define parent agents with necessary variables + agent = model.newAgent("agent") + agent.newVariableInt("x") + agent.newVariableInt("y") + agent.newVariableInt("last_x", -1) + agent.newVariableInt("last_y", -1) + agent.newVariableInt("last_resources_x", -1) + agent.newVariableInt("last_resources_y", -1) + agent.newVariableFloat("current_cell_score", 0.0) + agent.newVariableFloat("priority", 1.0) + agent.newState("default") + + cell = model.newAgent("cell") + cell.newVariableInt("x") + cell.newVariableInt("y") + cell.newVariableInt("is_occupied", 0) + cell.newVariableFloat("cell_score", 0.0) + cell.newState("default") + + # Bind to submodel + bug_vars = pyflamegpu.map_string_string() + bug_states = pyflamegpu.map_string_string() + bug_states["active"] = "default" + move_submodel.setMovingAgent("agent", bug_vars, bug_states, True) + + env_vars = pyflamegpu.map_string_string() + env_states = pyflamegpu.map_string_string() + env_states["active"] = "default" + move_submodel.setEnvironmentAgent("cell", env_vars, env_states, True) + + sim = pyflamegpu.CUDASimulation(model) + sim.SimulationConfig().steps = 1 + + # Initialize the grid: 3x3 cells + cell_pop = pyflamegpu.AgentVector(cell, WIDTH * HEIGHT) + for x in range(WIDTH): + for y in range(HEIGHT): + c = cell_pop[x * HEIGHT + y] + c.setVariableInt("x", x) + c.setVariableInt("y", y) + c.setVariableFloat("cell_score", 0.0) + + # Set a high "reward" at (1, 1) + cell_pop[1 * HEIGHT + 1].setVariableFloat("cell_score", 10.0) + sim.setPopulationData(cell_pop) + + # Initialize 1 agent at (0, 0) + agent_pop = pyflamegpu.AgentVector(agent, 1) + agent_pop[0].setVariableInt("x", 0) + agent_pop[0].setVariableInt("y", 0) + agent_pop[0].setVariableFloat("priority", 1.0) + sim.setPopulationData(agent_pop) + + # Execute one simulation step + sim.step() + + # Verify the agent moved to the high-score cell (1, 1) + sim.getPopulationData(agent_pop) + assert agent_pop[0].getVariableInt("x") == 1 + assert agent_pop[0].getVariableInt("y") == 1 + + # Verify occupancy status was updated + sim.getPopulationData(cell_pop) + assert cell_pop[0 * HEIGHT + 0].getVariableInt("is_occupied") == 0 # Left (0,0) + assert cell_pop[1 * HEIGHT + 1].getVariableInt("is_occupied") == 1 # Entered (1,1) + + """ + Test 3: Collision Avoidance + Verifies that when two agents try to move to the same cell, only one succeeds. + """ + def test_collision_avoidance(self): + model = pyflamegpu.ModelDescription("parent_model") + WIDTH = 3 + HEIGHT = 3 + + move_submodel = pyflamegpu.SingleAgentDiscreteMovement() + move_submodel.addSingleAgentDiscreteMovementSubmodel(model, WIDTH, HEIGHT) + model.newLayer().addSubModel(move_submodel.getSubModelDescription()) + + agent = model.newAgent("agent") + agent.newVariableInt("x") + agent.newVariableInt("y") + agent.newVariableInt("last_x", -1) + agent.newVariableInt("last_y", -1) + agent.newVariableInt("last_resources_x", -1) + agent.newVariableInt("last_resources_y", -1) + agent.newVariableFloat("current_cell_score", 0.0) + agent.newVariableFloat("priority", 0.0) + agent.newState("default") + + cell = model.newAgent("cell") + cell.newVariableInt("x") + cell.newVariableInt("y") + cell.newVariableInt("is_occupied", 0) + cell.newVariableFloat("cell_score", 0.0) + cell.newState("default") + + bug_vars = pyflamegpu.map_string_string() + bug_states = pyflamegpu.map_string_string() + bug_states["active"] = "default" + move_submodel.setMovingAgent("agent", bug_vars, bug_states, True) + + env_vars = pyflamegpu.map_string_string() + env_states = pyflamegpu.map_string_string() + env_states["active"] = "default" + move_submodel.setEnvironmentAgent("cell", env_vars, env_states, True) + + sim = pyflamegpu.CUDASimulation(model) + sim.SimulationConfig().steps = 1 + + # Grid initialization + cell_pop = pyflamegpu.AgentVector(cell, WIDTH * HEIGHT) + for i in range(WIDTH * HEIGHT): + cell_pop[i].setVariableInt("x", i // HEIGHT) + cell_pop[i].setVariableInt("y", i % HEIGHT) + cell_pop[i].setVariableFloat("cell_score", 0.0) + + cell_pop[1 * HEIGHT + 1].setVariableFloat("cell_score", 10.0) # Target + cell_pop[0 * HEIGHT + 1].setVariableFloat("cell_score", 1.0) # Agent 0 start score + cell_pop[2 * HEIGHT + 1].setVariableFloat("cell_score", 1.0) # Agent 1 start score + sim.setPopulationData(cell_pop) + + # Two agents: A at (0,1) with Priority 10, B at (2,1) with Priority 5. + agent_pop = pyflamegpu.AgentVector(agent, 2) + agent_pop[0].setVariableInt("x", 0) + agent_pop[0].setVariableInt("y", 1) + agent_pop[0].setVariableFloat("priority", 10.0) + agent_pop[1].setVariableInt("x", 2) + agent_pop[1].setVariableInt("y", 1) + agent_pop[1].setVariableFloat("priority", 5.0) + sim.setPopulationData(agent_pop) + + sim.step() + + sim.getPopulationData(agent_pop) + # Agent 0 (higher priority) should be at (1,1) + assert agent_pop[0].getVariableInt("x") == 1 + assert agent_pop[0].getVariableInt("y") == 1 + # Agent 1 (lower priority) should have failed and stayed at (2,1) + assert agent_pop[1].getVariableInt("x") == 2 + assert agent_pop[1].getVariableInt("y") == 1 + + sim.getPopulationData(cell_pop) + assert cell_pop[1 * HEIGHT + 1].getVariableInt("is_occupied") == 1 + + """ + Test 4: Resource Memory + Verifies that last_resources_x/y are updated when moving to a cell with score > 0. + """ + def test_resource_memory(self): + model = pyflamegpu.ModelDescription("parent_model") + WIDTH = 3 + HEIGHT = 3 + + move_submodel = pyflamegpu.SingleAgentDiscreteMovement() + move_submodel.addSingleAgentDiscreteMovementSubmodel(model, WIDTH, HEIGHT) + model.newLayer().addSubModel(move_submodel.getSubModelDescription()) + + agent = model.newAgent("agent") + agent.newVariableInt("x") + agent.newVariableInt("y") + agent.newVariableInt("last_x", -1) + agent.newVariableInt("last_y", -1) + agent.newVariableInt("last_resources_x", -1) + agent.newVariableInt("last_resources_y", -1) + agent.newVariableFloat("current_cell_score", 0.0) + agent.newVariableFloat("priority", 1.0) + agent.newState("default") + + cell = model.newAgent("cell") + cell.newVariableInt("x") + cell.newVariableInt("y") + cell.newVariableInt("is_occupied", 0) + cell.newVariableFloat("cell_score", 0.0) + cell.newState("default") + + bug_vars = pyflamegpu.map_string_string() + bug_states = pyflamegpu.map_string_string() + bug_states["active"] = "default" + move_submodel.setMovingAgent("agent", bug_vars, bug_states, True) + + env_vars = pyflamegpu.map_string_string() + env_states = pyflamegpu.map_string_string() + env_states["active"] = "default" + move_submodel.setEnvironmentAgent("cell", env_vars, env_states, True) + + sim = pyflamegpu.CUDASimulation(model) + sim.SimulationConfig().steps = 1 + + cell_pop = pyflamegpu.AgentVector(cell, WIDTH * HEIGHT) + for i in range(WIDTH * HEIGHT): + cell_pop[i].setVariableInt("x", i // HEIGHT) + cell_pop[i].setVariableInt("y", i % HEIGHT) + cell_pop[i].setVariableFloat("cell_score", 0.0) + + # High score at (1,1) + cell_pop[1 * HEIGHT + 1].setVariableFloat("cell_score", 10.0) + sim.setPopulationData(cell_pop) + + agent_pop = pyflamegpu.AgentVector(agent, 1) + agent_pop[0].setVariableInt("x", 0) + agent_pop[0].setVariableInt("y", 0) + agent_pop[0].setVariableInt("last_resources_x", -1) + agent_pop[0].setVariableInt("last_resources_y", -1) + sim.setPopulationData(agent_pop) + + sim.step() + + sim.getPopulationData(agent_pop) + # Agent moved to (1,1) + assert agent_pop[0].getVariableInt("x") == 1 + assert agent_pop[0].getVariableInt("y") == 1 + # Resource memory should be updated + assert agent_pop[0].getVariableInt("last_resources_x") == 1 + assert agent_pop[0].getVariableInt("last_resources_y") == 1 + + """ + Test 5: Grid Boundaries + Verifies that an agent at the corner moves correctly and doesn't crash or go out of bounds. + """ + def test_grid_boundaries(self): + model = pyflamegpu.ModelDescription("parent_model") + WIDTH = 2 + HEIGHT = 2 + + move_submodel = pyflamegpu.SingleAgentDiscreteMovement() + move_submodel.addSingleAgentDiscreteMovementSubmodel(model, WIDTH, HEIGHT) + model.newLayer().addSubModel(move_submodel.getSubModelDescription()) + + agent = model.newAgent("agent") + agent.newVariableInt("x") + agent.newVariableInt("y") + agent.newVariableInt("last_x", -1) + agent.newVariableInt("last_y", -1) + agent.newVariableInt("last_resources_x", -1) + agent.newVariableInt("last_resources_y", -1) + agent.newVariableFloat("current_cell_score", 0.0) + agent.newVariableFloat("priority", 1.0) + agent.newState("default") + + cell = model.newAgent("cell") + cell.newVariableInt("x") + cell.newVariableInt("y") + cell.newVariableInt("is_occupied", 0) + cell.newVariableFloat("cell_score", 0.0) + cell.newState("default") + + bug_vars = pyflamegpu.map_string_string() + bug_states = pyflamegpu.map_string_string() + bug_states["active"] = "default" + move_submodel.setMovingAgent("agent", bug_vars, bug_states, True) + + env_vars = pyflamegpu.map_string_string() + env_states = pyflamegpu.map_string_string() + env_states["active"] = "default" + move_submodel.setEnvironmentAgent("cell", env_vars, env_states, True) + + sim = pyflamegpu.CUDASimulation(model) + sim.SimulationConfig().steps = 1 + + cell_pop = pyflamegpu.AgentVector(cell, WIDTH * HEIGHT) + for i in range(WIDTH * HEIGHT): + cell_pop[i].setVariableInt("x", i // HEIGHT) + cell_pop[i].setVariableInt("y", i % HEIGHT) + cell_pop[i].setVariableFloat("cell_score", 0.0) + + # High score at (1,1) + cell_pop[1 * HEIGHT + 1].setVariableFloat("cell_score", 10.0) + sim.setPopulationData(cell_pop) + + # Agent at (0,1) - on the edge. + agent_pop = pyflamegpu.AgentVector(agent, 1) + agent_pop[0].setVariableInt("x", 0) + agent_pop[0].setVariableInt("y", 1) + sim.setPopulationData(agent_pop) + + sim.step() + + sim.getPopulationData(agent_pop) + # Should move to (1,1) + assert agent_pop[0].getVariableInt("x") == 1 + assert agent_pop[0].getVariableInt("y") == 1 + +if __name__ == "__main__": + import unittest + unittest.main() + +if __name__ == "__main__": + import unittest + unittest.main() diff --git a/tests/test_cases/stockAgent/test_single_agent_discrete_movement.cu b/tests/test_cases/stockAgent/test_single_agent_discrete_movement.cu new file mode 100644 index 000000000..7b61c1b8c --- /dev/null +++ b/tests/test_cases/stockAgent/test_single_agent_discrete_movement.cu @@ -0,0 +1,336 @@ +#include "flamegpu/flamegpu.h" +#include "flamegpu/stockAgent/subModels/SingleAgentDiscreteMovement.h" +#include "gtest/gtest.h" + +namespace flamegpu { +namespace stockAgent { +namespace submodels { + +/** + * Test 1: Initialization & Validation + * Verifies that the submodel correctly validates its agent and variable bindings. + */ +TEST(SingleAgentDiscreteMovementTest, Initialization) { + ModelDescription model("parent_model"); + SingleAgentDiscreteMovement move_submodel; + + // Should throw if we try to bind before calling addSingleAgentDiscreteMovementSubmodel + EXPECT_THROW(move_submodel.setMovingAgent("agent"), exception::InvalidSubModel); + + // Initialize the submodel + move_submodel.addSingleAgentDiscreteMovementSubmodel(model, 10, 10); + + // Setup a valid parent agent for moving + auto agent = model.newAgent("agent"); + agent.newVariable("x"); + agent.newVariable("y"); + agent.newVariable("last_x"); + agent.newVariable("last_y"); + agent.newVariable("last_resources_x"); + agent.newVariable("last_resources_y"); + agent.newVariable("current_cell_score"); + agent.newState("default"); + + // Setup a valid parent agent for the environment grid + auto cell = model.newAgent("cell"); + cell.newVariable("x"); + cell.newVariable("y"); + cell.newVariable("is_occupied"); + cell.newVariable("cell_score"); + cell.newState("default"); + + // Bind agents: auto_map=true handles variables with matching names, + // but we must explicitly map internal "active" state to parent "default" state. + move_submodel.setMovingAgent("agent", {}, {{"active", "default"}}, true); + move_submodel.setEnvironmentAgent("cell", {}, {{"active", "default"}}, true); + + // Validation should pass now + EXPECT_NO_THROW(move_submodel.validate()); + + // Check getName and getSubModelDescription + EXPECT_EQ(move_submodel.getName(), "SingleAgentDiscreteMovement"); + EXPECT_NO_THROW(move_submodel.getSubModelDescription()); +} + +/** + * Test 2: Basic Greedy Movement + * Verifies that an agent at (0,0) moves to (1,1) if that cell has a higher score. + */ +TEST(SingleAgentDiscreteMovementTest, SimpleMove) { + ModelDescription model("parent_model"); + int WIDTH = 3; + int HEIGHT = 3; + + SingleAgentDiscreteMovement move_submodel; + move_submodel.addSingleAgentDiscreteMovementSubmodel(model, WIDTH, HEIGHT); + + // Submodels must be added to a layer to be executed during the simulation step + model.newLayer().addSubModel(move_submodel.getSubModelDescription()); + + // Define parent agents with necessary variables + auto agent = model.newAgent("agent"); + agent.newVariable("x"); + agent.newVariable("y"); + agent.newVariable("last_x", -1); + agent.newVariable("last_y", -1); + agent.newVariable("last_resources_x", -1); + agent.newVariable("last_resources_y", -1); + agent.newVariable("current_cell_score", 0.0f); + agent.newVariable("priority", 1.0f); + agent.newState("default"); + + auto cell = model.newAgent("cell"); + cell.newVariable("x"); + cell.newVariable("y"); + cell.newVariable("is_occupied", 0); + cell.newVariable("cell_score", 0.0f); + cell.newState("default"); + + // Bind to submodel + move_submodel.setMovingAgent("agent", {}, {{"active", "default"}}, true); + move_submodel.setEnvironmentAgent("cell", {}, {{"active", "default"}}, true); + + CUDASimulation sim(model); + sim.SimulationConfig().steps = 1; + + // Initialize the grid: 3x3 cells + auto cell_pop = AgentVector(cell, WIDTH * HEIGHT); + for (int x = 0; x < WIDTH; ++x) { + for (int y = 0; y < HEIGHT; ++y) { + auto c = cell_pop[x * HEIGHT + y]; + c.setVariable("x", x); + c.setVariable("y", y); + c.setVariable("cell_score", 0.0f); + } + } + // Set a high "reward" at (1, 1) + cell_pop[1 * HEIGHT + 1].setVariable("cell_score", 10.0f); + sim.setPopulationData(cell_pop); + + // Initialize 1 agent at (0, 0) + auto agent_pop = AgentVector(agent, 1); + agent_pop[0].setVariable("x", 0); + agent_pop[0].setVariable("y", 0); + agent_pop[0].setVariable("priority", 1.0f); + sim.setPopulationData(agent_pop); + + // Execute one simulation step + sim.step(); + + // Verify the agent moved to the high-score cell (1, 1) + sim.getPopulationData(agent_pop); + EXPECT_EQ(agent_pop[0].getVariable("x"), 1); + EXPECT_EQ(agent_pop[0].getVariable("y"), 1); + + // Verify occupancy status was updated + sim.getPopulationData(cell_pop); + EXPECT_EQ(cell_pop[0 * HEIGHT + 0].getVariable("is_occupied"), 0); // Left (0,0) + EXPECT_EQ(cell_pop[1 * HEIGHT + 1].getVariable("is_occupied"), 1); // Entered (1,1) +} + +/** + * Test 3: Collision Avoidance + * Verifies that when two agents try to move to the same cell, only one succeeds. + * Note: Now relies on the submodel's InitFunction to auto-sync occupancy. + */ +TEST(SingleAgentDiscreteMovementTest, CollisionAvoidance) { + ModelDescription model("parent_model"); + int WIDTH = 3; + int HEIGHT = 3; + + SingleAgentDiscreteMovement move_submodel; + move_submodel.addSingleAgentDiscreteMovementSubmodel(model, WIDTH, HEIGHT); + model.newLayer().addSubModel(move_submodel.getSubModelDescription()); + + auto agent = model.newAgent("agent"); + agent.newVariable("x"); + agent.newVariable("y"); + agent.newVariable("last_x", -1); + agent.newVariable("last_y", -1); + agent.newVariable("last_resources_x", -1); + agent.newVariable("last_resources_y", -1); + agent.newVariable("current_cell_score", 0.0f); + agent.newVariable("priority", 0.0f); + agent.newState("default"); + + auto cell = model.newAgent("cell"); + cell.newVariable("x"); + cell.newVariable("y"); + cell.newVariable("is_occupied", 0); + cell.newVariable("cell_score", 0.0f); + cell.newState("default"); + + move_submodel.setMovingAgent("agent", {}, {{"active", "default"}}, true); + move_submodel.setEnvironmentAgent("cell", {}, {{"active", "default"}}, true); + + CUDASimulation sim(model); + sim.SimulationConfig().steps = 1; + + // Grid initialization: + // is_occupied is 0 by default. + // The submodel's internal InitFunction will automatically detect the agents + // and set is_occupied to 1 at (0,1) and (2,1) before the first move starts. + auto cell_pop = AgentVector(cell, WIDTH * HEIGHT); + for (int i = 0; i < WIDTH * HEIGHT; ++i) { + cell_pop[i].setVariable("x", i / HEIGHT); + cell_pop[i].setVariable("y", i % HEIGHT); + cell_pop[i].setVariable("cell_score", 0.0f); + } + cell_pop[1 * HEIGHT + 1].setVariable("cell_score", 10.0f); // Target + cell_pop[0 * HEIGHT + 1].setVariable("cell_score", 1.0f); // Agent 0 start score + cell_pop[2 * HEIGHT + 1].setVariable("cell_score", 1.0f); // Agent 1 start score + sim.setPopulationData(cell_pop); + + // Two agents: A at (0,1) with Priority 10, B at (2,1) with Priority 5. + auto agent_pop = AgentVector(agent, 2); + agent_pop[0].setVariable("x", 0); + agent_pop[0].setVariable("y", 1); + agent_pop[0].setVariable("priority", 10.0f); + agent_pop[1].setVariable("x", 2); + agent_pop[1].setVariable("y", 1); + agent_pop[1].setVariable("priority", 5.0f); + sim.setPopulationData(agent_pop); + + sim.step(); + + sim.getPopulationData(agent_pop); + // Agent 0 (higher priority) should be at (1,1) + EXPECT_EQ(agent_pop[0].getVariable("x"), 1); + EXPECT_EQ(agent_pop[0].getVariable("y"), 1); + // Agent 1 (lower priority) should have failed and stayed at (2,1) + EXPECT_EQ(agent_pop[1].getVariable("x"), 2); + EXPECT_EQ(agent_pop[1].getVariable("y"), 1); + + sim.getPopulationData(cell_pop); + EXPECT_EQ(cell_pop[1 * HEIGHT + 1].getVariable("is_occupied"), 1); +} + +/** + * Test 4: Resource Memory + * Verifies that last_resources_x/y are updated when moving to a cell with score > 0. + */ +TEST(SingleAgentDiscreteMovementTest, ResourceMemory) { + ModelDescription model("parent_model"); + int WIDTH = 3; + int HEIGHT = 3; + + SingleAgentDiscreteMovement move_submodel; + move_submodel.addSingleAgentDiscreteMovementSubmodel(model, WIDTH, HEIGHT); + model.newLayer().addSubModel(move_submodel.getSubModelDescription()); + + auto agent = model.newAgent("agent"); + agent.newVariable("x"); + agent.newVariable("y"); + agent.newVariable("last_x", -1); + agent.newVariable("last_y", -1); + agent.newVariable("last_resources_x", -1); + agent.newVariable("last_resources_y", -1); + agent.newVariable("current_cell_score", 0.0f); + agent.newVariable("priority", 1.0f); + agent.newState("default"); + + auto cell = model.newAgent("cell"); + cell.newVariable("x"); + cell.newVariable("y"); + cell.newVariable("is_occupied", 0); + cell.newVariable("cell_score", 0.0f); + cell.newState("default"); + + move_submodel.setMovingAgent("agent", {}, {{"active", "default"}}, true); + move_submodel.setEnvironmentAgent("cell", {}, {{"active", "default"}}, true); + + CUDASimulation sim(model); + sim.SimulationConfig().steps = 1; + + auto cell_pop = AgentVector(cell, WIDTH * HEIGHT); + for (int i = 0; i < WIDTH * HEIGHT; ++i) { + cell_pop[i].setVariable("x", i / HEIGHT); + cell_pop[i].setVariable("y", i % HEIGHT); + cell_pop[i].setVariable("cell_score", 0.0f); + } + // High score at (1,1) + cell_pop[1 * HEIGHT + 1].setVariable("cell_score", 10.0f); + sim.setPopulationData(cell_pop); + + auto agent_pop = AgentVector(agent, 1); + agent_pop[0].setVariable("x", 0); + agent_pop[0].setVariable("y", 0); + agent_pop[0].setVariable("last_resources_x", -1); + agent_pop[0].setVariable("last_resources_y", -1); + sim.setPopulationData(agent_pop); + + sim.step(); + + sim.getPopulationData(agent_pop); + // Agent moved to (1,1) + EXPECT_EQ(agent_pop[0].getVariable("x"), 1); + EXPECT_EQ(agent_pop[0].getVariable("y"), 1); + // Resource memory should be updated + EXPECT_EQ(agent_pop[0].getVariable("last_resources_x"), 1); + EXPECT_EQ(agent_pop[0].getVariable("last_resources_y"), 1); +} + +/** + * Test 5: Grid Boundaries + * Verifies that an agent at the corner moves correctly and doesn't crash or go out of bounds. + */ +TEST(SingleAgentDiscreteMovementTest, GridBoundaries) { + ModelDescription model("parent_model"); + int WIDTH = 2; + int HEIGHT = 2; + + SingleAgentDiscreteMovement move_submodel; + move_submodel.addSingleAgentDiscreteMovementSubmodel(model, WIDTH, HEIGHT); + model.newLayer().addSubModel(move_submodel.getSubModelDescription()); + + auto agent = model.newAgent("agent"); + agent.newVariable("x"); + agent.newVariable("y"); + agent.newVariable("last_x", -1); + agent.newVariable("last_y", -1); + agent.newVariable("last_resources_x", -1); + agent.newVariable("last_resources_y", -1); + agent.newVariable("current_cell_score", 0.0f); + agent.newVariable("priority", 1.0f); + agent.newState("default"); + + auto cell = model.newAgent("cell"); + cell.newVariable("x"); + cell.newVariable("y"); + cell.newVariable("is_occupied", 0); + cell.newVariable("cell_score", 0.0f); + cell.newState("default"); + + move_submodel.setMovingAgent("agent", {}, {{"active", "default"}}, true); + move_submodel.setEnvironmentAgent("cell", {}, {{"active", "default"}}, true); + + CUDASimulation sim(model); + sim.SimulationConfig().steps = 1; + + auto cell_pop = AgentVector(cell, WIDTH * HEIGHT); + for (int i = 0; i < WIDTH * HEIGHT; ++i) { + cell_pop[i].setVariable("x", i / HEIGHT); + cell_pop[i].setVariable("y", i % HEIGHT); + cell_pop[i].setVariable("cell_score", 0.0f); + } + // High score at (1,1) + cell_pop[1 * HEIGHT + 1].setVariable("cell_score", 10.0f); + sim.setPopulationData(cell_pop); + + // Agent at (0,1) - on the edge. + auto agent_pop = AgentVector(agent, 1); + agent_pop[0].setVariable("x", 0); + agent_pop[0].setVariable("y", 1); + sim.setPopulationData(agent_pop); + + sim.step(); + + sim.getPopulationData(agent_pop); + // Should move to (1,1) + EXPECT_EQ(agent_pop[0].getVariable("x"), 1); + EXPECT_EQ(agent_pop[0].getVariable("y"), 1); +} + +} // namespace submodels +} // namespace stockAgent +} // namespace flamegpu