diff --git a/.gitignore b/.gitignore index 4c2e7458..d1a38deb 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,11 @@ cmake-build-*/ #mac .DS_Store + +#savedata/ #issues with xml save location + +savedata/GPAgent/*.xml +#AgentData_*.xml + + +site diff --git a/.gitmodules b/.gitmodules index 55d6c6d9..8544886a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "third_party/SFML"] path = third_party/SFML url = https://github.com/SFML/SFML.git +[submodule "third_party/tinyxml2"] + path = third_party/tinyxml2 + url = https://github.com/leethomason/tinyxml2.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 748132fd..5bba2f19 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,27 +6,27 @@ project(CSE_491) # For now, default to building both the main app and tests set(BUILD_MAIN 1) set(BUILD_TESTS 1) -if("${CMAKE_BUILD_TYPE}" STREQUAL "Test") - set(BUILD_MAIN 0) - set(BUILD_TESTS 1) -endif() +if ("${CMAKE_BUILD_TYPE}" STREQUAL "Test") + set(BUILD_MAIN 0) + set(BUILD_TESTS 1) +endif () # Create a function to make .cmake files simpler function(add_source_to_target TARGET_NAME SOURCE_PATH) - message(STATUS "Loading source: ${SOURCE_PATH}") - target_sources(${TARGET_NAME} - PRIVATE ${CMAKE_SOURCE_DIR}/${SOURCE_PATH} - ) + message(STATUS "Loading source: ${SOURCE_PATH}") + target_sources(${TARGET_NAME} + PRIVATE ${CMAKE_SOURCE_DIR}/${SOURCE_PATH} + ) endfunction() # Set the necessary C++ flags, some of which are configuration-specific set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_FLAGS "-Wall -Wextra -Wcast-align -Winfinite-recursion -Wnon-virtual-dtor -Wnull-dereference -Woverloaded-virtual -pedantic") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wcast-align -Winfinite-recursion -Wnon-virtual-dtor -Wnull-dereference -Woverloaded-virtual -pedantic") set(CMAKE_CXX_FLAGS_DEBUG "-g") set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG") set(CMAKE_CXX_FLAGS_MINSIZEREL "-DNDEBUG") set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-O2 -g") - +set(CMAKE_CXX_FLAGS_COVERAGE "-O2 -g -fcoverage-mapping -fprofile-instr-generate -fprofile-arcs") # Place all executables in the executable directory set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/executable) @@ -34,64 +34,154 @@ set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/executable) # Move assets to build directory file(COPY ./assets DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) + +#option(BUILD_CLANG_LLVM "Build with clang for LLVM" OFF) +option(SANITIZE_MEMORY "Build with sanitizers for GP" OFF) +if (SANITIZE_MEMORY) + + set(BUILD_MAIN 0) + set(BUILD_TESTS 0) + + set(CMAKE_CXX_COMPILER "/opt/homebrew/opt/llvm/bin/clang++") + + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=memory") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=memory") + + set(XML_SRC_DIR third_party/tinyxml2) + set(XML_BUILD_DIR xml_build) + add_subdirectory(${XML_SRC_DIR} ${XML_BUILD_DIR}) + + #added the executable + add_executable(gp_train_main source/gp_train_main.cpp) + + #linking the targets + target_sources(gp_train_main PRIVATE source/core/Entity.cpp) + target_include_directories(gp_train_main + PRIVATE ${CMAKE_SOURCE_DIR}/source/core + ${CMAKE_SOURCE_DIR}/source/Agents + ) + target_link_libraries(gp_train_main + PRIVATE tinyxml2 + PRIVATE pthread + ) + + +endif () + + + + + +# Build the gp_train_main executable, if requested without SFML and Catch2 +option(BUILD_GP_ONLY "Build only gp_main.cpp" OFF) +if (BUILD_GP_ONLY) + set(BUILD_MAIN 0) + set(BUILD_TESTS 0) + + # Configure all of tinyxml2 + set(XML_SRC_DIR third_party/tinyxml2) + set(XML_BUILD_DIR xml_build) + add_subdirectory(${XML_SRC_DIR} ${XML_BUILD_DIR}) + + #added the executable + add_executable(gp_train_main source/gp_train_main.cpp) + + #linking the targets + target_sources(gp_train_main PRIVATE source/core/Entity.cpp) + target_include_directories(gp_train_main + PRIVATE ${CMAKE_SOURCE_DIR}/source/core + ${CMAKE_SOURCE_DIR}/source/Agents + ) + target_link_libraries(gp_train_main + PRIVATE tinyxml2 + PRIVATE pthread + ) + +endif () + + # Build the main application executables, if requested -if(${BUILD_MAIN}) - - # Configure all of SFML - set(SFML_SRC_DIR third_party/SFML) - set(SFML_BUILD_DIR sfml_build) - add_subdirectory(${SFML_SRC_DIR} ${SFML_BUILD_DIR}) - - - # Find all the main files for the various applications - # Currently this means any *.cpp file in the root of source - file(GLOB EXE_SOURCES CONFIGURE_DEPENDS RELATIVE ${CMAKE_SOURCE_DIR}/source source/*.cpp) - message(STATUS "List of main files to build: ${EXE_SOURCES}") - - # Loop through each executable and build it! - foreach(EXE_SOURCE ${EXE_SOURCES}) - # Rip the .cpp off the end of the string - string(REPLACE ".cpp" "" EXE_NAME ${EXE_SOURCE}) - # Create list of source files (currently just the one .cpp file) - # Create executable and link to includes / libraries - add_executable(${EXE_NAME} ${CMAKE_SOURCE_DIR}/source/${EXE_SOURCE} ${CMAKE_SOURCE_DIR}/source/core/Entity.cpp) - target_include_directories(${EXE_NAME} - PRIVATE ${CMAKE_SOURCE_DIR}/source - ) - target_link_libraries(${EXE_NAME} - PRIVATE sfml-window sfml-audio sfml-graphics sfml-system sfml-network - ) - if(EXISTS ${CMAKE_SOURCE_DIR}/source/${EXE_NAME}.cmake) - message(STATUS "Loading ${EXE_NAME}.cmake") - include(${CMAKE_SOURCE_DIR}/source/${EXE_NAME}.cmake) - else() - message(WARNING "Cannot find ${EXE_NAME}.cmake") - endif() - endforeach() -endif() +if (${BUILD_MAIN}) +# line to set santizers +# set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=[sanitizer]") +# can be set to address, undefined, thread, memory, or leak -# Build the test executables, if requested -if(${BUILD_TESTS}) - - # Configure Catch - set(CATCH_SRC_DIR third_party/Catch2) - set(CATCH_BUILD_DIR catch_build) - add_subdirectory(${CATCH_SRC_DIR} ${CATCH_BUILD_DIR}) - - # Configure only the networking portion of SFML - if(NOT ${BUILD_MAIN}) - set(SFML_BUILD_WINDOW FALSE) - set(SFML_BUILD_GRAPHICS FALSE) - set(SFML_BUILD_AUDIO FALSE) + # Configure all of SFML set(SFML_SRC_DIR third_party/SFML) set(SFML_BUILD_DIR sfml_build) add_subdirectory(${SFML_SRC_DIR} ${SFML_BUILD_DIR}) - endif() - # Setup CTest - include(CTest) - enable_testing() - # Tunnel into test directory CMake infrastructure - add_subdirectory(tests) -endif() + #configure the xml library + set(XML_SRC_DIR third_party/tinyxml2) + set(XML_BUILD_DIR xml_build) + add_subdirectory(${XML_SRC_DIR} ${XML_BUILD_DIR}) + + + # Find all the main files for the various applications + # Currently this means any *.cpp file in the root of source + file(GLOB EXE_SOURCES CONFIGURE_DEPENDS RELATIVE ${CMAKE_SOURCE_DIR}/source source/*.cpp) + message(STATUS "List of main files to build: ${EXE_SOURCES}") + + + + # Loop through each executable and build it! + foreach (EXE_SOURCE ${EXE_SOURCES}) + # Rip the .cpp off the end of the string + string(REPLACE ".cpp" "" EXE_NAME ${EXE_SOURCE}) + # Create list of source files (currently just the one .cpp file) + # Create executable and link to includes / libraries + add_executable(${EXE_NAME} ${CMAKE_SOURCE_DIR}/source/${EXE_SOURCE} ${CMAKE_SOURCE_DIR}/source/core/Entity.cpp) + target_include_directories(${EXE_NAME} + PRIVATE ${CMAKE_SOURCE_DIR}/source + ) + target_link_libraries(${EXE_NAME} + PRIVATE sfml-window sfml-audio sfml-graphics sfml-system sfml-network + ) + + target_link_libraries(${EXE_NAME} + PRIVATE tinyxml2 + ) + + if (EXISTS ${CMAKE_SOURCE_DIR}/source/${EXE_NAME}.cmake) + message(STATUS "Loading ${EXE_NAME}.cmake") + include(${CMAKE_SOURCE_DIR}/source/${EXE_NAME}.cmake) + else () + message(WARNING "Cannot find ${EXE_NAME}.cmake") + endif () + endforeach () +endif () + +# Build the test executables, if requested +if (${BUILD_TESTS}) + + # Configure Catch + set(CATCH_SRC_DIR third_party/Catch2) + set(CATCH_BUILD_DIR catch_build) + add_subdirectory(${CATCH_SRC_DIR} ${CATCH_BUILD_DIR}) + + + # Configure only the networking portion of SFML + if (NOT ${BUILD_MAIN}) + set(SFML_BUILD_WINDOW FALSE) + set(SFML_BUILD_GRAPHICS FALSE) + set(SFML_BUILD_AUDIO FALSE) + set(SFML_SRC_DIR third_party/SFML) + set(SFML_BUILD_DIR sfml_build) + add_subdirectory(${SFML_SRC_DIR} ${SFML_BUILD_DIR}) + + set(XML_SRC_DIR third_party/tinyxml2) + set(XML_BUILD_DIR xml_build) + add_subdirectory(${XML_SRC_DIR} ${XML_BUILD_DIR}) + + + endif () + + + # Setup CTest + include(CTest) + enable_testing() + + # Tunnel into test directory CMake infrastructure + add_subdirectory(tests) +endif () diff --git a/assets/grids/default_maze2.grid b/assets/grids/default_maze2.grid new file mode 100644 index 00000000..2ef68706 --- /dev/null +++ b/assets/grids/default_maze2.grid @@ -0,0 +1,29 @@ + # # # # +# ### # ########## # ########## # ######### # +# # # # # # # # # # # # +# # ### ## # ### # ############ ##### ### # # +# # # # # # # # # # # +# ##### # # # ### # ###### ###### ##### ### # +# # # # # # # # # # # +##### # ###### # ###### # ## # ###### ####### # ## # +# # # # ## # # # # +# ############## ##### # # # ###### # ######### # +# # # # # # # # # # +############### # ### ###### # # #### # # ##### # # +# # # # # # # # # # # +# ############### ##### # # ######## ##### # # # +# # # # # # # # +# #################### ########### # ######### # +# # # # # +# # # ################### ############# ########## # +# # # # # +# ################ ############### ####### # +# # # # # # # +# # # # # ##################################### # # +# # # # # # # # # +# # # # # # ################################### # # +# # # # # # # # +# # ################################ # # +# # # # # # # # # # +# # # # # # # ############################### # # + # # # # # # # # # # diff --git a/assets/grids/empty_maze.grid b/assets/grids/empty_maze.grid new file mode 100644 index 00000000..5b833eab --- /dev/null +++ b/assets/grids/empty_maze.grid @@ -0,0 +1,9 @@ + * + * + * + * + * + * + * + * + * diff --git a/docs/Group_7.md b/docs/Group_7.md index e69de29b..8c167417 100644 --- a/docs/Group_7.md +++ b/docs/Group_7.md @@ -0,0 +1,85 @@ +# Group 7 : Genetic Programming Agents +-- -- +authors: Aman, Simon, Rajmeet, Jason + + + + + +(Img: Rajmeet, Simon, Jason, Aman) + +## Introduction + +## GP Agent Base Class + +## LGP Agent + +## CGP Agent + +## GP Loop + + +## It runs on my machine +we have used cmake to ensure that our code compiles on all platforms. but.... +we have tested our code on the following machines/architectures: +- Windows 11 +- Windows 10 +- Ubuntu 20.04 +- HPCC Cluster Centos7 +- MacOS Sonoma (ARM) +- mlcollard/linux-dev (Docker Container) + +Tested in the following IDEs: +- CLion +- VSCode + +Tested on the following compilers: +- gcc 9.3.0 +- Apple clang 12.0.0 +- LLVM clang 11.0.0 + +### Profiled with and optimized with: +- clion profiler +
/ + + + +- Xcode instruments +
+ + +- intel vtune +
+ + +- very sleepy +Didnt deserve a screenshot. /s + +- code coverage in clion + + + +### Sanitized with: + +- clang sanitizer Memory +- valgrind +- gcc sanitizer Memory +- gcc sanitizer address + - Used to find and fix memory UB in the code. + + + +## Other Contributions + +### EasyLogging +Created a logging class that is can be used to log debug messages in debug mode. Teams can be specified to log with different levels of verbosity. This is useful for debugging and profiling. + +### CMake +Initial cmake setup for the project. This is useful for cross platform compilation and testing. + +### serializationUsingTinyXML2 +Created and tested a serialization class that can be used to serialize and deserialize objects to and from xml files. This is useful for saving and loading the state of the GP agents. +Implemented serialization pattern using tinyxml2 library. + +### mkdocs documentation +Created and tested a mkdocs documentation for the project. This is useful for creating a website for the project. \ No newline at end of file diff --git a/docs/assets/GP_Group7/CodeCoverage_Clion.png b/docs/assets/GP_Group7/CodeCoverage_Clion.png new file mode 100644 index 00000000..ffbebb6e Binary files /dev/null and b/docs/assets/GP_Group7/CodeCoverage_Clion.png differ diff --git a/docs/assets/GP_Group7/Group7Photo.jpeg b/docs/assets/GP_Group7/Group7Photo.jpeg new file mode 100644 index 00000000..b934a44a Binary files /dev/null and b/docs/assets/GP_Group7/Group7Photo.jpeg differ diff --git a/docs/assets/GP_Group7/ProfilerGP_Clion.png b/docs/assets/GP_Group7/ProfilerGP_Clion.png new file mode 100644 index 00000000..d14f7764 Binary files /dev/null and b/docs/assets/GP_Group7/ProfilerGP_Clion.png differ diff --git a/docs/assets/GP_Group7/ProfilerGP_IntelVtune.png b/docs/assets/GP_Group7/ProfilerGP_IntelVtune.png new file mode 100644 index 00000000..8e0e267b Binary files /dev/null and b/docs/assets/GP_Group7/ProfilerGP_IntelVtune.png differ diff --git a/docs/assets/GP_Group7/ProfilerGP_Xcode.png b/docs/assets/GP_Group7/ProfilerGP_Xcode.png new file mode 100644 index 00000000..071f2a26 Binary files /dev/null and b/docs/assets/GP_Group7/ProfilerGP_Xcode.png differ diff --git a/docs/assets/GP_Group7/UB_Behavior.png b/docs/assets/GP_Group7/UB_Behavior.png new file mode 100644 index 00000000..32a618cd Binary files /dev/null and b/docs/assets/GP_Group7/UB_Behavior.png differ diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..362e93f8 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,34 @@ +site_name: "CSE 491 Documentation" +site_description: "Documentation for CSE 491 Group 7 GP Agent" + +theme: + name: material + palette: + primary: green + accent: green + +plugins: + - search + - mkdoxy: + projects: + Core Project: # name of project must be alphanumeric + numbers (without spaces) + src-dirs: ./source/core # path to source code (support multiple paths separated by space) => INPUT + full-doc: True # if you want to generate full documentation + + doxy-cfg: # standard doxygen configuration (key: value) + FILE_PATTERNS: "*.cpp *.h*" # specify file patterns to filter out + RECURSIVE: True # recursive search in source directories + HIDE_UNDOC_MEMBERS: YES + HIDE_SCOPE_NAMES: YES + EXTRACT_ALL: NO + + GP Agents: + src-dirs: ./source/Agents/GP + full-doc: True + + doxy-cfg: + FILE_PATTERNS: "*.cpp *.h*" + RECURSIVE: True + HIDE_UNDOC_MEMBERS: YES + HIDE_SCOPE_NAMES: YES + EXTRACT_ALL: NO diff --git a/savedata/GPAgent/Z_gp_folder_save.push b/savedata/GPAgent/Z_gp_folder_save.push new file mode 100644 index 00000000..0b90e2b8 --- /dev/null +++ b/savedata/GPAgent/Z_gp_folder_save.push @@ -0,0 +1 @@ +sup \ No newline at end of file diff --git a/source/Agents/GP/CGPAgent.hpp b/source/Agents/GP/CGPAgent.hpp new file mode 100644 index 00000000..3e33ae50 --- /dev/null +++ b/source/Agents/GP/CGPAgent.hpp @@ -0,0 +1,120 @@ +/** + * This file is part of the Fall 2023, CSE 491 course project. + * @brief An Agent based on genetic programming. + * @note Status: PROPOSAL + **/ + +#pragma once + +#include +#include +#include +#include +#include + +#include "GPAgentBase.hpp" +#include "GraphBuilder.hpp" + +namespace cowboys { + /// Don't know the maximum size a state can be, arbitrary large number + constexpr size_t INPUT_SIZE = 6; + + /// Number of computational layers for each agent + constexpr size_t NUM_LAYERS = 3; + + /// The number of nodes in each layer + constexpr size_t NUM_NODES_PER_LAYER = 5; + + /// The number of layers preceding a node's layer that the node can reference + constexpr size_t LAYERS_BACK = 2; + + /// @brief An agent based on cartesian genetic programming. + class CGPAgent : public GPAgentBase { + protected: + /// The genotype for this agent. + CGPGenotype genotype; + + /// The decision graph for this agent. + std::unique_ptr decision_graph; + + + + public: + CGPAgent(size_t id, const std::string &name) : GPAgentBase(id, name) {} + CGPAgent(size_t id, const std::string &name, const CGPGenotype &genotype) + : GPAgentBase(id, name), genotype(genotype) {} + + + void PrintAgent() override { + std::cout << "Genotype: " << genotype.Export() << std::endl; + } + + void MutateAgent(double mutation = 0.8) override { + auto graph_builder = GraphBuilder(); + + genotype.MutateDefault(mutation, *this); + + decision_graph = graph_builder.CartesianGraph(genotype, FUNCTION_SET, this); + } + /// @brief Setup graph. + /// @return Success. + bool Initialize() override { + + // Create a default genotype if none was provided in the constructor + if (genotype.GetNumFunctionalNodes() == 0) { + genotype = CGPGenotype({INPUT_SIZE, action_map.size(), NUM_LAYERS, NUM_NODES_PER_LAYER, LAYERS_BACK}); + } + + genotype.SetSeed(rand()); + + // Mutate the beginning genotype, might not want this. + MutateAgent(0.2); + + return true; + } + + size_t GetAction(const cse491::WorldGrid &grid, const cse491::type_options_t &type_options, + const cse491::item_map_t &item_set, const cse491::agent_map_t &agent_set) override { + auto inputs = EncodeState(grid, type_options, item_set, agent_set, this, extra_state); + size_t action_to_take = decision_graph->MakeDecision(inputs, EncodeActions(action_map)); + return action_to_take; + } + + + void Serialize(tinyxml2::XMLDocument& doc, tinyxml2::XMLElement* parentElem, double fitness = -1) override { + auto agentElem = doc.NewElement("CGPAgent"); + parentElem->InsertEndChild(agentElem); + + auto genotypeElem = doc.NewElement("genotype"); + genotypeElem->SetText(genotype.Export().c_str()); + if (fitness != -1) + genotypeElem->SetAttribute("fitness", fitness); + + genotypeElem->SetAttribute("seed" , seed); + + agentElem->InsertEndChild(genotypeElem); + + } + + + /// @brief Get the genotype for this agent. + /// @return A const reference to the genotype for this agent. + const CGPGenotype &GetGenotype() const { return genotype; } + + + /// @brief Copies the genotype and behavior of another CGPAgent into this agent. + /// @param other The CGPAgent to copy. + void Configure(const CGPAgent &other) { + genotype = other.GetGenotype(); + decision_graph = GraphBuilder().CartesianGraph(genotype, FUNCTION_SET, this); + } + + /// @brief Copy the behavior of another agent into this agent. + /// @param other The agent to copy. + void Copy(const GPAgentBase &other) override { + assert(dynamic_cast(&other) != nullptr); + Configure(dynamic_cast(other)); + } + }; + +} // End of namespace cowboys diff --git a/source/Agents/GP/CGPGenotype.hpp b/source/Agents/GP/CGPGenotype.hpp new file mode 100644 index 00000000..45ab657c --- /dev/null +++ b/source/Agents/GP/CGPGenotype.hpp @@ -0,0 +1,771 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/WorldBase.hpp" +#include "GPAgentBase.hpp" +#include "GraphNode.hpp" + +namespace cowboys { + + /// The separator between each parameter in the header, defining the cartesian graph. + constexpr char HEADER_SEP = ','; + /// The separator between the header and the genotype. + constexpr char HEADER_END = ';'; + /// The separator between each attribute in a node. + constexpr char NODE_GENE_SEP = '.'; + /// The separator between each node in the genotype. + constexpr char NODE_SEP = ':'; + + /// @brief A namespace for base64 encoding and decoding. Does not convert to and from base64 in the typical way. Only + /// guarantees that x == b64_inv(b64(x)), aside from doubles which have problems with precision, + /// so x ~= b64_inv(b64(x)). + namespace base64 { + /// The characters used to represent digits in base64. + static constexpr char CHARS[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/"; + static constexpr size_t MAX_CHAR = *std::max_element(CHARS, CHARS + 64); + static constexpr std::array CHAR_TO_IDX = []() { + std::array indices{}; + for (size_t i = 0; i < 64; ++i) { + indices[CHARS[i]] = i; + } + return indices; + }(); + + /// @brief Converts a number in base10 to base64. + /// @param ull The number to convert. + /// @return The number in base64 as a string. + static std::string ULLToB64(size_t ull) { + if (ull == 0) + return "0"; + // Range of numbers represented by n digits of base b excluding 0: b^(n-1) <= x <= b^n - 1 + // -> n-1 <= log_b(x) -> n-1 = floor(log_b(x)) -> n = 1 + floor(log_b(x)) + // Or x + 1 <= b^n -> log_b(x + 1) <= n -> ceil(log_b(x + 1)) = n; But this can cause an overflow when adding 1 to + // the max value of size_t + size_t n = 1 + std::floor(std::log(ull) / std::log(64)); + std::string result(n, ' '); + for (size_t i = 0; ull > 0; ++i) { + result[n - 1 - i] = CHARS[ull % 64]; + ull /= 64; + } + return result; + } + + /// @brief Converts a number in base64 to base10. + /// @param num_base64 The number in base64 as a string. + /// @return The number in base10. + static size_t B64ToULL(std::string num_base64) { + size_t result = 0; + for (size_t i = 0; i < num_base64.size(); ++i) { + const char ch = num_base64[i]; + const size_t coeff = std::pow(64, num_base64.size() - i - 1); + result += CHAR_TO_IDX[ch] * coeff; + } + return result; + } + + /// @brief Converts a binary string to a base64 string. + /// @param binary A string of 1s and 0s representing binary. + /// @return The binary string in base64. + static std::string B2ToB64(const std::string &binary) { + // 2^6 = 64, so we can encode 6 bits at a time + + size_t remainder = binary.size() % 6; + if (remainder != 0) { + // Pad the binary string with 0s to make it divisible by 6 + return B2ToB64(std::string(6 - remainder, '0') + binary); + } + + std::string result; + result.reserve(binary.size() / 6); + bool all_zeros = true; + for (size_t i = 0; i < binary.size(); i += 6) { + std::string buffer = binary.substr(i, 6); + size_t ull = std::bitset<6>(buffer).to_ulong(); + result += CHARS[ull]; + if (ull != 0) + all_zeros = false; + } + if (all_zeros) // If all 0s, compress to 1 character + return std::string(1, CHARS[0]); + return result; + } + + /// @brief Converts a base64 string to a binary string. + /// @param base64 A string of base64 characters. + /// @return The base64 string in binary. + static std::string B64ToB2(std::string base64) { + std::string result = ""; + for (size_t i = 0; i < base64.size(); ++i) { + const char ch = base64[i]; + const size_t ull = CHAR_TO_IDX[ch]; + result += std::bitset<6>(ull).to_string(); + } + // Remove leading 0s and return result: https://stackoverflow.com/a/31226728/13430191 + return result.erase(0, std::min(result.find_first_not_of(CHARS[0]), result.size() - 1)); + } + + /// @brief Converts a double to a base64 string. Assumes that the stoull(to_string(value)) is possible. Only + /// guarantees that x ~= b64_inv(b64(x)) due to precision errors. Empirically accurate up to 3 decimal places, e.g. + /// round(x, 3) = round(b64_inv(b64(x)), 3). + /// @param value The double to convert. + /// @return The double in base64. + static std::string DoubleToB64(double value) { + std::string double_str = std::to_string(value); + + // Sign + // Store if it is positive or negative using the first base64 character or the second + char sign_b64 = CHARS[0]; + if (value < 0) { + sign_b64 = CHARS[1]; + // Remove the negative sign + double_str.erase(0, 1); + } + + // Decimal point + size_t decimal_loc = std::min(double_str.find('.'), double_str.size()); + // Remove the decimal point (does nothing if decimal_loc == double_str.size()) + double_str.erase(decimal_loc, 1); + // Location of the decimal from the right end of the string, so that leading 0s that are dropped can be recovered + size_t decimal_loc_from_right = double_str.size() - decimal_loc; + // Store decimal location using 1 base64 character (arbitrary choice, assumes decimal_loc < 64) + char decimal_loc_b64 = CHARS[decimal_loc_from_right]; + + // ULL + // Take the rest of the string as a ULL + size_t ull = std::stoull(double_str); + // Convert to base64 + std::string ull_b64 = ULLToB64(ull); + // Return decimal location and ULL + return std::string({decimal_loc_b64, sign_b64}) + ull_b64; + } + + /// @brief Converts a base64 string to a double. See @ref DoubleToB64 for precision issues. + /// @param value The base64 string to convert. + /// @return The base64 string as a double. + static double B64ToDouble(const std::string &value) { + assert(value.size() > 0); + // Get decimal location + size_t decimal_loc_from_right = CHAR_TO_IDX[value[0]]; + // Get sign + double sign = value[1] == CHARS[0] ? 1 : -1; + // Get ULL + std::string ull = std::to_string(B64ToULL(value.substr(2))); + if (ull.size() < decimal_loc_from_right) + ull = std::string(decimal_loc_from_right - ull.size() + 1, CHARS[0]) + ull; + // Insert decimal point + ull.insert(ull.size() - decimal_loc_from_right, "."); + // Return double + return sign * std::stod(ull); + } + } // namespace base64 + + /// @brief Holds the representation of a cartesian graph node. + struct CGPNodeGene { + /// The input connections of this node. '1' means connected, '0' means not connected. + std::vector input_connections{}; + + /// The index of the function the node uses. + size_t function_idx{0}; + + /// The default output of the node. + double default_output{0}; + + /// @brief Compare two CGPNodeGenes for equality. + /// @param other The other CGPNodeGene to compare to. + /// @return True if the two CGPNodeGenes are equal, false otherwise. + inline bool operator==(const CGPNodeGene &other) const { + return input_connections == other.input_connections && function_idx == other.function_idx && + default_output == other.default_output; + } + }; + + /// @brief Holds the parameters that define the structure of a cartesian graph. + struct CGPParameters { + /// The number of inputs to the graph. + size_t num_inputs{0}; + /// The number of outputs from the graph. + size_t num_outputs{0}; + /// The number of middle layers in the graph. + size_t num_layers{0}; + /// The number of nodes per middle layer. + size_t num_nodes_per_layer{0}; + /// The number of layers backward that a node can connect to. + size_t layers_back{0}; + + CGPParameters() = default; + + /// Constructor for the cartesian graph parameters. + CGPParameters(size_t num_inputs, size_t num_outputs, size_t num_layers, size_t num_nodes_per_layer, + size_t layers_back) + : num_inputs(num_inputs), num_outputs(num_outputs), num_layers(num_layers), + num_nodes_per_layer(num_nodes_per_layer), layers_back(layers_back) {} + + /// @brief Returns the number of functional nodes in the graph. + /// @return The number of functional nodes in the graph. + size_t GetFunctionalNodeCount() const { return num_layers * num_nodes_per_layer + num_outputs; } + + /// @brief Check if two CGPParameters are equal. + /// @param other The other CGPParameters to compare to. + /// @return True if the two CGPParameters are equal, false otherwise. + inline bool operator==(const CGPParameters &other) const { + return num_inputs == other.num_inputs && num_outputs == other.num_outputs && num_layers == other.num_layers && + num_nodes_per_layer == other.num_nodes_per_layer && layers_back == other.layers_back; + } + }; + + /// @brief Holds all the information that uniquely defines a cartesian graph. + class CGPGenotype { + protected: + /// The parameters of the cartesian graph. + CGPParameters params; + + /// The node configurations. + std::vector nodes; + + /// The random number generator. + std::mt19937 rng; + + private: + /// @brief Encodes the header into a string. + /// @return The encoded header. + + std::string EncodeHeader() const { + std::string header; + header += std::to_string(params.num_inputs); + header += HEADER_SEP; + header += std::to_string(params.num_outputs); + header += HEADER_SEP; + header += std::to_string(params.num_layers); + header += HEADER_SEP; + header += std::to_string(params.num_nodes_per_layer); + header += HEADER_SEP; + header += std::to_string(params.layers_back); + return header; + } + + /// @brief Decodes the header of the genotype. + void DecodeHeader(const std::string &header) { + // Parse header and save to member variables + std::vector header_parts; + size_t start_pos = 0; + size_t comma_pos = header.find(HEADER_SEP, start_pos); + while (comma_pos != std::string::npos) { + header_parts.push_back(std::stoull(header.substr(start_pos, comma_pos - start_pos))); + start_pos = comma_pos + 1; + comma_pos = header.find(HEADER_SEP, start_pos); + } + header_parts.push_back(std::stoull(header.substr(start_pos))); + + if (header_parts.size() != 5) { + std::string message; + message += "Invalid genotype: Header should have 5 parameters but found "; + message += std::to_string(header_parts.size()); + message += "."; + throw std::runtime_error(message); + } + params.num_inputs = header_parts.at(0); + params.num_outputs = header_parts.at(1); + params.num_layers = header_parts.at(2); + params.num_nodes_per_layer = header_parts.at(3); + params.layers_back = header_parts.at(4); + } + + /// @brief Encodes the genotype into a string. + /// @return The encoded genotype. + std::string EncodeGenotype() const { + std::string genotype = ""; + for (const CGPNodeGene &node : nodes) { + // Input Connections + genotype += base64::B2ToB64(std::string(node.input_connections.cbegin(), node.input_connections.cend())); + genotype += NODE_GENE_SEP; + // Function index + genotype += base64::ULLToB64(node.function_idx); + genotype += NODE_GENE_SEP; + // Default output + genotype += base64::DoubleToB64(node.default_output); + // End of node + genotype += NODE_SEP; + } + return genotype; + } + + /// @brief Encodes the genotype into a string. + /// @return The encoded genotype. + std::string EncodeGenotypeRaw() const { + std::string genotype = ""; + for (const CGPNodeGene &node : nodes) { + // Input Connections + genotype += std::string(node.input_connections.cbegin(), node.input_connections.cend()); + genotype += NODE_GENE_SEP; + // Function index + genotype += std::to_string(node.function_idx); + genotype += NODE_GENE_SEP; + // Default output + genotype += std::to_string(node.default_output); + // End of node + genotype += NODE_SEP; + } + return genotype; + } + + /// @brief Decodes the genotype string and configures the node genes. Node gene vector should be initialized + /// before calling this. + void DecodeGenotype(const std::string &genotype) { + size_t node_gene_start = 0; + size_t node_gene_end = genotype.find(NODE_SEP, node_gene_start); + size_t node_idx = 0; + while (node_gene_end != std::string::npos) { + // Parse the node gene + std::string node_gene = genotype.substr(node_gene_start, node_gene_end - node_gene_start); + assert(node_idx < nodes.size()); + auto ¤t_node = nodes[node_idx]; + + // + // Input Connections + // + size_t sep_pos = node_gene.find(NODE_GENE_SEP); + assert(sep_pos != std::string::npos); + std::string input_connections_b64 = node_gene.substr(0, sep_pos); + std::string input_connections_b2 = base64::B64ToB2(input_connections_b64); + auto &input_connections = current_node.input_connections; + // If there were leading bits that were 0 when converted to base 64, they were dropped. Add them back. + std::string input_connections_str = std::string(input_connections.cbegin(), input_connections.cend()); + assert(input_connections.size() >= input_connections_b2.size()); // Invalid genotype if this fails + input_connections_b2 = + std::string(input_connections.size() - input_connections_b2.size(), '0') + input_connections_b2; + assert(input_connections.size() == input_connections_b2.size()); + for (size_t i = 0; i < input_connections_b2.size(); ++i) { + input_connections[i] = input_connections_b2[i]; + } + node_gene = node_gene.substr(sep_pos + 1); + + // + // Function index + // + sep_pos = node_gene.find(NODE_GENE_SEP); + std::string function_idx_str = node_gene.substr(0, std::min(sep_pos, node_gene.size())); + current_node.function_idx = base64::B64ToULL(function_idx_str); + node_gene = node_gene.substr(sep_pos + 1); + + // + // Default output + // + sep_pos = node_gene.find(NODE_GENE_SEP); + assert(sep_pos == std::string::npos); // Should be the last attribute + std::string default_output_str = node_gene.substr(0, std::min(sep_pos, node_gene.size())); + current_node.default_output = base64::B64ToDouble(default_output_str); + + // Move to next node gene + node_gene_start = node_gene_end + 1; + node_gene_end = genotype.find(NODE_SEP, node_gene_start); + ++node_idx; + } + } + + public: + /// @brief Default constructor for the cartesian graph genotype. Will have 0 functional nodes + CGPGenotype() = default; + /// @brief Constructor for the cartesian graph genotype. Initializes the genotype with the given parameters and + /// leaves everything default (nodes will be unconnected). + /// @param parameters The parameters of the cartesian graph. + CGPGenotype(const CGPParameters ¶meters) : params(parameters) { InitGenotype(); } + + // Rule of 5 + ~CGPGenotype() = default; + /// @brief Copy constructor for the cartesian graph genotype. + /// @param other The other cartesian graph genotype to copy from. + CGPGenotype(const CGPGenotype &other) { Configure(other.Export()); } + /// @brief Copy assignment operator for the cartesian graph genotype. + /// @param other The other cartesian graph genotype to copy from. + /// @return This cartesian graph genotype. + CGPGenotype &operator=(const CGPGenotype &other) { + Configure(other.Export()); + return *this; + } + /// @brief Move constructor for the cartesian graph genotype. + /// @param other The other cartesian graph genotype to move from. + CGPGenotype(CGPGenotype &&other) noexcept { + params = other.params; + nodes = std::move(other.nodes); + } + /// @brief Move assignment operator for the cartesian graph genotype. + /// @param other The other cartesian graph genotype to move from. + /// @return This cartesian graph genotype. + CGPGenotype &operator=(CGPGenotype &&other) noexcept { + params = other.params; + nodes = std::move(other.nodes); + return *this; + } + + /// @brief Configures this genotype from an encoded string. + /// @param encoded_genotype The encoded genotype. + /// @return This genotype. + CGPGenotype &Configure(const std::string &encoded_genotype) { + // Separate header and genotype + size_t newline_pos = encoded_genotype.find(HEADER_END); + if (newline_pos == std::string::npos) + throw std::runtime_error("Invalid genotype: No newline character found."); + std::string header = encoded_genotype.substr(0, newline_pos); + std::string genotype = encoded_genotype.substr(newline_pos + 1); + + // Parse header and save to member variables + DecodeHeader(header); + + // Initialize the genotype + InitGenotype(); + + // Decode genotype + DecodeGenotype(genotype); + + // Check if the number of functional nodes is correct + assert(nodes.size() == params.GetFunctionalNodeCount()); + + return *this; + } + + /// @brief Returns the iterator to the beginning of the node configurations. + /// @return The iterator to the beginning of the node configurations. + std::vector::iterator begin() { return nodes.begin(); } + + /// @brief Returns the iterator to the end of the node configurations. + /// @return The iterator to the end of the node configurations. + std::vector::iterator end() { return nodes.end(); } + + /// @brief Returns the const iterator to the beginning of the node configurations. + /// @return The const iterator to the beginning of the node configurations. + std::vector::const_iterator begin() const { return nodes.begin(); } + + /// @brief Returns the const iterator to the end of the node configurations. + /// @return The const iterator to the end of the node configurations. + std::vector::const_iterator end() const { return nodes.end(); } + + /// @brief Returns the const iterator to the beginning of the node configurations. + /// @return The const iterator to the beginning of the node configurations. + std::vector::const_iterator cbegin() const { return nodes.cbegin(); } + + /// @brief Returns the const iterator to the end of the node configurations. + /// @return The const iterator to the end of the node configurations. + std::vector::const_iterator cend() const { return nodes.cend(); } + + /// @brief Returns the number of possible connections in the graph. + /// @return The number of possible connections in the graph. + size_t GetNumPossibleConnections() const { + size_t num_connections = 0; + for (const CGPNodeGene &node : nodes) { + num_connections += node.input_connections.size(); + } + return num_connections; + } + + /// @brief Returns the number of connected connections in the graph. + /// @return The number of connected connections in the graph. + size_t GetNumConnections() const { + size_t num_connections = 0; + for (const CGPNodeGene &node : nodes) { + for (char con : node.input_connections) { + if (con == '1') + ++num_connections; + } + } + return num_connections; + } + + /// @brief Set the parameters of the cartesian graph. + /// @param params The parameters of the cartesian graph. Basically a 5-tuple. + void SetParameters(const CGPParameters ¶ms) { this->params = params; } + + /// @brief Returns the number of inputs to the graph. + /// @return The number of inputs to the graph. + size_t GetNumInputs() const { return params.num_inputs; } + + /// @brief Returns the number of outputs from the graph. + /// @return The number of outputs from the graph. + size_t GetNumOutputs() const { return params.num_outputs; } + + /// @brief Returns the number of middle layers in the graph. + /// @return The number of middle layers in the graph. + size_t GetNumLayers() const { return params.num_layers; } + + /// @brief Returns the number of nodes per middle layer. + /// @return The number of nodes per middle layer. + size_t GetNumNodesPerLayer() const { return params.num_nodes_per_layer; } + + /// @brief Returns the number of layers backward that a node can connect to. + /// @return The number of layers backward that a node can connect to. + size_t GetLayersBack() const { return params.layers_back; } + + /// @brief Returns the number of functional (non-input) nodes in the graph. + /// @return The number of functional (non-input) nodes in the graph. + size_t GetNumFunctionalNodes() const { return nodes.size(); } + + /// @brief Initializes an empty genotype with the cartesian graph parameters. + void InitGenotype() { + // Clear node configurations + nodes.clear(); + + // Input nodes won't have any inputs and no function, so they are skipped + + // Start at 1 to account for the input layer + for (size_t i = 1; i <= params.num_layers + 1; ++i) { + size_t layer_size = i == params.num_layers + 1 ? params.num_outputs : params.num_nodes_per_layer; + for (size_t j = 0; j < layer_size; ++j) { + // Count up possible input connections from each layer backwards + size_t valid_layers_back = std::min(params.layers_back, i); + size_t num_input_connections = valid_layers_back * params.num_nodes_per_layer; + if (i <= params.layers_back) { + num_input_connections -= params.num_nodes_per_layer; + num_input_connections += params.num_inputs; + } + // Create node gene using empty connections + std::vector input_connections(num_input_connections, '0'); + // Add the node configuration. With default values + nodes.push_back({input_connections}); + } + } + } + + /// @brief Exports this genotype into a string representation. + /// @return The string representation of this genotype. + std::string Export() const { + std::string header = EncodeHeader(); + std::string genotype = EncodeGenotype(); + return header + HEADER_END + genotype; + } + + /// @brief Exports this genotype into a string representation. + /// @return The string representation of this genotype. + std::string ExportRaw() const { + std::string header = EncodeHeader(); + std::string genotype = EncodeGenotypeRaw(); + return header + HEADER_END + genotype; + } + + /// @brief Sets the seed of the random number generator. + CGPGenotype &SetSeed(size_t seed) { + rng.seed(seed); + return *this; + } + + /// @brief Mutates the genotype. + /// @param mutation_rate Value between 0 and 1 representing the probability of mutating a value. + /// @param mutation The function to use for mutating the output. The function will receive the node gene as a + /// parameter. + /// @return This genotype. + CGPGenotype &Mutate(double mutation_rate, std::function mutation) { + assert(mutation_rate >= 0.0 && mutation_rate <= 1.0); + std::uniform_real_distribution dist_mutation(0.0, 1.0); + for (CGPNodeGene &node : nodes) + if (dist_mutation(rng) < mutation_rate) + mutation(node); + return *this; + } + + /// @brief Mutates the input connections of the genotype. + /// @param mutation_rate The probability of mutating a connection. For a given connection, if it is chosen to be + /// mutated, there is a 50% chance it will stay the same. + /// @param agent The agent to use for random number generation. + /// @return This genotype. + CGPGenotype &MutateConnections(double mutation_rate, GPAgentBase &agent) { + std::uniform_int_distribution dist(0, 1); + Mutate(mutation_rate, [&agent](CGPNodeGene &node) { + for (char &con : node.input_connections) { + con = agent.GetRandomULL(2) == 0 ? '0' : '1'; + } + }); + return *this; + } + + /// @brief Mutates the genotype by changing the function of each node with a given probability between 0 and 1. + /// @param mutation_rate The probability of changing the function of a node. + /// @param num_functions The number of functions available to the nodes. + /// @return This genotype. + CGPGenotype &MutateFunctions(double mutation_rate, size_t num_functions, GPAgentBase &agent) { + Mutate(mutation_rate, + [num_functions, &agent](CGPNodeGene &node) { node.function_idx = agent.GetRandomULL(num_functions); }); + return *this; + } + + /// @brief Mutates the genotype, changing the default output of nodes with probability between 0 and 1. + /// @param mutation_rate Value between 0 and 1 representing the probability of mutating each value. + /// @param min The minimum value to generate for mutation. + /// @param max The maximum value to generate for mutation. + /// @return This genotype. + CGPGenotype &MutateOutputs(double mutation_rate, double mean, double std, GPAgentBase &agent, + bool additive = true) { + Mutate(mutation_rate, [mean, std, &agent, additive](CGPNodeGene &node) { + double mutation = agent.GetRandomNormal(mean, std); + if (additive) { + node.default_output += mutation; + // Clamp to prevent overflow during genotype export + double min = std::numeric_limits::lowest(); + double max = std::numeric_limits::max(); + // Wrap random double in stod(to_string(.)) to reliably export and import genotype from string. + node.default_output = std::stod(std::to_string(std::clamp(node.default_output, min, max))); + } else { + node.default_output = std::stod(std::to_string(mutation)); + } + }); + return *this; + } + + /// @brief Mutates the header of the genotype. + /// @param mutation_rate Value between 0 and 1 representing the probability of mutating each value. + /// @param agent The agent to use for random number generation. + /// @return This genotype. + CGPGenotype &MutateHeader(double mutation_rate, GPAgentBase &agent) { + + // Must expand the genotype in a way so that the behavior is preserved + + // Can mutate number of inputs and outputs to adapt to changing state and action spaces, but not doing it for + // now + + // Mutate layers back + if (agent.GetRandom() < mutation_rate) { + // Update params + params.layers_back += 1; + // Add empty connections to each node at the front + // Start at 1 to account for the input layer + for (size_t i = 1; i <= params.num_layers + 1; ++i) { + size_t layer_size = i == params.num_layers + 1 ? params.num_outputs : params.num_nodes_per_layer; + for (size_t j = 0; j < layer_size; ++j) { + + // Get the old number of input connections + auto &curr_connections = nodes[(i - 1) * params.num_nodes_per_layer + j].input_connections; + size_t old_num_input_connections = curr_connections.size(); + + // Get the new number of input connections + size_t valid_layers_back = std::min(params.layers_back, i); + size_t num_input_connections = valid_layers_back * params.num_nodes_per_layer; + if (i <= params.layers_back) { + num_input_connections -= params.num_nodes_per_layer; + num_input_connections += params.num_inputs; + } + + // Push empty connections to the front of the vector of input connections + size_t num_needed = num_input_connections - old_num_input_connections; + if (num_needed > 0) { + // Create empty connections + std::vector input_connections(num_needed, '0'); + // Insert the empty connections into the front of the vector + curr_connections.insert(curr_connections.cbegin(), input_connections.cbegin(), input_connections.cend()); + } + } + } + } + + // Mutate number of nodes in each layer + if (agent.GetRandom() < mutation_rate) { + // Add a node to each middle layer and update connections for middle and output layers + std::vector new_nodes; + for (size_t i = 1; i <= params.num_layers + 1; ++i) { + // Add the nodes in this layer to the new node vector + size_t layer_start = (i - 1) * params.num_nodes_per_layer; + size_t layer_size = i == params.num_layers + 1 ? params.num_outputs : params.num_nodes_per_layer; + size_t layer_end = layer_start + layer_size; + size_t valid_layers_back = std::min(params.layers_back, i); + + // Get the number of connections for the new node + size_t new_num_connections = valid_layers_back * (params.num_nodes_per_layer + 1); + if (i <= params.layers_back) { + new_num_connections -= params.num_nodes_per_layer + 1; + new_num_connections += params.num_inputs; + } + size_t num_needed = new_num_connections - nodes[layer_start].input_connections.size(); + new_nodes.insert(new_nodes.cend(), nodes.cbegin() + layer_start, nodes.cbegin() + layer_end); + + // For middle layers, add a new node + if (i != params.num_layers + 1) { + auto newNode = std::vector(new_num_connections, '0'); + new_nodes.push_back({newNode}); + } + + // Add the extra connections for each node in this layer + if (i == 1) + // First layer doesn't have any connections to add because the input layer is unchanged + continue; + + size_t new_layer_start = (i - 1) * (params.num_nodes_per_layer + 1); + for (size_t j = 0; j < layer_size; ++j) { + // Add an empty connection at the end of each layer of connections in the valid layers back + assert(new_layer_start + j < new_nodes.size()); + auto &connections = new_nodes[new_layer_start + j].input_connections; + // Only iterate over the valid layers back that are middle layers, not including the input layer + for (size_t k = 0; k < num_needed; ++k) { + // Insert in reverse order to keep indices correct + size_t insert_pos = params.num_nodes_per_layer * (num_needed - k); + connections.insert(connections.cbegin() + insert_pos, '0'); + assert(*(connections.cbegin() + insert_pos) == '0'); + } + } + } + // Update params + params.num_nodes_per_layer += 1; + nodes = std::move(new_nodes); + // Check if everything is correct + assert(nodes.size() == params.GetFunctionalNodeCount()); + for (size_t i = 1; i < params.num_layers + 1; ++i) { + size_t layer_start = (i - 1) * params.num_nodes_per_layer; + size_t layer_size = i == params.num_layers + 1 ? params.num_outputs : params.num_nodes_per_layer; + size_t valid_layers_back = std::min(params.layers_back, i); + for (size_t j = 0; j < layer_size; ++j) { + // Check that the number of connections is correct + size_t num_connections = valid_layers_back * params.num_nodes_per_layer; + if (i <= params.layers_back) { + num_connections -= params.num_nodes_per_layer; + num_connections += params.num_inputs; + } + assert(nodes[layer_start + j].input_connections.size() == num_connections); + } + } + } + + return *this; + } + + /// @brief Performs a mutation on the genotype with default parameters. + /// @param mutation_rate Value between 0 and 1 representing the probability of mutating each value. + /// @param agent The agent to use for random number generation. + /// @param num_functions The number of functions available to the nodes. + /// @return This genotype. + CGPGenotype &MutateDefault(double mutation_rate, GPAgentBase &agent, size_t num_functions = FUNCTION_SET.size()) { + MutateHeader(mutation_rate, agent); + MutateConnections(mutation_rate, agent); + MutateFunctions(mutation_rate, num_functions, agent); + MutateOutputs(mutation_rate, 0, 1, agent); + return *this; + } + + /// @brief Check if two CGPGenotypes are equal. CGPParameters and CGPNodeGenes should be equal. + /// @param other The other CGPGenotype to compare to. + /// @return True if the two CGPGenotypes are equal, false otherwise. + inline bool operator==(const CGPGenotype &other) const { + if (params != other.params) // Compare CGPParameters for equality + return false; + if (std::ranges::size(nodes) != std::ranges::size(other.nodes)) // # of genes should be equal + return false; + bool all_same = true; + for (auto it = cbegin(), it2 = other.cbegin(); it != cend(); ++it, ++it2) { + all_same = all_same && (*it == *it2); // Compare CGPNodeGenes for equality + } + return all_same; + } + + /// @brief Write the genotype representation to an output stream. + /// @param os The output stream to write to. + /// @param genotype The genotype to write. + /// @return The output stream. + friend std::ostream &operator<<(std::ostream &os, const CGPGenotype &genotype) { + os << genotype.ExportRaw(); + return os; + } + }; +} // namespace cowboys \ No newline at end of file diff --git a/source/Agents/GP/GPAgent.hpp b/source/Agents/GP/GPAgent.hpp index 3a332138..e4152d61 100644 --- a/source/Agents/GP/GPAgent.hpp +++ b/source/Agents/GP/GPAgent.hpp @@ -21,7 +21,7 @@ #include "./GPAgentSensors.hpp" /** - * @brief Namespace for GPagent and its related classes + * @brief Namespace for GPAgent and its related classes * * @note yeeeeeeeehaaaaaaaaa 🤠 */ diff --git a/source/Agents/GP/GPAgentBase.hpp b/source/Agents/GP/GPAgentBase.hpp new file mode 100644 index 00000000..962d48f6 --- /dev/null +++ b/source/Agents/GP/GPAgentBase.hpp @@ -0,0 +1,111 @@ +/** + * This file is part of the Fall 2023, CSE 491 course project. + * @brief An Agent based on genetic programming. + * @note Status: PROPOSAL + **/ + +#pragma once + +#include +#include +#include +#include +#include + +#include "tinyxml2.h" + +#include "../../core/AgentBase.hpp" + +namespace cowboys { + class GPAgentBase : public cse491::AgentBase { + protected: + std::unordered_map extra_state; ///< A map of extra state information. + unsigned int seed = 0; ///< Seed for the random number generator. + std::mt19937 rng{seed}; ///< Random number generator. + std::uniform_real_distribution uni_dist; ///< Uniform distribution. + std::normal_distribution norm_dist; ///< Normal distribution. + + public: + GPAgentBase(size_t id, const std::string &name) : AgentBase(id, name) { extra_state["previous_action"] = 0; } + ~GPAgentBase() = default; + + /// @brief Setup graph. + /// @return Success. + bool Initialize() override { return true; } + + /// Choose the action to take a step in the appropriate direction. + size_t SelectAction(const cse491::WorldGrid &grid, const cse491::type_options_t &type_options, + const cse491::item_map_t &item_set, const cse491::agent_map_t &agent_set) override { + size_t action = GetAction(grid, type_options, item_set, agent_set); + + // Update extra state information. + extra_state["previous_action"] = action; + + return action; + } + + virtual size_t GetAction(const cse491::WorldGrid &grid, const cse491::type_options_t &type_options, + const cse491::item_map_t &item_set, const cse491::agent_map_t &agent_set) = 0; + + /// @brief Get a map of extra state information + /// @return Map of extra state information + const std::unordered_map GetExtraState() const { return extra_state; } + + /// @brief Mutate this agent. + /// @param mutation_rate The mutation rate. Between 0 and 1. + virtual void MutateAgent(double mutation_rate = 0.8) = 0; + + /// @brief Copy the behavior of another agent into this agent. + /// @param other The agent to copy. Should be the same type. + virtual void Copy(const GPAgentBase &other) = 0; + + virtual void PrintAgent(){ + + }; + + virtual void Serialize(tinyxml2::XMLDocument &doc, tinyxml2::XMLElement *parentElem, double fitness = -1) = 0; + + virtual std::string Export() { return ""; } + + virtual void Reset(bool /*hard*/ = false) { extra_state["previous_action"] = 0; }; + + // virtual void crossover(const GPAgentBase &other) {}; + // virtual void Import(const std::string &genotype) {}; + + // -- Random Number Generation -- + + /// @brief Set the seed used to initialize this RNG + void SetSeed(unsigned int seed) { + this->seed = seed; + rng.seed(seed); + } + + /// @brief Get the seed used to initialize this RNG + unsigned int GetSeed() const { return seed; } + + /// @brief Return a uniform random value between 0.0 and 1.0 + double GetRandom() { return uni_dist(rng); } + + /// @brief Return a uniform random value between 0.0 and max + double GetRandom(double max) { return GetRandom() * max; } + + /// @brief Return a uniform random value between min and max + double GetRandom(double min, double max) { + assert(max > min); + return min + GetRandom(max - min); + } + + /// @brief Return a uniform random unsigned long long between 0 (inclusive) and max (exclusive) + size_t GetRandomULL(size_t max) { return static_cast(GetRandom(max)); } + + /// @brief Return a gaussian random value with mean 0.0 and sd 1.0 + double GetRandomNormal() { return norm_dist(rng); } + + /// @brief Return a gaussian random value with provided mean and sd. + double GetRandomNormal(double mean, double sd = 1.0) { + assert(sd > 0); + return mean + norm_dist(rng) * sd; + } + }; + +} // End of namespace cowboys diff --git a/source/Agents/GP/GPAgentSensors.hpp b/source/Agents/GP/GPAgentSensors.hpp index 1a7ef861..095cd4dc 100644 --- a/source/Agents/GP/GPAgentSensors.hpp +++ b/source/Agents/GP/GPAgentSensors.hpp @@ -10,8 +10,6 @@ * * @static currently a static class * - * @note TODO: might have to move this over to core of the project - * @note TODO: might have to refactor to make it one function??? * * @author @amantham20 * @details currenly supports only wall distance sensors for left, right, top @@ -32,9 +30,9 @@ class Sensors { * @brief print the positions of the agent only during debug mode * @param printstring */ - [[maybe_unused]] static void debugPosition(const std::string &printstring) { + [[maybe_unused]] static void debugPosition(const std::string &/*printstring*/) { #ifndef NDEBUG - std::cout << printstring << std::endl; +// std::cout << printstring << std::endl; #endif } @@ -105,4 +103,4 @@ class Sensors { return SensorDirection::LEFT; } }; -} // namespace cowboys +} // namespace cowboys \ No newline at end of file diff --git a/source/Agents/GP/GPTrainingLoop.hpp b/source/Agents/GP/GPTrainingLoop.hpp new file mode 100644 index 00000000..ba8f7703 --- /dev/null +++ b/source/Agents/GP/GPTrainingLoop.hpp @@ -0,0 +1,884 @@ + +#pragma once + +#include "../../core/AgentBase.hpp" +#include "../../core/WorldBase.hpp" +//#include "GPAgent.hpp" +#include "GPAgentBase.hpp" + + +#include "CGPAgent.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#include "tinyxml2.h" + + + +namespace cowboys { + + constexpr unsigned int TRAINING_SEED = 0; ///< If this is 0, then a random seed will be used + + template + class GPTrainingLoop { + private: + + std::vector> environments; + std::vector> agents; + std::vector> TEMPAgentFitness; + + + tinyxml2::XMLDocument topAgentsDoc; + tinyxml2::XMLDocument lastGenerationsTopAgentsDoc; // <- Saves the last 5 generations + tinyxml2::XMLDocument allOfLastGeneration; + tinyxml2::XMLDocument metaData; + + + tinyxml2::XMLElement *rootTopAllGenerations = topAgentsDoc.NewElement("GPLoopTopOfAllGeneration"); + tinyxml2::XMLElement *rootTopLastGenerations = lastGenerationsTopAgentsDoc.NewElement("GPLoopLastNGeneration"); + tinyxml2::XMLElement *rootAllOfLastGeneration = allOfLastGeneration.NewElement("GPLoopAllOfLastGeneration"); + tinyxml2::XMLElement *rootMetaData = metaData.NewElement("GPLoopMetaData"); + + + std::vector> sortedAgents = std::vector>(); + + /** + * Default Grid + */ +// const std::vector STARTPOSITIONS = {cse491::GridPosition(0,0), cse491::GridPosition(22,5) , cse491::GridPosition(22,1) , cse491::GridPosition(0,8), cse491::GridPosition(22,8)}; +// const std::vector STARTPOSITIONS = {cse491::GridPosition(0,0), cse491::GridPosition(22,5) }; +// const std::vector STARTPOSITIONS = {cse491::GridPosition(22,5) }; +// const std::vector STARTPOSITIONS = {cse491::GridPosition(0,0)}; + + +/** + * Group 8 Grid + */ +// const std::vector STARTPOSITIONS = {cse491::GridPosition(0,2), cse491::GridPosition(49,2) , cse491::GridPosition(49,19) , cse491::GridPosition(4,19), cse491::GridPosition(28,10)}; + + +/** + * Default Grid 2 + */ + const std::vector STARTPOSITIONS = {cse491::GridPosition(0,0), cse491::GridPosition(50,0) , cse491::GridPosition(0,28) , cse491::GridPosition(50,28)}; + + /// ArenaIDX, AgentIDX, EndPosition + std::vector>> endPositions = std::vector>>(); + std::vector>> independentAgentFitness = std::vector>>(); + + public: + + /** + * @brief: constructor + */ + GPTrainingLoop() { + + topAgentsDoc.InsertFirstChild(rootTopAllGenerations); + + rootMetaData = metaData.NewElement("GPLoopMetaData"); + metaData.InsertFirstChild(rootMetaData); + + ResetMainTagLastGenerations(); + } + + /** + * Resets the xml for data that needs to be overwritten + */ + void ResetMainTagLastGenerations() { + rootTopLastGenerations = lastGenerationsTopAgentsDoc.NewElement("GPLoop"); + lastGenerationsTopAgentsDoc.InsertFirstChild(rootTopLastGenerations); + + rootAllOfLastGeneration = allOfLastGeneration.NewElement("GPLoopAllOfLastGeneration"); + allOfLastGeneration.InsertFirstChild(rootAllOfLastGeneration); + } + + /** + * @brief Initialize the training loop with a number of environments and agents per environment. + * @param numArenas + * @param NumAgentsForArena + */ + void Initialize(size_t numArenas = 5, size_t NumAgentsForArena = 100) { + + unsigned int seed = TRAINING_SEED; + if (seed == 0) { + seed = std::random_device()(); + } + std::cout << "Using seed: " << seed << std::endl; + + + for (size_t i = 0; i < numArenas; ++i) { + // instantiate a new environment + environments.emplace_back(std::make_unique(seed)); + + + agents.push_back(std::vector()); + + endPositions.push_back(std::vector>()); + independentAgentFitness.push_back(std::vector>()); + + for (size_t j = 0; j < NumAgentsForArena; ++j) { + + endPositions[i].push_back(std::vector()); + independentAgentFitness[i].push_back(std::vector()); + + for (size_t k = 0; k < STARTPOSITIONS.size(); ++k) { + endPositions[i][j].push_back(cse491::GridPosition(0, 0)); + independentAgentFitness[i][j].push_back(0); + } + + cowboys::GPAgentBase &addedAgent = static_cast(environments[i]->template AddAgent( + "Agent " + std::to_string(j))); + addedAgent.SetPosition(0, 0); + addedAgent.SetSeed(seed); + + agents[i].emplace_back(&addedAgent); + + } + + } + + Printgrid(STARTPOSITIONS); + + + const size_t numAgents = numArenas * NumAgentsForArena; + + std::stringstream ss; + ss.imbue(std::locale("")); + ss << std::fixed << numAgents; + + std::cout << "number of agents " << std::fixed << ss.str() << std::endl; + + } + + + /** + * Simple and temporary fitness function + * @param agent + * @param startPosition + * @return + */ + double SimpleFitnessFunction(cse491::AgentBase &agent, cse491::GridPosition startPosition) { + double fitness = 0; + + // Euclidean distance + cse491::GridPosition currentPosition = agent.GetPosition(); + double distance = std::sqrt(std::pow(currentPosition.GetX() - startPosition.GetX(), 2) + + std::pow(currentPosition.GetY() - startPosition.GetY(), 2)); + + double score = distance; + + fitness += score; + + // Agent complexity, temporarily doing this in a bad way + if (auto *cgp = dynamic_cast(&agent)) { + auto genotype = cgp->GetGenotype(); + double connection_complexity = + static_cast(genotype.GetNumConnections()) / genotype.GetNumPossibleConnections(); + + double functional_nodes = genotype.GetNumFunctionalNodes(); + double node_complexity = functional_nodes / (functional_nodes + 1); + + double complexity = connection_complexity + node_complexity; + fitness -= complexity; + } + + return fitness; + } + + /** + * Gets the path of the save location + * @return + */ + static std::filesystem::path getSystemPath() { + /// XML save filename data + std::string relativePath = "../../savedata/GPAgent/"; + std::filesystem::path absolutePath = std::filesystem::absolute(relativePath); + std::filesystem::path normalizedAbsolutePath = std::filesystem::canonical(absolutePath); + return normalizedAbsolutePath; + } + + /** + * Gets the date and time as a string + * @return + */ + static std::string getDateStr() { + auto now = std::chrono::system_clock::now(); + std::time_t now_time = std::chrono::system_clock::to_time_t(now); + + // Format the date and time as a string (hour-minute-second) + std::tm tm_time = *std::localtime(&now_time); + std::ostringstream oss; + oss << std::put_time(&tm_time, "%Y-%m-%d__%H_%M_%S"); + std::string dateTimeStr = oss.str(); + + return dateTimeStr; + } + + /** + * + * @param maxThreads + * @param numberOfTurns + */ + void ThreadTrainLoop(size_t maxThreads = 1, int numberOfTurns = 100) { + std::vector threads; + + size_t threadsComplete = 0; + + + for (size_t arena = 0; arena < environments.size(); ++arena) { + if (maxThreads == 0 || threads.size() < maxThreads) { + threads.emplace_back(&GPTrainingLoop::RunArena, this, arena, numberOfTurns); + + } else { + // Wait for one of the existing threads to finish + threads[0].join(); + threads.erase(threads.begin()); + threadsComplete++; + threads.emplace_back(&GPTrainingLoop::RunArena, this, arena, numberOfTurns); + } + + + size_t barWidth = 64; + float progress = (float) (arena+1) / environments.size(); + size_t pos = barWidth * progress; + std::cout << "["; + for (size_t i = 0; i < barWidth; ++i) { + if (i < pos) std::cout << "="; + else if (i == pos) std::cout << ">"; + else std::cout << " "; + } + std::cout << "] " << int(progress * 100.0) << " % - " << threadsComplete << " threads done\r"; + std::cout.flush(); + } + + // Wait for all threads to finish + for (auto &thread: threads) { + if (thread.joinable()) { + thread.join(); + threadsComplete+=maxThreads; + } + } + + std::cout << std::endl; + std::cout << "All threads done" << std::endl; + + + } + + /** + * @brief: runs the Genetic Programming training loop for a number of generations to evolve the agents + * + * @param numGenerations + * @param numberOfTurns + * @param maxThreads + */ + void Run(size_t numGenerations, + size_t numberOfTurns = 100, + size_t maxThreads = 0, bool saveData = false) { + + auto startTime = std::chrono::high_resolution_clock::now(); + + SaveDataParams saveDataParams(0); + saveDataParams.save = saveData; + saveDataParams.saveMetaData = true; +// saveDataParams.saveAllAgentData = true; +// +// saveDataParams.saveTopAgents = true; +// saveDataParams.saveLastGenerations = true; + + for (size_t generation = 0; generation < numGenerations; ++generation) { + + auto generationStartTime = std::chrono::high_resolution_clock::now(); + saveDataParams.updateGeneration(generation); + + InitTEMPAgentFitness(); + ThreadTrainLoop(maxThreads, numberOfTurns); + + std::cout << std::endl; + + sortedAgents.clear(); + SortThemAgents(); + + int countMaxAgents = AgentsAnalysisComputationsAndPrint(generation); + + saveDataParams.countMaxAgents = countMaxAgents; + SaveDataCheckPoint(saveDataParams); + + GpLoopMutateHelper(); + resetEnvironments(); + + auto generationEndTime = std::chrono::high_resolution_clock::now(); + auto generationDuration = std::chrono::duration_cast( + generationEndTime - generationStartTime); + std::cout << "Generation " << generation << " took " << generationDuration.count() / 1000000.0 << " seconds" + << std::endl; + + } + + auto endTime = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(endTime - startTime); + std::cout << "Time taken by run: " << duration.count() / 1000000.0 << " seconds" << std::endl; + + if (saveData) { + + } + + MemGOBYE(); + } + + /** + * SaveDataParams for saving data in checkpoints + */ + struct SaveDataParams { + size_t generation; + bool save = false; + bool saveAllAgentData = false; + bool saveMetaData = true; + bool saveTopAgents = false; + bool saveLastGenerations = false; + + size_t countMaxAgents = 0; + + std::string dateTimeStr = getDateStr(); + std::filesystem::path normalizedAbsolutePath = getSystemPath(); + + size_t checkPointEvery = 5; + + SaveDataParams(size_t gen) : generation(gen) {} + + void updateGeneration(size_t gen) { generation = gen; } + }; + + + /** + * Saves checkpoint data to XML files everyso often + * @param params + */ + void SaveDataCheckPoint(const SaveDataParams ¶ms) { + if (!params.save) { + return; + } + + allOfLastGeneration.Clear(); + rootAllOfLastGeneration = allOfLastGeneration.NewElement("GPLoopAllOfLastGeneration"); + allOfLastGeneration.InsertFirstChild(rootAllOfLastGeneration); + + size_t totalNumberOfAgents = agents.size() * agents[0].size(); + + SerializeAgents(params.generation, rootAllOfLastGeneration, allOfLastGeneration, totalNumberOfAgents); + + if (params.saveTopAgents) { + SerializeAgents(params.generation, rootTopAllGenerations, topAgentsDoc); + } + + if (params.saveLastGenerations){ + SerializeAgents(params.generation, rootTopLastGenerations, lastGenerationsTopAgentsDoc, 5); + } + + std::string dateTimeStr = params.dateTimeStr; + std::filesystem::path normalizedAbsolutePath = params.normalizedAbsolutePath; + + + if (params.generation % params.checkPointEvery != 0) { + return; + } + + + if (params.saveMetaData) { + const std::string metaDataFilename = "metaData_" + dateTimeStr + ".xml"; + auto metaDataFullPath = normalizedAbsolutePath / metaDataFilename; + saveXMLDoc(metaData, metaDataFullPath.string()); + } + + if (params.saveAllAgentData) { + const std::string allAgentDataFilename = "allAgentData_" + dateTimeStr + ".xml"; + auto allAgentDataFullPath = normalizedAbsolutePath / allAgentDataFilename; + saveXMLDoc(allOfLastGeneration, allAgentDataFullPath.string()); + } + + + + if (params.saveTopAgents) { + const std::string filename = "AgentData_" + dateTimeStr + ".xml"; + auto fullPath = normalizedAbsolutePath / filename; + + saveXMLDoc(topAgentsDoc, fullPath.string()); + } + + if (params.saveLastGenerations) { + const std::string lastGenerationsFilename = "lastGenerations_" + dateTimeStr + ".xml"; + auto lastGenerationsFullPath = normalizedAbsolutePath / lastGenerationsFilename; + saveXMLDoc(lastGenerationsTopAgentsDoc, lastGenerationsFullPath.string()); + } + + + std::cout << "@@@@@@@@@@@@@@@@@@@@@@ " << "DataSaved" << " @@@@@@@@@@@@@@@@@@@@@@" << std::endl; + + lastGenerationsTopAgentsDoc.Clear(); + ResetMainTagLastGenerations(); + } + + + /** + * Helper function to format the data analysis + * @param pos + * @param precision + * @return + */ + std::string FormatPosition(const cse491::GridPosition & pos, int precision = 0) { + std::stringstream ss; + ss << std::fixed << std::setprecision(precision) << "[" << pos.GetX() << "," << pos.GetY() << "]"; + return ss.str(); + } + + + + /** + * Computes agents analysis metrics + * + * @param generation + * @return + */ + int AgentsAnalysisComputationsAndPrint(int generation, double deltaForMaxFitness = 0.1) { + // print average fitness + double averageFitness = 0; + double maxFitness = -10000; + + + std::pair bestAgent = std::make_pair(-1, -1); + + int countMaxAgents = 0; + for (size_t arena = 0; arena < environments.size(); ++arena) { + for (size_t a = 0; a < agents[arena].size(); ++a) { + averageFitness += TEMPAgentFitness[arena][a]; + + + if (abs(TEMPAgentFitness[arena][a] - maxFitness) > deltaForMaxFitness && + TEMPAgentFitness[arena][a] > maxFitness) { + maxFitness = TEMPAgentFitness[arena][a]; + bestAgent = std::make_pair(arena, a); + countMaxAgents = 1; + } + + if (abs(TEMPAgentFitness[arena][a] - maxFitness) < deltaForMaxFitness) { + countMaxAgents++; + } + } + } + + averageFitness /= (environments.size() * agents[0].size()); + + + std::cout << "Generation " << generation << " complete" << std::endl; + std::cout << "Average fitness: " << averageFitness << " "; + std::cout << "Max fitness: " << maxFitness << std::endl; + + const char *tagName = ("generation_" + std::to_string(generation)).c_str(); + + auto *generationTag = metaData.NewElement(tagName); + + generationTag->SetAttribute("averageFitness", averageFitness); + generationTag->SetAttribute("maxFitness", maxFitness); + generationTag->SetAttribute("bestAgentIDX", bestAgent.second); + + rootMetaData->InsertFirstChild(generationTag); + + + + + std::cout << "Best agent: AGENT[" << bestAgent.first << "," << bestAgent.second << "] " << std::endl; + + std::cout << "Best Agent Final Positions" << std::endl; + + Printgrid(endPositions[bestAgent.first][bestAgent.second], 'A'); + + + + auto calculateDistance = [](const cse491::GridPosition& startPosition, const cse491::GridPosition & currentPosition) { + return std::sqrt(std::pow(currentPosition.GetX() - startPosition.GetX(), 2) + + std::pow(currentPosition.GetY() - startPosition.GetY(), 2)); + }; + + int columnWidth = 10; // Adjust as needed + + std::cout << std::left << std::setw(columnWidth) << "Start" + << std::setw(columnWidth) << "Final" + << "Distance\n"; + for (size_t i = 0; i < STARTPOSITIONS.size(); ++i) { + std::cout << std::setw(columnWidth) << FormatPosition(STARTPOSITIONS[i]) + << std::setw(columnWidth) << FormatPosition(endPositions[bestAgent.first][bestAgent.second][i]); + + + double distance = calculateDistance(STARTPOSITIONS[i], endPositions[bestAgent.first][bestAgent.second][i]); + std::cout << std::fixed << std::setprecision(2) << std::setw(6) << distance; + + + std::cout << std::endl; + } + + std::cout << "with an average score of " << TEMPAgentFitness[bestAgent.first][bestAgent.second] << std::endl; + std::cout << std::endl; + + + std::cout << "Number of agents with max fitness: " << countMaxAgents << std::endl; + std::cout << "------------------------------------------------------------------" << std::endl; + return countMaxAgents; + } + + /** + * @brief: sort the agents based on their fitness + */ + void SortThemAgents() { + for (size_t arena = 0; arena < environments.size(); ++arena) { + for (size_t a = 0; a < agents[arena].size(); ++a) { + sortedAgents.push_back(std::make_pair(arena, a)); + } + } + + std::sort(sortedAgents.begin(), sortedAgents.end(), + [&](const std::pair &a, const std::pair &b) { + return TEMPAgentFitness[a.first][a.second] > TEMPAgentFitness[b.first][b.second]; + }); + } + + + void saveXMLDoc(tinyxml2::XMLDocument ¶mdoc, std::string fullPath) { + if (paramdoc.SaveFile(fullPath.c_str()) == tinyxml2::XML_SUCCESS) { + // std::filesystem::path fullPath = std::filesystem::absolute("example.xml"); + std::cout << "XML file saved successfully to: " << fullPath << std::endl; + } else { + std::cout << "Error saving XML file." << std::endl; +// std::cout << "Error ID: " << paramdoc.ErrorID() << std::endl; + std::cout << "Error for path" << fullPath << std::endl; + } + } + + + /** + * @brief: Serializes the agents to an XML file. + * + * @param countMaxAgents + * @param generation + * @param topN + */ + void SerializeAgents(int generation, tinyxml2::XMLElement *rootElement, tinyxml2::XMLDocument ¶mDocument, + size_t topN = 5) { + + const char *tagName = ("generation_" + std::to_string(generation)).c_str(); + + auto *generationTag = paramDocument.NewElement(tagName); + + rootElement->InsertFirstChild(generationTag); + + for (size_t i = 0; i < std::min(sortedAgents.size(), topN); ++i) { + auto [arenaIDX, agentIDX] = sortedAgents[i]; + agents[arenaIDX][agentIDX]->Serialize(paramDocument, generationTag, TEMPAgentFitness[arenaIDX][agentIDX]); + } + + + } + + /** + * @brief: initialize the TEMP agent fitness vector + */ + void InitTEMPAgentFitness() { + for (size_t arena = 0; arena < environments.size(); ++arena) { + TEMPAgentFitness.push_back(std::vector(agents[arena].size(), 0)); + } + } + + + /** + * @brief Helper function for the GP loop mutate function. + * This function mutates the agents. + * This function is called in a thread. + * + * @param start : The start index of the agents to mutate. + * @param end : The end index of the agents to mutate. + * @param sortedAgents : The sorted agents' index vector. + * @param agents : The agents vector. + * @param mutationRate: The mutation rate. + */ + void MutateAgents(int start, int end, const std::vector> &sortedAgents, + std::vector> &agents, double mutationRate) { + for (int i = start; i < end; i++) { + auto [arenaIDX, agentIDX] = sortedAgents[i]; + agents[arenaIDX][agentIDX]->MutateAgent(mutationRate); + + if (i % (sortedAgents.size() / 10) == 0) { + std::cout << " --- mutation complete " << (i * 1.0 / sortedAgents.size()) << std::endl; + } + } + } + + /** + * @brief Helper function for the GP loop mutate function. + * This function copies the elite agents and mutates them. + * This function is called in a thread. + * for th + * @param start + * @param end + * @param sortedAgents + * @param agents + * @param elitePopulationSize + */ + void MutateAndCopyAgents(int start, int end, const std::vector> &sortedAgents, + std::vector> &agents, int elitePopulationSize) { + for (int i = start; i < end; i++) { + auto [arenaIDX, agentIDX] = sortedAgents[i]; + auto eliteINDEX = rand() % elitePopulationSize; + auto [eliteArenaIDX, eliteAgentIDX] = sortedAgents[eliteINDEX]; + + agents[arenaIDX][agentIDX]->Copy(*agents[eliteArenaIDX][eliteAgentIDX]); + agents[arenaIDX][agentIDX]->MutateAgent(0.01); + + if (i % (sortedAgents.size() / 10) == 0) { + std::cout << " --- mutation complete " << (i * 1.0 / sortedAgents.size()) << std::endl; + } + } + } + + /** + * @brief Helper function for the GP loop mutate function. + * + */ + void GpLoopMutateHelper() { + + constexpr double ELITE_POPULATION_PERCENT = 0.1; + constexpr double UNFIT_POPULATION_PERCENT = 0.2; + + + const int ELITE_POPULATION_SIZE = int(ELITE_POPULATION_PERCENT * sortedAgents.size()); + + + double averageEliteFitness = 0; + for (int i = 0; i < ELITE_POPULATION_SIZE; i++) { + auto [arenaIDX, agentIDX] = sortedAgents[i]; + averageEliteFitness += TEMPAgentFitness[arenaIDX][agentIDX]; + } + averageEliteFitness /= ELITE_POPULATION_SIZE; + + std::cout << " --- average elite score " << averageEliteFitness << "------ " << std::endl; + + + const int MIDDLE_MUTATE_ENDBOUND = int(sortedAgents.size() * (1 - UNFIT_POPULATION_PERCENT)); + const int MIDDLE_MUTATE_STARTBOUND = int(ELITE_POPULATION_PERCENT * sortedAgents.size()); + + // Determine the number of threads to use + const int num_threads = std::thread::hardware_concurrency(); + + std::vector threads; + + // Calculate the number of agents per thread + int agents_per_thread = (MIDDLE_MUTATE_ENDBOUND - MIDDLE_MUTATE_STARTBOUND) / num_threads; + + // Launch threads for the first loop + for (int i = 0; i < num_threads; ++i) { + int start = MIDDLE_MUTATE_STARTBOUND + i * agents_per_thread; + int end = (i == num_threads - 1) ? MIDDLE_MUTATE_ENDBOUND : start + agents_per_thread; + threads.push_back(std::thread([this, start, end] { + this->MutateAgents(start, end, sortedAgents, agents, 0.05); + })); + } + + // Join the threads + for (auto &t: threads) { + t.join(); + } + + threads.clear(); + + // Second loop - copy and mutate agents + // int unfitAgents = int(sortedAgents.size() * UNFIT_POPULATION_PERCENT); + agents_per_thread = (sortedAgents.size() - MIDDLE_MUTATE_ENDBOUND) / num_threads; + for (int i = 0; i < num_threads; ++i) { + int start = MIDDLE_MUTATE_ENDBOUND + i * agents_per_thread; + int end = (i == num_threads - 1) ? sortedAgents.size() : start + agents_per_thread; + + threads.push_back(std::thread([this, start, end, ELITE_POPULATION_SIZE] { + this->MutateAndCopyAgents(start, end, sortedAgents, agents, ELITE_POPULATION_SIZE); + })); + } + + for (auto &t: threads) { + t.join(); + } + + + } + + /** + * @brief Prints the grid for a single arena. + * @param arenaId + * @author: @amantham20 + */ + void Printgrid(const std::vector &positions, char symbol = 'S') { + + if (environments.empty()) { + std::cout << "No environments to print" << std::endl; + return; + } + + size_t arena = 0; + auto &grid = environments[arena]->GetGrid(); + std::vector symbol_grid(grid.GetHeight()); + + + const auto &type_options = environments[arena]->GetCellTypes(); + // Load the world into the symbol_grid; + for (size_t y = 0; y < grid.GetHeight(); ++y) { + symbol_grid[y].resize(grid.GetWidth()); + for (size_t x = 0; x < grid.GetWidth(); ++x) { + symbol_grid[y][x] = type_options[grid.At(x, y)].symbol; + } + } + + + + for (size_t pos_idx = 0; pos_idx < positions.size(); ++pos_idx) { + symbol_grid[positions[pos_idx].CellY()][positions[pos_idx].CellX()] = symbol; + } + + +// const auto &agent_set = agents[arena]; +// for (const auto &agent_ptr: agent_set) { +// cse491::GridPosition pos = agent_ptr->GetPosition(); +// char c = '*'; +// if (agent_ptr->HasProperty("symbol")) { +// c = agent_ptr->template GetProperty("symbol"); +// } +// symbol_grid[pos.CellY()][pos.CellX()] = c; +// } + + std::cout << " "; + for (size_t x = 0; x < grid.GetWidth(); ++x) { + if (x % 10 == 0 && x != 0) { + std::cout << x / 10; // Print the ten's place of the column number + } else { + std::cout << " "; // Space for non-marker columns + } + } + std::cout << "\n"; + + // Print column numbers + std::cout << " "; // Space for row numbers + for (size_t x = 0; x < grid.GetWidth(); ++x) { + std::cout << x % 10; // Print only the last digit of the column number + } + std::cout << "\n"; + + // Print out the symbol_grid with a box around it. + std::cout << " +" << std::string(grid.GetWidth(), '-') << "+\n"; + for (size_t y = 0; y < grid.GetHeight(); ++y) { + + if (y % 10 == 0 && y != 0) { + std::cout << y / 10 << " "; // Print the ten's place of the row number + } else { + std::cout << " "; // Space for non-marker rows + } + + // Print row number + std::cout << y % 10 << "|"; // Print only the last digit of the row number + for (char cell: symbol_grid[y]) { + std::cout << cell; + } + std::cout << "|\n"; + } + + std::cout << " +" << std::string(grid.GetWidth(), '-') << "+\n"; + std::cout << std::endl; + } + + + + /** + * @brief Resets the environments to their initial state. + * This function is called after each generation. + * This function currently only soft resets the environments. + */ + void resetEnvironments() { + + for (size_t arena = 0; arena < environments.size(); ++arena) { + for (size_t a = 0; a < agents[arena].size(); ++a) { + agents[arena][a]->Reset(); + } + } + + TEMPAgentFitness.clear(); + } + + /** + * @brief Runs the training loop for a single arena. + * This function is called in a thread. + * Each arena is run in a separate thread. + * + * @author: @amantham20 + * @param arena : The arena to run. + * @param numberOfTurns : The number of turns to run the arena for. + */ + void RunArena(size_t arena, size_t numberOfTurns) { + for (size_t startPos_idx = 0; startPos_idx < STARTPOSITIONS.size(); ++startPos_idx) { + for(size_t a = 0; a < agents[arena].size(); ++a) { + agents[arena][a]->SetPosition(STARTPOSITIONS[startPos_idx]); + } + + for (size_t turn = 0; turn < numberOfTurns; turn++) { + environments[arena]->RunAgents(); + environments[arena]->UpdateWorld(); + } + for (size_t a = 0; a < agents[arena].size(); ++a) { + double tempscore = SimpleFitnessFunction(*agents[arena][a], STARTPOSITIONS[startPos_idx]); + auto tempEndPosition = agents[arena][a]->GetPosition(); + endPositions[arena][a][startPos_idx] = tempEndPosition; + independentAgentFitness[arena][a][startPos_idx] = tempscore; + TEMPAgentFitness[arena][a] += tempscore; + + } + + } + + for (size_t a = 0; a < agents[arena].size(); ++a) { + std::vector scores = independentAgentFitness[arena][a]; + auto computeMedian = [&scores]() -> double { + std::vector temp(scores); // Copy the data + std::sort(temp.begin(), temp.end()); + + size_t n = temp.size(); + return n % 2 ? temp[n / 2] : (temp[n / 2 - 1] + temp[n / 2]) / 2.0; + }; + + +// TEMPAgentFitness[arena][a] /= STARTPOSITIONS.size(); +// TEMPAgentFitness[arena][a] += computeMedian(); + double min = *std::min_element(scores.begin(), scores.end()); + double avg = TEMPAgentFitness[arena][a] / STARTPOSITIONS.size(); +// TEMPAgentFitness[arena][a] = 0.7 * min + 0.3 * avg; + TEMPAgentFitness[arena][a] = min; + } + + } + + /** + * @brief Clears the memory of the training loop. + */ + void MemGOBYE() { + + TEMPAgentFitness.clear(); + environments.clear(); + agents.clear(); + sortedAgents.clear(); + + } + + ~GPTrainingLoop() = default; + }; +} \ No newline at end of file diff --git a/source/Agents/GP/Graph.hpp b/source/Agents/GP/Graph.hpp new file mode 100644 index 00000000..458710ba --- /dev/null +++ b/source/Agents/GP/Graph.hpp @@ -0,0 +1,138 @@ + +#pragma once + +#include +#include +#include +#include +#include + +#include "../../core/AgentBase.hpp" +#include "GraphNode.hpp" + +namespace cowboys { + using GraphLayer = std::vector>; + + /// @brief A graph of nodes that can be used to make decisions. + class Graph { + protected: + /// Layers of nodes in the graph. + std::vector layers; + + public: + Graph() = default; + ~Graph() = default; + + /// @brief Get the number of nodes in the graph. + /// @return The number of nodes in the graph. + size_t GetNodeCount() const { + return std::accumulate(layers.cbegin(), layers.cend(), 0, + [](size_t sum, const auto &layer) { return sum + layer.size(); }); + } + + /// @brief Get the number of layers in the graph. + /// @return The number of layers in the graph. + size_t GetLayerCount() const { return layers.size(); } + + /// @brief Makes a decision based on the inputs and the action vector. + /// @param inputs The inputs to the graph. + /// @param action_vec The action vector. + /// @return The action to take. + size_t MakeDecision(const std::vector &inputs, const std::vector &actions) { + if (layers.size() == 0) + return actions.at(0); + + // Set inputs of input layer + size_t i = 0; + for (auto &node : layers[0]) { + double input = 0; + if (i < inputs.size()) + input = inputs.at(i); + node->SetDefaultOutput(input); + ++i; + } + + // Get output of last layer + std::vector outputs; + for (auto &node : layers.back()) { + outputs.push_back(node->GetOutput()); + } + + // Choose the action with the highest output + auto max_output = std::max_element(outputs.cbegin(), outputs.cend()); + size_t index = std::distance(outputs.cbegin(), max_output); + + // If index is out of bounds, return the last action + size_t action = 0; + if (index >= actions.size()) + action = actions.back(); + else // Otherwise, return the action at the index + action = actions.at(index); + return action; + } + + /// @brief Add a layer to the graph. Purely organizational, but important for CGP for determining the "layers back" + /// parameter. + /// @param layer The layer of nodes to add. + void AddLayer(const GraphLayer &layer) { layers.push_back(layer); } + + /// @brief Returns a vector of functional (non-input) nodes in the graph. + /// @return A vector of functional nodes in the graph. + std::vector> GetFunctionalNodes() const { + std::vector> functional_nodes; + for (size_t i = 1; i < layers.size(); ++i) { + functional_nodes.insert(functional_nodes.cend(), layers.at(i).cbegin(), layers.at(i).cend()); + } + return functional_nodes; + } + + /// @brief Returns a vector of all nodes in the graph. + /// @return A vector of all nodes in the graph. + std::vector> GetNodes() const { + std::vector> all_nodes; + for (auto &layer : layers) { + all_nodes.insert(all_nodes.cend(), layer.cbegin(), layer.cend()); + } + return all_nodes; + } + }; + + /// @brief Encodes the actions from an agent's action map into a vector of + /// size_t, representing action IDs. + /// @param action_map The action map from the agent. + /// @return A vector of size_t, representing action IDs. + std::vector EncodeActions(const std::unordered_map &action_map) { + std::vector actions; + for (const auto &[action_name, action_id] : action_map) { + actions.push_back(action_id); + } + // Sort the actions so that they are in a consistent order. + std::sort(actions.begin(), actions.end()); + return actions; + } + + /// @brief Translates state into nodes for the decision graph. + /// @return A vector of doubles for the decision graph. + std::vector EncodeState(const cse491::WorldGrid &grid, const cse491::type_options_t & /*type_options*/, + const cse491::item_map_t & /*item_set*/, const cse491::agent_map_t & /*agent_set*/, + const cse491::Entity *agent, + const std::unordered_map &extra_agent_state) { + /// TODO: Implement this function properly. + std::vector inputs; + + auto current_position = agent->GetPosition(); + + double current_state = grid.At(current_position); + double above_state = grid.IsValid(current_position.Above()) ? grid.At(current_position.Above()) : 0.; + double below_state = grid.IsValid(current_position.Below()) ? grid.At(current_position.Below()) : 0.; + double left_state = grid.IsValid(current_position.ToLeft()) ? grid.At(current_position.ToLeft()) : 0.; + double right_state = grid.IsValid(current_position.ToRight()) ? grid.At(current_position.ToRight()) : 0.; + + double prev_action = extra_agent_state.at("previous_action"); + + inputs.insert(inputs.end(), {prev_action, current_state, above_state, below_state, left_state, right_state}); + + return inputs; + } + +} // namespace cowboys diff --git a/source/Agents/GP/GraphBuilder.hpp b/source/Agents/GP/GraphBuilder.hpp new file mode 100644 index 00000000..28c1df22 --- /dev/null +++ b/source/Agents/GP/GraphBuilder.hpp @@ -0,0 +1,163 @@ +#pragma once + +#include "CGPGenotype.hpp" +#include "Graph.hpp" + +namespace cowboys { + + /// @brief A class for building graphs. Graphs are a generic representation, so this class is used to build the + /// specific format of a Cartesian Graph, and also preset graphs. + class GraphBuilder { + public: + GraphBuilder() = default; + ~GraphBuilder() = default; + + /// @brief Creates a decision graph from a CGP genotype. + /// @param genotype The genotype to create the decision graph from. + /// @param function_set The set of functions available to the decision graph. + /// @param agent The agent that will be using the decision graph. + /// @return The decision graph. + std::unique_ptr CartesianGraph(const CGPGenotype &genotype, const std::vector &function_set, + const cse491::AgentBase *agent = nullptr) { + auto decision_graph = std::make_unique(); + + // + // Add all the nodes + // + // Input layer + GraphLayer input_layer; + for (size_t i = 0; i < genotype.GetNumInputs(); ++i) { + input_layer.emplace_back(std::make_shared(0)); + } + decision_graph->AddLayer(input_layer); + + // Middle Layers + for (size_t i = 0; i < genotype.GetNumLayers(); ++i) { + GraphLayer layer; + for (size_t j = 0; j < genotype.GetNumNodesPerLayer(); ++j) { + layer.emplace_back(std::make_shared(0)); + } + decision_graph->AddLayer(layer); + } + + // Action layer + GraphLayer output_layer; + for (size_t i = 0; i < genotype.GetNumOutputs(); ++i) { + output_layer.emplace_back(std::make_shared(0)); + } + decision_graph->AddLayer(output_layer); + + // + // Configure graph based on genotype + // + + auto functional_nodes = decision_graph->GetFunctionalNodes(); + auto all_nodes = decision_graph->GetNodes(); + auto nodes_it = functional_nodes.cbegin(); + auto genes_it = genotype.cbegin(); + // Iterator distances should be the same + assert(std::distance(functional_nodes.cend(), functional_nodes.cbegin()) == + std::distance(genotype.cend(), genotype.cbegin())); + // Get the iterator of all nodes and move it to the start of the first functional node + auto all_nodes_it = all_nodes.cbegin() + genotype.GetNumInputs(); + for (; nodes_it != functional_nodes.end() && genes_it != genotype.cend(); ++nodes_it, ++genes_it) { + // Advance the all nodes iterator if we are at the start of a new layer + auto dist = std::distance(functional_nodes.cbegin(), nodes_it); + if (dist != 0 && dist % genotype.GetNumNodesPerLayer() == 0) { + std::advance(all_nodes_it, genotype.GetNumNodesPerLayer()); + } + + auto &[connections, function_idx, output] = *genes_it; + (*nodes_it)->SetFunctionPointer(NodeFunction{function_set.at(function_idx), agent}); + (*nodes_it)->SetDefaultOutput(output); + + // Copy the all nodes iterator and move it backwards by the number of connections + auto nodes_it_copy = all_nodes_it; + std::advance(nodes_it_copy, -connections.size()); + // Add the inputs to the node + for (auto &connection : connections) { + if (connection != '0') { + (*nodes_it)->AddInput(*nodes_it_copy); + } + ++nodes_it_copy; + } + } + + return decision_graph; + } + + /// @brief Creates a decision graph for pacing up and down in a + /// MazeWorld. Assumes that the inputs are in the format: prev_action, + /// current_state, above_state, below_state, left_state, right_state + /// @param action_vec Assumes that the action outputs are in the format: + /// up, down, left, right + /// @return The decision graph for a vertical pacer. + std::unique_ptr VerticalPacer() { + auto decision_graph = std::make_unique(); + + GraphLayer input_layer; + std::shared_ptr prev_action = std::make_shared(0); + std::shared_ptr current_state = std::make_shared(0); + std::shared_ptr above_state = std::make_shared(0); + std::shared_ptr below_state = std::make_shared(0); + std::shared_ptr left_state = std::make_shared(0); + std::shared_ptr right_state = std::make_shared(0); + input_layer.insert(input_layer.end(), + {prev_action, current_state, above_state, below_state, left_state, right_state}); + decision_graph->AddLayer(input_layer); + + // state == 1 => floor which is walkable + GraphLayer obstruction_layer; + std::shared_ptr up_not_blocked = std::make_shared(AnyEq); + up_not_blocked->AddInputs(GraphLayer{above_state, std::make_shared(1)}); + std::shared_ptr down_not_blocked = std::make_shared(AnyEq); + down_not_blocked->AddInputs(GraphLayer{below_state, std::make_shared(1)}); + obstruction_layer.insert(obstruction_layer.end(), {up_not_blocked, down_not_blocked}); + decision_graph->AddLayer(obstruction_layer); + + // Separate previous action into up and down nodes + GraphLayer prev_action_layer; + std::shared_ptr up_prev_action = std::make_shared(AnyEq); + up_prev_action->AddInputs(GraphLayer{prev_action, std::make_shared(1)}); + std::shared_ptr down_prev_action = std::make_shared(AnyEq); + down_prev_action->AddInputs(GraphLayer{prev_action, std::make_shared(2)}); + prev_action_layer.insert(prev_action_layer.end(), {up_prev_action, down_prev_action}); + decision_graph->AddLayer(prev_action_layer); + + GraphLayer moving_layer; + // If up_not_blocked and up_prev_action ? return 1 : return 0 + // If down_not_blocked and down_prev_action ? return 1 : return 0 + std::shared_ptr keep_up = std::make_shared(And); + keep_up->AddInputs(GraphLayer{up_not_blocked, up_prev_action}); + std::shared_ptr keep_down = std::make_shared(And); + keep_down->AddInputs(GraphLayer{down_not_blocked, down_prev_action}); + moving_layer.insert(moving_layer.end(), {keep_up, keep_down}); + decision_graph->AddLayer(moving_layer); + + // If down_blocked, turn_up + // If up_blocked, turn_down + GraphLayer turn_layer; + std::shared_ptr turn_up = std::make_shared(Not); + turn_up->AddInputs(GraphLayer{down_not_blocked}); + std::shared_ptr turn_down = std::make_shared(Not); + turn_down->AddInputs(GraphLayer{up_not_blocked}); + turn_layer.insert(turn_layer.end(), {turn_up, turn_down}); + decision_graph->AddLayer(turn_layer); + + // Output layer, up, down, left, right + GraphLayer action_layer; + // move up = keep_up + turn_up, + // move down = keep_down + turn_down, + std::shared_ptr up = std::make_shared(Sum); + up->AddInputs(GraphLayer{keep_up, turn_up}); + std::shared_ptr down = std::make_shared(Sum); + down->AddInputs(GraphLayer{keep_down, turn_down}); + std::shared_ptr left = std::make_shared(0); + std::shared_ptr right = std::make_shared(0); + action_layer.insert(action_layer.end(), {up, down, left, right}); + decision_graph->AddLayer(action_layer); + + return decision_graph; + } + }; +} // namespace cowboys \ No newline at end of file diff --git a/source/Agents/GP/GraphNode.hpp b/source/Agents/GP/GraphNode.hpp new file mode 100644 index 00000000..cf98e13d --- /dev/null +++ b/source/Agents/GP/GraphNode.hpp @@ -0,0 +1,415 @@ +#pragma once + +// Macro for parallel execution, add -DPARALLEL flag to CMAKE_CXX_FLAGS when building to enable +#if PARALLEL +#include +#define PAR std::execution::par, +#else +#define PAR +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/AgentBase.hpp" +#include "../../core/WorldBase.hpp" +#include "../AgentLibary.hpp" +#include "GPAgentSensors.hpp" + +namespace cowboys { + class GraphNode; ///< Forward declaration of GraphNode + /// Function pointer for a node function. + using InnerFunction = double (*)(const GraphNode &, const cse491::AgentBase &); + /// @brief A function pointer wrapper that holds extra arguments for the function pointer. + struct NodeFunction { + InnerFunction function{nullptr}; + const cse491::AgentBase *agent{nullptr}; + double operator()(const GraphNode &node) const { return function(node, *agent); } + bool IsNull() const { return function == nullptr; } + }; + + /// @brief A node in a decision graph. + /// @note This should always be a shared pointer. Caching will not work otherwise. + class GraphNode : public std::enable_shared_from_this { + protected: + /// The input nodes to this node. + std::vector> inputs; + + /// The function that operates on the outputs from a node's input nodes. + NodeFunction function_pointer; + + /// The default output of this node. + double default_output{0}; + + /// The nodes connected to this node's output. + std::vector outputs; + + /// The cached output of this node. + mutable double cached_output{0}; + + /// Flag indicating whether the cached output is valid. + mutable bool cached_output_valid{false}; + + /// @brief Add an output node to this node. Used for cache invalidation. + /// @param node The node to add as an output. + void AddOutput(GraphNode *node) { outputs.push_back(node); } + + /// @brief Invalidates this node's cache and the caches of all nodes that depend on this node. + void RecursiveInvalidateCache() const { + cached_output_valid = false; + for (auto &output : outputs) { + output->RecursiveInvalidateCache(); + } + } + + public: + GraphNode() = default; + ~GraphNode() = default; + + /// TODO: Check guidelines for this + GraphNode(double default_value) : default_output{default_value} {} + GraphNode(NodeFunction function) : function_pointer{function} {} + GraphNode(InnerFunction function) : function_pointer{function} {} + + /// @brief Get the output of this node. Performs caching. + /// @return The output of this node. + double GetOutput() const { + if (cached_output_valid) + return cached_output; + + double result = default_output; + // Invoke function pointer if it exists + if (!function_pointer.IsNull()) { + result = function_pointer(*this); + } + + // Cache the output + cached_output = result; + cached_output_valid = true; + + return result; + } + + /// @brief Get the output values of the inputs of this node. + /// @return A vector of doubles representing the input values. + std::vector GetInputValues() const { + std::vector values; + values.reserve(inputs.size()); + std::transform(inputs.cbegin(), inputs.cend(), std::back_inserter(values), + [](const auto &node) { return node->GetOutput(); }); + return values; + } + + /// @brief Get the output values of the inputs of this node given an array of indices. + /// @tparam N The size of the indices array. + /// @param indices The indices of the inputs to get the output values of. + /// @return A vector of doubles representing the input values in the same order of the indices. + template std::optional> GetInputValues(const std::array &indices) const { + size_t max_index = *std::max_element(indices.cbegin(), indices.cend()); + if (max_index >= inputs.size()) + return std::nullopt; + std::vector values; + values.reserve(N); + std::transform(indices.cbegin(), indices.cend(), std::back_inserter(values), + [this](const auto &index) { return inputs.at(index)->GetOutput(); }); + return values; + } + + /// @brief Set the function pointer of this node. + /// @param function The function for this node to use. + void SetFunctionPointer(NodeFunction function) { + function_pointer = function; + RecursiveInvalidateCache(); + } + + /// @brief Set the function pointer of this node. + /// @param inner_function The inner function for this node to use. Will be wrapped in a NodeFunction. + void SetFunctionPointer(InnerFunction inner_function) { + function_pointer = NodeFunction{inner_function}; + RecursiveInvalidateCache(); + } + + /// @brief Add an input node to this node. + /// @param node The node to add as an input. + void AddInput(std::shared_ptr node) { + inputs.push_back(node); + // Add a weak pointer to this node to the input node's outputs + node->AddOutput(this); + RecursiveInvalidateCache(); + } + + /// @brief Append nodes in a vector to this node's list of inputs. + /// @param nodes The nodes to add as inputs. + void AddInputs(const std::vector> &nodes) { + inputs.insert(inputs.cend(), nodes.cbegin(), nodes.cend()); + RecursiveInvalidateCache(); + } + + /// @brief Set the input nodes of this node. + /// @param nodes + void SetInputs(std::vector> nodes) { + inputs = nodes; + RecursiveInvalidateCache(); + } + + /// @brief Set the default output of this node. + /// @param value The new default output. + void SetDefaultOutput(double value) { + if (default_output != value) { + default_output = value; + RecursiveInvalidateCache(); + } + } + + /// @brief Get the default output of this node. + /// @return The default output. + double GetDefaultOutput() const { return default_output; } + + /// @brief Check if the cached output is valid. + /// @return True if the cached output is valid, false otherwise. + bool IsCacheValid() const { return cached_output_valid; } + }; + + /// @brief Returns the sum all inputs. + /// @param node The node to get the inputs from. + /// @return The function result as a double. + double Sum(const GraphNode &node, const cse491::AgentBase &) { + auto vals = node.GetInputValues(); + return std::reduce(PAR vals.cbegin(), vals.cend(), 0.); + } + + /// @brief Returns 1 if all inputs are not equal to 0, 0 otherwise. + /// @param node The node to get the inputs from. + /// @return The function result as a double. + double And(const GraphNode &node, const cse491::AgentBase &) { + auto vals = node.GetInputValues(); + return std::any_of(vals.cbegin(), vals.cend(), [](const double val) { return val == 0.; }) ? 0. : 1.; + } + + /// @brief Returns 1 if any of the inputs besides the first are equal to the first + /// input, 0 otherwise. + /// @param node The node to get the inputs from. + /// @return The function result as a double. + double AnyEq(const GraphNode &node, const cse491::AgentBase &) { + std::vector vals = node.GetInputValues(); + if (vals.size() == 0) + return node.GetDefaultOutput(); + for (size_t i = 1; i < vals.size(); ++i) { + if (vals.at(0) == vals.at(i)) + return 1.; + } + return 0.; + } + + /// @brief Returns 1 if the first input is equal to 0 or there are no inputs, 0 otherwise. + /// @param node The node to get the inputs from. + /// @return The function result as a double. + double Not(const GraphNode &node, const cse491::AgentBase &) { + auto vals = node.GetInputValues<1>(std::array{0}); + if (!vals.has_value()) + return node.GetDefaultOutput(); + return (*vals)[0] == 0. ? 1. : 0.; + } + + /// @brief Returns the input with index 0 if the condition (input with index + /// 1) is not 0. + /// @param node The node to get the inputs from. + /// @return The function result as a double. + double Gate(const GraphNode &node, const cse491::AgentBase &) { + auto vals = node.GetInputValues<2>(std::array{0, 1}); + if (!vals.has_value()) + return node.GetDefaultOutput(); + return (*vals)[1] != 0. ? (*vals)[0] : 0.; + } + + /// @brief Sums the sin(x) of all inputs. + /// @param node The node to get the inputs from. + /// @return The function result as a double. + double Sin(const GraphNode &node, const cse491::AgentBase &) { + std::vector vals = node.GetInputValues(); + return std::transform_reduce(PAR vals.cbegin(), vals.cend(), 0., std::plus{}, + [](const double val) { return std::sin(val); }); + } + + /// @brief Sums the cos(x) of all inputs. + /// @param node The node to get the inputs from. + /// @return The function result as a double. + double Cos(const GraphNode &node, const cse491::AgentBase &) { + std::vector vals = node.GetInputValues(); + return std::transform_reduce(PAR vals.cbegin(), vals.cend(), 0., std::plus{}, + [](const double val) { return std::cos(val); }); + } + + /// @brief Returns the product of all inputs. + /// @param node The node to get the inputs from. + /// @return The function result as a double. + double Product(const GraphNode &node, const cse491::AgentBase &) { + auto vals = node.GetInputValues(); + return std::reduce(PAR vals.cbegin(), vals.cend(), 1., std::multiplies{}); + } + + /// @brief Returns the sum of the reciprocal of all inputs. + /// @param node The node to get the inputs from. + /// @return The function result as a double. + double Reciprocal(const GraphNode &node, const cse491::AgentBase &) { + auto vals = node.GetInputValues(); + return std::transform_reduce(PAR vals.cbegin(), vals.cend(), 0., std::plus{}, + [](const double val) { return 1. / (val + std::numeric_limits::epsilon()); }); + } + + /// @brief Returns the sum of the exp(x) of all inputs. + /// @param node The node to get the inputs from. + /// @return The function result as a double. + double Exp(const GraphNode &node, const cse491::AgentBase &) { + std::vector vals = node.GetInputValues(); + return std::transform_reduce(PAR vals.cbegin(), vals.cend(), 0., std::plus{}, + [](const double val) { return std::exp(val); }); + } + + /// @brief Returns 1 if all inputs are in ascending, 0 otherwise. If only one input, then defaults to 1. + /// @param node The node to get the inputs from. + /// @return The function result as a double. + double LessThan(const GraphNode &node, const cse491::AgentBase &) { + std::vector vals = node.GetInputValues(); + return std::ranges::is_sorted(vals, std::less{}); + } + + /// @brief Returns 1 if all inputs are in ascending, 0 otherwise. If only one input, then defaults to 1. + /// @param node The node to get the inputs from. + /// @return The function result as a double. + double GreaterThan(const GraphNode &node, const cse491::AgentBase &) { + std::vector vals = node.GetInputValues(); + return std::ranges::is_sorted(vals, std::greater{}); + } + + /// @brief Returns the maximum value of all inputs. + /// @param node The node to get the inputs from. + /// @return The function result as a double. + double Max(const GraphNode &node, const cse491::AgentBase &) { + std::vector vals = node.GetInputValues(); + if (vals.empty()) + return node.GetDefaultOutput(); + return *std::max_element(vals.cbegin(), vals.cend()); + } + + /// @brief Returns the minimum value of all inputs. + /// @param node The node to get the inputs from. + /// @return The function result as a double. + double Min(const GraphNode &node, const cse491::AgentBase &) { + std::vector vals = node.GetInputValues(); + if (vals.empty()) + return node.GetDefaultOutput(); + return *std::min_element(vals.cbegin(), vals.cend()); + } + + /// @brief Returns the sum of negated inputs. + /// @param node The node to get the inputs from. + /// @return The function result as a double. + double NegSum(const GraphNode &node, const cse491::AgentBase &agent) { return -Sum(node, agent); } + + /// @brief Returns the sum of squared inputs. + /// @param node The node to get the inputs from. + /// @return The function result as a double. + double Square(const GraphNode &node, const cse491::AgentBase &) { + std::vector vals = node.GetInputValues(); + return std::transform_reduce(PAR vals.cbegin(), vals.cend(), 0., std::plus{}, + [](const double val) { return val * val; }); + } + + /// @brief Returns the sum of positively clamped inputs. + /// @param node The node to get the inputs from. + /// @return The function result as a double. + double PosClamp(const GraphNode &node, const cse491::AgentBase &) { + std::vector vals = node.GetInputValues(); + return std::transform_reduce(PAR vals.cbegin(), vals.cend(), 0., std::plus{}, + [](const double val) { return std::max(0., val); }); + } + + /// @brief Returns the sum of negatively clamped inputs. + /// @param node The node to get the inputs from. + /// @return The function result as a double. + double NegClamp(const GraphNode &node, const cse491::AgentBase &) { + std::vector vals = node.GetInputValues(); + return std::transform_reduce(PAR vals.cbegin(), vals.cend(), 0., std::plus{}, + [](const double val) { return std::min(0., val); }); + } + + /// @brief Returns the sum of square root of positively clamped inputs. + /// @param node The node to get the inputs from. + /// @return The function result as a double. + double Sqrt(const GraphNode &node, const cse491::AgentBase &) { + std::vector vals = node.GetInputValues(); + return std::transform_reduce(PAR vals.cbegin(), vals.cend(), 0., std::plus{}, + [](const double val) { return std::sqrt(std::max(0., val)); }); + } + + /// @brief Returns the distance to the nearest obstruction upwards from the agent. + /// @param agent The agent that the node belongs to. + /// @return The distance to the nearest obstruction upwards. + double WallDistanceUp(const GraphNode &, const cse491::AgentBase &agent) { + return Sensors::wallDistance(agent.GetWorld().GetGrid(), agent, SensorDirection::ABOVE); + } + + /// @brief Returns the distance to the nearest obstruction downwards from the agent. + /// @param agent The agent that the node belongs to. + /// @return The distance to the nearest obstruction downwards. + double WallDistanceDown(const GraphNode &, const cse491::AgentBase &agent) { + return Sensors::wallDistance(agent.GetWorld().GetGrid(), agent, SensorDirection::BELOW); + } + + /// @brief Returns the distance to the nearest obstruction to the left of the agent. + /// @param agent The agent that the node belongs to. + /// @return The distance to the nearest obstruction to the left. + double WallDistanceLeft(const GraphNode &, const cse491::AgentBase &agent) { + return Sensors::wallDistance(agent.GetWorld().GetGrid(), agent, SensorDirection::LEFT); + } + + /// @brief Returns the distance to the nearest obstruction to the right of the agent. + /// @param agent The agent that the node belongs to. + /// @return The distance to the nearest obstruction to the right. + double WallDistanceRight(const GraphNode &, const cse491::AgentBase &agent) { + return Sensors::wallDistance(agent.GetWorld().GetGrid(), agent, SensorDirection::RIGHT); + } + + /// @brief Returns the distance to the grid position represented by the first two inputs using A*. + /// @param node The node to get the inputs from. + /// @param agent The agent that the node belongs to. + /// @return The distance to the grid position using A* + double AStarDistance(const GraphNode &node, const cse491::AgentBase &agent) { + auto vals = node.GetInputValues<2>(std::array{0, 1}); + if (!vals.has_value()) + return node.GetDefaultOutput(); + auto vals2 = *vals; + auto goal_position = cse491::GridPosition(vals2[0], vals2[1]); + auto path = walle::GetShortestPath(agent.GetPosition(), goal_position, agent.GetWorld(), agent); + return path.size(); + } + + /// A vector of all the node functions + static const std::vector NODE_FUNCTION_SET{ + nullptr, Sum, And, AnyEq, Not, Gate, Sin, Cos, Product, Exp, + LessThan, GreaterThan, Max, Min, NegSum, Square, PosClamp, NegClamp, Sqrt}; + /// A vector of all the sensor functions + static const std::vector SENSOR_FUNCTION_SET{WallDistanceUp, WallDistanceDown, WallDistanceLeft, + WallDistanceRight, AStarDistance}; + + /// A vector of all the node functions and sensors + static const std::vector FUNCTION_SET = []() { + std::vector functions; + functions.reserve(NODE_FUNCTION_SET.size() + SENSOR_FUNCTION_SET.size()); + functions.insert(functions.cend(), NODE_FUNCTION_SET.cbegin(), NODE_FUNCTION_SET.cend()); + functions.insert(functions.cend(), SENSOR_FUNCTION_SET.cbegin(), SENSOR_FUNCTION_SET.cend()); + return functions; + }(); +} // namespace cowboys \ No newline at end of file diff --git a/source/Agents/GP/Group7_GPA.md b/source/Agents/GP/Group7_GPA.md index 511fd42c..710503ed 100644 --- a/source/Agents/GP/Group7_GPA.md +++ b/source/Agents/GP/Group7_GPA.md @@ -1,5 +1,43 @@ # Group 7 GP Agent +# Release Freeze Update: + +Here is our plan @mercere99 + +Things to get done ( Group 7) + +- Implement better fitness functions ** Undecided** + - implementing A* as another sensor + +- Saving the state of the best agents overall: **Rajmeet** + +- CGPA: **Simon** + - Crossover: Graph (goes in the base GP class) + +- HPCC: **Aman** + - Serialization? just of the genotype + - adding in multithreading for mutation + - multiple start positions + +- Integration testing? + +- Crossover for LGPA + - Generate a new list using two parents. + +- Export (waiting on the worlds) + +- LGPA agent: **Jason** + - get the current pure virtual functions working + - tests + + +Things to ask +- Ask world groups: what agents do they want (attack, chase, etc) with a decent description of the world. +- Data collection team (we're getting data from them) + +Goals to achieve by the end: +- End up with agents that exhibit semi intelligent behavior (they can chase the player around, attack, do whatever the world wants them to do) + # Demo October 18th ## Current progress: diff --git a/source/Agents/GP/LGPAgent.hpp b/source/Agents/GP/LGPAgent.hpp index 9a013a33..ca958444 100644 --- a/source/Agents/GP/LGPAgent.hpp +++ b/source/Agents/GP/LGPAgent.hpp @@ -1,181 +1,253 @@ #pragma once -#include +#include +#include #include #include -#include -#include - +#include #include "../../core/AgentBase.hpp" #include "GPAgentSensors.hpp" -/// Generated by CHATGPT - -namespace cowboys { -const int LISTSIZE = 100; - -EXPERIMENTAL_CLASS class LGPAgent : public cse491::AgentBase { - protected: - // A dictionary of actions and a dictionary of sensors - // A sensor is a function that takes in a grid and returns a value (e.g. - // distance to nearest agent) - - // For example group 1 has a function for the shortest path - - std::vector possibleInstructionsList = {}; - std::vector actionsList = {"up", "down", "left", "right"}; - std::vector operationsList = {"lessthan", "greaterthan", - "equals"}; - std::vector resultsList = std::vector(LISTSIZE); - - std::vector> instructionsList = {}; - size_t currentInstructionIndex = 0; - - std::vector sensorsList; - std::vector sensorsNamesList = {"getLeft", "getRight", "getUp", - "getDown"}; - - std::random_device rd; - std::mt19937 gen; - - public: - EXPERIMENTAL_FUNCTION LGPAgent(size_t id, const std::string &name) - : AgentBase(id, name) { - gen = std::mt19937(rd()); - } - - ~LGPAgent() override = default; - - /// @brief This agent needs a specific set of actions to function. - /// @return Success. - EXPERIMENTAL_FUNCTION bool Initialize() override { - possibleInstructionsList = EncodeActions(action_map, sensorsNamesList); - GenerateRandomActionList(); - return true; - } - - EXPERIMENTAL_FUNCTION void GenerateRandomActionList() { - // generate a random list of actions - std::uniform_int_distribution dist( - 0, possibleInstructionsList.size() - 1); - std::uniform_int_distribution dist2(0, resultsList.size() - 1); - for (int i = 0; i < LISTSIZE; i++) { - instructionsList.push_back(std::make_tuple( - possibleInstructionsList[dist(gen)], dist2(gen), dist2(gen))); - } - -#ifndef NDEBUG - for (auto i = 0; i < LISTSIZE; i++) { - std::cout << get<0>(instructionsList[i]) << " "; - } - - std::cout << std::endl; -#endif - } - - /// @brief Encodes the actions from an agent's action map into a vector of - /// string, representing action names. - /// @param action_map The action map from the agent. - /// @return A vector of strings, representing action names. - EXPERIMENTAL_FUNCTION static std::vector EncodeActions( - const std::unordered_map &action_map, - const std::vector &sensorsNamesList) { - std::vector instructions; - for (const auto &[action_name, action_id] : action_map) { - instructions.push_back(action_name); - } - instructions.push_back("lessthan"); - instructions.push_back("greaterthan"); - instructions.push_back("equals"); - - for (const auto &sensor : sensorsNamesList) { - instructions.push_back(sensor); - } - - return instructions; - } - - EXPERIMENTAL_FUNCTION size_t - SelectAction([[maybe_unused]] const cse491::WorldGrid &grid, - [[maybe_unused]] const cse491::type_options_t &type_options, - [[maybe_unused]] const cse491::item_map_t &item_map, - [[maybe_unused]] const cse491::agent_map_t &agent_map) override { - std::string action; - std::string sensor; - std::string operation; - auto instruction = instructionsList[currentInstructionIndex]; - int i = 0; - -#ifndef NDEBUG - std::cout << "=========================================" << std::endl; - - Sensors::wallDistance(grid, *this, SensorDirection::LEFT); - Sensors::wallDistance(grid, *this, SensorDirection::RIGHT); - Sensors::wallDistance(grid, *this, SensorDirection::ABOVE); - Sensors::wallDistance(grid, *this, SensorDirection::BELOW); -#endif - - if (currentInstructionIndex != 0) { - resultsList[currentInstructionIndex - 1] = action_result; - } else { - resultsList[LISTSIZE - 1] = action_result; - } - - while (i < LISTSIZE && action.empty()) { - if (std::find(actionsList.begin(), actionsList.end(), - get<0>(instruction)) != actionsList.end()) { - action = get<0>(instruction); - } else if (std::find(sensorsNamesList.begin(), sensorsNamesList.end(), - get<0>(instruction)) != sensorsNamesList.end()) { - // the instruction is in the sensor list (getLeft, getRight, getUp, - // getDown) - sensor = get<0>(instruction); - - SensorDirection direction = Sensors::getSensorDirectionEnum(sensor); - int distance = Sensors::wallDistance(grid, *this, direction); - resultsList[currentInstructionIndex] = distance; - } - - else { - // the instruction is an operation (lessthan, greaterthan, equals) - operation = get<0>(instruction); - if (operation == "lessthan") { - if (get<1>(instruction) < get<2>(instruction)) { - resultsList[currentInstructionIndex] = 1; - ++currentInstructionIndex; - } else { - resultsList[currentInstructionIndex] = 0; - } - } else if (operation == "greaterthan") { - if (get<1>(instruction) > get<2>(instruction)) { - resultsList[currentInstructionIndex] = 1; - ++currentInstructionIndex; - } else { - resultsList[currentInstructionIndex] = 0; - } - } else if (operation == "equals") { - if (get<1>(instruction) == get<2>(instruction)) { - resultsList[currentInstructionIndex] = 1; - ++currentInstructionIndex; - } else { - resultsList[currentInstructionIndex] = 0; +#include "./GPAgentBase.hpp" + +namespace cowboys +{ + const int LISTSIZE = 100; + + class LGPAgent : public GPAgentBase + { + protected: + // A dictionary of actions and a dictionary of sensors + // A sensor is a function that takes in a grid and returns a value (e.g. distance to nearest agent) + + // For example group 1 has a function for the shortest path + + std::vector possibleInstructionsList = {}; + std::vector actionsList = {}; + std::vector operationsList = {"lessthan", "greaterthan", "equals"}; + std::vector sensorsNamesList = {"getLeft", "getRight", "getUp", "getDown"}; + std::vector resultsList; + + std::vector> instructionsList = {}; + size_t currentInstructionIndex = 0; + + std::random_device rd; + std::mt19937 gen; + + public: + LGPAgent(size_t id, const std::string &name) : GPAgentBase(id, name) + { + gen = std::mt19937(rd()); + + for (auto i = 0; i < LISTSIZE; i++) + { + resultsList.push_back(0); + } + } + + + + /// @brief This agent needs a specific set of actions to function. + /// @return Success. + bool Initialize() override + { + possibleInstructionsList = EncodeActions(action_map, sensorsNamesList, operationsList, actionsList); + GenerateRandomActionList(); + return true; + } + + void GenerateRandomActionList() + { + // generate a random list of actions + std::uniform_int_distribution dist(0, possibleInstructionsList.size() - 1); + std::uniform_int_distribution dist2(0, LISTSIZE - 1); + for (int i = 0; i < LISTSIZE; i++) + { + instructionsList.push_back(std::make_tuple(possibleInstructionsList[dist(gen)], dist2(gen), dist2(gen))); + } + + } + + /// @brief Encodes the actions from an agent's action map into a vector of string, representing action names. + /// @param action_map The action map from the agent. + /// @return A vector of strings, representing action names. + static std::vector EncodeActions(const std::unordered_map &action_map, const std::vector &sensorsNamesList, const std::vector &operationsList, std::vector &actionsList) + { + std::vector instructions; + for (const auto &[action_name, action_id] : action_map) + { + instructions.push_back(action_name); + actionsList.push_back(action_name); + } + for (const auto &sensor : operationsList) + { + instructions.push_back(sensor); + } + + for (const auto &sensor : sensorsNamesList) + { + instructions.push_back(sensor); + } + + return instructions; + } + + + void MutateAgent(double mutation_rate = 0.01) override + { + std::uniform_int_distribution rnd_mutate(1, 100); + std::uniform_int_distribution dist(0, possibleInstructionsList.size() - 1); + std::uniform_int_distribution dist2(0, LISTSIZE - 1); + + for (auto i = 0; i < LISTSIZE; i++) + { + if (rnd_mutate(gen) / 100.0 <= mutation_rate) + { + instructionsList[i] = std::make_tuple(possibleInstructionsList[dist(gen)], dist2(gen), dist2(gen)); + } + } + + +// resultsList = std::vector(LISTSIZE); + resultsList.clear(); + resultsList.resize(LISTSIZE); + currentInstructionIndex = 0; + } + + /// @brief Get the instruction list for this agent. + /// @return A const reference to the instruction list for this agent. + const std::vector> &GetInstructionsList(){ return instructionsList; } + + /// @brief Copies the behavior of another LGPAgent into this agent. + /// @param other The LGPAgent to copy. + void Configure(const LGPAgent &other) { + instructionsList = other.instructionsList; + possibleInstructionsList = other.possibleInstructionsList; + actionsList = other.actionsList; + operationsList = other.operationsList; + sensorsNamesList = other.sensorsNamesList; + resultsList = other.resultsList; + currentInstructionIndex = other.currentInstructionIndex; + } + + /// @brief Copy the behavior of another agent into this agent. + /// @param other The agent to copy. + void Copy(const GPAgentBase &other) override + { + assert(dynamic_cast(&other) != nullptr); + Configure(dynamic_cast(other)); + } + + std::string Export() { + return ""; + } + + size_t GetAction([[maybe_unused]] const cse491::WorldGrid &grid, + [[maybe_unused]] const cse491::type_options_t &type_options, + [[maybe_unused]] const cse491::item_map_t &item_set, + [[maybe_unused]] const cse491::agent_map_t &agent_set) override + { + std::string action; + std::string sensor; + std::string operation; + auto instruction = instructionsList[currentInstructionIndex]; + int i = 0; + + if (currentInstructionIndex != 0) + { + resultsList[currentInstructionIndex - 1] = action_result; + } + else + { + resultsList[LISTSIZE - 1] = action_result; + } + + while (i < LISTSIZE * 2 && action.empty()) + { + if (std::find(actionsList.begin(), actionsList.end(), std::get<0>(instruction)) != actionsList.end()) + { + action = std::get<0>(instruction); + } + else if (std::find(sensorsNamesList.begin(), sensorsNamesList.end(), std::get<0>(instruction)) != sensorsNamesList.end()) + { + // the instruction is in the sensor list (getLeft, getRight, getUp, getDown) + sensor = std::get<0>(instruction); + + SensorDirection direction = Sensors::getSensorDirectionEnum(sensor); + int distance = Sensors::wallDistance(grid, *this, direction); + + + resultsList[currentInstructionIndex] = distance; + + + } + else + { + // the instruction is an operation (lessthan, greaterthan, equals) + operation = std::get<0>(instruction); + if (operation == "lessthan") + { + if (std::get<1>(instruction) < std::get<2>(instruction)) + { + resultsList[currentInstructionIndex] = 1; + } + else + { + resultsList[currentInstructionIndex] = 0; + ++currentInstructionIndex; + } + } + else if (operation == "greaterthan") + { + if (std::get<1>(instruction) > std::get<2>(instruction)) + { + resultsList[currentInstructionIndex] = 1; + } + else + { + resultsList[currentInstructionIndex] = 0; + ++currentInstructionIndex; + } + } + else if (operation == "equals") + { + if (std::get<1>(instruction) == std::get<2>(instruction)) + { + resultsList[currentInstructionIndex] = 1; + } + else + { + resultsList[currentInstructionIndex] = 0; + ++currentInstructionIndex; + } + } + } + + ++currentInstructionIndex; + if (currentInstructionIndex >= LISTSIZE) + { + currentInstructionIndex = 0; + } + ++i; + instruction = instructionsList[currentInstructionIndex]; + } + if (!action.empty()) + { + return action_map[action]; + } + + return 0; + } + + void Serialize(tinyxml2::XMLDocument &, tinyxml2::XMLElement *, double fitness = -1) override {} + + void PrintAgent() override { + for (auto i = 0; i < LISTSIZE; i++) + { + std::cout << std::get<0>(instructionsList[i]) << " "; } + std::cout << std::endl; } - } - - ++currentInstructionIndex; - if (currentInstructionIndex >= instructionsList.size()) { - currentInstructionIndex = 0; - } - ++i; - instruction = instructionsList[currentInstructionIndex]; - } - if (!action.empty()) { - return action_map[action]; - } - - return 0; - } -}; -}; // namespace cowboys + }; +} \ No newline at end of file diff --git a/source/Makefile b/source/Makefile deleted file mode 100644 index 62db4170..00000000 --- a/source/Makefile +++ /dev/null @@ -1,41 +0,0 @@ -FLAGS_version = -std=c++20 -FLAGS_warn = -Wall -Wextra -Wcast-align -Winfinite-recursion -Wnon-virtual-dtor -Wnull-dereference -Woverloaded-virtual -Wpedantic -FLAGS_all = $(FLAGS_version) $(FLAGS_warn) - -FLAGS_debug = $(FLAGS_all) -g # Setup for running a debugger -FLAGS_opt = $(FLAGS_all) -O3 -DNDEBUG # Fastest possible executable -FLAGS_quick = $(FLAGS_all) -DNDEBUG # Fastest possible compilation -FLAGS_grumpy = $(FLAGS_all) -Wconversion -Weffc++ # Some extra picky warnings - -CXX = g++ - -# List of all available executables -EXECUTABLES = simple - -default: simple - -add_subdirectory(test) - -# Generic rule to build default version of any executable -$(EXECUTABLES): %: - $(CXX) $@_main.cpp $(FLAGS_all) -o $@ - -# Generic rule to build debug version of any executable -debug-%: - $(CXX) $*_main.cpp $(FLAGS_debug) -o $* - -# Generic rule to build opt version of any executable -opt-%: - $(CXX) $*_main.cpp $(FLAGS_opt) -o $* - -# Generic rule to build quick version of any executable -quick-%: - $(CXX) $*_main.cpp $(FLAGS_quick) -o $* - - -FILES_backup = *~ *.dSYM -FILES_generated = *.o $(EXECUTABLES) - -clean: - rm -rf $(FILES_backup) $(FILES_generated) - diff --git a/source/Worlds/MazeWorld.hpp b/source/Worlds/MazeWorld.hpp index 45486317..c5a1b325 100644 --- a/source/Worlds/MazeWorld.hpp +++ b/source/Worlds/MazeWorld.hpp @@ -28,7 +28,7 @@ class MazeWorld : public WorldBase { } public: - MazeWorld() { + MazeWorld(unsigned int seed = 0) : WorldBase(seed) { floor_id = AddCellType("floor", "Floor that you can easily walk over.", ' '); wall_id = AddCellType("wall", "Impenetrable wall that you must find a way around.", '#'); main_grid.Read("../assets/grids/default_maze.grid", type_options); diff --git a/source/XML_main.cpp b/source/XML_main.cpp new file mode 100644 index 00000000..4914c01e --- /dev/null +++ b/source/XML_main.cpp @@ -0,0 +1,11 @@ + +#include +#include "tinyxml2.h" + + + +int main(){ + + + +} \ No newline at end of file diff --git a/source/core/AgentBase.hpp b/source/core/AgentBase.hpp index 2cac51aa..cea0f792 100644 --- a/source/core/AgentBase.hpp +++ b/source/core/AgentBase.hpp @@ -100,6 +100,15 @@ namespace cse491 { virtual void Notify(const std::string & /*message*/, const std::string & /*msg_type*/="none") { } + +// virtual void Serialize(std::ostream & os) {}; +// +// virtual void deserialize(std::istream & is) {}; +// +// void storeAgentData(std::string name) { +// +// } + }; } // End of namespace cse491 diff --git a/source/core/CoreObject.hpp b/source/core/CoreObject.hpp index b2f761e1..88ddf737 100644 --- a/source/core/CoreObject.hpp +++ b/source/core/CoreObject.hpp @@ -75,13 +75,13 @@ namespace cse491 { // The functions below can be used in derived classes to implement above functionality. /// @brief Set up beginning of the serialization for this class (allows checking later) - /// @param os Output stream to serialize into. + /// @param os Output stream to Serialize into. void StartSerialize(std::ostream & os) const { os << ":::START " << GetTypeName() << "\n"; } /// @brief Set up end of the serialization for this class (allows checking later) - /// @param os Output stream to serialize into. + /// @param os Output stream to Serialize into. void EndSerialize(std::ostream & os) const { os << ":::END " << GetTypeName() << "\n"; } diff --git a/source/core/DEVELOPER_NOTES.md b/source/core/DEVELOPER_NOTES.md index 6abd475c..36db4735 100644 --- a/source/core/DEVELOPER_NOTES.md +++ b/source/core/DEVELOPER_NOTES.md @@ -36,7 +36,7 @@ only on files above it in the core. ## `Entity.hpp` - Should we use a better/different structure for properties? Right now properties can only have a `double` value, but we could use `std::variant` to allow for a set of allowed values, or even `std::any`. -- If we have Entity derive from `CoreObject`, we have to think about how to keep requirements for serialize functions on subsequent derived classes. One option is to simply provide tools (like a `SerializeEntity()` function), but don't build the required virtual functions yet. Still, it would be nice to be able to require correctness (or at least detect common errors, like forgetting to run `SerializeEntity()`) +- If we have Entity derive from `CoreObject`, we have to think about how to keep requirements for Serialize functions on subsequent derived classes. One option is to simply provide tools (like a `SerializeEntity()` function), but don't build the required virtual functions yet. Still, it would be nice to be able to require correctness (or at least detect common errors, like forgetting to run `SerializeEntity()`) ## `AgentBase.hpp` diff --git a/source/core/GridPosition.hpp b/source/core/GridPosition.hpp index 51d0b7a1..3f2b17ed 100644 --- a/source/core/GridPosition.hpp +++ b/source/core/GridPosition.hpp @@ -29,6 +29,8 @@ namespace cse491 { GridPosition(double x, double y) : x(x), y(y) { } GridPosition(const GridPosition &) = default; + ~GridPosition() = default; + GridPosition & operator=(const GridPosition &) = default; // -- Accessors -- diff --git a/source/gp_main.cpp b/source/gp_main.cpp new file mode 100644 index 00000000..d68f5db1 --- /dev/null +++ b/source/gp_main.cpp @@ -0,0 +1,28 @@ +/** + * This file is part of the Fall 2023, CSE 491 course project. + * @brief A simplistic main file to demonstrate a system. + * @note Status: PROPOSAL + **/ + +// Include the modules that we will be using. +#include "Agents/PacingAgent.hpp" +#include "Interfaces/TrashInterface.hpp" +#include "Worlds/MazeWorld.hpp" + +#include "Agents/GP/GPAgentBase.hpp" +#include "Agents/GP/LGPAgent.hpp" + +int main() { + cse491::MazeWorld world; + world.AddAgent("Pacer 1").SetPosition(3, 1); + + //GP agent + world.AddAgent("GP 1").SetPosition(1, 0).SetProperty("symbol", 'G'); + + + // Human agent + world.AddAgent("Interface").SetProperty("symbol", '@'); + + + world.Run(); +} diff --git a/source/gp_train_main.cpp b/source/gp_train_main.cpp new file mode 100644 index 00000000..2544b48d --- /dev/null +++ b/source/gp_train_main.cpp @@ -0,0 +1,39 @@ + +#include "Agents/GP/GPTrainingLoop.hpp" +#include "Agents/GP/LGPAgent.hpp" +#include "Worlds/MazeWorld.hpp" + +#include "Interfaces/TrashInterface.hpp" +#include "Worlds/ManualWorld.hpp" + + +#include + + +#include + +int main() { + + const int num_threads = std::thread::hardware_concurrency(); + std::cout << "Number of threads: " << num_threads << std::endl; + + + auto start_time = std::chrono::high_resolution_clock::now(); +// for (size_t i = 0; i < 20; ++i){ + cowboys::GPTrainingLoop loop; + + + loop.Initialize(100, 1000); + loop.Run(11, 100, num_threads, true); +// } + + auto end_time = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end_time - start_time); + + auto seconds = duration.count() / 1000000.0; + std::cout << "Time taken by Training: " << seconds << " seconds" << std::endl; + + + + return 0; +} \ No newline at end of file diff --git a/source/simple_cgp_main.cpp b/source/simple_cgp_main.cpp new file mode 100644 index 00000000..492e6c52 --- /dev/null +++ b/source/simple_cgp_main.cpp @@ -0,0 +1,21 @@ +/** + * This file is part of the Fall 2023, CSE 491 course project. + * @brief A simplistic main file to test the CGP agent in the maze world. + * @note Status: PROPOSAL + **/ + +// Include the modules that we will be using. +#include "Agents/PacingAgent.hpp" +#include "Agents/GP/CGPAgent.hpp" +#include "Interfaces/TrashInterface.hpp" +#include "Worlds/MazeWorld.hpp" + +int main() { + cse491::MazeWorld world; + world.AddAgent("Pacer 1").SetPosition(3, 1); + world.AddAgent("Pacer 2").SetPosition(6, 1); + world.AddAgent("GP 1").SetProperty("symbol", 'G').SetPosition(1, 0); + world.AddAgent("Interface").SetProperty("symbol", '@'); + + world.Run(); +} diff --git a/tests/unit/Agents/GP/CGPAgent.cpp b/tests/unit/Agents/GP/CGPAgent.cpp new file mode 100644 index 00000000..a8bef79c --- /dev/null +++ b/tests/unit/Agents/GP/CGPAgent.cpp @@ -0,0 +1,55 @@ +/** + * This file is part of the Fall 2023, CSE 491 course project. + * @brief Unit tests for source/[Deprecated]Group7_GP_Agent/CGPAgent.hpp + **/ + +// Catch2 +#define CATCH_CONFIG_MAIN +#include + +#include "Agents/GP/CGPAgent.hpp" + +using namespace cowboys; + +TEST_CASE("CGPAgent construction", "[group7][agent]") { + SECTION("CGPAgent construction") { + CGPAgent agent(0, "agent"); + CHECK(agent.GetGenotype().GetNumPossibleConnections() == 0); + CHECK(agent.GetGenotype().GetNumFunctionalNodes() == 0); + } + SECTION("CGPAgent construction with genotype") { + CGPGenotype genotype({8, 4, 2, 10, 2}); + + CGPAgent agent(0, "agent", genotype); + CHECK(agent.GetGenotype().GetNumPossibleConnections() == 8 * 10 + (8 + 10) * 10 + (10 + 10) * 4); + CHECK(agent.GetGenotype().GetNumFunctionalNodes() == 2 * 10 + 4); + } +} +TEST_CASE("Copying", "[group7][agent][genotype]") { + SECTION("Different size graphs") { + CGPGenotype genotype({8, 4, 2, 10, 2}); + CGPAgent agent(0, "agent", genotype); + CGPAgent agent2(1, "agent2"); + CHECK(agent.GetGenotype() != agent2.GetGenotype()); + + // Generic references to CGPAgent + GPAgentBase &agent_ref = agent; + GPAgentBase &agent2_ref = agent2; + agent2_ref.Copy(agent_ref); // Copy agent into agent2 + CHECK(agent.GetGenotype() == agent2.GetGenotype()); + } + SECTION("Mutation") { + CGPGenotype genotype({8, 4, 2, 10, 2}); + CGPAgent agent(0, "agent", genotype); + CGPAgent agent2(1, "agent2", genotype); + CHECK(agent.GetGenotype() == agent2.GetGenotype()); + agent.MutateAgent(1); + CHECK(agent.GetGenotype() != agent2.GetGenotype()); + + // Generic references to CGPAgent + GPAgentBase &agent_ref = agent; + GPAgentBase &agent2_ref = agent2; + agent2_ref.Copy(agent_ref); // Copy agent into agent2 + CHECK(agent.GetGenotype() == agent2.GetGenotype()); + } +} diff --git a/tests/unit/Agents/GP/CGPGenotype.cpp b/tests/unit/Agents/GP/CGPGenotype.cpp new file mode 100644 index 00000000..df067868 --- /dev/null +++ b/tests/unit/Agents/GP/CGPGenotype.cpp @@ -0,0 +1,300 @@ +/** + * This file is part of the Fall 2023, CSE 491 course project. + * @brief Unit tests for source/[Deprecated]Group7_GP_Agent/CGPGenotype.hpp + **/ + +// Catch2 +#define CATCH_CONFIG_MAIN +#include + +#include "Agents/GP/CGPGenotype.hpp" +#include "Agents/GP/CGPAgent.hpp" +#include + +using namespace cowboys; + +CGPAgent mock_agent(0, "mock"); + +TEST_CASE("Genotype construction", "[group7][genotype]") { + SECTION("Parameters constructor") { + auto genotype = CGPGenotype({8, 4, 2, 10, 2}); + // Each node can look back 2 layers + // 1st layer has 10 nodes, 8 connections each to the inputs + // 2nd layer has 10 nodes, 8 connections to the inputs + 10 connections each to the 1st layer + // Output layer has 4 nodes, 10 connections each to the 2nd layer + 10 connections each to the 1st layer + CHECK(genotype.GetNumPossibleConnections() == 8 * 10 + (8 + 10) * 10 + (10 + 10) * 4); + CHECK(genotype.GetNumConnections() == 0); + + genotype = CGPGenotype({8, 4, 2, 10, 3}); + // Each node can look back 3 layers + // 1st layer has 10 nodes, 8 connections each to the inputs + // 2nd layer has 10 nodes, 10 connections each to the 1st layer + 8 connections to the inputs + // Output layer has 4 nodes, 10 connections each to the 2nd layer + 10 connections each to the 1st layer + 8 + // connections to the inputs + CHECK(genotype.GetNumPossibleConnections() == 8 * 10 + (8 + 10) * 10 + (8 + 10 + 10) * 4); + CHECK(genotype.GetNumConnections() == 0); + + genotype = CGPGenotype({8, 4, 2, 10, 10}); + // Each node can look back 10 layers, but there are only 4 layers so each layer can look backwards all layers + CHECK(genotype.GetNumPossibleConnections() == 8 * 10 + (8 + 10) * 10 + (8 + 10 + 10) * 4); + CHECK(genotype.GetNumConnections() == 0); + } +} +TEST_CASE("Genotype iterators", "[group7][genotype]") { + SECTION("Genotype iterators") { + CGPGenotype genotype({8, 4, 2, 10, 2}); + auto it = genotype.begin(); + // We ignore the input nodes, so the first node should have 8 input connections + CHECK(it->input_connections.size() == 8); + + // Iterate through all nodes, checking input connections to differentiate them + it = genotype.begin(); + // Layer 1 + for (size_t i = 0; i < 10; ++i) { + CHECK(it->input_connections.size() == 8); + ++it; + } + // Layer 2 + for (size_t i = 0; i < 10; ++i) { + CHECK(it->input_connections.size() == 18); + CHECK(it->function_idx == 0); + ++it; + } + // Output layer + for (size_t i = 0; i < 4; ++i) { + CHECK(it->input_connections.size() == 20); + CHECK(it->function_idx == 0); + ++it; + } + CHECK(it == genotype.end()); + + // All nodes should have 0 input connections when initialized by default + bool all_0s = true; + for (auto it = genotype.begin(); it != genotype.end(); ++it) + all_0s = all_0s && std::ranges::all_of(it->input_connections, [](char c) { return c == '0'; }); + CHECK(all_0s); + } +} +TEST_CASE("Genotype mutation", "[group7][genotype]") { + // Graph should be big enough to reduce chance of false positives + CGPGenotype genotype({10, 10, 200, 10, 10}); + SECTION("Mutate connections") { + // Each connection will have a 0% chance of being mutated + genotype.MutateConnections(0., mock_agent); + bool all_0s = true; + for (auto it = genotype.begin(); it != genotype.end(); ++it) + all_0s = all_0s && std::ranges::all_of(it->input_connections, [](char c) { return c == '0'; }); + CHECK(all_0s); + CHECK(genotype.GetNumConnections() == 0); + + // Each connection will have a 100% chance of being mutated + genotype.MutateConnections(1., mock_agent); + all_0s = true; + for (auto it = genotype.begin(); it != genotype.end(); ++it) + all_0s = all_0s && std::ranges::all_of(it->input_connections, [](char c) { return c == '0'; }); + CHECK_FALSE(all_0s); + CHECK_FALSE(genotype.GetNumConnections() == 0); + } + SECTION("Mutate functions") { + bool all_default = true; + for (auto it = genotype.begin(); it != genotype.end(); ++it) { + all_default = all_default && it->function_idx == 0; + } + CHECK(all_default); + CHECK(genotype.GetNumConnections() == 0); + + // Nothing should change + genotype.MutateFunctions(0., 100, mock_agent); + all_default = true; + for (auto it = genotype.begin(); it != genotype.end(); ++it) { + all_default = all_default && it->function_idx == 0; + } + CHECK(all_default); + CHECK(genotype.GetNumConnections() == 0); + + // Most should change + genotype.MutateFunctions(1., 100, mock_agent); + all_default = true; + for (auto it = genotype.begin(); it != genotype.end(); ++it) { + all_default = all_default && it->function_idx == 0; + } + CHECK_FALSE(all_default); + CHECK(genotype.GetNumConnections() == 0); + } + SECTION("Mutate outputs") { + bool all_default = true; + for (auto it = genotype.begin(); it != genotype.end(); ++it) { + all_default = all_default && it->default_output == 0; + } + CHECK(all_default); + CHECK(genotype.GetNumConnections() == 0); + + // Nothing should change + genotype.MutateOutputs(0., 0, 100, mock_agent); + all_default = true; + for (auto it = genotype.begin(); it != genotype.end(); ++it) { + all_default = all_default && it->default_output == 0; + } + CHECK(all_default); + CHECK(genotype.GetNumConnections() == 0); + + // Should change + genotype.MutateOutputs(1., 0, 100, mock_agent); + all_default = true; + for (auto it = genotype.begin(); it != genotype.end(); ++it) { + all_default = all_default && it->default_output == 0; + } + CHECK_FALSE(all_default); + CHECK(genotype.GetNumConnections() == 0); + } + SECTION("Mutate default") { + bool all_default = true; + for (auto it = genotype.begin(); it != genotype.end(); ++it) { + all_default = all_default && it->default_output == 0; + all_default = all_default && it->function_idx == 0; + all_default = all_default && std::ranges::all_of(it->input_connections, [](char c) { return c == '0'; }); + } + CHECK(all_default); + CHECK(genotype.GetNumConnections() == 0); + + // Mutate with 0 probability, nothing should change + genotype.MutateDefault(0., mock_agent); + all_default = true; + for (auto it = genotype.begin(); it != genotype.end(); ++it) { + all_default = all_default && it->default_output == 0; + all_default = all_default && it->function_idx == 0; + all_default = all_default && std::ranges::all_of(it->input_connections, [](char c) { return c == '0'; }); + } + CHECK(all_default); + CHECK(genotype.GetNumConnections() == 0); + + // Mutate with 100% probability, everything should have a high chance of changing (input connections will have a 1/2 + // chance of changing (connected vs not connected) while functions will have a 1-1/n chance of changing with n functions) + genotype.MutateDefault(1., mock_agent); + all_default = true; + for (auto it = genotype.begin(); it != genotype.end(); ++it) { + all_default = all_default && it->default_output == 0; + all_default = all_default && it->function_idx == 0; + all_default = all_default && std::ranges::all_of(it->input_connections, [](char c) { return c == '0'; }); + } + CHECK_FALSE(all_default); + CHECK_FALSE(genotype.GetNumConnections() == 0); + } +} +TEST_CASE("base64", "[group7][base64]") { + SECTION("ULL") { + auto max_ull = std::numeric_limits::max(); + // 64 bits for ull; 1 char represents 6 bits => 64 / 6 = 10.6666 => 11 chars + // 64 bits set to all 1s => 10 chars of largest char + another char for left over bits + // 64 % 6 = 4 => 4 bits left over => 4 bits of 1s => 16th char + std::string max_encoded_ull = base64::CHARS[15] + std::string(10, base64::CHARS[63]); + CHECK(base64::ULLToB64(max_ull) == max_encoded_ull); + CHECK(base64::B64ToULL(max_encoded_ull) == max_ull); + } + SECTION("Binary") { + std::string max_encoded_ull = base64::CHARS[15] + std::string(10, base64::CHARS[63]); + CHECK(base64::B2ToB64(std::string(64, '1')) == max_encoded_ull); + CHECK(base64::B64ToB2(max_encoded_ull) == std::string(64, '1')); + } + SECTION("Double") { + auto rng = std::mt19937(std::random_device{}()); + // Test large range of doubles + // Use int32 min and max to avoid stoull error with large doubles + auto min = std::numeric_limits::min(); + auto max = std::numeric_limits::max(); + auto dist = std::uniform_real_distribution(min, max); + auto fix_double = [](double d) { return std::stod(std::to_string(d)); }; + for (size_t i = 0; i < 100; ++i) { + auto d = fix_double(dist(rng)); + CHECK(d == base64::B64ToDouble(base64::DoubleToB64(d))); + } + // Some specific cases + CHECK(0 == base64::B64ToDouble(base64::DoubleToB64(0))); + CHECK(1 == base64::B64ToDouble(base64::DoubleToB64(1))); + CHECK(-1 == base64::B64ToDouble(base64::DoubleToB64(-1))); + // Smaller range doubles + dist = std::uniform_real_distribution(-1, 1); + for (size_t i = 0; i < 100; ++i) { + auto d = fix_double(dist(rng)); + CHECK(d == base64::B64ToDouble(base64::DoubleToB64(d))); + } + } +} +TEST_CASE("Genotype overloads", "[group7][genotype]") { + SECTION("operator== and operator!=") { + auto genotype = CGPGenotype({7, 2, 0, 10, 3}); + auto genotype2 = CGPGenotype({7, 2, 0, 10, 3}); + CHECK(genotype == genotype2); + + genotype2 = CGPGenotype({7, 2, 0, 10, 2}); + CHECK_FALSE(genotype == genotype2); + CHECK(genotype != genotype2); + + genotype2 = CGPGenotype({7, 2, 0, 10, 3}).MutateConnections(1, mock_agent); + CHECK_FALSE(genotype == genotype2); + CHECK(genotype != genotype2); + + genotype2 = CGPGenotype({7, 2, 0, 10, 3}).MutateFunctions(1, 100, mock_agent); + CHECK_FALSE(genotype == genotype2); + CHECK(genotype != genotype2); + + genotype2 = CGPGenotype({7, 2, 0, 10, 3}).MutateOutputs(1, 0, 10000, mock_agent); + CHECK_FALSE(genotype == genotype2); + CHECK(genotype != genotype2); + } + SECTION("Copy constructor") { + auto genotype = CGPGenotype({7, 2, 0, 10, 3}).MutateDefault(1, mock_agent); + auto genotype2 = CGPGenotype(genotype); + CHECK(genotype == genotype2); + } +} +TEST_CASE("Genotype configuration", "[group7][genotype]") { + SECTION("Exporting and configuration") { + CGPGenotype genotype({8, 4, 10, 10, 2}); + CGPGenotype genotype2 = CGPGenotype().Configure(genotype.Export()); + CHECK(genotype == genotype2); + + genotype.begin()->function_idx = 1; + CHECK_FALSE(genotype == genotype2); + genotype2.begin()->function_idx = 1; + CHECK(genotype == genotype2); + + genotype.begin()->input_connections = std::vector(8, '1'); + CHECK_FALSE(genotype == genotype2); + genotype2.begin()->input_connections = std::vector(8, '1'); + CHECK(genotype == genotype2); + + genotype.begin()->input_connections = std::vector(8, '0'); + CHECK_FALSE(genotype == genotype2); + genotype2 = CGPGenotype().Configure(genotype.Export()); + CHECK(genotype == genotype2); + + genotype.begin()->default_output = 1; + CHECK_FALSE(genotype == genotype2); + genotype2.begin()->default_output = 1; + CHECK(genotype == genotype2); + + // + // These tests could fail, should be unlikely + // + genotype.MutateConnections(1, mock_agent); + CHECK_FALSE(genotype == genotype2); + genotype2 = CGPGenotype().Configure(genotype.Export()); + CHECK(genotype == genotype2); + + genotype.MutateFunctions(1, 100, mock_agent); + CHECK_FALSE(genotype == genotype2); + genotype2 = CGPGenotype().Configure(genotype.Export()); + CHECK(genotype == genotype2); + + genotype.MutateOutputs(1, 0, 10000, mock_agent); + CHECK_FALSE(genotype == genotype2); + genotype2 = CGPGenotype().Configure(genotype.Export()); + CHECK(genotype == genotype2); + } + SECTION("Copy constructor"){ + CGPGenotype genotype({8, 4, 10, 10, 2}); + CGPGenotype genotype2(genotype); + CHECK(genotype == genotype2); + } +} \ No newline at end of file diff --git a/tests/unit/Agents/GP/Graph.cpp b/tests/unit/Agents/GP/Graph.cpp new file mode 100644 index 00000000..359572d2 --- /dev/null +++ b/tests/unit/Agents/GP/Graph.cpp @@ -0,0 +1,45 @@ +/** + * This file is part of the Fall 2023, CSE 491 course project. + * @brief Unit tests for source/[Deprecated]Group7_GP_Agent/Graph.hpp + **/ + +// Catch2 +#define CATCH_CONFIG_MAIN +#include + +#include "Agents/GP/Graph.hpp" + +using namespace cowboys; + +TEST_CASE("Graph", "[group7][graph]") { + SECTION("Empty Graph") { + std::vector actions{1, 2, 3, 4}; + Graph graph; + CHECK(graph.GetLayerCount() == 0); + CHECK(graph.GetNodeCount() == 0); + + // Default output the first action + std::vector inputs = {1.0, 2.0, 3.0}; + CHECK(graph.MakeDecision(inputs, actions) == actions.at(0)); + + std::vector actions2{10, 2, 3, 4}; + CHECK(graph.MakeDecision(inputs, actions2) == actions2.at(0)); + } + + SECTION("Non-Empty Graph") { + Graph graph; + GraphLayer layer1; + layer1.push_back(std::make_shared()); + layer1.push_back(std::make_shared()); + layer1.push_back(std::make_shared()); + graph.AddLayer(layer1); + CHECK(graph.GetLayerCount() == 1); + CHECK(graph.GetNodeCount() == 3); + + GraphLayer layer2; + layer2.push_back(std::make_shared()); + graph.AddLayer(layer2); + CHECK(graph.GetLayerCount() == 2); + CHECK(graph.GetNodeCount() == 4); + } +} diff --git a/tests/unit/Agents/GP/GraphBuilder.cpp b/tests/unit/Agents/GP/GraphBuilder.cpp new file mode 100644 index 00000000..c3f3d06c --- /dev/null +++ b/tests/unit/Agents/GP/GraphBuilder.cpp @@ -0,0 +1,88 @@ +/** + * This file is part of the Fall 2023, CSE 491 course project. + * @brief Unit tests for source/[Deprecated]Group7_GP_Agent/GraphBuilder.hpp + **/ + +// Catch2 +#define CATCH_CONFIG_MAIN +#include + +#include "Agents/GP/CGPGenotype.hpp" +#include "Agents/GP/GraphBuilder.hpp" +#include "Agents/GP/CGPAgent.hpp" + +using namespace cowboys; + +CGPAgent mock_agent(0, "mock"); + +TEST_CASE("Cartesian Graph", "[group7][graph][cartesian]") { + constexpr size_t INPUT_SIZE = 10; + constexpr size_t NUM_OUTPUTS = 10; + constexpr size_t NUM_LAYERS = 2; // Middle layers, does not include input or output layers + constexpr size_t NUM_NODES_PER_LAYER = 10; // Nodes per middle layer + constexpr size_t LAYERS_BACK = 2; // How many layers back a node can connect to + SECTION("Cartesian Graph construction") { + GraphBuilder builder; + + CGPGenotype genotype({INPUT_SIZE, NUM_OUTPUTS, NUM_LAYERS, NUM_NODES_PER_LAYER, LAYERS_BACK}); + auto graph = builder.CartesianGraph(genotype, NODE_FUNCTION_SET); + + // Input layer + middle layers + output layer + size_t expected_layer_count = NUM_LAYERS + 2; + CHECK(graph->GetLayerCount() == expected_layer_count); + + size_t num_input_nodes = INPUT_SIZE; + size_t num_middle_nodes = NUM_LAYERS * NUM_NODES_PER_LAYER; + size_t num_output_nodes = NUM_OUTPUTS; + size_t expected_node_count = num_input_nodes + num_middle_nodes + num_output_nodes; + CHECK(graph->GetNodeCount() == expected_node_count); + } + SECTION("Cartesian graph mutated") { + std::vector actions(NUM_OUTPUTS, 0); + std::vector inputs(INPUT_SIZE, 0); + for (size_t i = 0; i < NUM_OUTPUTS; ++i) + actions.at(i) = i; + GraphBuilder builder; + bool choose_same_action = true; + size_t action = 0; + + // Test that the graph is not always choosing the same action when mutated with different seeds + size_t iterations = 100; + for (size_t i = 0; i < iterations; ++i) { + CGPGenotype genotype({INPUT_SIZE, NUM_OUTPUTS, NUM_LAYERS, NUM_NODES_PER_LAYER, LAYERS_BACK}); + genotype.SetSeed(i).MutateDefault(1, mock_agent, NODE_FUNCTION_SET.size()); + auto graph = builder.CartesianGraph(genotype, NODE_FUNCTION_SET); + auto new_action = graph->MakeDecision(inputs, actions); + choose_same_action = choose_same_action && (new_action == action); + action = new_action; + } + // Could fail, but should be very unlikely + CHECK_FALSE(choose_same_action); + } + SECTION("Cartesian graph mutated header") { + // Mutating the header should not affect graph behavior to help with mutation locality + std::vector actions(NUM_OUTPUTS, 0); + std::vector inputs(INPUT_SIZE, 0); + for (size_t i = 0; i < NUM_OUTPUTS; ++i) + actions.at(i) = i; + GraphBuilder builder; + CGPGenotype base({INPUT_SIZE, NUM_OUTPUTS, NUM_LAYERS, NUM_NODES_PER_LAYER, LAYERS_BACK}); + auto graph = builder.CartesianGraph(base, NODE_FUNCTION_SET); + bool choose_same_action = true; + size_t action = graph->MakeDecision(inputs, actions); + + // The graph should always choose the same action when mutated with different seeds + size_t iterations = 100; + for (size_t i = 0; i < iterations; ++i) { + auto copy = base; + copy.SetSeed(i).MutateHeader(1, mock_agent); + CHECK_FALSE(copy == base); + auto expanded_graph = builder.CartesianGraph(copy, NODE_FUNCTION_SET); + auto new_action = expanded_graph->MakeDecision(inputs, actions); + choose_same_action = choose_same_action && (new_action == action); + action = new_action; + } + // Could fail, but should be very unlikely + CHECK(choose_same_action); + } +} \ No newline at end of file diff --git a/tests/unit/Agents/GP/GraphNode.cpp b/tests/unit/Agents/GP/GraphNode.cpp new file mode 100644 index 00000000..63e438ce --- /dev/null +++ b/tests/unit/Agents/GP/GraphNode.cpp @@ -0,0 +1,417 @@ +/** + * This file is part of the Fall 2023, CSE 491 course project. + * @brief Unit tests for source/[Deprecated]Group7_GP_Agent/GraphNode.hpp + **/ + +// Catch2 +#define CATCH_CONFIG_MAIN +#include + +#include "Agents/GP/GraphNode.hpp" + +using namespace cowboys; + +TEST_CASE("GraphNode", "[group7][graphnode]") { + SECTION("Empty GraphNode") { + GraphNode node; + CHECK(node.GetOutput() == 0); + node = GraphNode(7); + CHECK(node.GetOutput() == 7); + } + auto simple_add = [](const GraphNode &node, const cse491::AgentBase &) { + auto vals = node.GetInputValues<2>({0, 1}).value_or(std::vector{0, 0}); + return vals[0] + vals[1]; + }; + SECTION("GraphNode function pointers") { + GraphNode node; + // Function to sum the first two inputs + node.SetFunctionPointer(simple_add); + + // Not enough inputs + CHECK(node.GetOutput() == 0); + + auto node1 = std::make_shared(3); + node.AddInput(node1); + // Still not enough inputs + CHECK(node.GetOutput() == 0); + + auto node2 = std::make_shared(4); + node.AddInput(node2); + // Now we have enough inputs + CHECK(node.GetOutput() == 7); + } + SECTION("GraphNode function pointer constructor") { + GraphNode node(simple_add); + auto node1 = std::make_shared(3); + auto node2 = std::make_shared(4); + node.AddInput(node1); + node.AddInput(node2); + CHECK(node.GetOutput() == 7); + } +} + +TEST_CASE("GraphNode function set", "[group7][functionset]") { + std::shared_ptr node = std::make_shared(111); + SECTION("nullptr") { + CHECK(node->GetOutput() == node->GetDefaultOutput()); + node->SetFunctionPointer(nullptr); + CHECK(node->GetOutput() == node->GetDefaultOutput()); + } + SECTION("Sum") { + node->SetFunctionPointer(Sum); + CHECK_NOTHROW(node->GetOutput()); // No inputs + auto input = std::make_shared(1); + node->AddInput(input); + CHECK(node->GetOutput() == 1); + input->SetDefaultOutput(3); + CHECK(node->GetOutput() == 3); + node->AddInput(std::make_shared(4)); + CHECK(node->GetOutput() == 7); + node->AddInput(std::make_shared(5)); + CHECK(node->GetOutput() == 12); + } + SECTION("AnyEq") { + node->SetFunctionPointer(AnyEq); + CHECK_NOTHROW(node->GetOutput()); // No inputs + node->AddInput(std::make_shared(3)); // The value to check for equality + CHECK(node->GetOutput() == 0); + node->AddInput(std::make_shared(4)); // None equal + CHECK(node->GetOutput() == 0); + node->AddInput(std::make_shared(5)); // None equal + CHECK(node->GetOutput() == 0); + node->AddInput(std::make_shared(3)); // One equal + CHECK(node->GetOutput() == 1); + node->AddInput(std::make_shared(4)); // One equal + CHECK(node->GetOutput() == 1); + } + SECTION("And") { + node->SetFunctionPointer(And); + CHECK_NOTHROW(node->GetOutput()); // No inputs + node->AddInput(std::make_shared(1)); // True + CHECK(node->GetOutput() == 1); + node->AddInput(std::make_shared(5)); // True + CHECK(node->GetOutput() == 1); + node->AddInput(std::make_shared(-1)); // True + CHECK(node->GetOutput() == 1); + node->AddInput(std::make_shared(0)); // False + CHECK(node->GetOutput() == 0); + node->AddInput(std::make_shared(1)); // False + CHECK(node->GetOutput() == 0); + } + SECTION("Not") { + node->SetFunctionPointer(Not); + CHECK_NOTHROW(node->GetOutput()); // No inputs + auto input = std::make_shared(); + node->AddInput(input); + CHECK(node->GetOutput() == 1); // No inputs, but default to true (return 1) + + input->SetDefaultOutput(1); // True -> 0 + CHECK(node->GetOutput() == 0); + + input->SetDefaultOutput(0); // False -> 1 + CHECK(node->GetOutput() == 1); + + input->SetDefaultOutput(10); // True -> 0 + CHECK(node->GetOutput() == 0); + } + SECTION("Gate") { + node->SetFunctionPointer(Gate); + CHECK_NOTHROW(node->GetOutput()); // No inputs + auto input = std::make_shared(5); + node->AddInput(input); + // One input, default to node default output + CHECK(node->GetOutput() == node->GetDefaultOutput()); + + auto condition = std::make_shared(); + node->AddInput(condition); + // condition returns 0 => Condition is false + CHECK(node->GetOutput() == 0); + + condition->SetDefaultOutput(1); + // condition returns 1 => Condition is true + CHECK(node->GetOutput() == 5); + + input->SetDefaultOutput(10); + CHECK(node->GetOutput() == 10); + + condition->SetDefaultOutput(0); + CHECK(node->GetOutput() == 0); + } + SECTION("Sin") { + node->SetFunctionPointer(Sin); + CHECK_NOTHROW(node->GetOutput()); // No inputs + auto input = std::make_shared(0); + node->AddInput(input); + CHECK(node->GetOutput() == 0); + + input->SetDefaultOutput(1); + CHECK(node->GetOutput() == std::sin(1)); + + auto input2 = std::make_shared(2); + node->AddInput(input2); + CHECK(node->GetOutput() == std::sin(1) + std::sin(2)); + } + SECTION("Cos") { + node->SetFunctionPointer(Cos); + CHECK_NOTHROW(node->GetOutput()); // No inputs + auto input = std::make_shared(0); + node->AddInput(input); + CHECK(node->GetOutput() == 1); + + input->SetDefaultOutput(1); + CHECK(node->GetOutput() == std::cos(1)); + + auto input2 = std::make_shared(2); + node->AddInput(input2); + CHECK(node->GetOutput() == std::cos(1) + std::cos(2)); + } + SECTION("Product") { + node->SetFunctionPointer(Product); + CHECK_NOTHROW(node->GetOutput()); // No inputs + auto input = std::make_shared(0); + node->AddInput(input); + CHECK(node->GetOutput() == 0); + + input->SetDefaultOutput(2); + CHECK(node->GetOutput() == 2); + + auto input2 = std::make_shared(3); + node->AddInput(input2); + CHECK(node->GetOutput() == 6); + + input->SetDefaultOutput(4); + CHECK(node->GetOutput() == 12); + } + SECTION("Exp") { + node->SetFunctionPointer(Exp); + CHECK_NOTHROW(node->GetOutput()); // No inputs + auto input = std::make_shared(0); + node->AddInput(input); + CHECK(node->GetOutput() == 1); + + input->SetDefaultOutput(1); + CHECK(node->GetOutput() == std::exp(1)); + + auto input2 = std::make_shared(2); + node->AddInput(input2); + CHECK(node->GetOutput() == std::exp(1) + std::exp(2)); + } + SECTION("LessThan") { + node->SetFunctionPointer(LessThan); + CHECK_NOTHROW(node->GetOutput()); // No inputs + auto input = std::make_shared(0); + node->AddInput(input); + CHECK(node->GetOutput() == 1); // Default to true + + input->SetDefaultOutput(1); + CHECK(node->GetOutput() == 1); // Default to true + + auto input2 = std::make_shared(2); + node->AddInput(input2); + CHECK(node->GetOutput() == 1); // 1 < 2 = true + + input->SetDefaultOutput(3); + CHECK(node->GetOutput() == 0); // 3 < 2 = false + } + SECTION("GreaterThan") { + node->SetFunctionPointer(GreaterThan); + CHECK_NOTHROW(node->GetOutput()); // No inputs + auto input = std::make_shared(0); + node->AddInput(input); + CHECK(node->GetOutput() == 1); // Default to true + + input->SetDefaultOutput(1); + CHECK(node->GetOutput() == 1); // Default to true + + auto input2 = std::make_shared(2); + node->AddInput(input2); + CHECK(node->GetOutput() == 0); // 1 > 2 = false + + input->SetDefaultOutput(3); + CHECK(node->GetOutput() == 1); // 3 > 2 = true + } + SECTION("Max") { + node->SetFunctionPointer(Max); + CHECK_NOTHROW(node->GetOutput()); // No inputs + auto input = std::make_shared(0); + node->AddInput(input); + CHECK(node->GetOutput() == 0); + + input->SetDefaultOutput(1); + CHECK(node->GetOutput() == 1); + + auto input2 = std::make_shared(2); + node->AddInput(input2); + CHECK(node->GetOutput() == 2); + + input->SetDefaultOutput(3); + CHECK(node->GetOutput() == 3); + } + SECTION("Min") { + node->SetFunctionPointer(Min); + CHECK_NOTHROW(node->GetOutput()); // No inputs + auto input = std::make_shared(0); + node->AddInput(input); + CHECK(node->GetOutput() == 0); + + input->SetDefaultOutput(1); + CHECK(node->GetOutput() == 1); + + auto input2 = std::make_shared(-1); + node->AddInput(input2); + CHECK(node->GetOutput() == -1); + + input->SetDefaultOutput(-2); + CHECK(node->GetOutput() == -2); + } + SECTION("NegSum") { + node->SetFunctionPointer(NegSum); + CHECK_NOTHROW(node->GetOutput()); // No inputs + auto input = std::make_shared(0); + node->AddInput(input); + CHECK(node->GetOutput() == 0); + + input->SetDefaultOutput(1); + CHECK(node->GetOutput() == -1); + + auto input2 = std::make_shared(2); + node->AddInput(input2); + CHECK(node->GetOutput() == -3); + + input->SetDefaultOutput(3); + CHECK(node->GetOutput() == -5); + } + SECTION("Square") { + node->SetFunctionPointer(Square); + CHECK_NOTHROW(node->GetOutput()); // No inputs + auto input = std::make_shared(0); + node->AddInput(input); + CHECK(node->GetOutput() == 0); + + input->SetDefaultOutput(1); + CHECK(node->GetOutput() == 1); + + auto input2 = std::make_shared(2); + node->AddInput(input2); + CHECK(node->GetOutput() == 5); // 1^2 + 2^2 = 5 + + input->SetDefaultOutput(3); + CHECK(node->GetOutput() == 13); // 3^2 + 2^2 = 13 + } + SECTION("PosClamp") { + node->SetFunctionPointer(PosClamp); + CHECK_NOTHROW(node->GetOutput()); // No inputs + auto input = std::make_shared(0); + node->AddInput(input); + CHECK(node->GetOutput() == 0); + + input->SetDefaultOutput(1); + CHECK(node->GetOutput() == 1); + + auto input2 = std::make_shared(-1); + node->AddInput(input2); + CHECK(node->GetOutput() == 1); // pclamp(1) + pclamp(-1) = 1 + 0 = 1 + + input->SetDefaultOutput(-2); + CHECK(node->GetOutput() == 0); // pclamp(-2) + pclamp(-1) = 0 + 0 = 0 + } + SECTION("NegClamp") { + node->SetFunctionPointer(NegClamp); + CHECK_NOTHROW(node->GetOutput()); // No inputs + auto input = std::make_shared(0); + node->AddInput(input); + CHECK(node->GetOutput() == 0); + + input->SetDefaultOutput(-1); + CHECK(node->GetOutput() == -1); + + auto input2 = std::make_shared(1); + node->AddInput(input2); + CHECK(node->GetOutput() == -1); // nclamp(-1) + nclamp(1) = 0 + -1 = -1 + + input->SetDefaultOutput(2); + CHECK(node->GetOutput() == 0); // nclamp(2) + nclamp(1) = 0 + 0 = 0 + } + SECTION("Sqrt") { + node->SetFunctionPointer(Sqrt); + CHECK_NOTHROW(node->GetOutput()); // No inputs + auto input = std::make_shared(0); + node->AddInput(input); + CHECK(node->GetOutput() == 0); + + input->SetDefaultOutput(1); + CHECK(node->GetOutput() == 1); + + auto input2 = std::make_shared(4); + node->AddInput(input2); + CHECK(node->GetOutput() == 3); // sqrt(1) + sqrt(4) = 1 + 2 = 3 + + input->SetDefaultOutput(9); + CHECK(node->GetOutput() == 5); // sqrt(9) + sqrt(4) = 3 + 2 = 5 + + // Clamp negative values to 0 + input->SetDefaultOutput(-1); + CHECK(node->GetOutput() == 2); // sqrt(0) + sqrt(4) = 0 + 2 = 2 + } +} + +TEST_CASE("Caching", "[GraphNode]") { + // Create a graph with 1 input and 2 outputs + std::shared_ptr a = std::make_shared(0); + std::shared_ptr b = std::make_shared(Sum); + std::shared_ptr c = std::make_shared(Sum); + b->AddInput(a); + c->AddInput(a); + + // Caches should be invalid + CHECK(a->IsCacheValid() == false); + CHECK(b->IsCacheValid() == false); + CHECK(c->IsCacheValid() == false); + + b->GetOutput(); + // a and b caches should be valid, c should be invalid + CHECK(a->IsCacheValid() == true); + CHECK(b->IsCacheValid() == true); + CHECK(c->IsCacheValid() == false); + + a->SetDefaultOutput(10); + // All caches should be invalid + CHECK(a->IsCacheValid() == false); + CHECK(b->IsCacheValid() == false); + CHECK(c->IsCacheValid() == false); + + c->GetOutput(); + // a and c caches should be valid, b should be invalid + CHECK(a->IsCacheValid() == true); + CHECK(b->IsCacheValid() == false); + CHECK(c->IsCacheValid() == true); + + b->GetOutput(); + // All caches should be valid + CHECK(a->IsCacheValid() == true); + CHECK(b->IsCacheValid() == true); + CHECK(c->IsCacheValid() == true); + + std::shared_ptr d = std::make_shared(5); + d->AddInput(c); + // All caches except d should be valid + CHECK(a->IsCacheValid() == true); + CHECK(b->IsCacheValid() == true); + CHECK(c->IsCacheValid() == true); + CHECK(d->IsCacheValid() == false); + + d->GetOutput(); + // All caches should be valid + CHECK(a->IsCacheValid() == true); + CHECK(b->IsCacheValid() == true); + CHECK(c->IsCacheValid() == true); + CHECK(d->IsCacheValid() == true); + + // Invalidate the cache of a + a->SetDefaultOutput(1); + // All caches should be invalid + CHECK(a->IsCacheValid() == false); + CHECK(b->IsCacheValid() == false); + CHECK(c->IsCacheValid() == false); + CHECK(d->IsCacheValid() == false); +} \ No newline at end of file diff --git a/tests/unit/Agents/GP/LGPAgent.cpp b/tests/unit/Agents/GP/LGPAgent.cpp new file mode 100644 index 00000000..881ca5cc --- /dev/null +++ b/tests/unit/Agents/GP/LGPAgent.cpp @@ -0,0 +1,45 @@ +/** + * This file is part of the Fall 2023, CSE 491 course project. + * @brief Unit tests for source/[Deprecated]Group7_GP_Agent/CGPAgent.hpp + **/ + +// Catch2 +#define CATCH_CONFIG_MAIN +#include + +#include "Agents/GP/LGPAgent.hpp" + +using namespace cowboys; + +TEST_CASE("LGPAgent construction", "[group7][agent]") { + SECTION("LGPAgent construction") { + LGPAgent agent(0, "agent"); + agent.Initialize(); + CHECK(agent.GetInstructionsList().size() == LISTSIZE); + } +} +TEST_CASE("Copying", "[group7][agent]") { + SECTION("Different lists (presumably)") { + LGPAgent agent(0, "agent"); + agent.Initialize(); + LGPAgent agent2(1, "agent2"); + agent2.Initialize(); + CHECK(agent.GetInstructionsList() != agent2.GetInstructionsList()); + + // Generic references to LGPAgent + GPAgentBase &agent_ref = agent; + GPAgentBase &agent2_ref = agent2; + agent2_ref.Copy(agent_ref); // Copy agent into agent2 + CHECK(agent.GetInstructionsList() == agent2.GetInstructionsList()); + } + SECTION("Mutation") { + LGPAgent agent(0, "agent"); + agent.Initialize(); + LGPAgent agent2(1, "agent2"); + agent2.Initialize(); + agent2.Copy(agent); + CHECK(agent.GetInstructionsList() == agent2.GetInstructionsList()); + agent.MutateAgent(1); + CHECK(agent.GetInstructionsList() != agent2.GetInstructionsList()); + } +} diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index b96df07d..587da426 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -7,13 +7,19 @@ function(create_tests NAME_PREFIX) # Loop through each .cpp file and create a test target with dependencies foreach(TARGET_SOURCE IN LISTS TARGET_SOURCES) + string(REPLACE ".cpp" "" TARGET_NAME ${TARGET_SOURCE}) set(EXE_NAME "${NAME_PREFIX}-${TARGET_NAME}") message(STATUS "Building tests for: ${EXE_NAME}") # Create target + + #add_executable(${EXE_NAME} ${TARGET_SOURCE}) + + add_executable(${EXE_NAME} ${TARGET_SOURCE} ${CMAKE_SOURCE_DIR}/source/core/Entity.cpp) + # Load in .cmake file for target, if it exists if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/${TARGET_NAME}.cmake) message(STATUS "Loading ${TARGET_NAME}.cmake") @@ -21,7 +27,7 @@ function(create_tests NAME_PREFIX) else() message(WARNING "Cannot find ${TARGET_NAME}.cmake") endif() - + # Add includes and libraries target_include_directories(${EXE_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/source @@ -29,6 +35,10 @@ function(create_tests NAME_PREFIX) target_link_libraries(${EXE_NAME} PRIVATE Catch2::Catch2WithMain ) + + target_link_libraries(${EXE_NAME} + PRIVATE tinyxml2 + ) # Add to ctest add_test(NAME ${EXE_NAME} COMMAND ${EXE_NAME} WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) @@ -37,7 +47,9 @@ endfunction() # Process all subdirectories add_subdirectory(core) + add_subdirectory(Agents) add_subdirectory(Worlds) add_subdirectory(Interfaces) add_subdirectory(DataCollection) +add_subdirectory(XML_formater) diff --git a/tests/unit/XML_formater/CMakeLists.txt b/tests/unit/XML_formater/CMakeLists.txt new file mode 100644 index 00000000..661460c4 --- /dev/null +++ b/tests/unit/XML_formater/CMakeLists.txt @@ -0,0 +1,6 @@ +set(TEST_PREFIX "${TEST_PREFIX}-xmlformater") + + + +# Call helper function from CMake file in tests directory +create_tests(${TEST_PREFIX}) diff --git a/tests/unit/XML_formater/XML_format.cpp b/tests/unit/XML_formater/XML_format.cpp new file mode 100644 index 00000000..103c2161 --- /dev/null +++ b/tests/unit/XML_formater/XML_format.cpp @@ -0,0 +1,25 @@ +/** + * This file is part of the Fall 2023, CSE 491 course project. + * @brief Unit tests for Data.hpp in source/core + **/ + +// Catch2 +#define CATCH_CONFIG_MAIN +#include + +#include "tinyxml2.h" + + +TEST_CASE("TinyXML2 can parse from string", "[xml][parse]") { + tinyxml2::XMLDocument doc; + tinyxml2::XMLError result = doc.Parse("value"); + + REQUIRE(result == tinyxml2::XML_SUCCESS); + + tinyxml2::XMLElement* root = doc.FirstChildElement("root"); + REQUIRE(root != nullptr); + + tinyxml2::XMLElement* child = root->FirstChildElement("child"); + REQUIRE(child != nullptr); + REQUIRE(std::string(child->GetText()) == "value"); +} diff --git a/tests/unit/XML_formater/XML_readfromfile.cmake b/tests/unit/XML_formater/XML_readfromfile.cmake new file mode 100644 index 00000000..4dc6b8bf --- /dev/null +++ b/tests/unit/XML_formater/XML_readfromfile.cmake @@ -0,0 +1,3 @@ +# move a the test file into a executable directory + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/test.xml ${CMAKE_BINARY_DIR}/executable/test.xml COPYONLY) diff --git a/tests/unit/XML_formater/XML_readfromfile.cpp b/tests/unit/XML_formater/XML_readfromfile.cpp new file mode 100644 index 00000000..778e5031 --- /dev/null +++ b/tests/unit/XML_formater/XML_readfromfile.cpp @@ -0,0 +1,41 @@ +/** + * This file is part of the Fall 2023, CSE 491 course project. + * @brief Unit tests for Data.hpp in source/core + **/ + +// Catch2 +#define CATCH_CONFIG_MAIN +#include + +#include "tinyxml2.h" + + +// Use the TEST_CASE macro from Catch2 to define a test case. +TEST_CASE("TinyXML2 can read from a local file", "[xml][read]") { + + + +//TODO: issues with saving at a specific location/directory and reading for a specific directory. +// works locally but not on github actions + +// tinyxml2::XMLDocument doc; +// +// +// tinyxml2::XMLError result = doc.LoadFile("test.xml"); +// +// +// REQUIRE(result == tinyxml2::XML_SUCCESS); +// +// +// tinyxml2::XMLElement* root = doc.FirstChildElement("root"); +// REQUIRE(root != nullptr); +// +// +// tinyxml2::XMLElement* child = root->FirstChildElement("child"); +// REQUIRE(child != nullptr); +// +// +// REQUIRE(std::string(child->GetText()) == "expected_value"); + + +} diff --git a/tests/unit/XML_formater/XML_serialization.cpp b/tests/unit/XML_formater/XML_serialization.cpp new file mode 100644 index 00000000..7f488047 --- /dev/null +++ b/tests/unit/XML_formater/XML_serialization.cpp @@ -0,0 +1,130 @@ + + +#include +#include "tinyxml2.h" +#include + +class Shape { +public: + virtual ~Shape() = default; + virtual void serialize(tinyxml2::XMLDocument& doc, tinyxml2::XMLElement* parentElem) const = 0; + virtual void deserialize(const tinyxml2::XMLElement* element) = 0; + + virtual Shape* deserializeObj(const tinyxml2::XMLElement* element) const = 0; +}; + +class Circle : public Shape { +private: + float radius; +public: + Circle(float r = 0.0f) : radius(r) {} + + void serialize(tinyxml2::XMLDocument& doc, tinyxml2::XMLElement* parentElem) const override { + tinyxml2::XMLElement* elem = doc.NewElement("Circle"); + elem->SetAttribute("radius", radius); + parentElem->InsertEndChild(elem); + } + + void deserialize(const tinyxml2::XMLElement* element) override { + if (element != nullptr) { + radius = element->FloatAttribute("radius"); + } + } + + Circle* deserializeObj(const tinyxml2::XMLElement* element) const override { + if (element == nullptr) return nullptr; + return new Circle(element->FloatAttribute("radius")); + } + + float getRadius() const { return radius; } +}; + +class Rectangle : public Shape { +private: + float width; + float height; +public: + Rectangle(float w = 0.0f, float h = 0.0f) : width(w), height(h) {} + + void serialize(tinyxml2::XMLDocument& doc, tinyxml2::XMLElement* parentElem) const override { + tinyxml2::XMLElement* elem = doc.NewElement("Rectangle"); + elem->SetAttribute("width", width); + elem->SetAttribute("height", height); + parentElem->InsertEndChild(elem); + } + + void deserialize(const tinyxml2::XMLElement* element) override { + if (element != nullptr) { + width = element->FloatAttribute("width"); + height = element->FloatAttribute("height"); + } + } + + Rectangle* deserializeObj(const tinyxml2::XMLElement* element) const override { + if (element == nullptr) return nullptr; + return new Rectangle(element->FloatAttribute("width"), element->FloatAttribute("height")); + } + + float getWidth() const { return width; } + float getHeight() const { return height; } +}; + + + +TEST_CASE("Shapes are serialized to XML correctly", "[serialization]") { + + tinyxml2::XMLDocument doc; + auto *root = doc.NewElement("Shapes"); + doc.InsertFirstChild(root); + + + Circle circle(5.0f); + Rectangle rectangle(10.0f, 2.0f); + + + circle.serialize(doc, root); + rectangle.serialize(doc, root); + + + SECTION("Circle is serialized correctly") { + tinyxml2::XMLElement *circleElement = root->FirstChildElement("Circle"); + REQUIRE(circleElement != nullptr); + REQUIRE(circleElement->FloatAttribute("radius") == 5.0f); + } + + SECTION("Rectangle is serialized correctly") { + tinyxml2::XMLElement *rectangleElement = root->FirstChildElement("Rectangle"); + REQUIRE(rectangleElement != nullptr); + REQUIRE(rectangleElement->FloatAttribute("width") == 10.0f); + REQUIRE(rectangleElement->FloatAttribute("height") == 2.0f); + } +} + + +TEST_CASE("Shapes are deserialized from XML correctly", "[deserialization]") { + tinyxml2::XMLDocument doc; + + SECTION("Circle is deserialized correctly") { + const char* xml = ""; + doc.Parse(xml); + const tinyxml2::XMLElement* circleElement = doc.FirstChildElement("Circle"); + Circle circle; + circle.deserialize(circleElement); + + REQUIRE(circle.getRadius() == Catch::Approx(10.0f)); + } + + SECTION("Rectangle is deserialized correctly") { + const char* xml = ""; + doc.Parse(xml); + const tinyxml2::XMLElement* rectangleElement = doc.FirstChildElement("Rectangle"); + Rectangle rectangle; + rectangle.deserialize(rectangleElement); + + REQUIRE(rectangle.getWidth() == Catch::Approx(15.0f)); + REQUIRE(rectangle.getHeight() == Catch::Approx(30.0f)); + } +} + + + diff --git a/tests/unit/XML_formater/test.xml b/tests/unit/XML_formater/test.xml new file mode 100644 index 00000000..50073bd0 --- /dev/null +++ b/tests/unit/XML_formater/test.xml @@ -0,0 +1,3 @@ + + expected_value + diff --git a/third_party/tinyxml2 b/third_party/tinyxml2 new file mode 160000 index 00000000..e0595609 --- /dev/null +++ b/third_party/tinyxml2 @@ -0,0 +1 @@ +Subproject commit e05956094c27117f989d22f25b75633123d72a83