From 618fceaba8d5bdb2b21fdf0cc2bc4f78175ce530 Mon Sep 17 00:00:00 2001 From: Grufoony Date: Mon, 15 Dec 2025 12:53:49 +0100 Subject: [PATCH 01/12] Init --- CMakeLists.txt | 11 ++++++++++- src/dsf/base/Dynamics.hpp | 15 +++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 31824b84..126d2d53 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -138,6 +138,15 @@ if(NOT simdjson_POPULATED) endif() # Check if the user has TBB installed find_package(TBB REQUIRED CONFIG) +# Get SQLiteCpp +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 +160,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/src/dsf/base/Dynamics.hpp b/src/dsf/base/Dynamics.hpp index b2e98f5a..b1a39cfc 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. @@ -46,6 +48,7 @@ namespace dsf { 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 +93,20 @@ 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 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 + /// @return const SQLite::Database&, The database connection + inline auto const& database() const { 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; } From bde3ab8983a2544ec351a55f6f3c99290a60b32b Mon Sep 17 00:00:00 2001 From: Grufoony Date: Thu, 5 Feb 2026 16:53:55 +0100 Subject: [PATCH 02/12] Exclude Jupyter Notebooks --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) 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 From fc080c35f0cadc1a224e8516953aebf19c885d30 Mon Sep 17 00:00:00 2001 From: Grufoony Date: Thu, 5 Feb 2026 17:27:35 +0100 Subject: [PATCH 03/12] Replace CSV output with SQL [WIP] --- examples/slow_charge_rb.cpp | 9 +- examples/slow_charge_tl.cpp | 9 +- src/dsf/base/Dynamics.hpp | 7 +- src/dsf/bindings.cpp | 13 +- src/dsf/mobility/RoadDynamics.hpp | 420 +++++++++++++++--------------- test/mobility/Test_dynamics.cpp | 374 +++++++++++++------------- 6 files changed, 420 insertions(+), 412 deletions(-) diff --git a/examples/slow_charge_rb.cpp b/examples/slow_charge_rb.cpp index 15a39434..57e0b0db 100644 --- a/examples/slow_charge_rb.cpp +++ b/examples/slow_charge_rb.cpp @@ -119,6 +119,9 @@ 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"); + std::cout << "Done." << std::endl; std::cout << "Running simulation...\n"; #ifdef PRINT_FLOWS @@ -176,13 +179,13 @@ int main(int argc, char** argv) { } if (dynamics.time_step() % 300 == 0) { - dynamics.saveCoilCounts(std::format("{}coil_counts.csv", OUT_FOLDER)); + dynamics.saveCoilCounts(); printLoadingBar(dynamics.time_step(), MAX_TIME); - dynamics.saveMacroscopicObservables(std::format("{}data.csv", OUT_FOLDER)); + dynamics.saveMacroscopicObservables(); } if (dynamics.time_step() % 10 == 0) { #ifdef PRINT_DENSITIES - dynamics.saveStreetDensities(OUT_FOLDER + "densities.csv", true); + dynamics.saveStreetDensities(true); #endif #ifdef PRINT_FLOWS streetFlow << dynamics.time_step(); diff --git a/examples/slow_charge_tl.cpp b/examples/slow_charge_tl.cpp index e508eef8..964fe47a 100644 --- a/examples/slow_charge_tl.cpp +++ b/examples/slow_charge_tl.cpp @@ -175,6 +175,9 @@ 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"); + const auto TM = dynamics.turnMapping(); std::cout << "Done." << std::endl; @@ -256,8 +259,8 @@ 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)); + dynamics.saveCoilCounts(); + dynamics.saveMacroscopicObservables(); // deltas.push_back(deltaAgents); // previousAgents = dynamics.agents().size(); #ifdef PRINT_TP @@ -294,7 +297,7 @@ int main(int argc, char** argv) { } if (dynamics.time_step() % 10 == 0) { #ifdef PRINT_DENSITIES - dynamics.saveStreetDensities(OUT_FOLDER + "densities.csv", true); + dynamics.saveStreetDensities(true); #endif #ifdef PRINT_FLOWS streetFlow << ';' << dynamics.time_step(); diff --git a/src/dsf/base/Dynamics.hpp b/src/dsf/base/Dynamics.hpp index b1a39cfc..d7ff1447 100644 --- a/src/dsf/base/Dynamics.hpp +++ b/src/dsf/base/Dynamics.hpp @@ -104,9 +104,12 @@ namespace dsf { /// @brief Get the name of the simulation /// @return const std::string&, The name of the simulation inline auto const& name() const { return m_name; }; - /// @brief Get the database connection - /// @return const SQLite::Database&, The database connection + /// @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; } diff --git a/src/dsf/bindings.cpp b/src/dsf/bindings.cpp index 4a5778e9..e2f2f97b 100644 --- a/src/dsf/bindings.cpp +++ b/src/dsf/bindings.cpp @@ -449,6 +449,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, @@ -686,31 +690,22 @@ PYBIND11_MODULE(dsf_cpp, m) { .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( diff --git a/src/dsf/mobility/RoadDynamics.hpp b/src/dsf/mobility/RoadDynamics.hpp index f0ae7cef..1a12dae5 100644 --- a/src/dsf/mobility/RoadDynamics.hpp +++ b/src/dsf/mobility/RoadDynamics.hpp @@ -356,54 +356,39 @@ 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 ';') + /// @brief Save the street densities to the connected database /// @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 ';') + /// @throw std::runtime_error if no database is connected + void saveStreetDensities(bool const normalized = true) const; + /// @brief Save the street speeds to the connected database /// @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 + /// @throw std::runtime_error if no database is connected + void saveStreetSpeeds(bool bNormalized = false) const; + /// @brief Save the street input counts to the connected database /// @param reset If true, the input counts are cleared after the computation - /// @param separator The separator character (default is ';') + /// @throw std::runtime_error if no database is connected /// @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") + void saveCoilCounts(bool reset = false); + /// @brief Save the travel data of the agents to the connected database /// @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 + /// @throw std::runtime_error if no database is connected + void saveTravelData(bool reset = false); + /// @brief Save the main macroscopic observables to the connected database + /// @throw std::runtime_error if no database is connected + /// @details The table contains the following columns: + /// - datetime: the datetime of the simulation + /// - time_step: the current time step /// - 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 + /// - mean_speed, std_speed (km/h): the mean speed of the agents + /// - mean_density, std_density (veh/km): the mean density of the streets + /// - mean_flow, std_flow (veh/h): the mean flow of the streets + /// - mean_traveltime, std_traveltime (min): the mean travel time of the agents + /// - mean_traveldistance, std_traveldistance (km): the mean travel distance of the agents + /// - mean_travelspeed, std_travelspeed (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 = ';'); + /// NOTE: the mean density is normalized in [0, 1] and travel data is reset after saving + void saveMacroscopicObservables(); /// @brief Print a summary of the dynamics to an output stream /// @param os The output stream to write to (default is std::cout) @@ -2309,198 +2294,191 @@ namespace dsf::mobility { 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(); + void RoadDynamics::saveStreetDensities(bool const normalized) 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 street_densities (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "datetime TEXT NOT NULL, " + "time_step INTEGER NOT NULL, " + "street_id INTEGER NOT NULL, " + "density REAL NOT NULL)"); + + // Begin transaction for better performance + SQLite::Transaction transaction(*this->database()); + SQLite::Statement insertStmt( + *this->database(), + "INSERT INTO street_densities (datetime, time_step, street_id, density) " + "VALUES (?, ?, ?, ?)"); + 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(); + insertStmt.bind(1, this->strDateTime()); + insertStmt.bind(2, static_cast(this->time_step())); + insertStmt.bind(3, static_cast(streetId)); + insertStmt.bind(4, pStreet->density(normalized)); + insertStmt.exec(); + insertStmt.reset(); + } + transaction.commit(); } 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(); + void RoadDynamics::saveStreetSpeeds(bool const bNormalized) 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 street_speeds (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "datetime TEXT NOT NULL, " + "time_step INTEGER NOT NULL, " + "street_id INTEGER NOT NULL, " + "speed REAL, " + "std REAL)"); + + // Begin transaction for better performance + SQLite::Transaction transaction(*this->database()); + SQLite::Statement insertStmt( + *this->database(), + "INSERT INTO street_speeds (datetime, time_step, street_id, speed, std) " + "VALUES (?, ?, ?, ?, ?)"); + 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; - } + insertStmt.bind(1, this->strDateTime()); + insertStmt.bind(2, static_cast(this->time_step())); + insertStmt.bind(3, static_cast(streetId)); - double speed{measure.mean}; - if (bNormalized) { - speed /= pStreet->maxSpeed(); + if (!measure.is_valid) { + // NULL for invalid speeds + insertStmt.bind(4); + insertStmt.bind(5); + } else { + double speed{measure.mean}; + double std{measure.std}; + if (bNormalized) { + speed /= pStreet->maxSpeed(); + std /= pStreet->maxSpeed(); + } + insertStmt.bind(4, speed); + insertStmt.bind(5, std); } - file << std::fixed << std::setprecision(2) << speed; + insertStmt.exec(); + insertStmt.reset(); } - file << std::endl; - file.close(); + transaction.commit(); } 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(); + void RoadDynamics::saveCoilCounts(bool reset) { + 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 coil_counts (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "datetime TEXT NOT NULL, " + "time_step INTEGER NOT NULL, " + "coil_name TEXT NOT NULL, " + "count INTEGER NOT NULL)"); + + // Begin transaction for better performance + SQLite::Transaction transaction(*this->database()); + SQLite::Statement insertStmt( + *this->database(), + "INSERT INTO coil_counts (datetime, time_step, coil_name, count) " + "VALUES (?, ?, ?, ?)"); + for (auto const& [streetId, pStreet] : this->graph().edges()) { if (pStreet->hasCoil()) { - file << separator << pStreet->counts(); + insertStmt.bind(1, this->strDateTime()); + insertStmt.bind(2, static_cast(this->time_step())); + insertStmt.bind(3, pStreet->counterName()); + insertStmt.bind(4, static_cast(pStreet->counts())); + insertStmt.exec(); + insertStmt.reset(); if (reset) { pStreet->resetCounter(); } } } - file << std::endl; - file.close(); + transaction.commit(); } 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); + void RoadDynamics::saveTravelData(bool reset) { + 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, " + "datetime TEXT NOT NULL, " + "time_step INTEGER NOT NULL, " + "distance REAL NOT NULL, " + "time REAL NOT NULL, " + "speed REAL NOT NULL)"); + + // Begin transaction for better performance + SQLite::Transaction transaction(*this->database()); + SQLite::Statement insertStmt( + *this->database(), + "INSERT INTO travel_data (datetime, time_step, distance, time, speed) " + "VALUES (?, ?, ?, ?, ?)"); - 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 += ','; - } + for (auto const& [distance, time] : m_travelDTs) { + insertStmt.bind(1, this->strDateTime()); + insertStmt.bind(2, static_cast(this->time_step())); + insertStmt.bind(3, distance); + insertStmt.bind(4, time); + insertStmt.bind(5, distance / time); + insertStmt.exec(); + insertStmt.reset(); } + transaction.commit(); - // 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; - } + void RoadDynamics::saveMacroscopicObservables() { + 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 macroscopic_observables (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "datetime TEXT NOT NULL, " + "time_step INTEGER NOT NULL, " + "n_ghost_agents INTEGER NOT NULL, " + "n_agents INTEGER NOT NULL, " + "mean_speed_kph REAL NOT NULL, " + "std_speed_kph REAL NOT NULL, " + "mean_density_vpk REAL NOT NULL, " + "std_density_vpk REAL NOT NULL, " + "mean_flow_vph REAL NOT NULL, " + "std_flow_vph REAL NOT NULL, " + "mean_traveltime_m REAL NOT NULL, " + "std_traveltime_m REAL NOT NULL, " + "mean_traveldistance_km REAL NOT NULL, " + "std_traveldistance_km REAL NOT NULL, " + "mean_travelspeed_kph REAL NOT NULL, " + "std_travelspeed_kph REAL NOT NULL)"); + 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.}, @@ -2546,19 +2524,33 @@ namespace dsf::mobility { 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(); + SQLite::Statement insertStmt( + *this->database(), + "INSERT INTO macroscopic_observables (" + "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) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + + insertStmt.bind(1, this->strDateTime()); + insertStmt.bind(2, static_cast(this->time_step())); + insertStmt.bind(3, static_cast(m_agents.size())); + insertStmt.bind(4, static_cast(this->nAgents())); + insertStmt.bind(5, mean_speed); + insertStmt.bind(6, std_speed); + insertStmt.bind(7, mean_density); + insertStmt.bind(8, std_density); + insertStmt.bind(9, mean_flow); + insertStmt.bind(10, std_flow); + insertStmt.bind(11, mean_travel_time); + insertStmt.bind(12, std_travel_time); + insertStmt.bind(13, mean_travel_distance); + insertStmt.bind(14, std_travel_distance); + insertStmt.bind(15, mean_travel_speed); + insertStmt.bind(16, std_travel_speed); + insertStmt.exec(); } template diff --git a/test/mobility/Test_dynamics.cpp b/test/mobility/Test_dynamics.cpp index fda3affe..05627341 100644 --- a/test/mobility/Test_dynamics.cpp +++ b/test/mobility/Test_dynamics.cpp @@ -5,6 +5,8 @@ #include "dsf/mobility/Intersection.hpp" #include "dsf/mobility/Agent.hpp" +#include + #include #include #include @@ -1083,253 +1085,263 @@ TEST_CASE("FirstOrderDynamics") { } } } - SUBCASE("Save functions with default filenames") { + SUBCASE("Save functions 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 saveCoilCounts 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); + const std::string testDbPath = "test_dynamics.db"; + // Remove existing test database if present + std::filesystem::remove(testDbPath); + + WHEN("We call a save function without connecting a database") { + THEN("An exception is thrown") { + CHECK_THROWS_AS(dynamics.saveStreetDensities(), std::runtime_error); + CHECK_THROWS_AS(dynamics.saveStreetSpeeds(), std::runtime_error); + CHECK_THROWS_AS(dynamics.saveCoilCounts(), std::runtime_error); + CHECK_THROWS_AS(dynamics.saveTravelData(), std::runtime_error); + } } - 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); + WHEN("We connect a database and call saveStreetDensities") { + dynamics.connectDataBase(testDbPath); - THEN("The file is created with correct header") { - std::ifstream file(testFile); - REQUIRE(file.is_open()); + // Evolve a few times to generate some data + for (int i = 0; i < 5; ++i) { + dynamics.evolve(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); + dynamics.saveStreetDensities(); - file.close(); - std::filesystem::remove(testFile); + THEN("The street_densities table is created with correct data") { + SQLite::Database db(testDbPath, SQLite::OPEN_READONLY); + SQLite::Statement query(db, "SELECT COUNT(*) FROM street_densities"); + REQUIRE(query.executeStep()); + CHECK(query.getColumn(0).getInt() == 2); // 2 streets + + SQLite::Statement cols(db, "SELECT street_id, density FROM street_densities"); + while (cols.executeStep()) { + auto streetId = cols.getColumn(0).getInt(); + auto density = cols.getColumn(1).getDouble(); + CHECK(streetId >= 0); + CHECK(streetId < 2); + CHECK(density >= 0.0); + } } + + std::filesystem::remove(testDbPath); } - WHEN("We call saveTravelData with default filename") { - const std::string testFile = "test_travel_data.csv"; - dynamics.saveTravelData(testFile); + WHEN("We connect a database and call saveStreetSpeeds") { + dynamics.connectDataBase(testDbPath); - THEN("The file is created with correct header") { - std::ifstream file(testFile); - REQUIRE(file.is_open()); + // Add agents so we have speed data + dynamics.addRandomAgents(5); + for (int i = 0; i < 3; ++i) { + dynamics.evolve(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("distances") != std::string::npos); - CHECK(header.find("times") != std::string::npos); - CHECK(header.find("speeds") != std::string::npos); + dynamics.saveStreetSpeeds(); - file.close(); - std::filesystem::remove(testFile); + THEN("The street_speeds table is created with correct data") { + SQLite::Database db(testDbPath, SQLite::OPEN_READONLY); + SQLite::Statement query(db, "SELECT COUNT(*) FROM street_speeds"); + REQUIRE(query.executeStep()); + CHECK(query.getColumn(0).getInt() == 2); // 2 streets + + SQLite::Statement cols(db, "SELECT street_id, speed FROM street_speeds"); + while (cols.executeStep()) { + auto streetId = cols.getColumn(0).getInt(); + CHECK(streetId >= 0); + CHECK(streetId < 2); + // Speed can be NULL if no agents + } } - } - 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 call saveStreetSpeeds with normalized flag") { + dynamics.connectDataBase(testDbPath); - 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()); + // Add agents so we have speed data + dynamics.addRandomAgents(10); + for (int i = 0; i < 3; ++i) { + dynamics.evolve(false); + } - std::string header; - std::getline(file, header); - CHECK(header.find("datetime") != std::string::npos); - CHECK(header.find("time_step") != std::string::npos); + dynamics.saveStreetSpeeds(true); // normalized - file.close(); - std::filesystem::remove(entry.path()); - break; - } + THEN("The speeds are normalized between 0 and 1") { + SQLite::Database db(testDbPath, SQLite::OPEN_READONLY); + SQLite::Statement query( + db, "SELECT speed FROM street_speeds WHERE speed IS NOT NULL"); + while (query.executeStep()) { + double speed = query.getColumn(0).getDouble(); + CHECK(speed >= 0.0); + CHECK(speed <= 1.0); } - - CHECK(fileFound); } + + std::filesystem::remove(testDbPath); } - 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); + WHEN("We connect a database and call saveCoilCounts") { + dynamics.connectDataBase(testDbPath); - THEN("The file is created with correct header") { - std::ifstream file(testFile); - REQUIRE(file.is_open()); + // Evolve to generate some counts + for (int i = 0; i < 3; ++i) { + dynamics.evolve(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); + dynamics.saveCoilCounts(); - file.close(); - std::filesystem::remove(testFile); + THEN("The coil_counts table is created with correct data") { + SQLite::Database db(testDbPath, SQLite::OPEN_READONLY); + SQLite::Statement query(db, "SELECT COUNT(*) FROM coil_counts"); + REQUIRE(query.executeStep()); + CHECK(query.getColumn(0).getInt() >= 1); // At least one coil + + SQLite::Statement cols(db, "SELECT coil_name, count FROM coil_counts"); + while (cols.executeStep()) { + auto coilName = cols.getColumn(0).getText(); + auto count = cols.getColumn(1).getInt(); + CHECK(!std::string(coilName).empty()); + CHECK(count >= 0); + } } + + std::filesystem::remove(testDbPath); } - WHEN("We call saveStreetSpeeds multiple times to test appending") { - const std::string testFile = "test_street_speeds_append.csv"; + WHEN("We connect a database and call saveCoilCounts with reset") { + dynamics.connectDataBase(testDbPath); - // Add some agents and evolve + // Evolve to generate some counts dynamics.addRandomAgents(5); - dynamics.evolve(false); + for (int i = 0; i < 5; ++i) { + dynamics.evolve(false); + } - // Save first time - dynamics.saveStreetSpeeds(testFile); + dynamics.saveCoilCounts(true); // with reset - // Evolve again and save - dynamics.evolve(false); - dynamics.saveStreetSpeeds(testFile); + THEN("The counter is reset after saving") { + auto const& street = dynamics.graph().edge(0); + CHECK(street->counts() == 0); + } - THEN("The file contains multiple rows with one header") { - std::ifstream file(testFile); - REQUIRE(file.is_open()); + std::filesystem::remove(testDbPath); + } - std::string line; - int lineCount = 0; - int headerCount = 0; + WHEN("We connect a database and call saveTravelData") { + dynamics.connectDataBase(testDbPath); - while (std::getline(file, line)) { - lineCount++; - if (line.find("datetime") != std::string::npos) { - headerCount++; - } - } + // Evolve until agent reaches destination (with limit) + for (int iter = 0; iter < 1000 && dynamics.nAgents() > 0; ++iter) { + dynamics.evolve(false); + } + + dynamics.saveTravelData(); - CHECK_EQ(headerCount, 1); // Only one header - CHECK_EQ(lineCount, 3); // Header + 2 data rows + 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(testFile); + SQLite::Statement cols(db, "SELECT distance, time, speed FROM travel_data"); + while (cols.executeStep()) { + auto distance = cols.getColumn(0).getDouble(); + auto time = cols.getColumn(1).getDouble(); + auto speed = cols.getColumn(2).getDouble(); + CHECK(distance > 0.0); + CHECK(time > 0.0); + CHECK(speed > 0.0); + CHECK(doctest::Approx(speed) == distance / time); + } } + + std::filesystem::remove(testDbPath); } - WHEN("We call saveStreetSpeeds with normalized flag") { - const std::string testFile = "test_street_speeds_normalized.csv"; + WHEN("We connect a database and call saveTravelData with reset") { + dynamics.connectDataBase(testDbPath); - // Add agents and evolve to generate some speeds - dynamics.addRandomAgents(10); - dynamics.evolve(false); - dynamics.evolve(false); + // Evolve until agent reaches destination (with limit) + for (int iter = 0; iter < 1000 && dynamics.nAgents() > 0; ++iter) { + dynamics.evolve(false); + } + + // Check that there is travel data + auto travelTime = dynamics.meanTravelTime(false); + CHECK(travelTime.mean > 0.0); - // Save normalized speeds - dynamics.saveStreetSpeeds(testFile, ',', true); + dynamics.saveTravelData(true); // with reset - THEN("The file is created and speeds are normalized") { - std::ifstream file(testFile); - REQUIRE(file.is_open()); + // After reset, there should be no travel data + auto travelTimeAfter = dynamics.meanTravelTime(false); + CHECK(travelTimeAfter.mean == 0.0); - std::string header; - std::getline(file, header); + std::filesystem::remove(testDbPath); + } - std::string dataLine; - std::getline(file, dataLine); + WHEN("We call save functions multiple times") { + dynamics.connectDataBase(testDbPath); - // 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 + // First save + dynamics.saveStreetDensities(); - // 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); - } - } + // Evolve and save again + dynamics.evolve(false); + dynamics.saveStreetDensities(); - file.close(); - std::filesystem::remove(testFile); + THEN("Multiple rows are inserted") { + SQLite::Database db(testDbPath, SQLite::OPEN_READONLY); + SQLite::Statement query(db, "SELECT COUNT(*) FROM street_densities"); + REQUIRE(query.executeStep()); + CHECK(query.getColumn(0).getInt() == 4); // 2 streets x 2 saves } - } - - WHEN("We call saveStreetSpeeds with empty string (default behavior)") { - // This tests the actual default filename generation - dynamics.saveStreetSpeeds(); - THEN("A file with datetime and name in filename is created") { - // Find the generated file - std::string pattern = "*_street_speeds.csv"; - bool fileFound = false; + std::filesystem::remove(testDbPath); + } - for (const auto& entry : std::filesystem::directory_iterator(".")) { - if (entry.path().filename().string().find("street_speeds.csv") != - std::string::npos) { - fileFound = true; + WHEN("We connect a database and call saveMacroscopicObservables") { + dynamics.connectDataBase(testDbPath); - // Check the file has correct header - std::ifstream file(entry.path()); - REQUIRE(file.is_open()); + // Add agents and evolve until some reach destination (with limit) + dynamics.addRandomAgents(10); + for (int iter = 0; iter < 1000 && dynamics.nAgents() > 0; ++iter) { + dynamics.evolve(false); + } - std::string header; - std::getline(file, header); - CHECK(header.find("datetime") != std::string::npos); - CHECK(header.find("time_step") != std::string::npos); + dynamics.saveMacroscopicObservables(); - file.close(); - std::filesystem::remove(entry.path()); - break; - } - } + THEN("The macroscopic_observables table is created with correct data") { + SQLite::Database db(testDbPath, SQLite::OPEN_READONLY); + SQLite::Statement query(db, "SELECT COUNT(*) FROM macroscopic_observables"); + REQUIRE(query.executeStep()); + CHECK(query.getColumn(0).getInt() == 1); - CHECK(fileFound); + SQLite::Statement cols( + db, + "SELECT n_ghost_agents, n_agents, mean_speed_kph, std_speed_kph, " + "mean_density_vpk, std_density_vpk, mean_flow_vph, std_flow_vph " + "FROM macroscopic_observables"); + REQUIRE(cols.executeStep()); + CHECK(cols.getColumn(0).getInt() >= 0); // n_ghost_agents + CHECK(cols.getColumn(1).getInt() >= 0); // n_agents + // Mean speed should be positive + CHECK(cols.getColumn(2).getDouble() >= 0); // mean_speed_kph } + + std::filesystem::remove(testDbPath); } } } From 2b4f2779f77b34d59196f40a115b84652edde47f Mon Sep 17 00:00:00 2001 From: Grufoony Date: Thu, 5 Feb 2026 17:45:29 +0100 Subject: [PATCH 04/12] Remove streetMeanSpeed functions --- examples/slow_charge_rb.cpp | 48 ------------- examples/slow_charge_tl.cpp | 48 ------------- src/dsf/mobility/FirstOrderDynamics.cpp | 55 --------------- src/dsf/mobility/FirstOrderDynamics.hpp | 15 ---- src/dsf/mobility/RoadDynamics.hpp | 91 +++++++++---------------- test/mobility/Test_dynamics.cpp | 38 ----------- 6 files changed, 33 insertions(+), 262 deletions(-) diff --git a/examples/slow_charge_rb.cpp b/examples/slow_charge_rb.cpp index 57e0b0db..971157a5 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; @@ -124,22 +122,6 @@ int main(int argc, char** argv) { 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}; @@ -186,40 +168,10 @@ int main(int argc, char** argv) { if (dynamics.time_step() % 10 == 0) { #ifdef PRINT_DENSITIES dynamics.saveStreetDensities(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 } ++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 964fe47a..75b98347 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; @@ -182,22 +180,6 @@ int main(int argc, char** argv) { 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"; @@ -298,40 +280,10 @@ int main(int argc, char** argv) { if (dynamics.time_step() % 10 == 0) { #ifdef PRINT_DENSITIES dynamics.saveStreetDensities(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 } ++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/mobility/FirstOrderDynamics.cpp b/src/dsf/mobility/FirstOrderDynamics.cpp index 42021128..d7143a34 100644 --- a/src/dsf/mobility/FirstOrderDynamics.cpp +++ b/src/dsf/mobility/FirstOrderDynamics.cpp @@ -56,59 +56,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..8ffc8331 100644 --- a/src/dsf/mobility/FirstOrderDynamics.hpp +++ b/src/dsf/mobility/FirstOrderDynamics.hpp @@ -33,20 +33,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 1a12dae5..6b9496cf 100644 --- a/src/dsf/mobility/RoadDynamics.hpp +++ b/src/dsf/mobility/RoadDynamics.hpp @@ -75,6 +75,11 @@ namespace dsf::mobility { std::optional m_dataUpdatePeriod; bool m_bCacheEnabled; bool m_forcePriorities{false}; + // Saving variables + std::time_t m_savingInterval{0}; + bool m_bSaveStreetSpeeds{false}; + bool m_bSaveStreetDensities{false}; + bool m_bSaveMacroscopicObservables{false}; private: /// @brief Kill an agent @@ -341,9 +346,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; @@ -2196,48 +2198,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 { @@ -2271,7 +2231,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); } @@ -2283,10 +2246,14 @@ 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); @@ -2485,24 +2452,32 @@ namespace dsf::mobility { std_travel_time{0.}, std_travel_speed{0.}; auto const& nEdges{this->graph().nEdges()}; auto const& nData{m_travelDTs.size()}; + std::size_t nValidSpeeds{0}; for (auto const& [streetId, pStreet] : this->graph().edges()) { - auto const& speed{this->streetMeanSpeed(streetId) * 3.6}; + auto const speedMeasure = pStreet->meanSpeed(); auto const& density{pStreet->density() * 1e3}; - auto const& flow{density * speed}; - mean_speed += speed; + if (speedMeasure.is_valid) { + auto const speed = speedMeasure.mean * 3.6; // to kph + auto const speed_std = speedMeasure.std * 3.6; + mean_speed += speed; + std_speed += speed * speed + speed_std * speed_std; + + auto const& flow{density * speed}; + mean_flow += flow; + std_flow += speed_std; + + ++nValidSpeeds; + } mean_density += density; - mean_flow += flow; - std_speed += speed * speed; std_density += density * density; - std_flow += flow * flow; } - mean_speed /= nEdges; + mean_speed /= nValidSpeeds; mean_density /= nEdges; - mean_flow /= nEdges; - std_speed = std::sqrt(std_speed / nEdges - mean_speed * mean_speed); + mean_flow /= nValidSpeeds; + std_speed = std::sqrt(std_speed / nValidSpeeds - 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); + std_flow = std::sqrt(std_flow / nValidSpeeds - mean_flow * mean_flow); for (auto const& [distance, time] : m_travelDTs) { mean_travel_distance += distance * 1e-3; diff --git a/test/mobility/Test_dynamics.cpp b/test/mobility/Test_dynamics.cpp index 05627341..81edded8 100644 --- a/test/mobility/Test_dynamics.cpp +++ b/test/mobility/Test_dynamics.cpp @@ -959,44 +959,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; From 23b1adceb88b4cf563376925da651d6ab2c2f834 Mon Sep 17 00:00:00 2001 From: Grufoony Date: Fri, 6 Feb 2026 11:44:06 +0100 Subject: [PATCH 05/12] One save function to rule them all --- examples/slow_charge_rb.cpp | 16 +- examples/slow_charge_tl.cpp | 16 +- src/dsf/bindings.cpp | 35 +- src/dsf/mobility/RoadDynamics.hpp | 566 ++++++++++++++---------------- test/mobility/Test_dynamics.cpp | 234 +++--------- 5 files changed, 342 insertions(+), 525 deletions(-) diff --git a/examples/slow_charge_rb.cpp b/examples/slow_charge_rb.cpp index 971157a5..908e5dd2 100644 --- a/examples/slow_charge_rb.cpp +++ b/examples/slow_charge_rb.cpp @@ -120,6 +120,13 @@ int main(int argc, char** argv) { // 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"; @@ -161,15 +168,10 @@ int main(int argc, char** argv) { } if (dynamics.time_step() % 300 == 0) { - dynamics.saveCoilCounts(); + // Data is now saved automatically by saveData() configuration printLoadingBar(dynamics.time_step(), MAX_TIME); - dynamics.saveMacroscopicObservables(); - } - if (dynamics.time_step() % 10 == 0) { -#ifdef PRINT_DENSITIES - dynamics.saveStreetDensities(true); -#endif } + // Street densities are now saved automatically by saveData() configuration ++progress; } // std::cout << std::endl; diff --git a/examples/slow_charge_tl.cpp b/examples/slow_charge_tl.cpp index 75b98347..b4b7303b 100644 --- a/examples/slow_charge_tl.cpp +++ b/examples/slow_charge_tl.cpp @@ -176,6 +176,13 @@ int main(int argc, char** argv) { // 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; @@ -241,8 +248,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(); - dynamics.saveMacroscopicObservables(); + // Data is now saved automatically by saveData() configuration // deltas.push_back(deltaAgents); // previousAgents = dynamics.agents().size(); #ifdef PRINT_TP @@ -277,11 +283,7 @@ int main(int argc, char** argv) { outTP << std::endl; #endif } - if (dynamics.time_step() % 10 == 0) { -#ifdef PRINT_DENSITIES - dynamics.saveStreetDensities(true); -#endif - } + // Street densities are now saved automatically by saveData() configuration ++progress; } // std::cout << std::endl; diff --git a/src/dsf/bindings.cpp b/src/dsf/bindings.cpp index e2f2f97b..2ae61bef 100644 --- a/src/dsf/bindings.cpp +++ b/src/dsf/bindings.cpp @@ -687,27 +687,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("normalized") = true, - dsf::g_docstrings.at("dsf::mobility::RoadDynamics::saveStreetDensities").c_str()) - .def("saveStreetSpeeds", - &dsf::mobility::FirstOrderDynamics::saveStreetSpeeds, - pybind11::arg("normalized") = false, - dsf::g_docstrings.at("dsf::mobility::RoadDynamics::saveStreetSpeeds").c_str()) - .def("saveCoilCounts", - &dsf::mobility::FirstOrderDynamics::saveCoilCounts, - pybind11::arg("reset") = false, - dsf::g_docstrings.at("dsf::mobility::RoadDynamics::saveCoilCounts").c_str()) - .def("saveTravelData", - &dsf::mobility::FirstOrderDynamics::saveTravelData, - pybind11::arg("reset") = false, - dsf::g_docstrings.at("dsf::mobility::RoadDynamics::saveTravelData").c_str()) - .def("saveMacroscopicObservables", - &dsf::mobility::FirstOrderDynamics::saveMacroscopicObservables, - 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/mobility/RoadDynamics.hpp b/src/dsf/mobility/RoadDynamics.hpp index 6b9496cf..624fa6b1 100644 --- a/src/dsf/mobility/RoadDynamics.hpp +++ b/src/dsf/mobility/RoadDynamics.hpp @@ -77,9 +77,9 @@ namespace dsf::mobility { bool m_forcePriorities{false}; // Saving variables std::time_t m_savingInterval{0}; - bool m_bSaveStreetSpeeds{false}; - bool m_bSaveStreetDensities{false}; - bool m_bSaveMacroscopicObservables{false}; + bool m_bSaveStreetData{false}; + bool m_bSaveTravelData{false}; + bool m_bSaveAverageStats{false}; private: /// @brief Kill an agent @@ -116,6 +116,12 @@ namespace dsf::mobility { virtual double m_streetEstimatedTravelTime( std::unique_ptr const& pStreet) const = 0; + void m_initStreetTable(); + + void m_initAvgStatsTable(); + + void m_initTravelDataTable(); + public: /// @brief Construct a new RoadDynamics object /// @param graph The graph representing the network @@ -200,6 +206,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 @@ -358,40 +369,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 to the connected database - /// @param normalized If true, the densities are normalized in [0, 1] dividing by the street capacity attribute - /// @throw std::runtime_error if no database is connected - void saveStreetDensities(bool const normalized = true) const; - /// @brief Save the street speeds to the connected database - /// @param bNormalized If true, the speeds are normalized in [0, 1] dividing by the street maxSpeed attribute - /// @throw std::runtime_error if no database is connected - void saveStreetSpeeds(bool bNormalized = false) const; - /// @brief Save the street input counts to the connected database - /// @param reset If true, the input counts are cleared after the computation - /// @throw std::runtime_error if no database is connected - /// @details NOTE: counts are saved only if the street has a coil on it - void saveCoilCounts(bool reset = false); - /// @brief Save the travel data of the agents to the connected database - /// @param reset If true, the travel speeds are cleared after the computation - /// @throw std::runtime_error if no database is connected - void saveTravelData(bool reset = false); - /// @brief Save the main macroscopic observables to the connected database - /// @throw std::runtime_error if no database is connected - /// @details The table contains the following columns: - /// - datetime: the datetime of the simulation - /// - time_step: the current time step - /// - 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, std_speed (km/h): the mean speed of the agents - /// - mean_density, std_density (veh/km): the mean density of the streets - /// - mean_flow, std_flow (veh/h): the mean flow of the streets - /// - mean_traveltime, std_traveltime (min): the mean travel time of the agents - /// - mean_traveldistance, std_traveldistance (km): the mean travel distance of the agents - /// - mean_travelspeed, std_travelspeed (km/h): the mean travel speed of the agents - /// - /// NOTE: the mean density is normalized in [0, 1] and travel data is reset after saving - void saveMacroscopicObservables(); - /// @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: @@ -1075,6 +1052,69 @@ namespace dsf::mobility { spdlog::debug("There are {} agents left in the list.", m_agents.size()); } + template + requires(is_numeric_v) + void RoadDynamics::m_initStreetTable() { + 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, " + "datetime TEXT NOT NULL, " + "time_step INTEGER NOT NULL, " + "street_id INTEGER NOT NULL, " + "coil TEXT, " + "density REAL, " + "avg_speed REAL, " + "std_speed REAL, " + "counts INTEGER)"); + + spdlog::info("Initialized road_data table in the database."); + } + template + requires(is_numeric_v) + void RoadDynamics::m_initAvgStatsTable() { + 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, " + "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() { + 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, " + "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::setErrorProbability(double errorProbability) { @@ -1220,6 +1260,37 @@ 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(); + } + + 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( @@ -1589,20 +1660,38 @@ 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; + }; + 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)}; @@ -1621,6 +1710,42 @@ 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 += speed; + std_speed += speed * speed + speed_std * speed_std; + + ++nValidEdges; + } + } + if (m_bSaveAverageStats) { + mean_density += density; + std_density += density * density; + } + + 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; + } + streetDataRecords.push_back(record); + } + } } } }); @@ -1641,7 +1766,97 @@ 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, street_id, " + "coil, density, avg_speed, std_speed, counts) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); + + for (auto const& record : streetDataRecords) { + insertStmt.bind(1, this->strDateTime()); + insertStmt.bind(2, static_cast(this->time_step())); + insertStmt.bind(3, static_cast(record.streetId)); + if (record.coilName.has_value()) { + insertStmt.bind(4, record.coilName.value()); + } else { + insertStmt.bind(4); + } + insertStmt.bind(5, record.density); + if (record.avgSpeed.has_value()) { + insertStmt.bind(6, record.avgSpeed.value()); + insertStmt.bind(7, record.stdSpeed.value()); + } else { + insertStmt.bind(6); + insertStmt.bind(7); + } + if (record.counts.has_value()) { + insertStmt.bind(8, static_cast(record.counts.value())); + } else { + insertStmt.bind(8); + } + 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, 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, distance); + insertStmt.bind(4, 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 (" + "datetime, time_step, n_ghost_agents, n_agents, " + "mean_speed_kph, std_speed_kph, mean_density_vpk, std_density_vpk) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); + insertStmt.bind(1, this->strDateTime()); + insertStmt.bind(2, static_cast(this->time_step())); + insertStmt.bind(3, static_cast(m_agents.size())); + insertStmt.bind(4, static_cast(this->nAgents())); + insertStmt.bind(5, mean_speed); + insertStmt.bind(6, std_speed); + insertStmt.bind(7, mean_density); + insertStmt.bind(8, std_density); + insertStmt.exec(); + } + } + Dynamics::m_evolve(); } @@ -2259,275 +2474,6 @@ namespace dsf::mobility { return Measurement(flows); } - template - requires(is_numeric_v) - void RoadDynamics::saveStreetDensities(bool const normalized) 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 street_densities (" - "id INTEGER PRIMARY KEY AUTOINCREMENT, " - "datetime TEXT NOT NULL, " - "time_step INTEGER NOT NULL, " - "street_id INTEGER NOT NULL, " - "density REAL NOT NULL)"); - - // Begin transaction for better performance - SQLite::Transaction transaction(*this->database()); - SQLite::Statement insertStmt( - *this->database(), - "INSERT INTO street_densities (datetime, time_step, street_id, density) " - "VALUES (?, ?, ?, ?)"); - - for (auto const& [streetId, pStreet] : this->graph().edges()) { - insertStmt.bind(1, this->strDateTime()); - insertStmt.bind(2, static_cast(this->time_step())); - insertStmt.bind(3, static_cast(streetId)); - insertStmt.bind(4, pStreet->density(normalized)); - insertStmt.exec(); - insertStmt.reset(); - } - transaction.commit(); - } - template - requires(is_numeric_v) - void RoadDynamics::saveStreetSpeeds(bool const bNormalized) 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 street_speeds (" - "id INTEGER PRIMARY KEY AUTOINCREMENT, " - "datetime TEXT NOT NULL, " - "time_step INTEGER NOT NULL, " - "street_id INTEGER NOT NULL, " - "speed REAL, " - "std REAL)"); - - // Begin transaction for better performance - SQLite::Transaction transaction(*this->database()); - SQLite::Statement insertStmt( - *this->database(), - "INSERT INTO street_speeds (datetime, time_step, street_id, speed, std) " - "VALUES (?, ?, ?, ?, ?)"); - - for (auto const& [streetId, pStreet] : this->graph().edges()) { - auto const measure = pStreet->meanSpeed(true); - insertStmt.bind(1, this->strDateTime()); - insertStmt.bind(2, static_cast(this->time_step())); - insertStmt.bind(3, static_cast(streetId)); - - if (!measure.is_valid) { - // NULL for invalid speeds - insertStmt.bind(4); - insertStmt.bind(5); - } else { - double speed{measure.mean}; - double std{measure.std}; - if (bNormalized) { - speed /= pStreet->maxSpeed(); - std /= pStreet->maxSpeed(); - } - insertStmt.bind(4, speed); - insertStmt.bind(5, std); - } - insertStmt.exec(); - insertStmt.reset(); - } - transaction.commit(); - } - template - requires(is_numeric_v) - void RoadDynamics::saveCoilCounts(bool reset) { - 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 coil_counts (" - "id INTEGER PRIMARY KEY AUTOINCREMENT, " - "datetime TEXT NOT NULL, " - "time_step INTEGER NOT NULL, " - "coil_name TEXT NOT NULL, " - "count INTEGER NOT NULL)"); - - // Begin transaction for better performance - SQLite::Transaction transaction(*this->database()); - SQLite::Statement insertStmt( - *this->database(), - "INSERT INTO coil_counts (datetime, time_step, coil_name, count) " - "VALUES (?, ?, ?, ?)"); - - for (auto const& [streetId, pStreet] : this->graph().edges()) { - if (pStreet->hasCoil()) { - insertStmt.bind(1, this->strDateTime()); - insertStmt.bind(2, static_cast(this->time_step())); - insertStmt.bind(3, pStreet->counterName()); - insertStmt.bind(4, static_cast(pStreet->counts())); - insertStmt.exec(); - insertStmt.reset(); - if (reset) { - pStreet->resetCounter(); - } - } - } - transaction.commit(); - } - template - requires(is_numeric_v) - void RoadDynamics::saveTravelData(bool reset) { - 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, " - "datetime TEXT NOT NULL, " - "time_step INTEGER NOT NULL, " - "distance REAL NOT NULL, " - "time REAL NOT NULL, " - "speed REAL NOT NULL)"); - - // Begin transaction for better performance - SQLite::Transaction transaction(*this->database()); - SQLite::Statement insertStmt( - *this->database(), - "INSERT INTO travel_data (datetime, time_step, distance, time, speed) " - "VALUES (?, ?, ?, ?, ?)"); - - for (auto const& [distance, time] : m_travelDTs) { - insertStmt.bind(1, this->strDateTime()); - insertStmt.bind(2, static_cast(this->time_step())); - insertStmt.bind(3, distance); - insertStmt.bind(4, time); - insertStmt.bind(5, distance / time); - insertStmt.exec(); - insertStmt.reset(); - } - transaction.commit(); - - if (reset) { - m_travelDTs.clear(); - } - } - template - requires(is_numeric_v) - void RoadDynamics::saveMacroscopicObservables() { - 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 macroscopic_observables (" - "id INTEGER PRIMARY KEY AUTOINCREMENT, " - "datetime TEXT NOT NULL, " - "time_step INTEGER NOT NULL, " - "n_ghost_agents INTEGER NOT NULL, " - "n_agents INTEGER NOT NULL, " - "mean_speed_kph REAL NOT NULL, " - "std_speed_kph REAL NOT NULL, " - "mean_density_vpk REAL NOT NULL, " - "std_density_vpk REAL NOT NULL, " - "mean_flow_vph REAL NOT NULL, " - "std_flow_vph REAL NOT NULL, " - "mean_traveltime_m REAL NOT NULL, " - "std_traveltime_m REAL NOT NULL, " - "mean_traveldistance_km REAL NOT NULL, " - "std_traveldistance_km REAL NOT NULL, " - "mean_travelspeed_kph REAL NOT NULL, " - "std_travelspeed_kph REAL NOT NULL)"); - - 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()}; - std::size_t nValidSpeeds{0}; - - for (auto const& [streetId, pStreet] : this->graph().edges()) { - auto const speedMeasure = pStreet->meanSpeed(); - auto const& density{pStreet->density() * 1e3}; - if (speedMeasure.is_valid) { - auto const speed = speedMeasure.mean * 3.6; // to kph - auto const speed_std = speedMeasure.std * 3.6; - mean_speed += speed; - std_speed += speed * speed + speed_std * speed_std; - - auto const& flow{density * speed}; - mean_flow += flow; - std_flow += speed_std; - - ++nValidSpeeds; - } - mean_density += density; - std_density += density * density; - } - mean_speed /= nValidSpeeds; - mean_density /= nEdges; - mean_flow /= nValidSpeeds; - std_speed = std::sqrt(std_speed / nValidSpeeds - mean_speed * mean_speed); - std_density = std::sqrt(std_density / nEdges - mean_density * mean_density); - std_flow = std::sqrt(std_flow / nValidSpeeds - 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); - - SQLite::Statement insertStmt( - *this->database(), - "INSERT INTO macroscopic_observables (" - "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) " - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); - - insertStmt.bind(1, this->strDateTime()); - insertStmt.bind(2, static_cast(this->time_step())); - insertStmt.bind(3, static_cast(m_agents.size())); - insertStmt.bind(4, static_cast(this->nAgents())); - insertStmt.bind(5, mean_speed); - insertStmt.bind(6, std_speed); - insertStmt.bind(7, mean_density); - insertStmt.bind(8, std_density); - insertStmt.bind(9, mean_flow); - insertStmt.bind(10, std_flow); - insertStmt.bind(11, mean_travel_time); - insertStmt.bind(12, std_travel_time); - insertStmt.bind(13, mean_travel_distance); - insertStmt.bind(14, std_travel_distance); - insertStmt.bind(15, mean_travel_speed); - insertStmt.bind(16, std_travel_speed); - insertStmt.exec(); - } - template requires(is_numeric_v) void RoadDynamics::summary(std::ostream& os) const { diff --git a/test/mobility/Test_dynamics.cpp b/test/mobility/Test_dynamics.cpp index 81edded8..deccb3aa 100644 --- a/test/mobility/Test_dynamics.cpp +++ b/test/mobility/Test_dynamics.cpp @@ -1047,13 +1047,13 @@ TEST_CASE("FirstOrderDynamics") { } } } - SUBCASE("Save functions to database") { + 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 saveCoilCounts + 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(); @@ -1063,244 +1063,118 @@ TEST_CASE("FirstOrderDynamics") { // Remove existing test database if present std::filesystem::remove(testDbPath); - WHEN("We call a save function without connecting a database") { - THEN("An exception is thrown") { - CHECK_THROWS_AS(dynamics.saveStreetDensities(), std::runtime_error); - CHECK_THROWS_AS(dynamics.saveStreetSpeeds(), std::runtime_error); - CHECK_THROWS_AS(dynamics.saveCoilCounts(), std::runtime_error); - CHECK_THROWS_AS(dynamics.saveTravelData(), std::runtime_error); - } - } - - WHEN("We connect a database and call saveStreetDensities") { + 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); - // Evolve a few times to generate some data + // Evolve a few times to generate and save data for (int i = 0; i < 5; ++i) { - dynamics.evolve(false); + dynamics.evolve(true); } - dynamics.saveStreetDensities(); - - THEN("The street_densities table is created with correct data") { + THEN("The road_data table is created with correct data") { SQLite::Database db(testDbPath, SQLite::OPEN_READONLY); - SQLite::Statement query(db, "SELECT COUNT(*) FROM street_densities"); + SQLite::Statement query(db, "SELECT COUNT(*) FROM road_data"); REQUIRE(query.executeStep()); - CHECK(query.getColumn(0).getInt() == 2); // 2 streets + CHECK(query.getColumn(0).getInt() >= 2); // At least 2 streets - SQLite::Statement cols(db, "SELECT street_id, density FROM street_densities"); + SQLite::Statement cols(db, + "SELECT street_id, density, avg_speed FROM road_data"); while (cols.executeStep()) { auto streetId = cols.getColumn(0).getInt(); - auto density = cols.getColumn(1).getDouble(); CHECK(streetId >= 0); CHECK(streetId < 2); - CHECK(density >= 0.0); - } - } - - std::filesystem::remove(testDbPath); - } - - WHEN("We connect a database and call saveStreetSpeeds") { - dynamics.connectDataBase(testDbPath); - - // Add agents so we have speed data - dynamics.addRandomAgents(5); - for (int i = 0; i < 3; ++i) { - dynamics.evolve(false); - } - - dynamics.saveStreetSpeeds(); - - THEN("The street_speeds table is created with correct data") { - SQLite::Database db(testDbPath, SQLite::OPEN_READONLY); - SQLite::Statement query(db, "SELECT COUNT(*) FROM street_speeds"); - REQUIRE(query.executeStep()); - CHECK(query.getColumn(0).getInt() == 2); // 2 streets - - SQLite::Statement cols(db, "SELECT street_id, speed FROM street_speeds"); - while (cols.executeStep()) { - auto streetId = cols.getColumn(0).getInt(); - CHECK(streetId >= 0); - CHECK(streetId < 2); - // Speed can be NULL if no agents - } - } - - std::filesystem::remove(testDbPath); - } - - WHEN("We connect a database and call saveStreetSpeeds with normalized flag") { - dynamics.connectDataBase(testDbPath); - - // Add agents so we have speed data - dynamics.addRandomAgents(10); - for (int i = 0; i < 3; ++i) { - dynamics.evolve(false); - } - - dynamics.saveStreetSpeeds(true); // normalized - - THEN("The speeds are normalized between 0 and 1") { - SQLite::Database db(testDbPath, SQLite::OPEN_READONLY); - SQLite::Statement query( - db, "SELECT speed FROM street_speeds WHERE speed IS NOT NULL"); - while (query.executeStep()) { - double speed = query.getColumn(0).getDouble(); - CHECK(speed >= 0.0); - CHECK(speed <= 1.0); } } std::filesystem::remove(testDbPath); } - WHEN("We connect a database and call saveCoilCounts") { - dynamics.connectDataBase(testDbPath); - - // Evolve to generate some counts - for (int i = 0; i < 3; ++i) { - dynamics.evolve(false); - } - - dynamics.saveCoilCounts(); - - THEN("The coil_counts table is created with correct data") { - SQLite::Database db(testDbPath, SQLite::OPEN_READONLY); - SQLite::Statement query(db, "SELECT COUNT(*) FROM coil_counts"); - REQUIRE(query.executeStep()); - CHECK(query.getColumn(0).getInt() >= 1); // At least one coil - - SQLite::Statement cols(db, "SELECT coil_name, count FROM coil_counts"); - while (cols.executeStep()) { - auto coilName = cols.getColumn(0).getText(); - auto count = cols.getColumn(1).getInt(); - CHECK(!std::string(coilName).empty()); - CHECK(count >= 0); - } - } - - std::filesystem::remove(testDbPath); - } - - WHEN("We connect a database and call saveCoilCounts with reset") { - dynamics.connectDataBase(testDbPath); - - // Evolve to generate some counts - dynamics.addRandomAgents(5); - for (int i = 0; i < 5; ++i) { - dynamics.evolve(false); - } - - dynamics.saveCoilCounts(true); // with reset - - THEN("The counter is reset after saving") { - auto const& street = dynamics.graph().edge(0); - CHECK(street->counts() == 0); - } - - std::filesystem::remove(testDbPath); - } - - WHEN("We connect a database and call saveTravelData") { + 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); // Evolve until agent reaches destination (with limit) for (int iter = 0; iter < 1000 && dynamics.nAgents() > 0; ++iter) { - dynamics.evolve(false); + dynamics.evolve(true); } - dynamics.saveTravelData(); - 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 - SQLite::Statement cols(db, "SELECT distance, time, speed FROM travel_data"); + 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(); - auto speed = cols.getColumn(2).getDouble(); CHECK(distance > 0.0); CHECK(time > 0.0); - CHECK(speed > 0.0); - CHECK(doctest::Approx(speed) == distance / time); } } std::filesystem::remove(testDbPath); } - WHEN("We connect a database and call saveTravelData with reset") { + 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); - // Evolve until agent reaches destination (with limit) - for (int iter = 0; iter < 1000 && dynamics.nAgents() > 0; ++iter) { - dynamics.evolve(false); + // Add agents and evolve + dynamics.addRandomAgents(10); + for (int iter = 0; iter < 10; ++iter) { + dynamics.evolve(true); } - // Check that there is travel data - auto travelTime = dynamics.meanTravelTime(false); - CHECK(travelTime.mean > 0.0); - - dynamics.saveTravelData(true); // with reset - - // After reset, there should be no travel data - auto travelTimeAfter = dynamics.meanTravelTime(false); - CHECK(travelTimeAfter.mean == 0.0); - - std::filesystem::remove(testDbPath); - } - - WHEN("We call save functions multiple times") { - dynamics.connectDataBase(testDbPath); - - // First save - dynamics.saveStreetDensities(); - - // Evolve and save again - dynamics.evolve(false); - dynamics.saveStreetDensities(); - - THEN("Multiple rows are inserted") { + THEN("The avg_stats table is created with correct data") { SQLite::Database db(testDbPath, SQLite::OPEN_READONLY); - SQLite::Statement query(db, "SELECT COUNT(*) FROM street_densities"); + SQLite::Statement query(db, "SELECT COUNT(*) FROM avg_stats"); REQUIRE(query.executeStep()); - CHECK(query.getColumn(0).getInt() == 4); // 2 streets x 2 saves + CHECK(query.getColumn(0).getInt() >= 1); + + 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 connect a database and call saveMacroscopicObservables") { + 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 until some reach destination (with limit) + // Add agents and evolve until some reach destination dynamics.addRandomAgents(10); for (int iter = 0; iter < 1000 && dynamics.nAgents() > 0; ++iter) { - dynamics.evolve(false); + dynamics.evolve(true); } - dynamics.saveMacroscopicObservables(); - - THEN("The macroscopic_observables table is created with correct data") { + THEN("All tables are created") { SQLite::Database db(testDbPath, SQLite::OPEN_READONLY); - SQLite::Statement query(db, "SELECT COUNT(*) FROM macroscopic_observables"); - REQUIRE(query.executeStep()); - CHECK(query.getColumn(0).getInt() == 1); - SQLite::Statement cols( - db, - "SELECT n_ghost_agents, n_agents, mean_speed_kph, std_speed_kph, " - "mean_density_vpk, std_density_vpk, mean_flow_vph, std_flow_vph " - "FROM macroscopic_observables"); - REQUIRE(cols.executeStep()); - CHECK(cols.getColumn(0).getInt() >= 0); // n_ghost_agents - CHECK(cols.getColumn(1).getInt() >= 0); // n_agents - // Mean speed should be positive - CHECK(cols.getColumn(2).getDouble() >= 0); // mean_speed_kph + SQLite::Statement roadQuery(db, "SELECT COUNT(*) FROM road_data"); + REQUIRE(roadQuery.executeStep()); + CHECK(roadQuery.getColumn(0).getInt() >= 1); + + SQLite::Statement avgQuery(db, "SELECT COUNT(*) FROM avg_stats"); + REQUIRE(avgQuery.executeStep()); + CHECK(avgQuery.getColumn(0).getInt() >= 1); + + SQLite::Statement travelQuery(db, "SELECT COUNT(*) FROM travel_data"); + REQUIRE(travelQuery.executeStep()); + CHECK(travelQuery.getColumn(0).getInt() >= 1); } std::filesystem::remove(testDbPath); From f24decb47b74b5fe7c97e451df382906e3e128d4 Mon Sep 17 00:00:00 2001 From: Grufoony Date: Fri, 6 Feb 2026 12:51:52 +0100 Subject: [PATCH 06/12] Add simulation id to keep using the same database --- examples/slow_charge_rb.cpp | 7 +-- examples/slow_charge_tl.cpp | 7 +-- src/dsf/base/Dynamics.hpp | 18 ++++++ src/dsf/mobility/FirstOrderDynamics.cpp | 79 +++++++++++++++++++++++ src/dsf/mobility/FirstOrderDynamics.hpp | 2 + src/dsf/mobility/RoadDynamics.hpp | 84 ++++++++++++++----------- 6 files changed, 153 insertions(+), 44 deletions(-) diff --git a/examples/slow_charge_rb.cpp b/examples/slow_charge_rb.cpp index 908e5dd2..a3ebe053 100644 --- a/examples/slow_charge_rb.cpp +++ b/examples/slow_charge_rb.cpp @@ -66,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{}; diff --git a/examples/slow_charge_tl.cpp b/examples/slow_charge_tl.cpp index b4b7303b..4b4c51de 100644 --- a/examples/slow_charge_tl.cpp +++ b/examples/slow_charge_tl.cpp @@ -72,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{}; diff --git a/src/dsf/base/Dynamics.hpp b/src/dsf/base/Dynamics.hpp index d7ff1447..b474fa3d 100644 --- a/src/dsf/base/Dynamics.hpp +++ b/src/dsf/base/Dynamics.hpp @@ -45,6 +45,7 @@ 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; @@ -101,6 +102,9 @@ namespace dsf { /// @brief Get the graph /// @return const network_t&, The 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 auto const& name() const { return m_name; }; @@ -140,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/mobility/FirstOrderDynamics.cpp b/src/dsf/mobility/FirstOrderDynamics.cpp index d7143a34..f8fbba53 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, *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, diff --git a/src/dsf/mobility/FirstOrderDynamics.hpp b/src/dsf/mobility/FirstOrderDynamics.hpp index 8ffc8331..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 diff --git a/src/dsf/mobility/RoadDynamics.hpp b/src/dsf/mobility/RoadDynamics.hpp index 624fa6b1..8f39f5e3 100644 --- a/src/dsf/mobility/RoadDynamics.hpp +++ b/src/dsf/mobility/RoadDynamics.hpp @@ -64,16 +64,17 @@ 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}; @@ -116,11 +117,13 @@ namespace dsf::mobility { virtual double m_streetEstimatedTravelTime( std::unique_ptr const& pStreet) const = 0; - void m_initStreetTable(); + void m_initStreetTable() const; - void m_initAvgStatsTable(); + void m_initAvgStatsTable() const; - void m_initTravelDataTable(); + void m_initTravelDataTable() const; + + virtual void m_dumpSimInfo() const = 0; public: /// @brief Construct a new RoadDynamics object @@ -1054,7 +1057,7 @@ namespace dsf::mobility { template requires(is_numeric_v) - void RoadDynamics::m_initStreetTable() { + void RoadDynamics::m_initStreetTable() const { if (!this->database()) { throw std::runtime_error( "No database connected. Call connectDataBase() before saving data."); @@ -1063,6 +1066,7 @@ namespace dsf::mobility { 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, " @@ -1076,7 +1080,7 @@ namespace dsf::mobility { } template requires(is_numeric_v) - void RoadDynamics::m_initAvgStatsTable() { + void RoadDynamics::m_initAvgStatsTable() const { if (!this->database()) { throw std::runtime_error( "No database connected. Call connectDataBase() before saving data."); @@ -1085,6 +1089,7 @@ namespace dsf::mobility { 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, " @@ -1098,7 +1103,7 @@ namespace dsf::mobility { } template requires(is_numeric_v) - void RoadDynamics::m_initTravelDataTable() { + void RoadDynamics::m_initTravelDataTable() const { if (!this->database()) { throw std::runtime_error( "No database connected. Call connectDataBase() before saving data."); @@ -1107,6 +1112,7 @@ namespace dsf::mobility { 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, " @@ -1146,6 +1152,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) { @@ -1282,6 +1289,8 @@ namespace dsf::mobility { m_initTravelDataTable(); } + this->m_dumpSimInfo(); + spdlog::info( "Data saving configured: interval={}s, avg_stats={}, street_data={}, " "travel_data={}", @@ -1773,31 +1782,32 @@ namespace dsf::mobility { SQLite::Transaction transaction(*this->database()); SQLite::Statement insertStmt( *this->database(), - "INSERT INTO road_data (datetime, time_step, street_id, " + "INSERT INTO road_data (datetime, time_step, simulation_id, street_id, " "coil, density, avg_speed, std_speed, counts) " - "VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"); for (auto const& record : streetDataRecords) { insertStmt.bind(1, this->strDateTime()); insertStmt.bind(2, static_cast(this->time_step())); - insertStmt.bind(3, static_cast(record.streetId)); + insertStmt.bind(3, static_cast(this->id())); + insertStmt.bind(4, static_cast(record.streetId)); if (record.coilName.has_value()) { - insertStmt.bind(4, record.coilName.value()); + insertStmt.bind(5, record.coilName.value()); } else { - insertStmt.bind(4); + insertStmt.bind(5); } - insertStmt.bind(5, record.density); + insertStmt.bind(6, record.density); if (record.avgSpeed.has_value()) { - insertStmt.bind(6, record.avgSpeed.value()); - insertStmt.bind(7, record.stdSpeed.value()); + insertStmt.bind(7, record.avgSpeed.value()); + insertStmt.bind(8, record.stdSpeed.value()); } else { - insertStmt.bind(6); insertStmt.bind(7); + insertStmt.bind(8); } if (record.counts.has_value()) { - insertStmt.bind(8, static_cast(record.counts.value())); + insertStmt.bind(9, static_cast(record.counts.value())); } else { - insertStmt.bind(8); + insertStmt.bind(9); } insertStmt.exec(); insertStmt.reset(); @@ -1807,16 +1817,17 @@ namespace dsf::mobility { 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, distance_m, travel_time_s) " - "VALUES (?, ?, ?, ?)"); + 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, distance); - insertStmt.bind(4, time); + insertStmt.bind(3, static_cast(this->id())); + insertStmt.bind(4, distance); + insertStmt.bind(5, time); insertStmt.exec(); insertStmt.reset(); } @@ -1842,17 +1853,18 @@ namespace dsf::mobility { SQLite::Statement insertStmt( *this->database(), "INSERT INTO avg_stats (" - "datetime, time_step, n_ghost_agents, n_agents, " + "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, this->strDateTime()); - insertStmt.bind(2, static_cast(this->time_step())); - insertStmt.bind(3, static_cast(m_agents.size())); - insertStmt.bind(4, static_cast(this->nAgents())); - insertStmt.bind(5, mean_speed); - insertStmt.bind(6, std_speed); - insertStmt.bind(7, mean_density); - insertStmt.bind(8, std_density); + "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())); + insertStmt.bind(6, mean_speed); + insertStmt.bind(7, std_speed); + insertStmt.bind(8, mean_density); + insertStmt.bind(9, std_density); insertStmt.exec(); } } From f4637ab7150fa55a4235de53449d717c70abdc52 Mon Sep 17 00:00:00 2001 From: Grufoony Date: Fri, 6 Feb 2026 13:15:05 +0100 Subject: [PATCH 07/12] MacOS things... --- src/dsf/mobility/FirstOrderDynamics.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dsf/mobility/FirstOrderDynamics.cpp b/src/dsf/mobility/FirstOrderDynamics.cpp index f8fbba53..7e6b5a7d 100644 --- a/src/dsf/mobility/FirstOrderDynamics.cpp +++ b/src/dsf/mobility/FirstOrderDynamics.cpp @@ -73,7 +73,7 @@ namespace dsf::mobility { insertSimStmt.bind(9); } if (this->m_meanTravelTime.has_value()) { - insertSimStmt.bind(10, *this->m_meanTravelTime); + insertSimStmt.bind(10, static_cast(*this->m_meanTravelTime)); } else { insertSimStmt.bind(10); } From bc228b770178fdd13ea70222ccc9c1cfde8044b6 Mon Sep 17 00:00:00 2001 From: Grufoony Date: Fri, 6 Feb 2026 13:15:21 +0100 Subject: [PATCH 08/12] Fix clang compilation warnings --- src/dsf/mdt/PointsCluster.hpp | 4 ++++ src/dsf/mdt/TrajectoryCollection.cpp | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) 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 From 2b9c336615210ed5d759f78264d10ff7994ae35a Mon Sep 17 00:00:00 2001 From: Grufoony Date: Fri, 6 Feb 2026 13:33:40 +0100 Subject: [PATCH 09/12] Remove unused and depprecated \'AdjacencyMatrix` class --- src/dsf/base/AdjacencyMatrix.cpp | 270 ----------------------------- src/dsf/base/AdjacencyMatrix.hpp | 117 ------------- src/dsf/base/Network.hpp | 16 -- src/dsf/bindings.cpp | 70 -------- src/dsf/dsf.hpp | 1 - src/dsf/mobility/RoadNetwork.cpp | 4 - src/dsf/mobility/RoadNetwork.hpp | 6 +- test/base/Test_AdjacencyMatrix.cpp | 161 ----------------- test/mobility/Test_graph.cpp | 16 -- 9 files changed, 1 insertion(+), 660 deletions(-) delete mode 100644 src/dsf/base/AdjacencyMatrix.cpp delete mode 100644 src/dsf/base/AdjacencyMatrix.hpp delete mode 100644 test/base/Test_AdjacencyMatrix.cpp 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/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 2ae61bef..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()) diff --git a/src/dsf/dsf.hpp b/src/dsf/dsf.hpp index 61a30e40..6458a5e7 100644 --- a/src/dsf/dsf.hpp +++ b/src/dsf/dsf.hpp @@ -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/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_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; From 64e5a6bb32e17d873adadf6125ce4f7b77b599be Mon Sep 17 00:00:00 2001 From: Grufoony Date: Fri, 6 Feb 2026 15:57:52 +0100 Subject: [PATCH 10/12] Save edge and node tables on db --- src/dsf/mobility/RoadDynamics.hpp | 81 ++++++++++++++++++++++ test/mobility/Test_dynamics.cpp | 110 +++++++++++++++++++++++++++++- 2 files changed, 190 insertions(+), 1 deletion(-) diff --git a/src/dsf/mobility/RoadDynamics.hpp b/src/dsf/mobility/RoadDynamics.hpp index 8f39f5e3..70d7dea0 100644 --- a/src/dsf/mobility/RoadDynamics.hpp +++ b/src/dsf/mobility/RoadDynamics.hpp @@ -125,6 +125,8 @@ namespace dsf::mobility { virtual void m_dumpSimInfo() const = 0; + void m_dumpNetwork() const; + public: /// @brief Construct a new RoadDynamics object /// @param graph The graph representing the network @@ -1120,6 +1122,84 @@ namespace dsf::mobility { 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 NOT NULL)"); + + // 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, "NULL"); + } + insertNodeStmt.exec(); + insertNodeStmt.reset(); + } + } template requires(is_numeric_v) @@ -1290,6 +1370,7 @@ namespace dsf::mobility { } this->m_dumpSimInfo(); + this->m_dumpNetwork(); spdlog::info( "Data saving configured: interval={}s, avg_stats={}, street_data={}, " diff --git a/test/mobility/Test_dynamics.cpp b/test/mobility/Test_dynamics.cpp index deccb3aa..e0ba6282 100644 --- a/test/mobility/Test_dynamics.cpp +++ b/test/mobility/Test_dynamics.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN @@ -1161,20 +1162,127 @@ TEST_CASE("FirstOrderDynamics") { dynamics.evolve(true); } - THEN("All tables are created") { + THEN("All tables are created with correct schema") { SQLite::Database db(testDbPath, SQLite::OPEN_READONLY); + // Check road_data table SQLite::Statement roadQuery(db, "SELECT COUNT(*) FROM road_data"); REQUIRE(roadQuery.executeStep()); CHECK(roadQuery.getColumn(0).getInt() >= 1); + SQLite::Statement roadSchema(db, "PRAGMA table_info(road_data)"); + std::set roadColumns; + while (roadSchema.executeStep()) { + roadColumns.insert(roadSchema.getColumn(1).getString()); + } + 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") == 1); + CHECK(roadColumns.count("avg_speed") == 1); + CHECK(roadColumns.count("std_speed") == 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); From 808fcc93e9948a973a101712f8687c849c68c1bc Mon Sep 17 00:00:00 2001 From: Grufoony Date: Fri, 6 Feb 2026 16:00:24 +0100 Subject: [PATCH 11/12] If on Windows, disable SQLITECPP_RUN_CPPCHECK to avoid issues with CPPCHECK --- CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 126d2d53..49868ad9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -139,6 +139,12 @@ 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 From f01e8590028fa9ea9a29afd9708898aaea54d672 Mon Sep 17 00:00:00 2001 From: Grufoony Date: Fri, 6 Feb 2026 17:09:44 +0100 Subject: [PATCH 12/12] Update webapp to use db --- webapp/index.html | 16 +++ webapp/script.js | 344 ++++++++++++++++++++++++++++++---------------- webapp/styles.css | 98 +++++++++++++ 3 files changed, 341 insertions(+), 117 deletions(-) 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..41add7dd 100644 --- a/webapp/script.js +++ b/webapp/script.js @@ -814,6 +814,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(); @@ -867,71 +869,139 @@ 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'); +// 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 distinct timestamps for the selected simulation + const timestampResult = db.exec( + `SELECT DISTINCT datetime FROM road_data WHERE simulation_id = ${selectedSimulationId} ORDER BY datetime` + ); + if (timestampResult.length === 0) return []; + + const timestamps = timestampResult[0].values.map(row => row[0]); + + // Get edge IDs in order + const edgeIds = edges.map(e => e.id); + + // Build density data for each timestamp + const densityData = []; + + for (const ts of timestamps) { + // Get all road_data for this timestamp and simulation + const dataResult = db.exec( + `SELECT street_id, density FROM road_data WHERE simulation_id = ${selectedSimulationId} AND datetime = '${ts}'` + ); + + const densityMap = {}; + if (dataResult.length > 0) { + dataResult[0].values.forEach(row => { + densityMap[row[0]] = row[1]; + }); } - 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); - } - - // If path doesn't start with server root, return as-is and hope for the best - return absolutePath; - }; + + // Build densities array in same order as edges + const densityArray = edgeIds.map(id => densityMap[id] || 0); + + densityData.push({ + datetime: new Date(ts), + 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) as mean_density, + AVG(avg_speed) as mean_speed, + 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 +1406,103 @@ 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 modal = document.getElementById('db-modal'); + 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%;