From 1665e903d9f60e865163b5ba68ba1a2c3ef224b5 Mon Sep 17 00:00:00 2001 From: Grufoony Date: Tue, 3 Feb 2026 10:02:55 +0100 Subject: [PATCH 1/5] Add `Street::meanSpeed` and `Dynamics::saveStreetSpeeds` functions --- benchmark/Bench_Street.cpp | 26 +++--- src/dsf/base/Dynamics.hpp | 28 +------ src/dsf/bindings.cpp | 6 ++ src/dsf/mobility/RoadDynamics.hpp | 55 ++++++++++-- src/dsf/mobility/Street.cpp | 20 ++++- src/dsf/mobility/Street.hpp | 15 +++- src/dsf/utility/Measurement.hpp | 31 +++++++ test/mobility/Test_dynamics.cpp | 134 +++++++++++++++++++++++++++++- test/mobility/Test_street.cpp | 111 ++++++++++++++++++++----- 9 files changed, 353 insertions(+), 73 deletions(-) create mode 100644 src/dsf/utility/Measurement.hpp diff --git a/benchmark/Bench_Street.cpp b/benchmark/Bench_Street.cpp index 07c22410..ec025e98 100644 --- a/benchmark/Bench_Street.cpp +++ b/benchmark/Bench_Street.cpp @@ -25,7 +25,7 @@ static void BM_Street_AddAgent(benchmark::State& state) { auto pItinerary = std::make_shared(1, 1); for (auto _ : state) { auto agent = std::make_unique(0, spawnTime++, pItinerary, 0); - street.addAgent(std::move(agent)); + street.addAgent(std::move(agent), spawnTime); } } @@ -35,7 +35,7 @@ static void BM_Street_Enqueue(benchmark::State& state) { auto pItinerary = std::make_shared(1, 1); for (int i = 0; i < 50; ++i) { auto agent = std::make_unique(0, spawnTime++, pItinerary, 0); - street.addAgent(std::move(agent)); + street.addAgent(std::move(agent), spawnTime); } size_t queueId = 0; for (auto _ : state) { @@ -52,13 +52,13 @@ static void BM_Street_Dequeue(benchmark::State& state) { auto pItinerary = std::make_shared(1, 1); for (int i = 0; i < 50; ++i) { auto agent = std::make_unique(0, spawnTime++, pItinerary, 0); - street.addAgent(std::move(agent)); + street.addAgent(std::move(agent), spawnTime); street.enqueue(0); } size_t index = 0; for (auto _ : state) { if (!street.queue(index).empty()) { - auto agent = street.dequeue(index); + auto agent = street.dequeue(index, spawnTime++); benchmark::DoNotOptimize(agent); } } @@ -70,7 +70,7 @@ static void BM_Street_nAgents(benchmark::State& state) { auto pItinerary = std::make_shared(1, 1); for (int i = 0; i < 50; ++i) { auto agent = std::make_unique(0, spawnTime++, pItinerary, 0); - street.addAgent(std::move(agent)); + street.addAgent(std::move(agent), spawnTime); if (i % 2 == 0) street.enqueue(0); } @@ -86,7 +86,7 @@ static void BM_Street_Density(benchmark::State& state) { auto pItinerary = std::make_shared(1, 1); for (int i = 0; i < 50; ++i) { auto agent = std::make_unique(0, spawnTime++, pItinerary, 0); - street.addAgent(std::move(agent)); + street.addAgent(std::move(agent), spawnTime); if (i % 2 == 0) street.enqueue(0); } @@ -102,7 +102,7 @@ static void BM_Street_nMovingAgents(benchmark::State& state) { auto pItinerary = std::make_shared(1, 1); for (int i = 0; i < 50; ++i) { auto agent = std::make_unique(0, spawnTime++, pItinerary, 0); - street.addAgent(std::move(agent)); + street.addAgent(std::move(agent), spawnTime); } for (auto _ : state) { int n = street.nMovingAgents(); @@ -116,7 +116,7 @@ static void BM_Street_nExitingAgents(benchmark::State& state) { auto pItinerary = std::make_shared(1, 1); for (int i = 0; i < 50; ++i) { auto agent = std::make_unique(0, spawnTime++, pItinerary, 0); - street.addAgent(std::move(agent)); + street.addAgent(std::move(agent), spawnTime); street.enqueue(0); } for (auto _ : state) { @@ -142,7 +142,7 @@ static void BM_CoilStreet_AddAgent(benchmark::State& state) { auto pItinerary = std::make_shared(1, 1); for (auto _ : state) { auto agent = std::make_unique(0, spawnTime++, pItinerary, 0); - street.addAgent(std::move(agent)); + street.addAgent(std::move(agent), spawnTime); } } @@ -153,10 +153,10 @@ static void BM_CoilStreet_MeanFlow(benchmark::State& state) { auto pItinerary = std::make_shared(1, 1); for (int i = 0; i < 50; ++i) { auto agent = std::make_unique(0, spawnTime++, pItinerary, 0); - street.addAgent(std::move(agent)); + street.addAgent(std::move(agent), spawnTime); street.enqueue(0); if (i % 2 == 0) { - auto dequeued = street.dequeue(0); + auto dequeued = street.dequeue(0, spawnTime++); } } for (auto _ : state) { @@ -172,13 +172,13 @@ static void BM_CoilStreet_Dequeue(benchmark::State& state) { auto pItinerary = std::make_shared(1, 1); for (int i = 0; i < 50; ++i) { auto agent = std::make_unique(0, spawnTime++, pItinerary, 0); - street.addAgent(std::move(agent)); + street.addAgent(std::move(agent), spawnTime); street.enqueue(0); } size_t index = 0; for (auto _ : state) { if (!street.queue(index).empty()) { - auto agent = street.dequeue(index); + auto agent = street.dequeue(index, spawnTime++); benchmark::DoNotOptimize(agent); } } diff --git a/src/dsf/base/Dynamics.hpp b/src/dsf/base/Dynamics.hpp index 25291847..d1782939 100644 --- a/src/dsf/base/Dynamics.hpp +++ b/src/dsf/base/Dynamics.hpp @@ -9,6 +9,7 @@ #pragma once #include "Network.hpp" +#include "../utility/Measurement.hpp" #include "../utility/Typedef.hpp" #include @@ -36,33 +37,6 @@ #include namespace dsf { - /// @brief The Measurement struct represents the mean of a quantity and its standard deviation - /// @tparam T The type of the quantity - /// @param mean The mean - /// @param std The standard deviation of the sample - template - struct Measurement { - T mean; - T std; - - Measurement(T mean, T std) : mean{mean}, std{std} {} - Measurement(std::span data) { - auto x_mean = static_cast(0), x2_mean = static_cast(0); - if (data.empty()) { - mean = static_cast(0); - std = static_cast(0); - return; - } - - std::for_each(data.begin(), data.end(), [&x_mean, &x2_mean](auto value) -> void { - x_mean += value; - x2_mean += value * value; - }); - mean = x_mean / data.size(); - std = std::sqrt(x2_mean / data.size() - mean * mean); - } - }; - /// @brief The Dynamics class represents the dynamics of the network. /// @tparam network_t The type of the network template diff --git a/src/dsf/bindings.cpp b/src/dsf/bindings.cpp index a7d04d18..7d3a7e53 100644 --- a/src/dsf/bindings.cpp +++ b/src/dsf/bindings.cpp @@ -684,6 +684,12 @@ PYBIND11_MODULE(dsf_cpp, m) { pybind11::arg("normalized") = true, pybind11::arg("separator") = ';', 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"), diff --git a/src/dsf/mobility/RoadDynamics.hpp b/src/dsf/mobility/RoadDynamics.hpp index 5b1dffaf..a6bb9f85 100644 --- a/src/dsf/mobility/RoadDynamics.hpp +++ b/src/dsf/mobility/RoadDynamics.hpp @@ -363,6 +363,13 @@ namespace dsf::mobility { void saveStreetDensities(std::string filename = std::string(), bool normalized = true, char const separator = ';') const; + /// @brief Save the street speeds in csv format + /// @param filename The name of the file (default is "{datetime}_{simulation_name}_street_speeds.csv") + /// @param separator The separator character (default is ';') + /// @param bNormalized If true, the speeds are normalized in [0, 1] dividing by the street maxSpeed attribute + void saveStreetSpeeds(std::string filename = std::string(), + char const separator = ';', + bool bNormalized = false) const; /// @brief Save the street input counts in csv format /// @param filename The name of the file /// @param reset If true, the input counts are cleared after the computation @@ -742,7 +749,7 @@ namespace dsf::mobility { timeTolerance, timeDiff); // Kill the agent - this->m_killAgent(pStreet->dequeue(queueIndex)); + this->m_killAgent(pStreet->dequeue(queueIndex, this->time_step())); ++m_nKilledAgents; continue; } @@ -898,7 +905,8 @@ namespace dsf::mobility { } } if (bArrived) { - auto pAgent = this->m_killAgent(pStreet->dequeue(queueIndex)); + auto pAgent = + this->m_killAgent(pStreet->dequeue(queueIndex, this->time_step())); ++m_nArrivedAgents; if (reinsert_agents) { // reset Agent's values @@ -920,7 +928,7 @@ namespace dsf::mobility { *nextStreet); continue; } - auto pAgent{pStreet->dequeue(queueIndex)}; + auto pAgent{pStreet->dequeue(queueIndex, this->time_step())}; spdlog::debug( "{} at time {} has been dequeued from street {} and enqueued on street {} " "with free time {}.", @@ -990,7 +998,7 @@ namespace dsf::mobility { pNode->id(), nextStreet->id(), pAgent->freeTime()); - nextStreet->addAgent(std::move(pAgent)); + nextStreet->addAgent(std::move(pAgent), this->time_step()); it = intersection.agents().erase(it); break; } @@ -1018,7 +1026,7 @@ namespace dsf::mobility { nextStreet->id(), pAgent->freeTime(), *pAgent); - nextStreet->addAgent(std::move(pAgent)); + nextStreet->addAgent(std::move(pAgent), this->time_step()); } else { return; } @@ -1351,7 +1359,7 @@ namespace dsf::mobility { this->setAgentSpeed(pAgent); pAgent->setFreeTime(this->time_step() + std::ceil(street->length() / pAgent->speed())); - street->addAgent(std::move(pAgent)); + street->addAgent(std::move(pAgent), this->time_step()); this->m_agents.pop_back(); } } @@ -2333,6 +2341,41 @@ namespace dsf::mobility { file << std::endl; file.close(); } + template + requires(is_numeric_v) + void RoadDynamics::saveStreetSpeeds(std::string filename, + char const separator, + bool const bNormalized) const { + if (filename.empty()) { + filename = this->m_safeDateTime() + '_' + this->m_safeName() + "_street_speeds.csv"; + } + bool bEmptyFile{false}; + { + std::ifstream file(filename); + bEmptyFile = file.peek() == std::ifstream::traits_type::eof(); + } + std::ofstream file(filename, std::ios::app); + if (!file.is_open()) { + throw std::runtime_error("Error opening file \"" + filename + "\" for writing."); + } + if (bEmptyFile) { + file << "datetime" << separator << "time_step"; + for (auto const& [streetId, pStreet] : this->graph().edges()) { + file << separator << streetId; + } + file << std::endl; + } + file << this->strDateTime() << separator << this->time_step(); + for (auto const& [streetId, pStreet] : this->graph().edges()) { + double speed{pStreet->meanSpeed(true).mean}; + if (bNormalized) { + speed /= pStreet->maxSpeed(); + } + file << separator << std::fixed << std::setprecision(2) << speed; + } + file << std::endl; + file.close(); + } template requires(is_numeric_v) void RoadDynamics::saveCoilCounts(const std::string& filename, diff --git a/src/dsf/mobility/Street.cpp b/src/dsf/mobility/Street.cpp index cf2bf1b8..abe4cc72 100644 --- a/src/dsf/mobility/Street.cpp +++ b/src/dsf/mobility/Street.cpp @@ -126,9 +126,10 @@ namespace dsf::mobility { m_counter->reset(); } - void Street::addAgent(std::unique_ptr pAgent) { + void Street::addAgent(std::unique_ptr pAgent, std::time_t const currentTime) { assert(!isFull()); - spdlog::debug("Adding {} on {}", *pAgent, *this); + spdlog::trace("Adding {} on {}", *pAgent, *this); + m_agentsInsertionTimes[pAgent->id()] = currentTime; m_movingAgents.push(std::move(pAgent)); if (m_counter.has_value() && m_counterPosition == CounterPosition::ENTRY) { ++(*m_counter); @@ -144,9 +145,15 @@ namespace dsf::mobility { ++(*m_counter); } } - std::unique_ptr Street::dequeue(std::size_t const& index) { + std::unique_ptr Street::dequeue(std::size_t const& index, + std::time_t const currentTime) { assert(!m_exitQueues[index].empty()); auto pAgent{std::move(m_exitQueues[index].front())}; + // Keep track of average speed + m_avgSpeeds.push_back(m_length / + (currentTime - m_agentsInsertionTimes[pAgent->id()])); + m_agentsInsertionTimes.erase(pAgent->id()); + m_exitQueues[index].pop(); if (m_counter.has_value() && m_counterPosition == CounterPosition::EXIT) { ++(*m_counter); @@ -202,5 +209,12 @@ namespace dsf::mobility { } return nAgents; } + Measurement Street::meanSpeed(bool const bReset) { + auto const avgSpeed{Measurement(m_avgSpeeds)}; + if (bReset) { + m_avgSpeeds.clear(); + } + return avgSpeed; + } }; // namespace dsf::mobility diff --git a/src/dsf/mobility/Street.hpp b/src/dsf/mobility/Street.hpp index 936586bc..d8e38186 100644 --- a/src/dsf/mobility/Street.hpp +++ b/src/dsf/mobility/Street.hpp @@ -12,6 +12,7 @@ #include "Road.hpp" #include "Sensors.hpp" #include "../utility/TypeTraits/is_numeric.hpp" +#include "../utility/Measurement.hpp" #include "../utility/queue.hpp" #include "../utility/Typedef.hpp" @@ -50,6 +51,8 @@ namespace dsf::mobility { std::vector>, AgentComparator> m_movingAgents; + std::unordered_map m_agentsInsertionTimes; + std::vector m_avgSpeeds; std::vector m_laneMapping; std::optional m_counter; CounterPosition m_counterPosition{CounterPosition::EXIT}; @@ -166,20 +169,28 @@ namespace dsf::mobility { /// @return double The number of agents on all queues for a given direction double nExitingAgents(Direction direction = Direction::ANY, bool normalizeOnNLanes = false) const final; + /// @brief Get the mean speed of the agents that have passed through the street + /// @param bReset If true, the average speed data is reset after the computation + /// @return Measurement The (mean, std) speed of the agents that have passed through the street + Measurement meanSpeed(bool const bReset = true); + /// @brief Get the street's lane mapping /// @return std::vector The street's lane mapping inline auto const& laneMapping() const { return m_laneMapping; } /// @brief Add an agent to the street /// @param pAgent The agent to add to the street - void addAgent(std::unique_ptr pAgent); + /// @param currentTime The current simulation time + void addAgent(std::unique_ptr pAgent, std::time_t const currentTime); /// @brief Add an agent to the street's queue /// @param queueId The id of the queue /// @throw std::runtime_error If the street's queue is full void enqueue(std::size_t const& queueId); /// @brief Remove an agent from the street's queue /// @param index The index of the queue + /// @param currentTime The current simulation time /// @return Id The id of the agent removed from the street's queue - std::unique_ptr dequeue(std::size_t const& index); + std::unique_ptr dequeue(std::size_t const& index, + std::time_t const currentTime); /// @brief Check if the street has a coil (dsf::Counter sensor) on it /// @return bool True if the street has a coil, false otherwise constexpr bool hasCoil() const { return m_counter.has_value(); }; diff --git a/src/dsf/utility/Measurement.hpp b/src/dsf/utility/Measurement.hpp new file mode 100644 index 00000000..3843af03 --- /dev/null +++ b/src/dsf/utility/Measurement.hpp @@ -0,0 +1,31 @@ +#pragma once + +namespace dsf { + /// @brief The Measurement struct represents the mean of a quantity and its standard deviation + /// @tparam T The type of the quantity + /// @param mean The mean + /// @param std The standard deviation of the sample + template + struct Measurement { + T mean; + T std; + + Measurement(T mean, T std) : mean{mean}, std{std} {} + template + Measurement(TContainer data) { + auto x_mean = static_cast(0), x2_mean = static_cast(0); + if (data.empty()) { + mean = static_cast(0); + std = static_cast(0); + return; + } + + std::for_each(data.begin(), data.end(), [&x_mean, &x2_mean](auto value) -> void { + x_mean += value; + x2_mean += value * value; + }); + mean = x_mean / data.size(); + std = std::sqrt(x2_mean / data.size() - mean * mean); + } + }; +} // namespace dsf \ No newline at end of file diff --git a/test/mobility/Test_dynamics.cpp b/test/mobility/Test_dynamics.cpp index 1744ede5..fda3affe 100644 --- a/test/mobility/Test_dynamics.cpp +++ b/test/mobility/Test_dynamics.cpp @@ -1199,6 +1199,138 @@ TEST_CASE("FirstOrderDynamics") { CHECK(fileFound); } } + + WHEN("We call saveStreetSpeeds with default filename") { + // Use explicit filename in test to avoid cluttering the workspace + const std::string testFile = "test_street_speeds.csv"; + dynamics.saveStreetSpeeds(testFile); + + THEN("The file is created with correct header") { + std::ifstream file(testFile); + REQUIRE(file.is_open()); + + std::string header; + std::getline(file, header); + CHECK(header.find("datetime") != std::string::npos); + CHECK(header.find("time_step") != std::string::npos); + CHECK(header.find("0") != std::string::npos); + CHECK(header.find("1") != std::string::npos); + + file.close(); + std::filesystem::remove(testFile); + } + } + + WHEN("We call saveStreetSpeeds multiple times to test appending") { + const std::string testFile = "test_street_speeds_append.csv"; + + // Add some agents and evolve + dynamics.addRandomAgents(5); + dynamics.evolve(false); + + // Save first time + dynamics.saveStreetSpeeds(testFile); + + // Evolve again and save + dynamics.evolve(false); + dynamics.saveStreetSpeeds(testFile); + + THEN("The file contains multiple rows with one header") { + std::ifstream file(testFile); + REQUIRE(file.is_open()); + + std::string line; + int lineCount = 0; + int headerCount = 0; + + while (std::getline(file, line)) { + lineCount++; + if (line.find("datetime") != std::string::npos) { + headerCount++; + } + } + + CHECK_EQ(headerCount, 1); // Only one header + CHECK_EQ(lineCount, 3); // Header + 2 data rows + + file.close(); + std::filesystem::remove(testFile); + } + } + + WHEN("We call saveStreetSpeeds with normalized flag") { + const std::string testFile = "test_street_speeds_normalized.csv"; + + // Add agents and evolve to generate some speeds + dynamics.addRandomAgents(10); + dynamics.evolve(false); + dynamics.evolve(false); + + // Save normalized speeds + dynamics.saveStreetSpeeds(testFile, ',', true); + + THEN("The file is created and speeds are normalized") { + std::ifstream file(testFile); + REQUIRE(file.is_open()); + + std::string header; + std::getline(file, header); + + std::string dataLine; + std::getline(file, dataLine); + + // Parse the data line to check normalized values are between 0 and 1 + std::stringstream ss(dataLine); + std::string token; + std::getline(ss, token, ','); // datetime + std::getline(ss, token, ','); // time_step + + // Check that speed values are between 0 and 1 (normalized) + while (std::getline(ss, token, ',')) { + if (!token.empty()) { + double speed = std::stod(token); + CHECK(speed >= 0.0); + CHECK(speed <= 1.0); + } + } + + file.close(); + std::filesystem::remove(testFile); + } + } + + 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; + + for (const auto& entry : std::filesystem::directory_iterator(".")) { + if (entry.path().filename().string().find("street_speeds.csv") != + std::string::npos) { + fileFound = true; + + // Check the file has correct header + std::ifstream file(entry.path()); + REQUIRE(file.is_open()); + + std::string header; + std::getline(file, header); + CHECK(header.find("datetime") != std::string::npos); + CHECK(header.find("time_step") != std::string::npos); + + file.close(); + std::filesystem::remove(entry.path()); + break; + } + } + + CHECK(fileFound); + } + } } } SUBCASE("Transition probabilities") { @@ -1363,7 +1495,7 @@ TEST_CASE("Stationary Weights Impact on Random Navigation") { agent->setStreetId(0); agent->setSpeed(10.0); agent->setFreeTime(0); - dynamics.graph().edge(0)->addAgent(std::move(agent)); + dynamics.graph().edge(0)->addAgent(std::move(agent), dynamics.time_step()); } // Evolve simulation diff --git a/test/mobility/Test_street.cpp b/test/mobility/Test_street.cpp index 7fe88a79..44ebfc80 100644 --- a/test/mobility/Test_street.cpp +++ b/test/mobility/Test_street.cpp @@ -174,11 +174,11 @@ TEST_CASE("Street") { a3.setFreeTime(7); Street street{1, std::make_pair(0, 1), 3.5}; - street.addAgent(std::make_unique(a1)); + street.addAgent(std::make_unique(a1), 0); CHECK_EQ(street.movingAgents().top()->freeTime(), 5); - street.addAgent(std::make_unique(a2)); + street.addAgent(std::make_unique(a2), 0); CHECK_EQ(street.movingAgents().top()->freeTime(), 3); - street.addAgent(std::make_unique(a3)); + street.addAgent(std::make_unique(a3), 0); CHECK_EQ(street.movingAgents().top()->freeTime(), 3); } SUBCASE("Enqueue") { @@ -192,14 +192,14 @@ TEST_CASE("Street") { Street street{1, std::make_pair(0, 1), 3.5}; // fill the queue - street.addAgent(std::make_unique(a1)); + street.addAgent(std::make_unique(a1), 0); street.enqueue(0); - street.addAgent(std::make_unique(a2)); + street.addAgent(std::make_unique(a2), 0); street.enqueue(0); CHECK_EQ(doctest::Approx(street.density()), 0.571429); - street.addAgent(std::make_unique(a3)); + street.addAgent(std::make_unique(a3), 0); street.enqueue(0); - street.addAgent(std::make_unique(a4)); + street.addAgent(std::make_unique(a4), 0); street.enqueue(0); CHECK(street.queue(0).front()); CHECK(street.queue(0).back()); @@ -220,26 +220,26 @@ TEST_CASE("Street") { Street street{1, std::make_pair(0, 1), 3.5}; // fill the queue - street.addAgent(std::make_unique(a1)); + street.addAgent(std::make_unique(a1), 0); street.enqueue(0); - street.addAgent(std::make_unique(a2)); + street.addAgent(std::make_unique(a2), 0); street.enqueue(0); - street.addAgent(std::make_unique(a3)); + street.addAgent(std::make_unique(a3), 0); street.enqueue(0); - street.addAgent(std::make_unique(a4)); + street.addAgent(std::make_unique(a4), 0); street.enqueue(0); CHECK(street.queue(0).front()); // dequeue - street.dequeue(0); + street.dequeue(0, 1); CHECK(street.queue(0).front()); // check that agent 2 is now at front // check that the length of the queue has decreased CHECK_EQ(street.queue(0).size(), 3); CHECK_EQ(street.queue(0).size(), 3); // check that the next agent dequeued is agent 2 - CHECK(street.dequeue(0)); + CHECK(street.dequeue(0, 2)); CHECK_EQ(street.queue(0).size(), 2); - street.dequeue(0); - street.dequeue(0); // the queue is now empty + street.dequeue(0, 3); + street.dequeue(0, 4); // the queue is now empty } SUBCASE("Angle") { /// This tests the angle method @@ -261,7 +261,7 @@ TEST_CASE("Street with a coil") { street.enableCounter("EntryCoil", dsf::mobility::CounterPosition::ENTRY); CHECK_EQ(street.counterName(), "EntryCoil"); WHEN("An agent is added") { - street.addAgent(std::make_unique(0, 0, nullptr, 0)); + street.addAgent(std::make_unique(0, 0, nullptr, 0), 0); THEN("The input flow is one immediately") { CHECK_EQ(street.counts(), 1); } street.enqueue(0); THEN("The input flow is still one") { CHECK_EQ(street.counts(), 1); } @@ -275,7 +275,7 @@ TEST_CASE("Street with a coil") { street.enableCounter("", dsf::mobility::CounterPosition::MIDDLE); CHECK_EQ(street.counterName(), "Coil_1"); WHEN("An agent is added") { - street.addAgent(std::make_unique(0, 0, nullptr, 0)); + street.addAgent(std::make_unique(0, 0, nullptr, 0), 0); THEN("The input flow is zero") { CHECK_EQ(street.counts(), 0); } street.enqueue(0); THEN("The input flow is one once enqueued") { CHECK_EQ(street.counts(), 1); } @@ -291,14 +291,83 @@ TEST_CASE("Street with a coil") { street.enableCounter("ExitCoil", dsf::mobility::CounterPosition::EXIT); CHECK_EQ(street.counterName(), "ExitCoil"); WHEN("An agent is added and enqueued") { - street.addAgent(std::make_unique(0, 0, nullptr, 0)); + street.addAgent(std::make_unique(0, 0, nullptr, 0), 0); street.enqueue(0); THEN("The input flow is zero") { CHECK_EQ(street.counts(), 0); } - street.dequeue(0); + street.dequeue(0, 1); THEN("The input flow is one after dequeue") { CHECK_EQ(street.counts(), 1); } } } } + + SUBCASE("meanSpeed") { + GIVEN("A street with multiple agents") { + Street street{1, std::make_pair(0, 1), 100.0, 20.0}; + + WHEN("No agents have exited") { + THEN("meanSpeed returns zero mean and std") { + auto measurement = street.meanSpeed(false); + CHECK_EQ(measurement.mean, 0.); + CHECK_EQ(measurement.std, 0.); + } + } + + WHEN("Agents are added and dequeued at different times") { + // Add agents at time 0 + street.addAgent(std::make_unique(0, 0, nullptr, 0), 0); + street.addAgent(std::make_unique(1, 0, nullptr, 0), 0); + street.addAgent(std::make_unique(2, 0, nullptr, 0), 0); + + // Enqueue them + street.enqueue(0); + street.enqueue(0); + street.enqueue(0); + + // Dequeue at time 5 (speed = 100/5 = 20 m/s for each) + street.dequeue(0, 5); + street.dequeue(0, 5); + street.dequeue(0, 5); + + THEN("meanSpeed returns correct mean and std without reset") { + auto measurement = street.meanSpeed(false); + CHECK_EQ(measurement.mean, 20.0); + CHECK_EQ(measurement.std, 0.0); + } + + THEN("meanSpeed data is not cleared when bReset=false") { + street.meanSpeed(false); + auto measurement = street.meanSpeed(false); + CHECK_EQ(measurement.mean, 20.0); + } + + THEN("meanSpeed data is cleared when bReset=true") { + street.meanSpeed(true); + auto measurement = street.meanSpeed(false); + CHECK_EQ(measurement.mean, 0.0); + CHECK_EQ(measurement.std, 0.0); + } + } + + WHEN("Agents exit at different times") { + // Agent 0 added at time 0, exits at time 10 (speed = 100/10 = 10 m/s) + street.addAgent(std::make_unique(0, 0, nullptr, 0), 0); + street.enqueue(0); + street.dequeue(0, 10); + + // Agent 1 added at time 0, exits at time 5 (speed = 100/5 = 20 m/s) + street.addAgent(std::make_unique(1, 0, nullptr, 0), 0); + street.enqueue(0); + street.dequeue(0, 5); + + THEN("meanSpeed returns correct mean and std") { + auto measurement = street.meanSpeed(false); + CHECK_EQ(measurement.mean, 15.0); // (10 + 20) / 2 + CHECK_EQ(measurement.std, + doctest::Approx(5.0)); // sqrt(((10-15)^2 + (20-15)^2)/2) + } + } + } + } } TEST_CASE("Road") { @@ -591,8 +660,8 @@ TEST_CASE("Street formatting") { // Add some agents auto agent1 = std::make_unique(0, 0, nullptr); auto agent2 = std::make_unique(1, 0, nullptr); - street.addAgent(std::move(agent1)); - street.addAgent(std::move(agent2)); + street.addAgent(std::move(agent1), 0); + street.addAgent(std::move(agent2), 0); std::string formatted = std::format("{}", street); CHECK(formatted.find("Street(id: 30") != std::string::npos); From c12f66d2233ef760541878f335b2a6f114bb5a81 Mon Sep 17 00:00:00 2001 From: Grufoony Date: Tue, 3 Feb 2026 10:21:03 +0100 Subject: [PATCH 2/5] Fixes --- src/dsf/bindings.cpp | 2 +- src/dsf/mobility/RoadDynamics.hpp | 10 +++++----- src/dsf/mobility/Street.hpp | 1 + src/dsf/utility/Measurement.hpp | 3 +++ 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/dsf/bindings.cpp b/src/dsf/bindings.cpp index 7d3a7e53..b2e6d786 100644 --- a/src/dsf/bindings.cpp +++ b/src/dsf/bindings.cpp @@ -681,8 +681,8 @@ PYBIND11_MODULE(dsf_cpp, m) { "saveStreetDensities", &dsf::mobility::FirstOrderDynamics::saveStreetDensities, pybind11::arg("filename"), - pybind11::arg("normalized") = true, pybind11::arg("separator") = ';', + pybind11::arg("normalized") = true, dsf::g_docstrings.at("dsf::mobility::RoadDynamics::saveStreetDensities").c_str()) .def("saveStreetSpeeds", &dsf::mobility::FirstOrderDynamics::saveStreetSpeeds, diff --git a/src/dsf/mobility/RoadDynamics.hpp b/src/dsf/mobility/RoadDynamics.hpp index a6bb9f85..c32bc9a5 100644 --- a/src/dsf/mobility/RoadDynamics.hpp +++ b/src/dsf/mobility/RoadDynamics.hpp @@ -358,11 +358,11 @@ namespace dsf::mobility { /// @brief Save the street densities in csv format /// @param filename The name of the file (default is "{datetime}_{simulation_name}_street_densities.csv") - /// @param normalized If true, the densities are normalized in [0, 1] /// @param separator The separator character (default is ';') + /// @param normalized If true, the densities are normalized in [0, 1] dividing by the street capacity attribute void saveStreetDensities(std::string filename = std::string(), - bool normalized = true, - char const separator = ';') const; + 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 ';') @@ -2310,8 +2310,8 @@ namespace dsf::mobility { template requires(is_numeric_v) void RoadDynamics::saveStreetDensities(std::string filename, - bool normalized, - char const separator) const { + char const separator, + bool const normalized) const { if (filename.empty()) { filename = this->m_safeDateTime() + '_' + this->m_safeName() + "_street_densities.csv"; diff --git a/src/dsf/mobility/Street.hpp b/src/dsf/mobility/Street.hpp index d8e38186..768cfaa1 100644 --- a/src/dsf/mobility/Street.hpp +++ b/src/dsf/mobility/Street.hpp @@ -26,6 +26,7 @@ #include #include #include +#include #include namespace dsf::mobility { diff --git a/src/dsf/utility/Measurement.hpp b/src/dsf/utility/Measurement.hpp index 3843af03..213f7750 100644 --- a/src/dsf/utility/Measurement.hpp +++ b/src/dsf/utility/Measurement.hpp @@ -1,5 +1,8 @@ #pragma once +#include +#include + namespace dsf { /// @brief The Measurement struct represents the mean of a quantity and its standard deviation /// @tparam T The type of the quantity From b11c4d95996e29769913f92459614f6fe59c3375 Mon Sep 17 00:00:00 2001 From: grufoony Date: Wed, 4 Feb 2026 10:19:25 +0100 Subject: [PATCH 3/5] Add `is_valid` attribute to Measurement struct --- src/dsf/utility/Measurement.hpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/dsf/utility/Measurement.hpp b/src/dsf/utility/Measurement.hpp index 213f7750..699e368b 100644 --- a/src/dsf/utility/Measurement.hpp +++ b/src/dsf/utility/Measurement.hpp @@ -8,20 +8,21 @@ namespace dsf { /// @tparam T The type of the quantity /// @param mean The mean /// @param std The standard deviation of the sample + /// @param is_valid True if the measurement is valid, false otherwise (i.e. checks if the sample is not empty) template struct Measurement { - T mean; - T std; + T mean = static_cast(0); + T std = static_cast(0); + bool is_valid = false; - Measurement(T mean, T std) : mean{mean}, std{std} {} + Measurement(T mean, T std) : mean{mean}, std{std}, is_valid{true} {} template - Measurement(TContainer data) { - auto x_mean = static_cast(0), x2_mean = static_cast(0); + Measurement(TContainer const& data) { if (data.empty()) { - mean = static_cast(0); - std = static_cast(0); return; } + is_valid = true; + auto x_mean = static_cast(0), x2_mean = static_cast(0); std::for_each(data.begin(), data.end(), [&x_mean, &x2_mean](auto value) -> void { x_mean += value; From 005ec1363d1b9479368dac780185f6f2329a75bf Mon Sep 17 00:00:00 2001 From: grufoony Date: Wed, 4 Feb 2026 10:20:11 +0100 Subject: [PATCH 4/5] Handle valid measures in `saveStreetSpeeds` function --- src/dsf/mobility/RoadDynamics.hpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/dsf/mobility/RoadDynamics.hpp b/src/dsf/mobility/RoadDynamics.hpp index c32bc9a5..f0ae7cef 100644 --- a/src/dsf/mobility/RoadDynamics.hpp +++ b/src/dsf/mobility/RoadDynamics.hpp @@ -2367,11 +2367,18 @@ namespace dsf::mobility { } file << this->strDateTime() << separator << this->time_step(); for (auto const& [streetId, pStreet] : this->graph().edges()) { - double speed{pStreet->meanSpeed(true).mean}; + auto const measure = pStreet->meanSpeed(true); + file << separator; + // If not valid, write empty value (less space w.r.t. NaN) + if (!measure.is_valid) { + continue; + } + + double speed{measure.mean}; if (bNormalized) { speed /= pStreet->maxSpeed(); } - file << separator << std::fixed << std::setprecision(2) << speed; + file << std::fixed << std::setprecision(2) << speed; } file << std::endl; file.close(); From fcb161cc8fb8220234dd21a02fdec2aad34055a0 Mon Sep 17 00:00:00 2001 From: grufoony Date: Wed, 4 Feb 2026 10:25:46 +0100 Subject: [PATCH 5/5] Version 4.7.8 --- src/dsf/dsf.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dsf/dsf.hpp b/src/dsf/dsf.hpp index e6ac9f73..6fbd08da 100644 --- a/src/dsf/dsf.hpp +++ b/src/dsf/dsf.hpp @@ -6,7 +6,7 @@ static constexpr uint8_t DSF_VERSION_MAJOR = 4; static constexpr uint8_t DSF_VERSION_MINOR = 7; -static constexpr uint8_t DSF_VERSION_PATCH = 7; +static constexpr uint8_t DSF_VERSION_PATCH = 8; static auto const DSF_VERSION = std::format("{}.{}.{}", DSF_VERSION_MAJOR, DSF_VERSION_MINOR, DSF_VERSION_PATCH);