diff --git a/.github/workflows/binding.yml b/.github/workflows/binding.yml index 31855414..3d383fe3 100644 --- a/.github/workflows/binding.yml +++ b/.github/workflows/binding.yml @@ -127,13 +127,14 @@ jobs: - name: Verify package metadata run: | python -c " - import pkg_resources + import importlib.metadata try: - dist = pkg_resources.get_distribution('dsf') - print(f'Package name: {dist.project_name}') + dist = importlib.metadata.distribution('dsf') + print(f'Package name: {dist.name}') print(f'Package version: {dist.version}') - print(f'Package location: {dist.location}') - except pkg_resources.DistributionNotFound: + location = str(dist.locate_file('')) + print(f'Package location: {location}') + except importlib.metadata.PackageNotFoundError: print('Warning: Package metadata not found') " diff --git a/.gitignore b/.gitignore index fe430808..c0b0732d 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,6 @@ test/data/*dsf webapp/data/* *egg-info* + +# Jupyter Notebook (temporary checks) +*.ipynb \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 31824b84..49868ad9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -138,6 +138,21 @@ if(NOT simdjson_POPULATED) endif() # Check if the user has TBB installed find_package(TBB REQUIRED CONFIG) +# Get SQLiteCpp +# Disable cppcheck on Windows to avoid issues with missing cppcheck installation +if(WIN32) + set(SQLITECPP_RUN_CPPCHECK + OFF + CACHE BOOL "Run cppcheck on SQLiteCpp" FORCE) +endif() +FetchContent_Declare( + SQLiteCpp + GIT_REPOSITORY https://github.com/SRombauts/SQLiteCpp + GIT_TAG 3.3.3) +FetchContent_GetProperties(SQLiteCpp) +if(NOT SQLiteCpp_POPULATED) + FetchContent_MakeAvailable(SQLiteCpp) +endif() add_library(dsf STATIC ${SOURCES}) target_compile_definitions(dsf PRIVATE SPDLOG_USE_STD_FORMAT) @@ -151,7 +166,7 @@ target_include_directories( target_include_directories(dsf PRIVATE ${rapidcsv_SOURCE_DIR}/src) # Link other libraries - no csv dependency needed now -target_link_libraries(dsf PRIVATE TBB::tbb simdjson::simdjson spdlog::spdlog) +target_link_libraries(dsf PUBLIC TBB::tbb SQLiteCpp PRIVATE simdjson::simdjson spdlog::spdlog) # Install dsf library install( diff --git a/README.md b/README.md index 03652f60..c64a58e1 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ [![SPDLOG](https://img.shields.io/badge/spdlog-1.17.0-blue.svg)](https://github.com/gabime/spdlog) [![CSV](https://img.shields.io/badge/rapidcsv-8.89-blue.svg)](https://github.com/d99kris/rapidcsv) [![JSON](https://img.shields.io/badge/simdjson-4.2.1-blue.svg)](https://github.com/simdjson/simdjson) +[![SQLite](https://img.shields.io/badge/SQLiteCpp-3.3.3-blue.svg)](https://github.com/SRombauts/SQLiteCpp) [![codecov](https://codecov.io/gh/physycom/DynamicalSystemFramework/graph/badge.svg?token=JV53J6IUJ3)](https://codecov.io/gh/physycom/DynamicalSystemFramework) The aim of this project is to rework the original [Traffic Flow Dynamics Model](https://github.com/Grufoony/TrafficFlowDynamicsModel). @@ -35,7 +36,7 @@ print(dsf.__version__) ## Installation (from source) ### Requirements -The project requires `C++20` or greater, `cmake`, `tbb` `simdjson`, `spdlog` and `rapidcsv`. +The project requires `C++20` or greater, `cmake`, `tbb` `simdjson`, `spdlog`, `rapidcsv` and `SQLiteCpp`. To install requirements on Ubuntu: ```shell sudo apt install cmake libtbb-dev diff --git a/examples/slow_charge_rb.cpp b/examples/slow_charge_rb.cpp index 15a39434..a3ebe053 100644 --- a/examples/slow_charge_rb.cpp +++ b/examples/slow_charge_rb.cpp @@ -27,8 +27,6 @@ std::atomic bExitFlag{false}; // uncomment these lines to print densities, flows and speeds #define PRINT_DENSITIES -// #define PRINT_FLOWS -// #define PRINT_SPEEDS using RoadNetwork = dsf::mobility::RoadNetwork; using Dynamics = dsf::mobility::FirstOrderDynamics; @@ -68,14 +66,13 @@ int main(int argc, char** argv) { std::to_string(SEED))}; // output folder constexpr auto MAX_TIME{static_cast(5e5)}; // maximum time of simulation - // Clear output folder or create it if it doesn't exist + // Create output folder if it doesn't exist (preserve existing database) if (!fs::exists(BASE_OUT_FOLDER)) { fs::create_directory(BASE_OUT_FOLDER); } - if (fs::exists(OUT_FOLDER)) { - fs::remove_all(OUT_FOLDER); + if (!fs::exists(OUT_FOLDER)) { + fs::create_directory(OUT_FOLDER); } - fs::create_directory(OUT_FOLDER); // Starting std::cout << "Using dsf version: " << dsf::version() << '\n'; RoadNetwork graph{}; @@ -119,24 +116,18 @@ int main(int argc, char** argv) { // dynamics.setForcePriorities(true); dynamics.setSpeedFluctuationSTD(0.1); + // Connect database for saving data + dynamics.connectDataBase(OUT_FOLDER + "simulation_data.db"); + + // Configure data saving: interval=10, saveAverageStats=true, saveStreetData=true +#ifdef PRINT_DENSITIES + dynamics.saveData(300, true, true, false); +#else + dynamics.saveData(300, true, false, false); +#endif + std::cout << "Done." << std::endl; std::cout << "Running simulation...\n"; -#ifdef PRINT_FLOWS - std::ofstream streetFlow(OUT_FOLDER + "flows.csv"); - streetFlow << "time"; - for (const auto& [id, street] : dynamics.graph().edges()) { - streetFlow << ';' << id; - } - streetFlow << '\n'; -#endif -#ifdef PRINT_SPEEDS - std::ofstream streetSpeed(OUT_FOLDER + "speeds.csv"); - streetSpeed << "time;"; - for (const auto& [id, street] : dynamics.graph().edges()) { - streetSpeed << ';' << id; - } - streetSpeed << '\n'; -#endif int deltaAgents{std::numeric_limits::max()}; int previousAgents{0}; @@ -176,47 +167,12 @@ int main(int argc, char** argv) { } if (dynamics.time_step() % 300 == 0) { - dynamics.saveCoilCounts(std::format("{}coil_counts.csv", OUT_FOLDER)); + // Data is now saved automatically by saveData() configuration printLoadingBar(dynamics.time_step(), MAX_TIME); - dynamics.saveMacroscopicObservables(std::format("{}data.csv", OUT_FOLDER)); - } - if (dynamics.time_step() % 10 == 0) { -#ifdef PRINT_DENSITIES - dynamics.saveStreetDensities(OUT_FOLDER + "densities.csv", true); -#endif -#ifdef PRINT_FLOWS - streetFlow << dynamics.time_step(); - for (const auto& [id, street] : dynamics.graph().edges()) { - const auto& meanSpeed = dynamics.streetMeanSpeed(id); - if (meanSpeed.has_value()) { - streetFlow << ';' << meanSpeed.value() * street->density(); - } else { - streetFlow << ';'; - } - } - streetFlow << std::endl; -#endif -#ifdef PRINT_SPEEDS - streetSpeed << dynamics.time_step(); - for (const auto& [id, street] : dynamics.graph().edges()) { - const auto& meanSpeed = dynamics.streetMeanSpeed(id); - if (meanSpeed.has_value()) { - streetSpeed << ';' << meanSpeed.value(); - } else { - streetSpeed << ';'; - } - } - streetSpeed << std::endl; -#endif } + // Street densities are now saved automatically by saveData() configuration ++progress; } -#ifdef PRINT_FLOWS - streetFlow.close(); -#endif -#ifdef PRINT_SPEEDS - streetSpeed.close(); -#endif // std::cout << std::endl; // std::map turnNames{ // {0, "left"}, {1, "straight"}, {2, "right"}, {3, "u-turn"}}; diff --git a/examples/slow_charge_tl.cpp b/examples/slow_charge_tl.cpp index e508eef8..4b4c51de 100644 --- a/examples/slow_charge_tl.cpp +++ b/examples/slow_charge_tl.cpp @@ -28,8 +28,6 @@ std::atomic bExitFlag{false}; // uncomment these lines to print densities, flows and speeds #define PRINT_DENSITIES -// #define PRINT_FLOWS -// #define PRINT_SPEEDS // #define PRINT_TP using RoadNetwork = dsf::mobility::RoadNetwork; @@ -74,14 +72,13 @@ int main(int argc, char** argv) { ERROR_PROBABILITY, std::to_string(SEED))}; // output folder constexpr auto MAX_TIME{static_cast(5e5)}; // maximum time of simulation - // Clear output folder or create it if it doesn't exist + // Create output folder if it doesn't exist (preserve existing database) if (!fs::exists(BASE_OUT_FOLDER)) { fs::create_directory(BASE_OUT_FOLDER); } - if (fs::exists(OUT_FOLDER)) { - fs::remove_all(OUT_FOLDER); + if (!fs::exists(OUT_FOLDER)) { + fs::create_directory(OUT_FOLDER); } - fs::create_directory(OUT_FOLDER); // Starting std::cout << "Using dsf version: " << dsf::version() << '\n'; RoadNetwork graph{}; @@ -175,26 +172,20 @@ int main(int argc, char** argv) { if (OPTIMIZE) dynamics.setDataUpdatePeriod(30); // Store data every 30 time steps + // Connect database for saving data + dynamics.connectDataBase(OUT_FOLDER + "simulation_data.db"); + + // Configure data saving: interval=10, saveAverageStats=true, saveStreetData=true +#ifdef PRINT_DENSITIES + dynamics.saveData(300, true, true, false); +#else + dynamics.saveData(300, true, false, false); +#endif + const auto TM = dynamics.turnMapping(); std::cout << "Done." << std::endl; std::cout << "Running simulation...\n"; -#ifdef PRINT_FLOWS - std::ofstream streetFlow(OUT_FOLDER + "flows.csv"); - streetFlow << "time"; - for (const auto& [id, street] : dynamics.graph().edges()) { - streetFlow << ';' << id; - } - streetFlow << '\n'; -#endif -#ifdef PRINT_SPEEDS - std::ofstream streetSpeed(OUT_FOLDER + "speeds.csv"); - streetSpeed << "time"; - for (const auto& [id, street] : dynamics.graph().edges()) { - streetSpeed << ';' << id; - } - streetSpeed << '\n'; -#endif #ifdef PRINT_TP std::ofstream outTP(OUT_FOLDER + "turn_probabilities.csv"); outTP << "time"; @@ -256,8 +247,7 @@ int main(int argc, char** argv) { if (dynamics.time_step() % 300 == 0) { // printLoadingBar(dynamics.time_step(), MAX_TIME); // deltaAgents = std::labs(dynamics.agents().size() - previousAgents); - dynamics.saveCoilCounts(std::format("{}coil_counts.csv", OUT_FOLDER)); - dynamics.saveMacroscopicObservables(std::format("{}data.csv", OUT_FOLDER)); + // Data is now saved automatically by saveData() configuration // deltas.push_back(deltaAgents); // previousAgents = dynamics.agents().size(); #ifdef PRINT_TP @@ -292,43 +282,9 @@ int main(int argc, char** argv) { outTP << std::endl; #endif } - if (dynamics.time_step() % 10 == 0) { -#ifdef PRINT_DENSITIES - dynamics.saveStreetDensities(OUT_FOLDER + "densities.csv", true); -#endif -#ifdef PRINT_FLOWS - streetFlow << ';' << dynamics.time_step(); - for (const auto& [id, street] : dynamics.graph().edges()) { - const auto& meanSpeed = dynamics.streetMeanSpeed(id); - if (meanSpeed.has_value()) { - streetFlow << ';' << meanSpeed.value() * street->density(); - } else { - streetFlow << ';'; - } - } - streetFlow << std::endl; -#endif -#ifdef PRINT_SPEEDS - streetSpeed << dynamics.time_step(); - for (const auto& [id, street] : dynamics.graph().edges()) { - const auto& meanSpeed = dynamics.streetMeanSpeed(id); - if (meanSpeed.has_value()) { - streetSpeed << ';' << meanSpeed.value(); - } else { - streetSpeed << ';'; - } - } - streetSpeed << std::endl; -#endif - } + // Street densities are now saved automatically by saveData() configuration ++progress; } -#ifdef PRINT_FLOWS - streetFlow.close(); -#endif -#ifdef PRINT_SPEEDS - streetSpeed.close(); -#endif // std::cout << std::endl; // std::map turnNames{ // {0, "left"}, {1, "straight"}, {2, "right"}, {3, "u-turn"}}; diff --git a/src/dsf/base/AdjacencyMatrix.cpp b/src/dsf/base/AdjacencyMatrix.cpp deleted file mode 100644 index 9766b09d..00000000 --- a/src/dsf/base/AdjacencyMatrix.cpp +++ /dev/null @@ -1,270 +0,0 @@ - -#include "AdjacencyMatrix.hpp" - -#include -#include -#include - -#include -#include - -namespace dsf { - - namespace test { - std::vector offsets(const AdjacencyMatrix& adj) { return adj.m_rowOffsets; } - - std::vector indices(const AdjacencyMatrix& adj) { return adj.m_columnIndices; } - } // namespace test - - /********************************************************************************* - * CONSTRUCTORS - **********************************************************************************/ - AdjacencyMatrix::AdjacencyMatrix() - : m_rowOffsets{std::vector(1, 0)}, - m_columnIndices{}, - m_colOffsets{std::vector(1, 0)}, - m_rowIndices{}, - m_n{0} {} - AdjacencyMatrix::AdjacencyMatrix(std::string const& fileName) { read(fileName); } - /********************************************************************************* - * OPERATORS - **********************************************************************************/ - bool AdjacencyMatrix::operator==(const AdjacencyMatrix& other) const { - return (m_rowOffsets == other.m_rowOffsets) && - (m_columnIndices == other.m_columnIndices) && (m_n == other.m_n); - } - bool AdjacencyMatrix::operator()(Id row, Id col) const { return contains(row, col); } - /********************************************************************************* - * METHODS - **********************************************************************************/ - - size_t AdjacencyMatrix::n() const { return m_n; } - size_t AdjacencyMatrix::size() const { - assert(m_columnIndices.size() == m_rowIndices.size()); - return m_columnIndices.size(); - } - bool AdjacencyMatrix::empty() const { - assert(m_columnIndices.size() == m_rowIndices.size()); - return m_columnIndices.empty(); - } - - void AdjacencyMatrix::insert(Id row, Id col) { - m_n = std::max(m_n, static_cast(row + 1)); - m_n = std::max(m_n, static_cast(col + 1)); - - // Ensure rowOffsets and colOffsets have at least m_n + 1 elements - while (m_rowOffsets.size() <= m_n) { - m_rowOffsets.push_back(m_rowOffsets.back()); - } - while (m_colOffsets.size() <= m_n) { - m_colOffsets.push_back(m_colOffsets.back()); - } - - assert(row + 1 < m_rowOffsets.size()); - assert(col + 1 < m_colOffsets.size()); - - // Increase row offsets for rows after the inserted row (CSR) - tbb::parallel_for_each( - m_rowOffsets.begin() + row + 1, m_rowOffsets.end(), [](Id& x) { x++; }); - - // Increase column offsets for columns after the inserted column (CSC) - tbb::parallel_for_each( - m_colOffsets.begin() + col + 1, m_colOffsets.end(), [](Id& x) { x++; }); - - // Insert column index at the correct position for CSR - auto csrOffset = m_rowOffsets[row + 1] - 1; - m_columnIndices.insert(m_columnIndices.begin() + csrOffset, col); - - // Insert row index at the correct position for CSC - auto cscOffset = m_colOffsets[col + 1] - 1; - m_rowIndices.insert(m_rowIndices.begin() + cscOffset, row); - } - - bool AdjacencyMatrix::contains(Id row, Id col) const { - if (row >= m_n or col >= m_n) { - throw std::out_of_range("Row or column index out of range."); - } - assert(row + 1 < m_rowOffsets.size()); - auto itFirst = m_columnIndices.begin() + m_rowOffsets[row]; - auto itLast = m_columnIndices.begin() + m_rowOffsets[row + 1]; - return std::find(itFirst, itLast, col) != itLast; - } - - std::vector AdjacencyMatrix::getRow(Id row) const { - if (row + 1 >= m_rowOffsets.size()) { - throw std::out_of_range( - std::format("Row index {} out of range [0, {}[.", row, m_n - 1)); - } - const auto lowerOffset = m_rowOffsets[row]; - const auto upperOffset = m_rowOffsets[row + 1]; - std::vector rowVector(upperOffset - lowerOffset); - - std::copy(m_columnIndices.begin() + m_rowOffsets[row], - m_columnIndices.begin() + m_rowOffsets[row + 1], - rowVector.begin()); - return rowVector; - } - std::vector AdjacencyMatrix::getCol(Id col) const { - assert(col + 1 < m_colOffsets.size()); - const auto lowerOffset = m_colOffsets[col]; - const auto upperOffset = m_colOffsets[col + 1]; - std::vector colVector(upperOffset - lowerOffset); - - std::copy(m_rowIndices.begin() + lowerOffset, - m_rowIndices.begin() + upperOffset, - colVector.begin()); - return colVector; - } - - std::vector> AdjacencyMatrix::elements() const { - std::vector> elements; - for (auto row = 0u; row < m_n; ++row) { - assert(row + 1 < m_rowOffsets.size()); - const auto lowerOffset = m_rowOffsets[row]; - const auto upperOffset = m_rowOffsets[row + 1]; - for (auto i = lowerOffset; i < upperOffset; ++i) { - elements.emplace_back(row, m_columnIndices[i]); - } - } - return elements; - } - - void AdjacencyMatrix::clear() { - m_rowOffsets = std::vector(1, 0); - m_colOffsets = std::vector(1, 0); - m_columnIndices.clear(); - m_rowIndices.clear(); - m_n = 0; - } - void AdjacencyMatrix::clearRow(Id row) { - // CSR: Clear row in column indices - assert(row + 1 < m_rowOffsets.size()); - const auto lowerOffset = m_rowOffsets[row]; - const auto upperOffset = m_rowOffsets[row + 1]; - m_columnIndices.erase(m_columnIndices.begin() + lowerOffset, - m_columnIndices.begin() + upperOffset); - std::transform( - DSF_EXECUTION m_rowOffsets.begin() + row + 1, - m_rowOffsets.end(), - m_rowOffsets.begin() + row + 1, - [upperOffset, lowerOffset](auto& x) { return x - (upperOffset - lowerOffset); }); - - // CSC: Clear the corresponding rows from column offsets - for (auto col = 0u; col < m_n; ++col) { - assert(col + 1 < m_colOffsets.size()); - const auto colLowerOffset = m_colOffsets[col]; - const auto colUpperOffset = m_colOffsets[col + 1]; - auto it = std::find(m_rowIndices.begin() + colLowerOffset, - m_rowIndices.begin() + colUpperOffset, - row); - if (it != m_rowIndices.begin() + colUpperOffset) { - // Remove row from rowIndices and update the rowOffsets - m_rowIndices.erase(it); - // Decrement the offsets for rows after the current row - std::transform(DSF_EXECUTION m_colOffsets.begin() + col + 1, - m_colOffsets.end(), - m_colOffsets.begin() + col + 1, - [](auto& x) { return x - 1; }); - } - } - } - - void AdjacencyMatrix::clearCol(Id col) { - // CSR: Clear column in row indices - for (auto row = 0u; row < m_n; ++row) { - assert(row + 1 < m_rowOffsets.size()); - const auto lowerOffset = m_rowOffsets[row]; - const auto upperOffset = m_rowOffsets[row + 1]; - auto it = std::find(m_columnIndices.begin() + lowerOffset, - m_columnIndices.begin() + upperOffset, - col); - if (it != m_columnIndices.begin() + upperOffset) { - m_columnIndices.erase(it); - // Decrement the offsets for rows after the current row - std::transform(DSF_EXECUTION m_rowOffsets.begin() + row + 1, - m_rowOffsets.end(), - m_rowOffsets.begin() + row + 1, - [](auto& x) { return x - 1; }); - } - } - - // CSC: Clear the column from row indices and update column offsets - assert(col + 1 < m_colOffsets.size()); - const auto lowerOffset = m_colOffsets[col]; - const auto upperOffset = m_colOffsets[col + 1]; - m_rowIndices.erase(m_rowIndices.begin() + lowerOffset, - m_rowIndices.begin() + upperOffset); - - // Adjust column offsets accordingly - std::transform( - DSF_EXECUTION m_colOffsets.begin() + col + 1, - m_colOffsets.end(), - m_colOffsets.begin() + col + 1, - [upperOffset, lowerOffset](auto& x) { return x - (upperOffset - lowerOffset); }); - } - - std::vector AdjacencyMatrix::getOutDegreeVector() const { - auto degVector = std::vector(m_n); - std::adjacent_difference( - m_rowOffsets.begin() + 1, m_rowOffsets.end(), degVector.begin()); - return degVector; - } - std::vector AdjacencyMatrix::getInDegreeVector() const { - auto degVector = std::vector(m_n); - std::adjacent_difference( - m_colOffsets.begin() + 1, m_colOffsets.end(), degVector.begin()); - return degVector; - } - - void AdjacencyMatrix::read(std::string const& fileName) { - std::ifstream inStream(fileName, std::ios::binary); - if (!inStream.is_open()) { - throw std::runtime_error("Error opening file \"" + fileName + "\" for reading."); - } - inStream.read(reinterpret_cast(&m_n), sizeof(size_t)); - m_rowOffsets.resize(m_n + 1); - inStream.read(reinterpret_cast(m_rowOffsets.data()), - m_rowOffsets.size() * sizeof(Id)); - m_columnIndices.resize(m_rowOffsets.back()); - inStream.read(reinterpret_cast(m_columnIndices.data()), - m_columnIndices.size() * sizeof(Id)); - inStream.close(); - // Initialize CSC format variables - m_colOffsets.resize(m_n + 1, 0); - m_rowIndices.resize(m_columnIndices.size()); - - // Compute CSC from CSR - std::vector colSizes(m_n, 0); - - // Count occurrences of each column index - for (const auto& col : m_columnIndices) { - colSizes[col]++; - } - - // Compute column offsets using an inclusive scan - std::inclusive_scan(colSizes.begin(), colSizes.end(), m_colOffsets.begin() + 1); - - // Fill CSC row indices - std::vector currentOffset = m_colOffsets; - for (Id row = 0; row < m_n; ++row) { - for (Id i = m_rowOffsets[row]; i < m_rowOffsets[row + 1]; ++i) { - Id col = m_columnIndices[i]; - m_rowIndices[currentOffset[col]++] = row; - } - } - } - - void AdjacencyMatrix::save(std::string const& fileName) const { - std::ofstream outStream(fileName, std::ios::binary); - if (!outStream.is_open()) { - throw std::runtime_error("Error opening file \"" + fileName + "\" for writing."); - } - outStream.write(reinterpret_cast(&m_n), sizeof(size_t)); - outStream.write(reinterpret_cast(m_rowOffsets.data()), - m_rowOffsets.size() * sizeof(Id)); - outStream.write(reinterpret_cast(m_columnIndices.data()), - m_columnIndices.size() * sizeof(Id)); - outStream.close(); - } - -} // namespace dsf diff --git a/src/dsf/base/AdjacencyMatrix.hpp b/src/dsf/base/AdjacencyMatrix.hpp deleted file mode 100644 index d5e24d62..00000000 --- a/src/dsf/base/AdjacencyMatrix.hpp +++ /dev/null @@ -1,117 +0,0 @@ -/// @file /src/dsf/headers/AdjacencyMatrix.hpp -/// @brief Defines the AdjacencyMatrix class. - -#pragma once - -#include -#include -#include -#include -#include - -#include "../utility/Typedef.hpp" - -namespace dsf { - - class AdjacencyMatrix; - - namespace test { - std::vector offsets(const AdjacencyMatrix& adj); - std::vector indices(const AdjacencyMatrix& adj); - } // namespace test - - /// @brief The AdjacencyMatrix class represents the adjacency matrix of the network. - /// @details The AdjacencyMatrix class represents the adjacency matrix of the network. - /// It is defined as \f$A = (a_{ij})\f$, where \f$a_{ij} \in \{0, 1\}\f$. - /// Moreover, \f$a_{ij} = 1\f$ if there is an edge from node \f$i\f$ to node \f$j\f$ and \f$a_{ij} = 0\f$ otherwise. - /// It is used to store the adjacency matrix of the network and to perform operations on it. - /// The adjacency matrix is stored in both CSR and CSC formats, to optimize access to rows and columns. - /// Thus, this matrix has very fast access, using double the memory of a standard CSR/CSC one. - class AdjacencyMatrix { - private: - // CSR format - std::vector m_rowOffsets; - std::vector m_columnIndices; - // CSC format - std::vector m_colOffsets; - std::vector m_rowIndices; - // Size of the matrix - size_t m_n; - - friend std::vector test::offsets(const AdjacencyMatrix& adj); - friend std::vector test::indices(const AdjacencyMatrix& adj); - - public: - /// @brief Construct a new AdjacencyMatrix object - AdjacencyMatrix(); - /// @brief Construct a new AdjacencyMatrix object using the @ref read method - /// @param fileName The name of the file containing the adjacency matrix - AdjacencyMatrix(std::string const& fileName); - - bool operator==(const AdjacencyMatrix& other) const; - /// @brief Get the link at the specified row and column - /// @param row The row index of the element - /// @param col The column index of the element - /// @return True if the link exists, false otherwise - /// @details This function actually returns element \f$a_{ij}\f$ of the adjacency matrix. - /// Where \f$i\f$ is the row index and \f$j\f$ is the column index. - bool operator()(Id row, Id col) const; - - /// @brief Get the number of links in the adjacency matrix - /// @return The number of links in the adjacency matrix - size_t size() const; - /// @brief Check if the adjacency matrix is empty - /// @return True if the adjacency matrix is empty, false otherwise - bool empty() const; - /// @brief Get the number of nodes in the adjacency matrix - /// @return The number of nodes in the adjacency matrix - size_t n() const; - /// @brief Inserts the link row -> col in the adjacency matrix - /// @param row The row index of the element - /// @param col The column index of the element - /// @details This function inserts the link \f$(row, col)\f$ in the adjacency matrix. - /// Where \f$row\f$ is the row index and \f$col\f$ is the column index. - void insert(Id row, Id col); - /// @brief Check if the link row -> col exists in the adjacency matrix - /// @param row The row index of the element - /// @param col The column index of the element - /// @return True if the link exists, false otherwise - /// @details This function actually returns element \f$a_{ij}\f$ of the adjacency matrix. - /// Where \f$i\f$ is the row index and \f$j\f$ is the column index. - bool contains(Id row, Id col) const; - /// @brief Get the row at the specified index - /// @param row The row index - /// @return The row at the specified index - std::vector getRow(Id row) const; - /// @brief Get the column at the specified index - /// @param col The column index - /// @return The column at the specified index - std::vector getCol(Id col) const; - /// @brief Get a vector containing all the links in the adjacency matrix as pairs of nodes - /// @return A vector containing all the links in the adjacency matrix as pairs of nodes - std::vector> elements() const; - - /// @brief Clear the adjacency matrix - void clear(); - /// @brief Clear the row at the specified index - /// @details The dimension of the matrix does not change. - void clearRow(Id row); - /// @brief Clear the column at the specified index - /// @details The dimension of the matrix does not change. - void clearCol(Id col); - - /// @brief Get the input degree vector of the adjacency matrix - /// @return The input degree vector of the adjacency matrix - std::vector getInDegreeVector() const; - /// @brief Get the output degree vector of the adjacency matrix - /// @return The output degree vector of the adjacency matrix - std::vector getOutDegreeVector() const; - - /// @brief Read the adjacency matrix from a binary file - /// @param fileName The name of the file containing the adjacency matrix - void read(std::string const& fileName); - /// @brief Write the adjacency matrix to a binary file - /// @param fileName The name of the file where the adjacency matrix will be written - void save(std::string const& fileName) const; - }; -} // namespace dsf diff --git a/src/dsf/base/Dynamics.hpp b/src/dsf/base/Dynamics.hpp index b2e98f5a..b474fa3d 100644 --- a/src/dsf/base/Dynamics.hpp +++ b/src/dsf/base/Dynamics.hpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,7 @@ #endif #include +#include namespace dsf { /// @brief The Dynamics class represents the dynamics of the network. @@ -43,9 +45,11 @@ namespace dsf { class Dynamics { private: network_t m_graph; + Id m_id; std::string m_name = "unnamed simulation"; std::time_t m_timeInit = 0; std::time_t m_timeStep = 0; + std::unique_ptr m_database; protected: tbb::task_arena m_taskArena; @@ -90,12 +94,26 @@ namespace dsf { /// @param timeEpoch The initial time as epoch time inline void setInitTime(std::time_t timeEpoch) { m_timeInit = timeEpoch; }; + inline void connectDataBase(std::string const& dbPath) { + m_database = std::make_unique( + dbPath, SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE); + } + /// @brief Get the graph /// @return const network_t&, The graph - inline const auto& graph() const { return m_graph; }; + inline auto const& graph() const { return m_graph; }; + /// @brief Get the id of the simulation + /// @return const Id&, The id of the simulation + inline auto const& id() const { return m_id; }; /// @brief Get the name of the simulation /// @return const std::string&, The name of the simulation - inline const auto& name() const { return m_name; }; + inline auto const& name() const { return m_name; }; + /// @brief Get the database connection (const version) + /// @return const std::unique_ptr&, The database connection + inline auto const& database() const { return m_database; } + /// @brief Get the database connection (mutable version for writing) + /// @return std::unique_ptr&, The database connection + inline auto& database() { return m_database; } /// @brief Get the current simulation time as epoch time /// @return std::time_t, The current simulation time as epoch time inline auto time() const { return m_timeInit + m_timeStep; } @@ -126,5 +144,19 @@ namespace dsf { m_generator.seed(*seed); } m_taskArena.initialize(); + // Take the current time and set id as YYYYMMDDHHMMSS + auto const now = std::chrono::system_clock::now(); +#ifdef __APPLE__ + std::time_t const t = std::chrono::system_clock::to_time_t(now); + std::ostringstream oss; + oss << std::put_time(std::localtime(&t), "%Y%m%d%H%M%S"); + m_id = std::stoull(oss.str()); +#else + m_id = std::stoull(std::format( + "{:%Y%m%d%H%M%S}", + std::chrono::floor( + std::chrono::current_zone()->to_local(std::chrono::system_clock::from_time_t( + std::chrono::system_clock::to_time_t(now)))))); +#endif } }; // namespace dsf \ No newline at end of file diff --git a/src/dsf/base/Network.hpp b/src/dsf/base/Network.hpp index 4005bc0d..c163d0df 100644 --- a/src/dsf/base/Network.hpp +++ b/src/dsf/base/Network.hpp @@ -3,7 +3,6 @@ #include #include -#include "AdjacencyMatrix.hpp" #include "Edge.hpp" #include "Node.hpp" @@ -22,10 +21,6 @@ namespace dsf { /// @brief Construct a new empty Network object Network() = default; - /// @brief Construct a new Network object - /// @param adj The adjacency matrix representing the network - explicit Network(AdjacencyMatrix const& adj); - /// @brief Get the nodes as an unordered map /// @return std::unordered_map> The nodes std::unordered_map> const& nodes() const; @@ -105,17 +100,6 @@ namespace dsf { return m_cantorHash(idPair.first, idPair.second); } - template - requires(std::is_base_of_v && std::is_base_of_v) - Network::Network(AdjacencyMatrix const& adj) { - auto const& values{adj.elements()}; - // Add as many nodes as adj.n() - addNDefaultNodes(adj.n()); - std::for_each(values.cbegin(), values.cend(), [&](auto const& pair) { - addEdge(m_cantorHash(pair), std::make_pair(pair.first, pair.second)); - }); - } - template requires(std::is_base_of_v && std::is_base_of_v) std::unordered_map> const& Network::nodes() diff --git a/src/dsf/bindings.cpp b/src/dsf/bindings.cpp index 4a5778e9..06fcff7f 100644 --- a/src/dsf/bindings.cpp +++ b/src/dsf/bindings.cpp @@ -78,80 +78,10 @@ PYBIND11_MODULE(dsf_cpp, m) { &dsf::Measurement::std, dsf::g_docstrings.at("dsf::Measurement::std").c_str()); - // Bind AdjacencyMatrix to main module (general graph structure) - pybind11::class_(m, "AdjacencyMatrix") - .def(pybind11::init<>(), - dsf::g_docstrings.at("dsf::AdjacencyMatrix::AdjacencyMatrix").c_str()) - .def(pybind11::init(), - pybind11::arg("fileName"), - dsf::g_docstrings.at("dsf::AdjacencyMatrix::AdjacencyMatrix") - .c_str()) // Added constructor - .def("n", - &dsf::AdjacencyMatrix::n, - dsf::g_docstrings.at("dsf::AdjacencyMatrix::n").c_str()) - .def("size", - &dsf::AdjacencyMatrix::size, - dsf::g_docstrings.at("dsf::AdjacencyMatrix::size").c_str()) - .def("empty", - &dsf::AdjacencyMatrix::empty, - dsf::g_docstrings.at("dsf::AdjacencyMatrix::empty").c_str()) // Added empty - .def("getRow", - &dsf::AdjacencyMatrix::getRow, - dsf::g_docstrings.at("dsf::AdjacencyMatrix::getRow").c_str()) - .def("getCol", - &dsf::AdjacencyMatrix::getCol, - dsf::g_docstrings.at("dsf::AdjacencyMatrix::getCol").c_str()) // Added getCol - .def( - "__call__", - [](const dsf::AdjacencyMatrix& self, dsf::Id i, dsf::Id j) { - return self(i, j); - }, - dsf::g_docstrings.at("dsf::AdjacencyMatrix::operator()").c_str()) - .def("insert", - &dsf::AdjacencyMatrix::insert, - dsf::g_docstrings.at("dsf::AdjacencyMatrix::insert").c_str()) // Added insert - .def("contains", - &dsf::AdjacencyMatrix::contains, - dsf::g_docstrings.at("dsf::AdjacencyMatrix::contains") - .c_str()) // Added contains - .def("elements", - &dsf::AdjacencyMatrix::elements, - dsf::g_docstrings.at("dsf::AdjacencyMatrix::elements") - .c_str()) // Added elements - .def("clear", - &dsf::AdjacencyMatrix::clear, - dsf::g_docstrings.at("dsf::AdjacencyMatrix::clear").c_str()) - .def("clearRow", - &dsf::AdjacencyMatrix::clearRow, - dsf::g_docstrings.at("dsf::AdjacencyMatrix::clearRow") - .c_str()) // Added clearRow - .def("clearCol", - &dsf::AdjacencyMatrix::clearCol, - dsf::g_docstrings.at("dsf::AdjacencyMatrix::clearCol") - .c_str()) // Added clearCol - .def("getInDegreeVector", - &dsf::AdjacencyMatrix::getInDegreeVector, - dsf::g_docstrings.at("dsf::AdjacencyMatrix::getInDegreeVector") - .c_str()) // Added getInDegreeVector - .def("getOutDegreeVector", - &dsf::AdjacencyMatrix::getOutDegreeVector, - dsf::g_docstrings.at("dsf::AdjacencyMatrix::getOutDegreeVector") - .c_str()) // Added getOutDegreeVector - .def("read", - &dsf::AdjacencyMatrix::read, - pybind11::arg("fileName"), - dsf::g_docstrings.at("dsf::AdjacencyMatrix::read").c_str()) // Added read - .def("save", - &dsf::AdjacencyMatrix::save, - pybind11::arg("fileName"), - dsf::g_docstrings.at("dsf::AdjacencyMatrix::save").c_str()); // Added save - // Bind mobility-related classes to mobility submodule pybind11::class_(mobility, "RoadNetwork") .def(pybind11::init<>(), dsf::g_docstrings.at("dsf::mobility::RoadNetwork::RoadNetwork").c_str()) - .def(pybind11::init(), - dsf::g_docstrings.at("dsf::mobility::RoadNetwork::RoadNetwork").c_str()) .def("nNodes", &dsf::mobility::RoadNetwork::nNodes, dsf::g_docstrings.at("dsf::Network::nNodes").c_str()) @@ -449,6 +379,10 @@ PYBIND11_MODULE(dsf_cpp, m) { }, pybind11::arg("datetime"), dsf::g_docstrings.at("dsf::Dynamics::setInitTime").c_str()) + .def("connectDataBase", + &dsf::mobility::FirstOrderDynamics::connectDataBase, + pybind11::arg("dbPath"), + dsf::g_docstrings.at("dsf::Dynamics::connectDataBase").c_str()) .def( "setForcePriorities", &dsf::mobility::FirstOrderDynamics::setForcePriorities, @@ -683,36 +617,20 @@ PYBIND11_MODULE(dsf_cpp, m) { }, pybind11::arg("reset") = true, dsf::g_docstrings.at("dsf::mobility::RoadDynamics::destinationCounts").c_str()) - .def( - "saveStreetDensities", - &dsf::mobility::FirstOrderDynamics::saveStreetDensities, - pybind11::arg("filename"), - pybind11::arg("separator") = ';', - pybind11::arg("normalized") = true, - dsf::g_docstrings.at("dsf::mobility::RoadDynamics::saveStreetDensities").c_str()) - .def("saveStreetSpeeds", - &dsf::mobility::FirstOrderDynamics::saveStreetSpeeds, - pybind11::arg("filename"), - pybind11::arg("separator") = ';', - pybind11::arg("normalized") = false, - dsf::g_docstrings.at("dsf::mobility::RoadDynamics::saveStreetSpeeds").c_str()) - .def("saveCoilCounts", - &dsf::mobility::FirstOrderDynamics::saveCoilCounts, - pybind11::arg("filename"), - pybind11::arg("reset") = false, - pybind11::arg("separator") = ';', - dsf::g_docstrings.at("dsf::mobility::RoadDynamics::saveCoilCounts").c_str()) - .def("saveTravelData", - &dsf::mobility::FirstOrderDynamics::saveTravelData, - pybind11::arg("filename"), - pybind11::arg("reset") = false, - dsf::g_docstrings.at("dsf::mobility::RoadDynamics::saveTravelData").c_str()) - .def("saveMacroscopicObservables", - &dsf::mobility::FirstOrderDynamics::saveMacroscopicObservables, - pybind11::arg("filename"), - pybind11::arg("separator") = ';', - dsf::g_docstrings.at("dsf::mobility::RoadDynamics::saveMacroscopicObservables") - .c_str()) + .def("saveData", + &dsf::mobility::FirstOrderDynamics::saveData, + pybind11::arg("saving_interval"), + pybind11::arg("save_average_stats") = false, + pybind11::arg("save_street_data") = false, + pybind11::arg("save_travel_data") = false, + "Configure data saving during simulation.\n\n" + "Args:\n" + " saving_interval: Interval in time steps between data saves\n" + " save_average_stats: Whether to save average statistics (speed, density, " + "flow)\n" + " save_street_data: Whether to save per-street data (density, speed, coil " + "counts)\n" + " save_travel_data: Whether to save travel data (distance, travel time)") .def( "summary", [](dsf::mobility::FirstOrderDynamics& self) { diff --git a/src/dsf/dsf.hpp b/src/dsf/dsf.hpp index 245e31a6..bf18d640 100644 --- a/src/dsf/dsf.hpp +++ b/src/dsf/dsf.hpp @@ -7,9 +7,9 @@ #include #include -static constexpr uint8_t DSF_VERSION_MAJOR = 4; -static constexpr uint8_t DSF_VERSION_MINOR = 7; -static constexpr uint8_t DSF_VERSION_PATCH = 9; +static constexpr uint8_t DSF_VERSION_MAJOR = 5; +static constexpr uint8_t DSF_VERSION_MINOR = 0; +static constexpr uint8_t DSF_VERSION_PATCH = 0; static auto const DSF_VERSION = std::format("{}.{}.{}", DSF_VERSION_MAJOR, DSF_VERSION_MINOR, DSF_VERSION_PATCH); @@ -32,7 +32,6 @@ namespace dsf { }; } // namespace dsf -#include "base/AdjacencyMatrix.hpp" #include "base/Edge.hpp" #include "base/SparseMatrix.hpp" #include "mobility/Agent.hpp" diff --git a/src/dsf/mdt/PointsCluster.hpp b/src/dsf/mdt/PointsCluster.hpp index 006aa4b2..6899f5d0 100644 --- a/src/dsf/mdt/PointsCluster.hpp +++ b/src/dsf/mdt/PointsCluster.hpp @@ -28,6 +28,10 @@ namespace dsf::mdt { /// @brief Copy constructor for PointsCluster. /// @param other The PointsCluster to copy from. PointsCluster(PointsCluster const& other) = default; + /// @brief Copy assignment operator for PointsCluster. + /// @param other The PointsCluster to copy from. + /// @return Reference to this PointsCluster. + PointsCluster& operator=(PointsCluster const& other) = default; /// @brief Add an activity point to the cluster. /// @param activityPoint The activity point to add. void addActivityPoint(ActivityPoint const& activityPoint) noexcept; diff --git a/src/dsf/mdt/TrajectoryCollection.cpp b/src/dsf/mdt/TrajectoryCollection.cpp index 910187ab..14a4b8cc 100644 --- a/src/dsf/mdt/TrajectoryCollection.cpp +++ b/src/dsf/mdt/TrajectoryCollection.cpp @@ -131,8 +131,7 @@ namespace dsf::mdt { &check_min_duration, min_points_per_trajectory, cluster_radius_km, - max_speed_kph, - min_duration_min](auto& pair) { + max_speed_kph](auto& pair) { auto const& uid = pair.first; auto& trajectory = pair.second diff --git a/src/dsf/mobility/FirstOrderDynamics.cpp b/src/dsf/mobility/FirstOrderDynamics.cpp index 42021128..7e6b5a7d 100644 --- a/src/dsf/mobility/FirstOrderDynamics.cpp +++ b/src/dsf/mobility/FirstOrderDynamics.cpp @@ -9,6 +9,85 @@ namespace dsf::mobility { return pStreet->length() / (pStreet->maxSpeed() * m_speedFactor(pStreet->density(true))); } + void FirstOrderDynamics::m_dumpSimInfo() const { + // Dump simulation info (parameters) to the database, if connected + if (!this->database()) { + return; + } + // Create simulations table if it doesn't exist + SQLite::Statement createTableStmt(*this->database(), + "CREATE TABLE IF NOT EXISTS simulations (" + "id INTEGER PRIMARY KEY, " + "name TEXT, " + "alpha REAL, " + "speed_fluctuation_std REAL, " + "weight_function TEXT, " + "weight_threshold REAL NOT NULL, " + "error_probability REAL, " + "passage_probability REAL, " + "mean_travel_distance_m REAL, " + "mean_travel_time_s REAL, " + "stagnant_tolerance_factor REAL, " + "force_priorities BOOLEAN, " + "save_avg_stats BOOLEAN, " + "save_road_data BOOLEAN, " + "save_travel_data BOOLEAN)"); + createTableStmt.exec(); + // Insert simulation parameters into the simulations table + SQLite::Statement insertSimStmt( + *this->database(), + "INSERT INTO simulations (id, name, alpha, speed_fluctuation_std, " + "weight_function, weight_threshold, error_probability, passage_probability, " + "mean_travel_distance_m, mean_travel_time_s, stagnant_tolerance_factor, " + "force_priorities, save_avg_stats, save_road_data, save_travel_data) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + insertSimStmt.bind(1, static_cast(this->id())); + insertSimStmt.bind(2, this->name()); + insertSimStmt.bind(3, m_alpha); + insertSimStmt.bind(4, m_speedFluctuationSTD); + switch (this->m_pathWeight) { + case PathWeight::LENGTH: + insertSimStmt.bind(5, "LENGTH"); + break; + case PathWeight::TRAVELTIME: + insertSimStmt.bind(5, "TRAVELTIME"); + break; + case PathWeight::WEIGHT: + insertSimStmt.bind(5, "WEIGHT"); + break; + } + insertSimStmt.bind(6, this->m_weightTreshold); + if (this->m_errorProbability.has_value()) { + insertSimStmt.bind(7, *this->m_errorProbability); + } else { + insertSimStmt.bind(7); + } + if (this->m_passageProbability.has_value()) { + insertSimStmt.bind(8, *this->m_passageProbability); + } else { + insertSimStmt.bind(8); + } + if (this->m_meanTravelDistance.has_value()) { + insertSimStmt.bind(9, *this->m_meanTravelDistance); + } else { + insertSimStmt.bind(9); + } + if (this->m_meanTravelTime.has_value()) { + insertSimStmt.bind(10, static_cast(*this->m_meanTravelTime)); + } else { + insertSimStmt.bind(10); + } + if (this->m_timeToleranceFactor.has_value()) { + insertSimStmt.bind(11, *this->m_timeToleranceFactor); + } else { + insertSimStmt.bind(11); + } + insertSimStmt.bind(12, this->m_forcePriorities); + insertSimStmt.bind(13, this->m_bSaveAverageStats); + insertSimStmt.bind(14, this->m_bSaveStreetData); + insertSimStmt.bind(15, this->m_bSaveTravelData); + insertSimStmt.exec(); + } FirstOrderDynamics::FirstOrderDynamics(RoadNetwork& graph, bool useCache, std::optional seed, @@ -56,59 +135,4 @@ namespace dsf::mobility { } m_speedFluctuationSTD = speedFluctuationSTD; } - - double FirstOrderDynamics::streetMeanSpeed(Id streetId) const { - const auto& street{this->graph().edge(streetId)}; - if (street->nAgents() == 0) { - return street->maxSpeed(); - } - double meanSpeed{0.}; - Size n{0}; - if (street->nExitingAgents() == 0) { - n = static_cast(street->movingAgents().size()); - double alpha{m_alpha / street->capacity()}; - meanSpeed = street->maxSpeed() * n * (1. - 0.5 * alpha * (n - 1.)); - } else { - for (auto const& pAgent : street->movingAgents()) { - meanSpeed += pAgent->speed(); - ++n; - } - for (auto const& queue : street->exitQueues()) { - for (auto const& pAgent : queue) { - meanSpeed += pAgent->speed(); - ++n; - } - } - } - return meanSpeed / n; - } - - Measurement FirstOrderDynamics::streetMeanSpeed() const { - if (this->agents().empty()) { - return Measurement(0., 0.); - } - std::vector speeds; - speeds.reserve(this->graph().edges().size()); - for (const auto& [streetId, pStreet] : this->graph().edges()) { - speeds.push_back(this->streetMeanSpeed(streetId)); - } - return Measurement(speeds); - } - Measurement FirstOrderDynamics::streetMeanSpeed(double threshold, - bool above) const { - std::vector speeds; - speeds.reserve(this->graph().edges().size()); - for (const auto& [streetId, pStreet] : this->graph().edges()) { - if (above) { - if (pStreet->density(true) > threshold) { - speeds.push_back(this->streetMeanSpeed(streetId)); - } - } else { - if (pStreet->density(true) < threshold) { - speeds.push_back(this->streetMeanSpeed(streetId)); - } - } - } - return Measurement(speeds); - } } // namespace dsf::mobility \ No newline at end of file diff --git a/src/dsf/mobility/FirstOrderDynamics.hpp b/src/dsf/mobility/FirstOrderDynamics.hpp index 629d63cb..c24c10bc 100644 --- a/src/dsf/mobility/FirstOrderDynamics.hpp +++ b/src/dsf/mobility/FirstOrderDynamics.hpp @@ -11,6 +11,8 @@ namespace dsf::mobility { double m_streetEstimatedTravelTime(std::unique_ptr const& pStreet) const final; + void m_dumpSimInfo() const final; + public: /// @brief Construct a new First Order Dynamics object /// @param graph The graph representing the network @@ -33,20 +35,5 @@ namespace dsf::mobility { /// @param speedFluctuationSTD The standard deviation of the speed fluctuation /// @throw std::invalid_argument, If the standard deviation is negative void setSpeedFluctuationSTD(double speedFluctuationSTD); - /// @brief Get the mean speed of a street in \f$m/s\f$ - /// @return double The mean speed of the street or street->maxSpeed() if the street is empty - /// @details The mean speed of a street is given by the formula: - /// \f$ v_{\text{mean}} = v_{\text{max}} \left(1 - \frac{\alpha}{2} \left( n - 1\right) \right) \f$ - /// where \f$ v_{\text{max}} \f$ is the maximum speed of the street, \f$ \alpha \f$ is the minimum speed rateo divided by the capacity - /// and \f$ n \f$ is the number of agents in the street - double streetMeanSpeed(Id streetId) const override; - /// @brief Get the mean speed of the streets in \f$m/s\f$ - /// @return Measurement The mean speed of the agents and the standard deviation - Measurement streetMeanSpeed() const override; - /// @brief Get the mean speed of the streets with density above or below a threshold in \f$m/s\f$ - /// @param threshold The density threshold to consider - /// @param above If true, the function returns the mean speed of the streets with a density above the threshold, otherwise below - /// @return Measurement The mean speed of the agents and the standard deviation - Measurement streetMeanSpeed(double threshold, bool above) const override; }; } // namespace dsf::mobility \ No newline at end of file diff --git a/src/dsf/mobility/RoadDynamics.hpp b/src/dsf/mobility/RoadDynamics.hpp index f0ae7cef..4d38f034 100644 --- a/src/dsf/mobility/RoadDynamics.hpp +++ b/src/dsf/mobility/RoadDynamics.hpp @@ -64,17 +64,23 @@ namespace dsf::mobility { tbb::concurrent_vector> m_travelDTs; std::time_t m_previousOptimizationTime{0}; - private: + protected: std::function const&)> m_weightFunction; std::optional m_errorProbability{std::nullopt}; std::optional m_passageProbability{std::nullopt}; std::optional m_meanTravelDistance{std::nullopt}; std::optional m_meanTravelTime{std::nullopt}; - double m_weightTreshold; - std::optional m_timeToleranceFactor{std::nullopt}; std::optional m_dataUpdatePeriod; bool m_bCacheEnabled; + PathWeight m_pathWeight = PathWeight::TRAVELTIME; + double m_weightTreshold; + std::optional m_timeToleranceFactor{std::nullopt}; bool m_forcePriorities{false}; + // Saving variables + std::time_t m_savingInterval{0}; + bool m_bSaveStreetData{false}; + bool m_bSaveTravelData{false}; + bool m_bSaveAverageStats{false}; private: /// @brief Kill an agent @@ -111,6 +117,74 @@ namespace dsf::mobility { virtual double m_streetEstimatedTravelTime( std::unique_ptr const& pStreet) const = 0; + /// @brief Initialize the street data table. + /// This table contains the data of each street. Columns are: + /// + /// - id: The entry id (auto-incremented) + /// + /// - simulation_id: The simulation id + /// + /// - datetime: The datetime of the data entry + /// + /// - time_step: The time step of the data entry + /// + /// - street_id: The id of the street + /// + /// - coil: The name of the coil on the street (can be null) + /// + /// - density_vpk: The density in vehicles per kilometer + /// + /// - avg_speed_kph: The average speed in kilometers per hour + /// + /// - std_speed_kph: The standard deviation of the speed in kilometers per hour + /// + /// - counts: The counts of the coil sensor (can be null) + /// + /// - queue_length: The length of the queue on the street + void m_initStreetTable() const; + /// @brief Initialize the average stats table. + /// This table contains the average stats of the simulation at each time step. Columns are: + /// + /// - id: The entry id (auto-incremented) + /// + /// - simulation_id: The simulation id + /// + /// - datetime: The datetime of the data entry + /// + /// - time_step: The time step of the data entry + /// + /// - n_ghost_agents: The number of ghost agents + /// + /// - n_agents: The number of agents + /// + /// - mean_speed_kph: The mean speed of the agents in kilometers per hour + /// + /// - std_speed_kph: The standard deviation of the speed of the agents in kilometers per hour + /// + /// - mean_density_vpk: The mean density of the streets in vehicles per kilometer + /// + /// - std_density_vpk: The standard deviation of the density of the streets in vehicles per kilometer + void m_initAvgStatsTable() const; + /// @brief Initialize the travel data table. + /// This table contains the travel data of the agents. Columns are: + /// + /// - id: The entry id (auto-incremented) + /// + /// - simulation_id: The simulation id + /// + /// - datetime: The datetime of the data entry + /// + /// - time_step: The time step of the data entry + /// + /// - distance_m: The distance travelled by the agent in meters + /// + /// - travel_time_s: The travel time of the agent in seconds + void m_initTravelDataTable() const; + + virtual void m_dumpSimInfo() const = 0; + + void m_dumpNetwork() const; + public: /// @brief Construct a new RoadDynamics object /// @param graph The graph representing the network @@ -195,6 +269,11 @@ namespace dsf::mobility { /// @throws std::runtime_error if the turn counts map is not initialized void resetTurnCounts(); + void saveData(std::time_t const savingInterval, + bool const saveAverageStats = false, + bool const saveStreetData = false, + bool const saveTravelData = false); + /// @brief Update the paths of the itineraries based on the given weight function /// @param throw_on_empty If true, throws an exception if an itinerary has an empty path (default is true) /// If false, removes the itinerary with empty paths and the associated node from the origin/destination nodes @@ -341,9 +420,6 @@ namespace dsf::mobility { tbb::concurrent_unordered_map destinationCounts( bool const bReset = true) noexcept; - virtual double streetMeanSpeed(Id streetId) const; - virtual Measurement streetMeanSpeed() const; - virtual Measurement streetMeanSpeed(double, bool) const; /// @brief Get the mean density of the streets in \f$m^{-1}\f$ /// @return Measurement The mean density of the streets and the standard deviation Measurement streetMeanDensity(bool normalized = false) const; @@ -356,55 +432,6 @@ namespace dsf::mobility { /// @return Measurement The mean flow of the streets and the standard deviation Measurement streetMeanFlow(double threshold, bool above) const; - /// @brief Save the street densities in csv format - /// @param filename The name of the file (default is "{datetime}_{simulation_name}_street_densities.csv") - /// @param separator The separator character (default is ';') - /// @param normalized If true, the densities are normalized in [0, 1] dividing by the street capacity attribute - void saveStreetDensities(std::string filename = std::string(), - char const separator = ';', - bool const normalized = true) const; - /// @brief Save the street speeds in csv format - /// @param filename The name of the file (default is "{datetime}_{simulation_name}_street_speeds.csv") - /// @param separator The separator character (default is ';') - /// @param bNormalized If true, the speeds are normalized in [0, 1] dividing by the street maxSpeed attribute - void saveStreetSpeeds(std::string filename = std::string(), - char const separator = ';', - bool bNormalized = false) const; - /// @brief Save the street input counts in csv format - /// @param filename The name of the file - /// @param reset If true, the input counts are cleared after the computation - /// @param separator The separator character (default is ';') - /// @details NOTE: counts are saved only if the street has a coil on it - void saveCoilCounts(const std::string& filename, - bool reset = false, - char const separator = ';'); - /// @brief Save the travel data of the agents in csv format. - /// @details The file contains the following columns: - /// - time: the time of the simulation - /// - distances: the travel distances of the agents - /// - times: the travel times of the agents - /// - speeds: the travel speeds of the agents - /// @param filename The name of the file (default is "{datetime}_{simulation_name}_travel_data.csv") - /// @param reset If true, the travel speeds are cleared after the computation - void saveTravelData(std::string filename = std::string(), bool reset = false); - /// @brief Save the main macroscopic observables in csv format - /// @param filename The name of the file (default is "{datetime}_{simulation_name}_macroscopic_observables.csv") - /// @param separator The separator character (default is ';') - /// @details The file contains the following columns: - /// - time: the time of the simulation - /// - n_ghost_agents: the number of agents waiting to be inserted in the simulation - /// - n_agents: the number of agents currently in the simulation - /// - mean_speed - mean_speed_std (km/h): the mean speed of the agents - /// - mean_density - mean_density_std (veh/km): the mean density of the streets - /// - mean_flow - mean_flow_std (veh/h): the mean flow of the streets - /// - mean_traveltime - mean_traveltime_std (min): the mean travel time of the agents - /// - mean_traveldistance - mean_traveldistance_err (km): the mean travel distance of the agents - /// - mean_travelspeed - mean_travelspeed_std (km/h): the mean travel speed of the agents - /// - /// NOTE: the mean density is normalized in [0, 1] and reset is true for all observables which have such parameter - void saveMacroscopicObservables(std::string filename = std::string(), - char const separator = ';'); - /// @brief Print a summary of the dynamics to an output stream /// @param os The output stream to write to (default is std::cout) /// @details The summary includes: @@ -1088,6 +1115,151 @@ namespace dsf::mobility { spdlog::debug("There are {} agents left in the list.", m_agents.size()); } + template + requires(is_numeric_v) + void RoadDynamics::m_initStreetTable() const { + if (!this->database()) { + throw std::runtime_error( + "No database connected. Call connectDataBase() before saving data."); + } + // Create table if it doesn't exist + this->database()->exec( + "CREATE TABLE IF NOT EXISTS road_data (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "simulation_id INTEGER NOT NULL, " + "datetime TEXT NOT NULL, " + "time_step INTEGER NOT NULL, " + "street_id INTEGER NOT NULL, " + "coil TEXT, " + "density_vpk REAL, " + "avg_speed_kph REAL, " + "std_speed_kph REAL, " + "counts INTEGER, " + "queue_length INTEGER)"); + + spdlog::info("Initialized road_data table in the database."); + } + template + requires(is_numeric_v) + void RoadDynamics::m_initAvgStatsTable() const { + if (!this->database()) { + throw std::runtime_error( + "No database connected. Call connectDataBase() before saving data."); + } + // Create table if it doesn't exist + this->database()->exec( + "CREATE TABLE IF NOT EXISTS avg_stats (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "simulation_id INTEGER NOT NULL, " + "datetime TEXT NOT NULL, " + "time_step INTEGER NOT NULL, " + "n_ghost_agents INTEGER NOT NULL, " + "n_agents INTEGER NOT NULL, " + "mean_speed_kph REAL, " + "std_speed_kph REAL, " + "mean_density_vpk REAL NOT NULL, " + "std_density_vpk REAL NOT NULL)"); + + spdlog::info("Initialized avg_stats table in the database."); + } + template + requires(is_numeric_v) + void RoadDynamics::m_initTravelDataTable() const { + if (!this->database()) { + throw std::runtime_error( + "No database connected. Call connectDataBase() before saving data."); + } + // Create table if it doesn't exist + this->database()->exec( + "CREATE TABLE IF NOT EXISTS travel_data (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "simulation_id INTEGER NOT NULL, " + "datetime TEXT NOT NULL, " + "time_step INTEGER NOT NULL, " + "distance_m REAL NOT NULL, " + "travel_time_s REAL NOT NULL)"); + + spdlog::info("Initialized travel_data table in the database."); + } + template + requires(is_numeric_v) + void RoadDynamics::m_dumpNetwork() const { + if (!this->database()) { + throw std::runtime_error( + "No database connected. Call connectDataBase() before saving data."); + } + // Check if edges and nodes tables already exists + SQLite::Statement edgesQuery( + *this->database(), + "SELECT name FROM sqlite_master WHERE type='table' AND name='edges';"); + SQLite::Statement nodesQuery( + *this->database(), + "SELECT name FROM sqlite_master WHERE type='table' AND name='nodes';"); + bool edgesTableExists = edgesQuery.executeStep(); + bool nodesTableExists = nodesQuery.executeStep(); + if (edgesTableExists && nodesTableExists) { + spdlog::info( + "Edges and nodes tables already exist in the database. Skipping network dump."); + return; + } + + // Create edges table + this->database()->exec( + "CREATE TABLE IF NOT EXISTS edges (" + "id INTEGER PRIMARY KEY, " + "source INTEGER NOT NULL, " + "target INTEGER NOT NULL, " + "length REAL NOT NULL, " + "maxspeed REAL NOT NULL, " + "name TEXT, " + "nlanes INTEGER NOT NULL, " + "geometry TEXT NOT NULL)"); + // Create nodes table + this->database()->exec( + "CREATE TABLE IF NOT EXISTS nodes (" + "id INTEGER PRIMARY KEY, " + "type TEXT, " + "geometry TEXT)"); + + // Insert edges + SQLite::Statement insertEdgeStmt(*this->database(), + "INSERT INTO edges (id, source, target, length, " + "maxspeed, name, nlanes, geometry) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?);"); + for (const auto& [edgeId, pEdge] : this->graph().edges()) { + insertEdgeStmt.bind(1, static_cast(edgeId)); + insertEdgeStmt.bind(2, static_cast(pEdge->source())); + insertEdgeStmt.bind(3, static_cast(pEdge->target())); + insertEdgeStmt.bind(4, pEdge->length()); + insertEdgeStmt.bind(5, pEdge->maxSpeed()); + insertEdgeStmt.bind(6, pEdge->name()); + insertEdgeStmt.bind(7, pEdge->nLanes()); + insertEdgeStmt.bind(8, std::format("{}", pEdge->geometry())); + insertEdgeStmt.exec(); + insertEdgeStmt.reset(); + } + // Insert nodes + SQLite::Statement insertNodeStmt( + *this->database(), "INSERT INTO nodes (id, type, geometry) VALUES (?, ?, ?);"); + for (const auto& [nodeId, pNode] : this->graph().nodes()) { + insertNodeStmt.bind(1, static_cast(nodeId)); + if (pNode->isTrafficLight()) { + insertNodeStmt.bind(2, "traffic_light"); + } else if (pNode->isRoundabout()) { + insertNodeStmt.bind(2, "roundabout"); + } else { + insertNodeStmt.bind(2); + } + if (pNode->geometry().has_value()) { + insertNodeStmt.bind(3, std::format("{}", *pNode->geometry())); + } else { + insertNodeStmt.bind(3); + } + insertNodeStmt.exec(); + insertNodeStmt.reset(); + } + } + template requires(is_numeric_v) void RoadDynamics::setErrorProbability(double errorProbability) { @@ -1119,6 +1291,7 @@ namespace dsf::mobility { requires(is_numeric_v) void RoadDynamics::setWeightFunction(PathWeight const pathWeight, std::optional weightTreshold) { + m_pathWeight = pathWeight; switch (pathWeight) { case PathWeight::LENGTH: m_weightFunction = [](std::unique_ptr const& pStreet) { @@ -1233,6 +1406,40 @@ namespace dsf::mobility { } } + template + requires(is_numeric_v) + void RoadDynamics::saveData(std::time_t const savingInterval, + bool const saveAverageStats, + bool const saveStreetData, + bool const saveTravelData) { + m_savingInterval = savingInterval; + m_bSaveAverageStats = saveAverageStats; + m_bSaveStreetData = saveStreetData; + m_bSaveTravelData = saveTravelData; + + // Initialize the required tables + if (saveStreetData) { + m_initStreetTable(); + } + if (saveAverageStats) { + m_initAvgStatsTable(); + } + if (saveTravelData) { + m_initTravelDataTable(); + } + + this->m_dumpSimInfo(); + this->m_dumpNetwork(); + + spdlog::info( + "Data saving configured: interval={}s, avg_stats={}, street_data={}, " + "travel_data={}", + savingInterval, + saveAverageStats, + saveStreetData, + saveTravelData); + } + template requires(is_numeric_v) void RoadDynamics::setDestinationNodes( @@ -1602,20 +1809,39 @@ namespace dsf::mobility { template requires(is_numeric_v) void RoadDynamics::evolve(bool reinsert_agents) { + std::atomic mean_speed{0.}, mean_density{0.}; + std::atomic std_speed{0.}, std_density{0.}; + std::atomic nValidEdges{0}; + bool const bComputeStats = this->database() != nullptr && m_savingInterval > 0 && + this->time_step() % m_savingInterval == 0; + + // Struct to collect street data for batch insert after parallel section + struct StreetDataRecord { + Id streetId; + std::optional coilName; + double density; + std::optional avgSpeed; + std::optional stdSpeed; + std::optional counts; + std::size_t queueLength; + }; + tbb::concurrent_vector streetDataRecords; + spdlog::debug("Init evolve at time {}", this->time_step()); // move the first agent of each street queue, if possible, putting it in the next node bool const bUpdateData = m_dataUpdatePeriod.has_value() && this->time_step() % m_dataUpdatePeriod.value() == 0; auto const numNodes{this->graph().nNodes()}; + auto const numEdges{this->graph().nEdges()}; const unsigned int concurrency = std::thread::hardware_concurrency(); // Calculate a grain size to partition the nodes into roughly "concurrency" blocks const size_t grainSize = std::max(size_t(1), numNodes / concurrency); this->m_taskArena.execute([&] { tbb::parallel_for( - tbb::blocked_range(0, numNodes, grainSize), - [&](const tbb::blocked_range& range) { - for (size_t i = range.begin(); i != range.end(); ++i) { + tbb::blocked_range(0, numNodes, grainSize), + [&](const tbb::blocked_range& range) { + for (std::size_t i = range.begin(); i != range.end(); ++i) { auto const& pNode = this->graph().node(m_nodeIndices[i]); for (auto const& inEdgeId : pNode->ingoingEdges()) { auto const& pStreet{this->graph().edge(inEdgeId)}; @@ -1634,6 +1860,44 @@ namespace dsf::mobility { } } m_evolveStreet(pStreet, reinsert_agents); + if (bComputeStats) { + auto const& density{pStreet->density() * 1e3}; + + auto const speedMeasure = pStreet->meanSpeed(true); + if (speedMeasure.is_valid) { + auto const speed = speedMeasure.mean * 3.6; // to kph + auto const speed_std = speedMeasure.std * 3.6; + if (m_bSaveAverageStats) { + mean_speed.fetch_add(speed, std::memory_order_relaxed); + std_speed.fetch_add(speed * speed + speed_std * speed_std, + std::memory_order_relaxed); + + ++nValidEdges; + } + } + if (m_bSaveAverageStats) { + mean_density.fetch_add(density, std::memory_order_relaxed); + std_density.fetch_add(density * density, std::memory_order_relaxed); + } + + if (m_bSaveStreetData) { + // Collect data for batch insert after parallel section + StreetDataRecord record; + record.streetId = pStreet->id(); + record.density = density; + if (pStreet->hasCoil()) { + record.coilName = pStreet->counterName(); + record.counts = pStreet->counts(); + pStreet->resetCounter(); + } + if (speedMeasure.is_valid) { + record.avgSpeed = speedMeasure.mean * 3.6; // to kph + record.stdSpeed = speedMeasure.std * 3.6; + } + record.queueLength = pStreet->nExitingAgents(); + streetDataRecords.push_back(record); + } + } } } }); @@ -1654,7 +1918,106 @@ namespace dsf::mobility { }); }); this->m_evolveAgents(); - // cycle over agents and update their times + + if (bComputeStats) { + // Batch insert street data collected during parallel section + if (m_bSaveStreetData) { + SQLite::Transaction transaction(*this->database()); + SQLite::Statement insertStmt( + *this->database(), + "INSERT INTO road_data (datetime, time_step, simulation_id, street_id, " + "coil, density_vpk, avg_speed_kph, std_speed_kph, counts, queue_length) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + + for (auto const& record : streetDataRecords) { + insertStmt.bind(1, this->strDateTime()); + insertStmt.bind(2, static_cast(this->time_step())); + insertStmt.bind(3, static_cast(this->id())); + insertStmt.bind(4, static_cast(record.streetId)); + if (record.coilName.has_value()) { + insertStmt.bind(5, record.coilName.value()); + } else { + insertStmt.bind(5); + } + insertStmt.bind(6, record.density); + if (record.avgSpeed.has_value()) { + insertStmt.bind(7, record.avgSpeed.value()); + insertStmt.bind(8, record.stdSpeed.value()); + } else { + insertStmt.bind(7); + insertStmt.bind(8); + } + if (record.counts.has_value()) { + insertStmt.bind(9, static_cast(record.counts.value())); + } else { + insertStmt.bind(9); + } + insertStmt.bind(10, static_cast(record.queueLength)); + insertStmt.exec(); + insertStmt.reset(); + } + transaction.commit(); + } + + if (m_bSaveTravelData) { // Begin transaction for better performance + SQLite::Transaction transaction(*this->database()); + SQLite::Statement insertStmt(*this->database(), + "INSERT INTO travel_data (datetime, time_step, " + "simulation_id, distance_m, travel_time_s) " + "VALUES (?, ?, ?, ?, ?)"); + + for (auto const& [distance, time] : m_travelDTs) { + insertStmt.bind(1, this->strDateTime()); + insertStmt.bind(2, static_cast(this->time_step())); + insertStmt.bind(3, static_cast(this->id())); + insertStmt.bind(4, distance); + insertStmt.bind(5, time); + insertStmt.exec(); + insertStmt.reset(); + } + transaction.commit(); + m_travelDTs.clear(); + } + + if (m_bSaveAverageStats) { // Average Stats Table + mean_speed.store(mean_speed.load() / nValidEdges.load()); + mean_density.store(mean_density.load() / numEdges); + { + double std_speed_val = std_speed.load(); + double mean_speed_val = mean_speed.load(); + std_speed.store(std::sqrt(std_speed_val / nValidEdges.load() - + mean_speed_val * mean_speed_val)); + } + { + double std_density_val = std_density.load(); + double mean_density_val = mean_density.load(); + std_density.store(std::sqrt(std_density_val / numEdges - + mean_density_val * mean_density_val)); + } + SQLite::Statement insertStmt( + *this->database(), + "INSERT INTO avg_stats (" + "simulation_id, datetime, time_step, n_ghost_agents, n_agents, " + "mean_speed_kph, std_speed_kph, mean_density_vpk, std_density_vpk) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"); + insertStmt.bind(1, static_cast(this->id())); + insertStmt.bind(2, this->strDateTime()); + insertStmt.bind(3, static_cast(this->time_step())); + insertStmt.bind(4, static_cast(m_agents.size())); + insertStmt.bind(5, static_cast(this->nAgents())); + if (nValidEdges.load() > 0) { + insertStmt.bind(6, mean_speed); + insertStmt.bind(7, std_speed); + } else { + insertStmt.bind(6); + insertStmt.bind(7); + } + insertStmt.bind(8, mean_density); + insertStmt.bind(9, std_density); + insertStmt.exec(); + } + } + Dynamics::m_evolve(); } @@ -2211,48 +2574,6 @@ namespace dsf::mobility { return tempCounts; } - template - requires(is_numeric_v) - double RoadDynamics::streetMeanSpeed(Id streetId) const { - auto const& pStreet{this->graph().edge(streetId)}; - auto const nAgents{pStreet->nAgents()}; - if (nAgents == 0) { - return 0.; - } - double speed{0.}; - for (auto const& pAgent : pStreet->movingAgents()) { - speed += pAgent->speed(); - } - return speed / nAgents; - } - - template - requires(is_numeric_v) - Measurement RoadDynamics::streetMeanSpeed() const { - std::vector speeds; - speeds.reserve(this->graph().nEdges()); - for (const auto& [streetId, pStreet] : this->graph().edges()) { - speeds.push_back(streetMeanSpeed(streetId)); - } - return Measurement(speeds); - } - - template - requires(is_numeric_v) - Measurement RoadDynamics::streetMeanSpeed(double threshold, - bool above) const { - std::vector speeds; - speeds.reserve(this->graph().nEdges()); - for (const auto& [streetId, pStreet] : this->graph().edges()) { - if (above && (pStreet->density(true) > threshold)) { - speeds.push_back(streetMeanSpeed(streetId)); - } else if (!above && (pStreet->density(true) < threshold)) { - speeds.push_back(streetMeanSpeed(streetId)); - } - } - return Measurement(speeds); - } - template requires(is_numeric_v) Measurement RoadDynamics::streetMeanDensity(bool normalized) const { @@ -2286,7 +2607,10 @@ namespace dsf::mobility { std::vector flows; flows.reserve(this->graph().nEdges()); for (const auto& [streetId, pStreet] : this->graph().edges()) { - flows.push_back(pStreet->density() * this->streetMeanSpeed(streetId)); + auto const speedMeasure = pStreet->meanSpeed(); + if (speedMeasure.is_valid) { + flows.push_back(pStreet->density() * speedMeasure.mean); + } } return Measurement(flows); } @@ -2298,269 +2622,19 @@ namespace dsf::mobility { std::vector flows; flows.reserve(this->graph().nEdges()); for (const auto& [streetId, pStreet] : this->graph().edges()) { + auto const speedMeasure = pStreet->meanSpeed(); + if (!speedMeasure.is_valid) { + continue; + } if (above && (pStreet->density(true) > threshold)) { - flows.push_back(pStreet->density() * this->streetMeanSpeed(streetId)); + flows.push_back(pStreet->density() * speedMeasure.mean); } else if (!above && (pStreet->density(true) < threshold)) { - flows.push_back(pStreet->density() * this->streetMeanSpeed(streetId)); + flows.push_back(pStreet->density() * speedMeasure.mean); } } return Measurement(flows); } - template - requires(is_numeric_v) - void RoadDynamics::saveStreetDensities(std::string filename, - char const separator, - bool const normalized) const { - if (filename.empty()) { - filename = - this->m_safeDateTime() + '_' + this->m_safeName() + "_street_densities.csv"; - } - bool bEmptyFile{false}; - { - std::ifstream file(filename); - bEmptyFile = file.peek() == std::ifstream::traits_type::eof(); - } - std::ofstream file(filename, std::ios::app); - if (!file.is_open()) { - throw std::runtime_error("Error opening file \"" + filename + "\" for writing."); - } - if (bEmptyFile) { - file << "datetime" << separator << "time_step"; - for (auto const& [streetId, pStreet] : this->graph().edges()) { - file << separator << streetId; - } - file << std::endl; - } - file << this->strDateTime() << separator << this->time_step(); - for (auto const& [streetId, pStreet] : this->graph().edges()) { - // keep 2 decimal digits; - file << separator << std::scientific << std::setprecision(2) - << pStreet->density(normalized); - } - file << std::endl; - file.close(); - } - template - requires(is_numeric_v) - void RoadDynamics::saveStreetSpeeds(std::string filename, - char const separator, - bool const bNormalized) const { - if (filename.empty()) { - filename = this->m_safeDateTime() + '_' + this->m_safeName() + "_street_speeds.csv"; - } - bool bEmptyFile{false}; - { - std::ifstream file(filename); - bEmptyFile = file.peek() == std::ifstream::traits_type::eof(); - } - std::ofstream file(filename, std::ios::app); - if (!file.is_open()) { - throw std::runtime_error("Error opening file \"" + filename + "\" for writing."); - } - if (bEmptyFile) { - file << "datetime" << separator << "time_step"; - for (auto const& [streetId, pStreet] : this->graph().edges()) { - file << separator << streetId; - } - file << std::endl; - } - file << this->strDateTime() << separator << this->time_step(); - for (auto const& [streetId, pStreet] : this->graph().edges()) { - auto const measure = pStreet->meanSpeed(true); - file << separator; - // If not valid, write empty value (less space w.r.t. NaN) - if (!measure.is_valid) { - continue; - } - - double speed{measure.mean}; - if (bNormalized) { - speed /= pStreet->maxSpeed(); - } - file << std::fixed << std::setprecision(2) << speed; - } - file << std::endl; - file.close(); - } - template - requires(is_numeric_v) - void RoadDynamics::saveCoilCounts(const std::string& filename, - bool reset, - char const separator) { - bool bEmptyFile{false}; - { - std::ifstream file(filename); - bEmptyFile = file.peek() == std::ifstream::traits_type::eof(); - } - std::ofstream file(filename, std::ios::app); - if (!file.is_open()) { - throw std::runtime_error("Error opening file \"" + filename + "\" for writing."); - } - if (bEmptyFile) { - file << "datetime" << separator << "time_step"; - for (auto const& [streetId, pStreet] : this->graph().edges()) { - if (pStreet->hasCoil()) { - file << separator << pStreet->counterName(); - } - } - file << std::endl; - } - file << this->strDateTime() << separator << this->time_step(); - for (auto const& [streetId, pStreet] : this->graph().edges()) { - if (pStreet->hasCoil()) { - file << separator << pStreet->counts(); - if (reset) { - pStreet->resetCounter(); - } - } - } - file << std::endl; - file.close(); - } - template - requires(is_numeric_v) - void RoadDynamics::saveTravelData(std::string filename, bool reset) { - if (filename.empty()) { - filename = this->m_safeDateTime() + '_' + this->m_safeName() + "_travel_data.csv"; - } - bool bEmptyFile{false}; - { - std::ifstream file(filename); - bEmptyFile = file.peek() == std::ifstream::traits_type::eof(); - } - std::ofstream file(filename, std::ios::app); - if (!file.is_open()) { - throw std::runtime_error("Error opening file \"" + filename + "\" for writing."); - } - if (bEmptyFile) { - file << "datetime;time_step;distances;times;speeds" << std::endl; - } - - // Construct strings efficiently with proper formatting - std::ostringstream oss; - oss << std::fixed << std::setprecision(2); - - std::string strTravelDistances, strTravelTimes, strTravelSpeeds; - strTravelDistances.reserve(m_travelDTs.size() * - 10); // Rough estimate for numeric strings - strTravelTimes.reserve(m_travelDTs.size() * 10); - strTravelSpeeds.reserve(m_travelDTs.size() * 10); - - for (auto it = m_travelDTs.cbegin(); it != m_travelDTs.cend(); ++it) { - oss.str(""); // Clear the stream - oss << it->first; - strTravelDistances += oss.str(); - - oss.str(""); - oss << it->second; - strTravelTimes += oss.str(); - - oss.str(""); - oss << (it->first / it->second); - strTravelSpeeds += oss.str(); - - if (it != m_travelDTs.cend() - 1) { - strTravelDistances += ','; - strTravelTimes += ','; - strTravelSpeeds += ','; - } - } - - // Write all data at once - file << this->strDateTime() << ';' << this->time_step() << ';' << strTravelDistances - << ';' << strTravelTimes << ';' << strTravelSpeeds << std::endl; - - file.close(); - if (reset) { - m_travelDTs.clear(); - } - } - template - requires(is_numeric_v) - void RoadDynamics::saveMacroscopicObservables(std::string filename, - char const separator) { - if (filename.empty()) { - filename = this->m_safeDateTime() + '_' + this->m_safeName() + - "_macroscopic_observables.csv"; - } - bool bEmptyFile{false}; - { - std::ifstream file(filename); - bEmptyFile = file.peek() == std::ifstream::traits_type::eof(); - } - std::ofstream file(filename, std::ios::app); - if (!file.is_open()) { - throw std::runtime_error("Error opening file \"" + filename + "\" for writing."); - } - if (bEmptyFile) { - constexpr auto strHeader{ - "datetime;time_step;n_ghost_agents;n_agents;mean_speed_kph;std_speed_kph;" - "mean_density_vpk;std_density_vpk;mean_flow_vph;std_flow_vph;mean_" - "traveltime_m;std_traveltime_m;mean_traveldistance_km;std_traveldistance_" - "km;mean_travelspeed_kph;std_travelspeed_kph\n"}; - file << strHeader; - } - double mean_speed{0.}, mean_density{0.}, mean_flow{0.}, mean_travel_distance{0.}, - mean_travel_time{0.}, mean_travel_speed{0.}; - double std_speed{0.}, std_density{0.}, std_flow{0.}, std_travel_distance{0.}, - std_travel_time{0.}, std_travel_speed{0.}; - auto const& nEdges{this->graph().nEdges()}; - auto const& nData{m_travelDTs.size()}; - - for (auto const& [streetId, pStreet] : this->graph().edges()) { - auto const& speed{this->streetMeanSpeed(streetId) * 3.6}; - auto const& density{pStreet->density() * 1e3}; - auto const& flow{density * speed}; - mean_speed += speed; - mean_density += density; - mean_flow += flow; - std_speed += speed * speed; - std_density += density * density; - std_flow += flow * flow; - } - mean_speed /= nEdges; - mean_density /= nEdges; - mean_flow /= nEdges; - std_speed = std::sqrt(std_speed / nEdges - mean_speed * mean_speed); - std_density = std::sqrt(std_density / nEdges - mean_density * mean_density); - std_flow = std::sqrt(std_flow / nEdges - mean_flow * mean_flow); - - for (auto const& [distance, time] : m_travelDTs) { - mean_travel_distance += distance * 1e-3; - mean_travel_time += time / 60.; - mean_travel_speed += distance / time * 3.6; - std_travel_distance += distance * distance * 1e-6; - std_travel_time += time * time / 3600.; - std_travel_speed += (distance / time) * (distance / time) * 12.96; - } - m_travelDTs.clear(); - - mean_travel_distance /= nData; - mean_travel_time /= nData; - mean_travel_speed /= nData; - std_travel_distance = std::sqrt(std_travel_distance / nData - - mean_travel_distance * mean_travel_distance); - std_travel_time = - std::sqrt(std_travel_time / nData - mean_travel_time * mean_travel_time); - std_travel_speed = - std::sqrt(std_travel_speed / nData - mean_travel_speed * mean_travel_speed); - - file << this->strDateTime() << separator; - file << this->time_step() << separator; - file << m_agents.size() << separator; - file << this->nAgents() << separator; - file << std::scientific << std::setprecision(2); - file << mean_speed << separator << std_speed << separator; - file << mean_density << separator << std_density << separator; - file << mean_flow << separator << std_flow << separator; - file << mean_travel_time << separator << std_travel_time << separator; - file << mean_travel_distance << separator << std_travel_distance << separator; - file << mean_travel_speed << separator << std_travel_speed << std::endl; - - file.close(); - } - template requires(is_numeric_v) void RoadDynamics::summary(std::ostream& os) const { diff --git a/src/dsf/mobility/RoadNetwork.cpp b/src/dsf/mobility/RoadNetwork.cpp index 84ca6500..14fed803 100644 --- a/src/dsf/mobility/RoadNetwork.cpp +++ b/src/dsf/mobility/RoadNetwork.cpp @@ -332,10 +332,6 @@ namespace dsf::mobility { this->m_edges.rehash(0); } - RoadNetwork::RoadNetwork() : Network{AdjacencyMatrix()}, m_capacity{0} {} - - RoadNetwork::RoadNetwork(AdjacencyMatrix const& adj) : Network{adj}, m_capacity{0} {} - std::size_t RoadNetwork::nCoils() const { return std::count_if(m_edges.cbegin(), m_edges.cend(), [](auto const& pair) { return pair.second->hasCoil(); diff --git a/src/dsf/mobility/RoadNetwork.hpp b/src/dsf/mobility/RoadNetwork.hpp index 2cf94c5c..adbcaca7 100644 --- a/src/dsf/mobility/RoadNetwork.hpp +++ b/src/dsf/mobility/RoadNetwork.hpp @@ -9,7 +9,6 @@ #pragma once -#include "../base/AdjacencyMatrix.hpp" #include "../base/Network.hpp" #include "RoadJunction.hpp" #include "Intersection.hpp" @@ -62,10 +61,7 @@ namespace dsf::mobility { void m_jsonEdgesImporter(std::ifstream& file); public: - RoadNetwork(); - /// @brief Construct a new RoadNetwork object - /// @param adj An adjacency matrix made by a SparseMatrix representing the graph's adjacency matrix - RoadNetwork(AdjacencyMatrix const& adj); + RoadNetwork() = default; // Disable copy constructor and copy assignment operator RoadNetwork(const RoadNetwork&) = delete; RoadNetwork& operator=(const RoadNetwork&) = delete; diff --git a/test/base/Test_AdjacencyMatrix.cpp b/test/base/Test_AdjacencyMatrix.cpp deleted file mode 100644 index ad1c0d4e..00000000 --- a/test/base/Test_AdjacencyMatrix.cpp +++ /dev/null @@ -1,161 +0,0 @@ -#include "dsf/base/AdjacencyMatrix.hpp" -#include "dsf/mobility/RoadNetwork.hpp" - -#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN -#include "doctest.h" - -using namespace dsf; - -TEST_CASE("Test default construction and insertion") { - AdjacencyMatrix adj; - adj.insert(0, 1); - auto offsets = test::offsets(adj); - auto indices = test::indices(adj); - CHECK_EQ(offsets.size(), 3); - CHECK_EQ(offsets[0], 0); - CHECK_EQ(offsets[1], 1); - CHECK_EQ(indices.size(), 1); - CHECK_EQ(indices[0], 1); - CHECK_EQ(adj.n(), 2); - - adj.insert(1, 2); - adj.insert(1, 3); - adj.insert(2, 3); - adj.insert(3, 4); - offsets = test::offsets(adj); - indices = test::indices(adj); - CHECK_EQ(offsets.size(), 6); - CHECK_EQ(offsets[0], 0); - CHECK_EQ(offsets[1], 1); - CHECK_EQ(offsets[2], 3); - CHECK_EQ(offsets[3], 4); - CHECK_EQ(offsets[4], 5); - CHECK_EQ(indices.size(), 5); - CHECK_EQ(indices[0], 1); - CHECK_EQ(indices[1], 2); - CHECK_EQ(indices[2], 3); - CHECK_EQ(indices[3], 3); - CHECK_EQ(indices[4], 4); - CHECK_EQ(adj.n(), 5); - - adj.insert(0, 0); - offsets = test::offsets(adj); - indices = test::indices(adj); - CHECK_EQ(offsets.size(), 6); - CHECK_EQ(offsets[0], 0); - CHECK_EQ(offsets[1], 2); - CHECK_EQ(offsets[2], 4); - CHECK_EQ(offsets[3], 5); - CHECK_EQ(offsets[4], 6); - CHECK_EQ(indices.size(), 6); - CHECK_EQ(indices[0], 1); - CHECK_EQ(indices[1], 0); - CHECK_EQ(indices[2], 2); - CHECK_EQ(indices[3], 3); - CHECK_EQ(indices[4], 3); - CHECK_EQ(indices[5], 4); - CHECK_EQ(adj.n(), 5); - - SUBCASE("Test contains") { - CHECK(adj(0, 1)); - CHECK(adj(1, 2)); - CHECK(adj(1, 3)); - CHECK(adj(2, 3)); - CHECK(adj(3, 4)); - CHECK_FALSE(adj(0, 2)); - CHECK_FALSE(adj(2, 0)); - CHECK_FALSE(adj(3, 3)); - CHECK_THROWS(adj(5, 0)); - CHECK_THROWS(adj(10, 0)); - CHECK_THROWS(adj(0, 5)); - CHECK_THROWS(adj(0, 10)); - } - SUBCASE("Test getCol") { - auto col0 = adj.getCol(0); - CHECK_EQ(col0.size(), 1); - CHECK_EQ(col0[0], 0); - auto col1 = adj.getCol(1); - CHECK_EQ(col1.size(), 1); - CHECK_EQ(col1[0], 0); - auto col2 = adj.getCol(2); - CHECK_EQ(col2.size(), 1); - CHECK_EQ(col2[0], 1); - auto col3 = adj.getCol(3); - CHECK_EQ(col3.size(), 2); - CHECK_EQ(col3[0], 1); - CHECK_EQ(col3[1], 2); - } - SUBCASE("Test getRow") { - auto row0 = adj.getRow(0); - CHECK_EQ(row0.size(), 2); - CHECK_EQ(row0[0], 1); - CHECK_EQ(row0[1], 0); - auto row1 = adj.getRow(1); - CHECK_EQ(row1.size(), 2); - CHECK_EQ(row1[0], 2); - CHECK_EQ(row1[1], 3); - auto row2 = adj.getRow(2); - CHECK_EQ(row2.size(), 1); - CHECK_EQ(row2[0], 3); - auto row3 = adj.getRow(3); - CHECK_EQ(row3.size(), 1); - CHECK_EQ(row3[0], 4); - } - SUBCASE("Test getInDegreeVector") { - auto inDegreeVector = adj.getInDegreeVector(); - CHECK_EQ(inDegreeVector.size(), adj.n()); - CHECK_EQ(inDegreeVector[0], 1); - CHECK_EQ(inDegreeVector[1], 1); - CHECK_EQ(inDegreeVector[2], 1); - CHECK_EQ(inDegreeVector[3], 2); - CHECK_EQ(inDegreeVector[4], 1); - } - SUBCASE("Test getOutDegreeVector") { - auto outDegreeVector = adj.getOutDegreeVector(); - CHECK_EQ(outDegreeVector.size(), 5); - CHECK_EQ(outDegreeVector[0], 2); - CHECK_EQ(outDegreeVector[1], 2); - CHECK_EQ(outDegreeVector[2], 1); - CHECK_EQ(outDegreeVector[3], 1); - CHECK_EQ(outDegreeVector[4], 0); - } -} - -TEST_CASE("Test insertion of random values") { - AdjacencyMatrix adj; - adj.insert(4, 2); - auto offsets = test::offsets(adj); - auto indices = test::indices(adj); - CHECK_EQ(offsets.size(), 6); - std::for_each( - offsets.begin(), offsets.begin() + 5, [](auto value) { CHECK(value == 0); }); - CHECK_EQ(offsets[5], 1); - CHECK_EQ(indices.size(), 1); - CHECK_EQ(indices[0], 2); - - adj.insert(63, 268); - offsets = test::offsets(adj); - indices = test::indices(adj); - CHECK(offsets.size() == 270); - std::for_each( - offsets.begin() + 5, offsets.begin() + 63, [](auto value) { CHECK(value == 1); }); - CHECK_EQ(offsets[64], 2); - CHECK_EQ(indices.size(), 2); - CHECK_EQ(indices[1], 268); - - adj.insert(2, 3); - offsets = test::offsets(adj); - indices = test::indices(adj); - CHECK_EQ(offsets.size(), 270); - CHECK_EQ(offsets[0], 0); - CHECK_EQ(offsets[2], 0); - CHECK_EQ(offsets[3], 1); - CHECK_EQ(offsets[4], 1); - CHECK_EQ(offsets[5], 2); - CHECK_EQ(offsets[63], 2); - CHECK_EQ(offsets[64], 3); - CHECK_EQ(indices.size(), 3); - CHECK_EQ(indices[0], 3); - CHECK_EQ(indices[1], 2); - CHECK_EQ(indices[2], 268); -} diff --git a/test/mobility/Test_dynamics.cpp b/test/mobility/Test_dynamics.cpp index fda3affe..5807b1aa 100644 --- a/test/mobility/Test_dynamics.cpp +++ b/test/mobility/Test_dynamics.cpp @@ -5,11 +5,14 @@ #include "dsf/mobility/Intersection.hpp" #include "dsf/mobility/Agent.hpp" +#include + #include #include #include #include #include +#include #include #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN @@ -957,44 +960,6 @@ TEST_CASE("FirstOrderDynamics") { } } } - SUBCASE("streetMeanSpeed") { - /// GIVEN: a dynamics object - /// WHEN: we evolve the dynamics - /// THEN: the agent mean speed is the same as the street mean speed - Road::setMeanVehicleLength(2.); - Street s1{0, std::make_pair(0, 1), 20., 20.}; - Street s2{1, std::make_pair(1, 2), 30., 15.}; - Street s3{2, std::make_pair(3, 1), 30., 15.}; - Street s4{3, std::make_pair(1, 4), 30., 15.}; - RoadNetwork graph2; - graph2.addStreets(s1, s2, s3, s4); - for (const auto& [id, pNode] : graph2.nodes()) { - pNode->setCapacity(4); - pNode->setTransportCapacity(4); - } - FirstOrderDynamics dynamics{graph2, false, 69, 0.5}; - dynamics.addItinerary(2, 2); - dynamics.updatePaths(); - for (int i = 0; i < 4; ++i) { - dynamics.addAgent(dynamics.itineraries().at(2), 0); - } - auto const& pStreet{dynamics.graph().edge(0)}; - dynamics.evolve(false); - dynamics.evolve(false); - CHECK_EQ(dynamics.streetMeanSpeed(0), 18.5); - // I don't think the mean speed of agents should be equal to the street's - // one... CHECK_EQ(dynamics.streetMeanSpeed().mean, - // dynamics.agentMeanSpeed().mean); CHECK_EQ(dynamics.streetMeanSpeed().std, - // 0.); street 1 density should be 0.4 so... - CHECK_EQ(dynamics.streetMeanSpeed(0.2, true).mean, 18.5); - CHECK_EQ(dynamics.streetMeanSpeed(0.2, true).std, 0.); - CHECK_EQ(dynamics.streetMeanSpeed(0.8, false).mean, 15.875); - CHECK_EQ(dynamics.streetMeanSpeed(0.8, false).std, doctest::Approx(1.51554)); - dynamics.evolve(false); - dynamics.evolve(false); - CHECK_EQ(pStreet->queue(0).size(), 2); - CHECK_EQ(dynamics.streetMeanSpeed(0), 0.); - } SUBCASE("Intersection right of way") { GIVEN("A dynamics object with five nodes and eight streets") { RoadNetwork graph2; @@ -1083,253 +1048,244 @@ TEST_CASE("FirstOrderDynamics") { } } } - SUBCASE("Save functions with default filenames") { + SUBCASE("saveData function to database") { GIVEN("A dynamics object with some streets and agents") { Street s1{0, std::make_pair(0, 1), 30., 15.}; Street s2{1, std::make_pair(1, 2), 30., 15.}; RoadNetwork graph2; graph2.addStreets(s1, s2); + graph2.addCoil(0); // Add coil for testing road_data with coils FirstOrderDynamics dynamics{graph2, false, 69, 0., dsf::PathWeight::LENGTH}; dynamics.addItinerary(2, 2); dynamics.updatePaths(); dynamics.addAgent(dynamics.itineraries().at(2), 0); - // Evolve a few times to generate some data - for (int i = 0; i < 5; ++i) { - dynamics.evolve(false); - } - - WHEN("We call saveStreetDensities with default filename") { - // Use explicit filename in test to avoid cluttering the workspace - const std::string testFile = "test_street_densities.csv"; - dynamics.saveStreetDensities(testFile); + const std::string testDbPath = "test_dynamics.db"; + // Remove existing test database if present + std::filesystem::remove(testDbPath); - THEN("The file is created with correct header") { - std::ifstream file(testFile); - REQUIRE(file.is_open()); + WHEN("We connect a database and configure saveData with street data") { + dynamics.connectDataBase(testDbPath); + // Configure saving: interval=1 (save every step), saveStreetData=true + dynamics.saveData(1, false, true, false); - std::string header; - std::getline(file, header); - CHECK(header.find("datetime") != std::string::npos); - CHECK(header.find("time_step") != std::string::npos); - CHECK(header.find("0") != std::string::npos); - CHECK(header.find("1") != std::string::npos); - - file.close(); - std::filesystem::remove(testFile); + // Evolve a few times to generate and save data + for (int i = 0; i < 5; ++i) { + dynamics.evolve(true); } - } - - WHEN("We call saveTravelData with default filename") { - const std::string testFile = "test_travel_data.csv"; - dynamics.saveTravelData(testFile); - - THEN("The file is created with correct header") { - std::ifstream file(testFile); - REQUIRE(file.is_open()); - std::string header; - std::getline(file, header); - CHECK(header.find("datetime") != std::string::npos); - CHECK(header.find("time_step") != std::string::npos); - CHECK(header.find("distances") != std::string::npos); - CHECK(header.find("times") != std::string::npos); - CHECK(header.find("speeds") != std::string::npos); + THEN("The road_data table is created with correct data") { + SQLite::Database db(testDbPath, SQLite::OPEN_READONLY); + SQLite::Statement query(db, "SELECT COUNT(*) FROM road_data"); + REQUIRE(query.executeStep()); + CHECK(query.getColumn(0).getInt() >= 2); // At least 2 streets - file.close(); - std::filesystem::remove(testFile); + SQLite::Statement cols( + db, "SELECT street_id, density_vpk, avg_speed_kph FROM road_data"); + while (cols.executeStep()) { + auto streetId = cols.getColumn(0).getInt(); + CHECK(streetId >= 0); + CHECK(streetId < 2); + } } - } - WHEN("We call saveMacroscopicObservables with default filename") { - const std::string testFile = "test_macroscopic_observables.csv"; - dynamics.saveMacroscopicObservables(testFile); - - THEN("The file is created with correct header") { - std::ifstream file(testFile); - REQUIRE(file.is_open()); - - std::string header; - std::getline(file, header); - CHECK(header.find("datetime") != std::string::npos); - CHECK(header.find("time_step") != std::string::npos); - CHECK(header.find("n_ghost_agents") != std::string::npos); - CHECK(header.find("n_agents") != std::string::npos); - CHECK(header.find("mean_speed_kph") != std::string::npos); - CHECK(header.find("mean_density_vpk") != std::string::npos); - CHECK(header.find("mean_flow_vph") != std::string::npos); - CHECK(header.find("mean_traveltime_m") != std::string::npos); - CHECK(header.find("mean_traveldistance_km") != std::string::npos); - CHECK(header.find("mean_travelspeed_kph") != std::string::npos); - - file.close(); - std::filesystem::remove(testFile); - } + std::filesystem::remove(testDbPath); } - WHEN("We call saveStreetDensities with empty string (default behavior)") { - // This tests the actual default filename generation - dynamics.saveStreetDensities(); + WHEN("We connect a database and configure saveData with travel data") { + dynamics.connectDataBase(testDbPath); + // Configure saving: interval=1, saveTravelData=true + dynamics.saveData(1, false, false, true); - THEN("A file with datetime and name in filename is created") { - // Find the generated file - std::string pattern = "*_street_densities.csv"; - bool fileFound = false; - - for (const auto& entry : std::filesystem::directory_iterator(".")) { - if (entry.path().filename().string().find("street_densities.csv") != - std::string::npos) { - fileFound = true; - - // Check the file has correct header - std::ifstream file(entry.path()); - REQUIRE(file.is_open()); + // Evolve until agent reaches destination (with limit) + for (int iter = 0; iter < 1000 && dynamics.nAgents() > 0; ++iter) { + dynamics.evolve(true); + } - std::string header; - std::getline(file, header); - CHECK(header.find("datetime") != std::string::npos); - CHECK(header.find("time_step") != std::string::npos); + THEN("The travel_data table is created with correct data") { + SQLite::Database db(testDbPath, SQLite::OPEN_READONLY); + SQLite::Statement query(db, "SELECT COUNT(*) FROM travel_data"); + REQUIRE(query.executeStep()); + CHECK(query.getColumn(0).getInt() >= 1); // At least one trip - file.close(); - std::filesystem::remove(entry.path()); - break; - } + SQLite::Statement cols(db, "SELECT distance_m, travel_time_s FROM travel_data"); + while (cols.executeStep()) { + auto distance = cols.getColumn(0).getDouble(); + auto time = cols.getColumn(1).getDouble(); + CHECK(distance > 0.0); + CHECK(time > 0.0); } - - CHECK(fileFound); } - } - - WHEN("We call saveStreetSpeeds with default filename") { - // Use explicit filename in test to avoid cluttering the workspace - const std::string testFile = "test_street_speeds.csv"; - dynamics.saveStreetSpeeds(testFile); - - THEN("The file is created with correct header") { - std::ifstream file(testFile); - REQUIRE(file.is_open()); - std::string header; - std::getline(file, header); - CHECK(header.find("datetime") != std::string::npos); - CHECK(header.find("time_step") != std::string::npos); - CHECK(header.find("0") != std::string::npos); - CHECK(header.find("1") != std::string::npos); - - file.close(); - std::filesystem::remove(testFile); - } + std::filesystem::remove(testDbPath); } - WHEN("We call saveStreetSpeeds multiple times to test appending") { - const std::string testFile = "test_street_speeds_append.csv"; - - // Add some agents and evolve - dynamics.addRandomAgents(5); - dynamics.evolve(false); - - // Save first time - dynamics.saveStreetSpeeds(testFile); - - // Evolve again and save - dynamics.evolve(false); - dynamics.saveStreetSpeeds(testFile); - - THEN("The file contains multiple rows with one header") { - std::ifstream file(testFile); - REQUIRE(file.is_open()); - - std::string line; - int lineCount = 0; - int headerCount = 0; + WHEN("We connect a database and configure saveData with average stats") { + dynamics.connectDataBase(testDbPath); + // Configure saving: interval=1, saveAverageStats=true + dynamics.saveData(1, true, false, false); - while (std::getline(file, line)) { - lineCount++; - if (line.find("datetime") != std::string::npos) { - headerCount++; - } - } + // Add agents and evolve + dynamics.addRandomAgents(10); + for (int iter = 0; iter < 10; ++iter) { + dynamics.evolve(true); + } - CHECK_EQ(headerCount, 1); // Only one header - CHECK_EQ(lineCount, 3); // Header + 2 data rows + THEN("The avg_stats table is created with correct data") { + SQLite::Database db(testDbPath, SQLite::OPEN_READONLY); + SQLite::Statement query(db, "SELECT COUNT(*) FROM avg_stats"); + REQUIRE(query.executeStep()); + CHECK(query.getColumn(0).getInt() >= 1); - file.close(); - std::filesystem::remove(testFile); + SQLite::Statement cols( + db, + "SELECT n_ghost_agents, n_agents, mean_speed_kph, std_speed_kph, " + "mean_density_vpk, std_density_vpk " + "FROM avg_stats"); + REQUIRE(cols.executeStep()); + CHECK(cols.getColumn(0).getInt() >= 0); // n_ghost_agents + CHECK(cols.getColumn(1).getInt() >= 0); // n_agents + CHECK(cols.getColumn(2).getDouble() >= 0); // mean_speed_kph } + + std::filesystem::remove(testDbPath); } - WHEN("We call saveStreetSpeeds with normalized flag") { - const std::string testFile = "test_street_speeds_normalized.csv"; + WHEN("We configure saveData with all options enabled") { + dynamics.connectDataBase(testDbPath); + // Configure saving: interval=1, all data types enabled + dynamics.saveData(1, true, true, true); - // Add agents and evolve to generate some speeds + // Add agents and evolve until some reach destination dynamics.addRandomAgents(10); - dynamics.evolve(false); - dynamics.evolve(false); - - // Save normalized speeds - dynamics.saveStreetSpeeds(testFile, ',', true); - - THEN("The file is created and speeds are normalized") { - std::ifstream file(testFile); - REQUIRE(file.is_open()); - - std::string header; - std::getline(file, header); - - std::string dataLine; - std::getline(file, dataLine); - - // Parse the data line to check normalized values are between 0 and 1 - std::stringstream ss(dataLine); - std::string token; - std::getline(ss, token, ','); // datetime - std::getline(ss, token, ','); // time_step - - // Check that speed values are between 0 and 1 (normalized) - while (std::getline(ss, token, ',')) { - if (!token.empty()) { - double speed = std::stod(token); - CHECK(speed >= 0.0); - CHECK(speed <= 1.0); - } - } - - file.close(); - std::filesystem::remove(testFile); + for (int iter = 0; iter < 1000 && dynamics.nAgents() > 0; ++iter) { + dynamics.evolve(true); } - } - WHEN("We call saveStreetSpeeds with empty string (default behavior)") { - // This tests the actual default filename generation - dynamics.saveStreetSpeeds(); + THEN("All tables are created with correct schema") { + SQLite::Database db(testDbPath, SQLite::OPEN_READONLY); - THEN("A file with datetime and name in filename is created") { - // Find the generated file - std::string pattern = "*_street_speeds.csv"; - bool fileFound = false; + // Check road_data table + SQLite::Statement roadQuery(db, "SELECT COUNT(*) FROM road_data"); + REQUIRE(roadQuery.executeStep()); + CHECK(roadQuery.getColumn(0).getInt() >= 1); - for (const auto& entry : std::filesystem::directory_iterator(".")) { - if (entry.path().filename().string().find("street_speeds.csv") != - std::string::npos) { - fileFound = true; - - // Check the file has correct header - std::ifstream file(entry.path()); - REQUIRE(file.is_open()); - - std::string header; - std::getline(file, header); - CHECK(header.find("datetime") != std::string::npos); - CHECK(header.find("time_step") != std::string::npos); - - file.close(); - std::filesystem::remove(entry.path()); - break; - } + SQLite::Statement roadSchema(db, "PRAGMA table_info(road_data)"); + std::set roadColumns; + while (roadSchema.executeStep()) { + roadColumns.insert(roadSchema.getColumn(1).getString()); } - - CHECK(fileFound); + CHECK(roadColumns.count("id") == 1); + CHECK(roadColumns.count("simulation_id") == 1); + CHECK(roadColumns.count("datetime") == 1); + CHECK(roadColumns.count("time_step") == 1); + CHECK(roadColumns.count("street_id") == 1); + CHECK(roadColumns.count("coil") == 1); + CHECK(roadColumns.count("density_vpk") == 1); + CHECK(roadColumns.count("avg_speed_kph") == 1); + CHECK(roadColumns.count("std_speed_kph") == 1); + CHECK(roadColumns.count("counts") == 1); + + // Check avg_stats table + SQLite::Statement avgQuery(db, "SELECT COUNT(*) FROM avg_stats"); + REQUIRE(avgQuery.executeStep()); + CHECK(avgQuery.getColumn(0).getInt() >= 1); + + SQLite::Statement avgSchema(db, "PRAGMA table_info(avg_stats)"); + std::set avgColumns; + while (avgSchema.executeStep()) { + avgColumns.insert(avgSchema.getColumn(1).getString()); + } + CHECK(avgColumns.count("id") == 1); + CHECK(avgColumns.count("simulation_id") == 1); + CHECK(avgColumns.count("datetime") == 1); + CHECK(avgColumns.count("time_step") == 1); + CHECK(avgColumns.count("n_ghost_agents") == 1); + CHECK(avgColumns.count("n_agents") == 1); + CHECK(avgColumns.count("mean_speed_kph") == 1); + CHECK(avgColumns.count("std_speed_kph") == 1); + CHECK(avgColumns.count("mean_density_vpk") == 1); + CHECK(avgColumns.count("std_density_vpk") == 1); + + // Check travel_data table + SQLite::Statement travelQuery(db, "SELECT COUNT(*) FROM travel_data"); + REQUIRE(travelQuery.executeStep()); + CHECK(travelQuery.getColumn(0).getInt() >= 1); + + SQLite::Statement travelSchema(db, "PRAGMA table_info(travel_data)"); + std::set travelColumns; + while (travelSchema.executeStep()) { + travelColumns.insert(travelSchema.getColumn(1).getString()); + } + CHECK(travelColumns.count("id") == 1); + CHECK(travelColumns.count("simulation_id") == 1); + CHECK(travelColumns.count("datetime") == 1); + CHECK(travelColumns.count("time_step") == 1); + CHECK(travelColumns.count("distance_m") == 1); + CHECK(travelColumns.count("travel_time_s") == 1); + + // Check simulations table + SQLite::Statement simQuery( + db, + "SELECT name FROM sqlite_master WHERE type='table' AND name='simulations'"); + REQUIRE(simQuery.executeStep()); + + SQLite::Statement simSchema(db, "PRAGMA table_info(simulations)"); + std::set simColumns; + while (simSchema.executeStep()) { + simColumns.insert(simSchema.getColumn(1).getString()); + } + CHECK(simColumns.count("id") == 1); + CHECK(simColumns.count("name") == 1); + CHECK(simColumns.count("alpha") == 1); + CHECK(simColumns.count("speed_fluctuation_std") == 1); + CHECK(simColumns.count("weight_function") == 1); + CHECK(simColumns.count("weight_threshold") == 1); + CHECK(simColumns.count("error_probability") == 1); + CHECK(simColumns.count("passage_probability") == 1); + CHECK(simColumns.count("mean_travel_distance_m") == 1); + CHECK(simColumns.count("mean_travel_time_s") == 1); + CHECK(simColumns.count("stagnant_tolerance_factor") == 1); + CHECK(simColumns.count("force_priorities") == 1); + CHECK(simColumns.count("save_avg_stats") == 1); + CHECK(simColumns.count("save_road_data") == 1); + CHECK(simColumns.count("save_travel_data") == 1); + + // Check edges table exists + SQLite::Statement edgesQuery( + db, "SELECT name FROM sqlite_master WHERE type='table' AND name='edges'"); + REQUIRE(edgesQuery.executeStep()); + + SQLite::Statement edgesSchema(db, "PRAGMA table_info(edges)"); + std::set edgesColumns; + while (edgesSchema.executeStep()) { + edgesColumns.insert(edgesSchema.getColumn(1).getString()); + } + CHECK(edgesColumns.count("id") == 1); + CHECK(edgesColumns.count("source") == 1); + CHECK(edgesColumns.count("target") == 1); + CHECK(edgesColumns.count("length") == 1); + CHECK(edgesColumns.count("maxspeed") == 1); + CHECK(edgesColumns.count("name") == 1); + CHECK(edgesColumns.count("nlanes") == 1); + CHECK(edgesColumns.count("geometry") == 1); + + // Check nodes table exists + SQLite::Statement nodesQuery( + db, "SELECT name FROM sqlite_master WHERE type='table' AND name='nodes'"); + REQUIRE(nodesQuery.executeStep()); + + SQLite::Statement nodesSchema(db, "PRAGMA table_info(nodes)"); + std::set nodesColumns; + while (nodesSchema.executeStep()) { + nodesColumns.insert(nodesSchema.getColumn(1).getString()); + } + CHECK(nodesColumns.count("id") == 1); + CHECK(nodesColumns.count("type") == 1); + CHECK(nodesColumns.count("geometry") == 1); } + + std::filesystem::remove(testDbPath); } } } diff --git a/test/mobility/Test_graph.cpp b/test/mobility/Test_graph.cpp index 0a69944e..3abb4f4f 100644 --- a/test/mobility/Test_graph.cpp +++ b/test/mobility/Test_graph.cpp @@ -3,7 +3,6 @@ #include "dsf/base/Node.hpp" #include "dsf/mobility/Road.hpp" #include "dsf/mobility/Street.hpp" -#include "dsf/base/AdjacencyMatrix.hpp" #include #include @@ -42,21 +41,6 @@ TEST_CASE("RoadNetwork") { CHECK_EQ(network.nEdges(), 1); CHECK_EQ(network.nNodes(), 2); } - SUBCASE("AdjacencyMatrix Constructor") { - AdjacencyMatrix sm; - sm.insert(0, 1); - sm.insert(1, 0); - sm.insert(1, 2); - sm.insert(2, 3); - sm.insert(3, 2); - RoadNetwork graph{sm}; - CHECK_EQ(graph.nNodes(), 4); - CHECK_EQ(graph.nEdges(), 5); - CHECK(graph.edge(1, 2)); - CHECK(graph.edge(2, 3)); - CHECK(graph.edge(3, 2)); - CHECK_THROWS_AS(graph.edge(2, 1), std::out_of_range); - } SUBCASE("Construction with addEdge") { RoadNetwork graph; diff --git a/webapp/config.json b/webapp/config.json deleted file mode 100644 index f32365ec..00000000 --- a/webapp/config.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "serverRoot": "/path/to/your/server/root", - "edges": "/path/to/your/server/root/edges.csv", - "densities": "/path/to/your/server/root/densities.csv", - "data": "/path/to/your/server/root/data.csv" -} diff --git a/webapp/index.html b/webapp/index.html index e2c79f94..96edb272 100644 --- a/webapp/index.html +++ b/webapp/index.html @@ -21,9 +21,25 @@ + + + + + +
diff --git a/webapp/script.js b/webapp/script.js index ab373564..36904be5 100644 --- a/webapp/script.js +++ b/webapp/script.js @@ -2,6 +2,11 @@ const baseZoom = 13; const map = L.map('map').setView([0, 0], 1); +// Grufoony - 9/2/2026 +// TODO: make this dynamic based on data range +const MAX_DENSITY = 200; +const MAX_DENSITY_INVERTED = 1 / MAX_DENSITY; + // Add OpenStreetMap tile layer with inverted grayscale effect const tileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OSM', @@ -677,6 +682,7 @@ L.CanvasEdges = L.Layer.extend({ // Calculate width based on density let density = this.densities[index] || 0; // Scale width: base width + density factor + density *= MAX_DENSITY_INVERTED; // Assuming density is roughly 0-1, but can be higher. // Let's cap the max width increase to avoid huge lines. const densityFactor = Math.min(density, 2.0); @@ -814,6 +820,8 @@ let timeStamp = new Date(); let highlightedEdge = null; let highlightedNode = null; let chart; +let db = null; +let selectedSimulationId = null; function formatTime(date) { const year = date.getFullYear(); @@ -826,7 +834,7 @@ function formatTime(date) { // Create a color scale for density values using three color stops const colorScale = d3.scaleLinear() - .domain([0, 0.5, 1]) + .domain([0, MAX_DENSITY / 2, MAX_DENSITY]) .range(["green", "yellow", "red"]); // Update node highlight position @@ -867,71 +875,142 @@ function updateEdgeInfo(edge) { `; } -// Auto-load data from config file on page load -async function loadDataFromConfig() { - try { - // Load config file - const configResponse = await fetch('config.json'); - if (!configResponse.ok) { - throw new Error('Could not load config.json'); - } - const config = await configResponse.json(); - - // Convert absolute paths to relative paths for HTTP access - const convertToRelativePath = (absolutePath, serverRoot) => { - if (!absolutePath) return null; - console.log('Converting path:', absolutePath, 'with server root:', serverRoot); - // If already a relative path or URL, use as-is - if (!absolutePath.startsWith('/') || absolutePath.startsWith('http')) { - return absolutePath; - } - - // Remove server root prefix to get path relative to server root - if (absolutePath.startsWith(serverRoot)) { - let relativePath = absolutePath.substring(serverRoot.length); - // Ensure it starts with / - if (relativePath.startsWith('/')) { - // Remove first / - relativePath = relativePath.substring(1); - } - // Add how many ../ are needed based on server root depth - const serverRootDepth = serverRoot.split('/').filter(part => part.length > 0).length; - let prefix = ''; - for (let i = 0; i < serverRootDepth; i++) { - prefix += '../'; - } - relativePath = prefix + relativePath; - // Remove leading / to make it relative - console.log('Converted to relative path:', relativePath.substring(1)); - return relativePath.substring(1); +// Parse geometry from LINESTRING format (for SQL database) +function parseGeometry(geometryStr) { + if (!geometryStr) return []; + const coordsStr = geometryStr.replace(/^LINESTRING\s*\(/, '').replace(/\)$/, ''); + return coordsStr.split(",").map(coordStr => { + const coords = coordStr.trim().split(/\s+/); + return { x: +coords[0], y: +coords[1] }; + }); +} + +// Load edges from SQLite database +function loadEdgesFromDB() { + const result = db.exec("SELECT id, source, target, length, maxspeed, name, nlanes, geometry FROM edges"); + if (result.length === 0) return []; + + const columns = result[0].columns; + const values = result[0].values; + + return values.map(row => { + const edge = {}; + columns.forEach((col, i) => { + edge[col] = row[i]; + }); + edge.geometry = parseGeometry(edge.geometry); + edge.maxspeed = +edge.maxspeed || 0; + edge.nlanes = +edge.nlanes || 1; + edge.length = +edge.length || 0; + return edge; + }); +} + +// Load road_data from SQLite for selected simulation and transform to density format +function loadRoadDataFromDB() { + // Get edge IDs in order + const edgeIds = edges.map(e => e.id); + + // Single query to get all data ordered by datetime and street_id + const result = db.exec( + `SELECT datetime, street_id, density_vpk FROM road_data WHERE simulation_id = ${selectedSimulationId} ORDER BY datetime, street_id` + ); + if (result.length === 0) return []; + + const densityData = []; + let currentTs = null; + let currentMap = {}; + + for (const row of result[0].values) { + const [ts, streetId, density] = row; + if (ts !== currentTs) { + if (currentTs !== null) { + // Build densities array in same order as edges for previous timestamp + const densityArray = edgeIds.map(id => currentMap[id] || 0); + densityData.push({ + datetime: new Date(currentTs), + densities: densityArray + }); } - - // If path doesn't start with server root, return as-is and hope for the best - return absolutePath; - }; + currentTs = ts; + currentMap = {}; + } + currentMap[streetId] = density; + } + + // Handle the last timestamp + if (currentTs !== null) { + const densityArray = edgeIds.map(id => currentMap[id] || 0); + densityData.push({ + datetime: new Date(currentTs), + densities: densityArray + }); + } + + return densityData; +} - // Fetch CSV files using paths from config - const edgesUrl = convertToRelativePath(config.edges, config.serverRoot); - const densitiesUrl = convertToRelativePath(config.densities, config.serverRoot); - const dataUrl = convertToRelativePath(config.data, config.serverRoot); - - // Load CSV data - Promise.all([ - d3.dsv(";", edgesUrl, parseEdges), - d3.dsv(";", densitiesUrl, parseDensity), - dataUrl ? d3.dsv(";", dataUrl, parseData).catch(e => { console.warn('data.csv not found or invalid', e); return []; }) : Promise.resolve([]) - ]).then(([edgesData, densityData, additionalData]) => { - edges = edgesData; - densities = densityData; - globalData = additionalData; - - // console.log("Edges:", edges); - // console.log("Densities:", densities); - - if (!edges.length || !densities.length) { - console.error("Missing CSV data."); - return; - } timeStamp = densities[0].datetime; +// Load global data (aggregated statistics per timestamp) +function loadGlobalDataFromDB() { + // Calculate mean density, avg_speed, etc. per timestamp for selected simulation + const result = db.exec(` + SELECT datetime, + AVG(density_vpk) as mean_density_vpk, + AVG(avg_speed_kph) as mean_speed_kph, + SUM(counts) as total_counts + FROM road_data + WHERE simulation_id = ${selectedSimulationId} + GROUP BY datetime + ORDER BY datetime + `); + + if (result.length === 0) return []; + + const columns = result[0].columns; + const values = result[0].values; + + return values.map(row => { + const data = { datetime: new Date(row[0]) }; + for (let i = 1; i < columns.length; i++) { + data[columns[i]] = +row[i] || 0; + } + return data; + }); +} + +// Get available simulations from database +function getSimulations() { + const result = db.exec("SELECT id, name FROM simulations ORDER BY id"); + if (result.length === 0) return []; + + return result[0].values.map(row => ({ + id: row[0], + name: row[1] || `Simulation ${row[0]}` + })); +} + +// Initialize the app after database and simulation are loaded +function initializeApp() { + // Load data from database + edges = loadEdgesFromDB(); + densities = loadRoadDataFromDB(); + globalData = loadGlobalDataFromDB(); + + console.log("Loaded edges:", edges.length); + console.log("Loaded density timestamps:", densities.length); + console.log("Loaded global data:", globalData.length); + + if (!edges.length) { + alert("No edges found in database."); + return; + } + + if (!densities.length) { + alert(`No road_data found for simulation ID ${selectedSimulationId}.`); + return; + } + + timeStamp = densities[0].datetime; // Calculate median center from edge geometries let allLats = []; @@ -1336,63 +1415,102 @@ async function loadDataFromConfig() { } }); - // Show slider and search - document.querySelector('.slider-container').style.display = 'block'; - - const legendContainer = document.querySelector('.legend-container'); - legendContainer.style.display = 'block'; - }).catch(error => { - console.error("Error loading CSV files:", error); - alert('Error loading data files. Please check the console and verify paths in config.json.'); - }); - } catch (error) { - console.error('Error:', error); - alert('Error loading config file. Please ensure config.json exists in the webapp directory.'); + // Show UI elements + document.querySelector('.slider-container').style.display = 'block'; + document.querySelector('.legend-container').style.display = 'block'; + if (globalData.length > 0) { + document.querySelector('.chart-container').style.display = 'block'; } } -// Load data when page loads -window.addEventListener('DOMContentLoaded', loadDataFromConfig); -function parseEdges(d) { - let geometry = []; - if (d.geometry) { - const coordsStr = d.geometry.replace(/^LINESTRING\s*\(/, '').replace(/\)$/, ''); - geometry = coordsStr.split(",").map(coordStr => { - const coords = coordStr.trim().split(/\s+/); - return { x: +coords[0], y: +coords[1] }; - }); +// Database loading and simulation selection via modal +document.addEventListener('DOMContentLoaded', () => { + const dbFileInput = document.getElementById('dbFileInput'); + const loadDbBtn = document.getElementById('loadDbBtn'); + const dbStatus = document.getElementById('db-status'); + + loadDbBtn.addEventListener('click', async () => { + const file = dbFileInput.files[0]; + if (!file) { + dbStatus.className = 'db-status error'; + dbStatus.textContent = 'Please select a database file.'; + return; + } + + dbStatus.className = 'db-status loading'; + dbStatus.textContent = 'Loading database...'; + loadDbBtn.disabled = true; + + try { + // Initialize sql.js + const SQL = await initSqlJs({ + locateFile: file => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.10.3/${file}` + }); + + // Read the file + const arrayBuffer = await file.arrayBuffer(); + const uint8Array = new Uint8Array(arrayBuffer); + + // Open the database + db = new SQL.Database(uint8Array); + + // Verify tables exist + const tables = db.exec("SELECT name FROM sqlite_master WHERE type='table'"); + const tableNames = tables.length > 0 ? tables[0].values.map(r => r[0]) : []; + + if (!tableNames.includes('edges')) { + throw new Error("Database missing 'edges' table"); } - return { - id: d.id, - source: d.source, - target: d.target, - name: d.name, - maxspeed: +d.maxspeed, - nlanes: +d.nlanes, - geometry: geometry, - coilcode: d.coilcode - }; -} - -// Parsing function for density CSV -function parseDensity(d) { - const datetime = new Date(d.datetime); - const densities = Object.keys(d) - .filter(key => !key.includes('time')) - .map(key => { - const val = d[key] ? d[key].trim() : ""; - return val === "" ? 0 : +val; - }); - return { datetime, densities }; -} - -// Parsing function for data CSV -function parseData(d) { - const result = { datetime: new Date(d.datetime) }; - for (const key in d) { - if (key !== 'datetime') { - result[key] = +d[key]; + if (!tableNames.includes('road_data')) { + throw new Error("Database missing 'road_data' table"); + } + if (!tableNames.includes('simulations')) { + throw new Error("Database missing 'simulations' table"); + } + + // Get available simulations + const simulations = getSimulations(); + + if (simulations.length === 0) { + throw new Error("No simulations found in database"); + } + + dbStatus.className = 'db-status success'; + dbStatus.textContent = `Database loaded! Found ${simulations.length} simulation(s).`; + + // Show simulation selector + setTimeout(() => { + showSimulationSelector(simulations); + }, 500); + + } catch (error) { + console.error('Database loading error:', error); + dbStatus.className = 'db-status error'; + dbStatus.textContent = `Error: ${error.message}`; + loadDbBtn.disabled = false; } + }); + + // Simulation selector function + function showSimulationSelector(simulations) { + const modalContent = document.querySelector('.modal-content'); + + modalContent.innerHTML = ` +

Select Simulation

+

Choose which simulation to visualize:

+
+ +
+
+ + `; + + document.getElementById('loadSimBtn').addEventListener('click', () => { + selectedSimulationId = parseInt(document.getElementById('simulationSelector').value); + document.getElementById('db-modal').classList.add('hidden'); + initializeApp(); + }); } - return result; -} \ No newline at end of file +}); diff --git a/webapp/styles.css b/webapp/styles.css index 5b1bdbf5..218de5f2 100644 --- a/webapp/styles.css +++ b/webapp/styles.css @@ -5,6 +5,104 @@ body, html { height: 100%; } + /* Database modal styles */ + .modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + z-index: 2000; + display: flex; + align-items: center; + justify-content: center; + } + + .modal.hidden { + display: none; + } + + .modal-content { + background: white; + padding: 30px; + border-radius: 10px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + max-width: 500px; + width: 90%; + text-align: center; + } + + .modal-content h2 { + margin-top: 0; + color: #333; + } + + .modal-content p { + color: #666; + margin-bottom: 20px; + } + + .db-input-group { + margin-bottom: 20px; + } + + .db-input-group input[type="file"] { + padding: 10px; + border: 2px dashed #ccc; + border-radius: 5px; + width: 100%; + box-sizing: border-box; + cursor: pointer; + } + + .db-input-group input[type="file"]:hover { + border-color: #4CAF50; + } + + .db-status { + margin-bottom: 15px; + padding: 10px; + border-radius: 5px; + font-size: 14px; + min-height: 20px; + } + + .db-status.error { + background: #ffebee; + color: #c62828; + } + + .db-status.success { + background: #e8f5e9; + color: #2e7d32; + } + + .db-status.loading { + background: #e3f2fd; + color: #1565c0; + } + + .load-db-btn { + background: #4CAF50; + color: white; + border: none; + padding: 12px 30px; + font-size: 16px; + border-radius: 5px; + cursor: pointer; + transition: background 0.3s; + } + + .load-db-btn:hover { + background: #45a049; + } + + .load-db-btn:disabled { + background: #ccc; + cursor: not-allowed; + } + #app-container { width: 100%; height: 100%;