diff --git a/CMakeLists.txt b/CMakeLists.txt index b13c9eb44..43361ab34 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -86,6 +86,7 @@ if(NOT NO_EXAMPLES) option(BUILD_EXAMPLE_ENSEMBLE "Enable building examples/ensemble" OFF) option(BUILD_EXAMPLE_SUGARSCAPE "Enable building examples/sugarscape" OFF) option(BUILD_EXAMPLE_DIFFUSION "Enable building examples/diffusion" OFF) + option(BUILD_EXAMPLE_SCHELLING_SEGREGATION "Enable building examples/schelling_segregation" OFF) endif() option(BUILD_SWIG_PYTHON "Enable python bindings via SWIG" OFF) @@ -176,6 +177,9 @@ endif() if(BUILD_ALL_EXAMPLES OR BUILD_EXAMPLE_DIFFUSION) add_subdirectory(examples/diffusion) endif() +if(BUILD_ALL_EXAMPLES OR BUILD_EXAMPLE_SCHELLING_SEGREGATION) + add_subdirectory(examples/schelling_segregation) +endif() # Add the tests directory (if required) if(BUILD_TESTS OR BUILD_TESTS_DEV) # g++ 7 is required for c++ tests to build. diff --git a/examples/schelling_segregation/CMakeLists.txt b/examples/schelling_segregation/CMakeLists.txt new file mode 100644 index 000000000..5e7e10559 --- /dev/null +++ b/examples/schelling_segregation/CMakeLists.txt @@ -0,0 +1,39 @@ +# Set the minimum cmake version to that which supports cuda natively. +cmake_minimum_required(VERSION VERSION 3.12 FATAL_ERROR) + +# Name the project and set languages +project(schelling_segregation CUDA CXX) + +# Set the location of the ROOT flame gpu project relative to this CMakeList.txt +get_filename_component(FLAMEGPU_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../.. REALPATH) + +# Include common rules. +include(${FLAMEGPU_ROOT}/cmake/common.cmake) + +# Define output location of binary files +if(CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR) + # If top level project + SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin/${CMAKE_BUILD_TYPE}/) +else() + # If called via add_subdirectory() + SET(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/../../bin/${CMAKE_BUILD_TYPE}/) +endif() + +# 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 +) + +# Option to enable/disable building the static library +option(VISUALISATION "Enable visualisation support" OFF) + +# Add the executable and set required flags for the target +add_flamegpu_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/schelling_segregation/src/main.cu b/examples/schelling_segregation/src/main.cu new file mode 100644 index 000000000..391be4498 --- /dev/null +++ b/examples/schelling_segregation/src/main.cu @@ -0,0 +1,357 @@ +#include +#include +#include +#include +#include + +#include "flamegpu/flamegpu.h" + +// Configurable properties +constexpr unsigned int GRID_WIDTH = 100; +constexpr float THRESHOLD = 0.70f; + +constexpr unsigned int A = 0; +constexpr unsigned int B = 1; +constexpr unsigned int UNOCCUPIED = 2; + +// Agents output their type +FLAMEGPU_AGENT_FUNCTION(output_type, flamegpu::MessageNone, flamegpu::MessageArray2D) { + FLAMEGPU->message_out.setVariable("type", FLAMEGPU->getVariable("type")); + FLAMEGPU->message_out.setIndex(FLAMEGPU->getVariable("pos", 0), FLAMEGPU->getVariable("pos", 1)); + return flamegpu::ALIVE; +} + +// Agents decide whether they are happy or not and whether or not their space is available +FLAMEGPU_AGENT_FUNCTION(determine_status, flamegpu::MessageArray2D, flamegpu::MessageNone) { + const unsigned int my_x = FLAMEGPU->getVariable("pos", 0); + const unsigned int my_y = FLAMEGPU->getVariable("pos", 1); + + unsigned int same_type_neighbours = 0; + unsigned int diff_type_neighbours = 0; + + // Iterate 3x3 Moore neighbourhood (this does not include the central cell) + unsigned int my_type = FLAMEGPU->getVariable("type"); + for (auto &message : FLAMEGPU->message_in.wrap(my_x, my_y)) { + int message_type = message.getVariable("type"); + same_type_neighbours += my_type == message_type; + diff_type_neighbours += (my_type != message_type) && (message_type != UNOCCUPIED); + } + + int isHappy = (static_cast(same_type_neighbours) / (same_type_neighbours + diff_type_neighbours)) > THRESHOLD; + FLAMEGPU->setVariable("happy", isHappy); + unsigned int my_next_type = ((my_type != UNOCCUPIED) && isHappy) ? my_type : UNOCCUPIED; + FLAMEGPU->setVariable("next_type", my_next_type); + FLAMEGPU->setVariable("movement_resolved", (my_type == UNOCCUPIED) || isHappy); + unsigned int my_availability = (my_type == UNOCCUPIED) || (isHappy == 0); + FLAMEGPU->setVariable("available", my_availability); + + return flamegpu::ALIVE; +} + +FLAMEGPU_AGENT_FUNCTION_CONDITION(is_available) { + return FLAMEGPU->getVariable("available"); +} +FLAMEGPU_AGENT_FUNCTION(output_available_locations, flamegpu::MessageNone, flamegpu::MessageArray) { + FLAMEGPU->message_out.setIndex(FLAMEGPU->getThreadIndex()); + FLAMEGPU->message_out.setVariable("id", FLAMEGPU->getID()); + return flamegpu::ALIVE; +} + +FLAMEGPU_HOST_FUNCTION(count_available_spaces) { + FLAMEGPU->environment.setProperty("spaces_available", FLAMEGPU->agent("agent").count("available", 1)); +} + +FLAMEGPU_AGENT_FUNCTION_CONDITION(is_moving) { + bool movementResolved = FLAMEGPU->getVariable("movement_resolved"); + return !movementResolved; +} +FLAMEGPU_AGENT_FUNCTION(bid_for_location, flamegpu::MessageArray, flamegpu::MessageBucket) { + // Select a location + unsigned int selected_index = FLAMEGPU->random.uniform(0, FLAMEGPU->environment.getProperty("spaces_available") - 1); + + // Get the location at that index + const auto& message = FLAMEGPU->message_in.at(selected_index); + const flamegpu::id_t selected_location = message.getVariable("id"); + + // Bid for that location + FLAMEGPU->message_out.setKey(selected_location - 1); + FLAMEGPU->message_out.setVariable("id", FLAMEGPU->getID()); + FLAMEGPU->message_out.setVariable("type", FLAMEGPU->getVariable("type")); + return flamegpu::ALIVE; +} + +FLAMEGPU_AGENT_FUNCTION(select_winners, flamegpu::MessageBucket, flamegpu::MessageArray) { + // First agent in the bucket wins + for (const auto& message : FLAMEGPU->message_in(FLAMEGPU->getID() - 1)) { + flamegpu::id_t winning_id = message.getVariable("id"); + FLAMEGPU->setVariable("next_type", message.getVariable("type")); + FLAMEGPU->setVariable("available", 0); + FLAMEGPU->message_out.setIndex(winning_id - 1); + FLAMEGPU->message_out.setVariable("won", 1); + break; + } + return flamegpu::ALIVE; +} + +FLAMEGPU_AGENT_FUNCTION(has_moved, flamegpu::MessageArray, flamegpu::MessageNone) { + const auto& message = FLAMEGPU->message_in.at(FLAMEGPU->getID() - 1); + if (message.getVariable("won")) { + FLAMEGPU->setVariable("movement_resolved", 1); + } + return flamegpu::ALIVE; +} + +FLAMEGPU_EXIT_CONDITION(movement_resolved) { + return (FLAMEGPU->agent("agent").count("movement_resolved", 0) == 0) ? flamegpu::EXIT : flamegpu::CONTINUE; +} + +FLAMEGPU_AGENT_FUNCTION(update_locations, flamegpu::MessageNone, flamegpu::MessageNone) { + FLAMEGPU->setVariable("type", FLAMEGPU->getVariable("next_type")); + return flamegpu::ALIVE; +} + +int main(int argc, const char ** argv) { + NVTX_RANGE("main"); + NVTX_PUSH("ModelDescription"); + + flamegpu::ModelDescription model("Schelling_segregation"); + + /** + * Messages + */ + { + { + flamegpu::MessageArray2D::Description &message = model.newMessage("type_message"); + message.newVariable("type"); + message.setDimensions(GRID_WIDTH, GRID_WIDTH); + } + } + + /** + * Agents + */ + { // Per cell agent + flamegpu::AgentDescription &agent = model.newAgent("agent"); + agent.newVariable("pos"); + agent.newVariable("type"); + agent.newVariable("next_type"); + agent.newVariable("happy"); + agent.newVariable("available"); + agent.newVariable("movement_resolved"); +#ifdef VISUALISATION + // Redundant seperate floating point position vars for vis + agent.newVariable("x"); + agent.newVariable("y"); +#endif + // Functions + agent.newFunction("output_type", output_type).setMessageOutput("type_message"); + agent.newFunction("determine_status", determine_status).setMessageInput("type_message"); + agent.newFunction("update_locations", update_locations); + } + + /** + * Movement resolution submodel + */ + + flamegpu::ModelDescription submodel("plan_movement"); + submodel.addExitCondition(movement_resolved); + { + // Environment + { + flamegpu::EnvironmentDescription& env = submodel.Environment(); + env.newProperty("spaces_available", 0); + } + + // Message types + { + { + flamegpu::MessageArray::Description &message = submodel.newMessage("available_location_message"); + message.newVariable("id"); + message.setLength(GRID_WIDTH*GRID_WIDTH); + } + { + flamegpu::MessageBucket::Description &message = submodel.newMessage("intent_to_move_message"); + message.newVariable("id"); + message.newVariable("type"); + message.setBounds(0, GRID_WIDTH * GRID_WIDTH); + } + { + flamegpu::MessageArray::Description &message = submodel.newMessage("movement_won_message"); + message.newVariable("won"); + message.setLength(GRID_WIDTH*GRID_WIDTH); + } + } + + // Agent types + { + flamegpu::AgentDescription &agent = submodel.newAgent("agent"); + agent.newVariable("pos"); + agent.newVariable("type"); + agent.newVariable("next_type"); + agent.newVariable("happy"); + agent.newVariable("available"); + agent.newVariable("movement_resolved"); + + // Functions + auto& outputLocationsFunction = agent.newFunction("output_available_locations", output_available_locations); + outputLocationsFunction.setMessageOutput("available_location_message"); + outputLocationsFunction.setFunctionCondition(is_available); + + auto& bidFunction = agent.newFunction("bid_for_location", bid_for_location); + bidFunction.setFunctionCondition(is_moving); + bidFunction.setMessageInput("available_location_message"); + bidFunction.setMessageOutput("intent_to_move_message"); + + auto& selectWinnersFunction = agent.newFunction("select_winners", select_winners); + selectWinnersFunction.setMessageInput("intent_to_move_message"); + selectWinnersFunction.setMessageOutput("movement_won_message"); + selectWinnersFunction.setMessageOutputOptional(true); + + agent.newFunction("has_moved", has_moved).setMessageInput("movement_won_message"); + } + // Control flow + { + // Available agents output their location (indexed by thread ID) + { + flamegpu::LayerDescription &layer = submodel.newLayer(); + layer.addAgentFunction(output_available_locations); + } + // Count the number of available spaces + { + flamegpu::LayerDescription &layer = submodel.newLayer(); + layer.addHostFunction(count_available_spaces); + } + // Unhappy agents bid for a new location + { + flamegpu::LayerDescription &layer = submodel.newLayer(); + layer.addAgentFunction(bid_for_location); + } + // Available locations check if anyone wants to move to them. If so, approve one and mark as unavailable + // Update next type to the type of the mover + // Output a message to inform the mover that they have been successful + { + flamegpu::LayerDescription &layer = submodel.newLayer(); + layer.addAgentFunction(select_winners); + } + // Movers mark themselves as resolved + { + flamegpu::LayerDescription &layer = submodel.newLayer(); + layer.addAgentFunction(has_moved); + } + } + } + flamegpu::SubModelDescription& plan_movement = model.newSubModel("plan_movement", submodel); + { + plan_movement.bindAgent("agent", "agent", true, true); + } + + /** + * Control flow + */ + { // Layer #1 + flamegpu::LayerDescription &layer = model.newLayer(); + layer.addAgentFunction(output_type); + } + { // Layer #2 + flamegpu::LayerDescription &layer = model.newLayer(); + layer.addAgentFunction(determine_status); + } + { + flamegpu::LayerDescription &layer = model.newLayer(); + layer.addSubModel(plan_movement); + } + { + flamegpu::LayerDescription &layer = model.newLayer(); + layer.addAgentFunction(update_locations); + } + { // Trying calling this again to fix vis + flamegpu::LayerDescription &layer = model.newLayer(); + layer.addAgentFunction(determine_status); + } + + + NVTX_POP(); + + /** + * Create Model Runner + */ + NVTX_PUSH("CUDAAgentModel creation"); + flamegpu::CUDASimulation cudaSimulation(model); + NVTX_POP(); + + /** + * Create visualisation + * @note FLAMEGPU2 doesn't currently have proper support for discrete/2d visualisations + */ +#ifdef VISUALISATION + flamegpu::visualiser::ModelVis &visualisation = cudaSimulation.getVisualisation(); + { + visualisation.setSimulationSpeed(2); + visualisation.setInitialCameraLocation(GRID_WIDTH / 2.0f, GRID_WIDTH / 2.0f, 225.0f); + visualisation.setInitialCameraTarget(GRID_WIDTH / 2.0f, GRID_WIDTH /2.0f, 0.0f); + visualisation.setCameraSpeed(0.001f * GRID_WIDTH); + visualisation.setViewClips(0.1f, 5000); + auto &agt = visualisation.addAgent("agent"); + // Position vars are named x, y, z; so they are used by default + agt.setModel(flamegpu::visualiser::Stock::Models::CUBE); // 5 unwanted faces! + agt.setModelScale(1.0f); + + flamegpu::visualiser::DiscreteColor cell_colors = flamegpu::visualiser::DiscreteColor("type", flamegpu::visualiser::Color{"#666"}); + cell_colors[A] = flamegpu::visualiser::Stock::Colors::RED; + cell_colors[B] = flamegpu::visualiser::Stock::Colors::BLUE; + agt.setColor(cell_colors); + } + visualisation.activate(); +#endif + + /** + * Initialisation + */ + NVTX_PUSH("CUDAAgentModel initialisation"); + cudaSimulation.initialise(argc, argv); + if (cudaSimulation.getSimulationConfig().input_file.empty()) { + std::default_random_engine rng; + + // Currently population has not been init, so generate an agent population on the fly + const unsigned int CELL_COUNT = GRID_WIDTH * GRID_WIDTH; + std::uniform_real_distribution normal(0, 1); + unsigned int i = 0; + flamegpu::AgentVector init_pop(model.Agent("agent"), CELL_COUNT); + for (unsigned int x = 0; x < GRID_WIDTH; ++x) { + for (unsigned int y = 0; y < GRID_WIDTH; ++y) { + flamegpu::AgentVector::Agent instance = init_pop[i++]; + instance.setVariable("pos", { x, y }); + // Will this cell be occupied + if (normal(rng) < 0.8) { + unsigned int type = normal(rng) < 0.5 ? A : B; + instance.setVariable("type", type); + instance.setVariable("happy", 0); + } else { + instance.setVariable("type", UNOCCUPIED); + } +#ifdef VISUALISATION + // Redundant separate floating point position vars for vis + instance.setVariable("x", static_cast(x)); + instance.setVariable("y", static_cast(y)); +#endif + } + } + cudaSimulation.setPopulationData(init_pop); + } + NVTX_POP(); + + /** + * Execution + */ + cudaSimulation.simulate(); + + /** + * Export Pop + */ + // cudaSimulation.exportData("end.xml"); + +#ifdef VISUALISATION + visualisation.join(); +#endif + return 0; +}