diff --git a/src/dsf/bindings.cpp b/src/dsf/bindings.cpp index 626fe35b..a7d04d18 100644 --- a/src/dsf/bindings.cpp +++ b/src/dsf/bindings.cpp @@ -261,6 +261,20 @@ PYBIND11_MODULE(dsf_cpp, m) { pybind11::arg("status"), dsf::g_docstrings.at("dsf::mobility::RoadNetwork::setStreetStatusByName") .c_str()) + .def("changeStreetNLanesById", + &dsf::mobility::RoadNetwork::changeStreetNLanesById, + pybind11::arg("streetId"), + pybind11::arg("nLanes"), + pybind11::arg("speedFactor") = std::nullopt, + dsf::g_docstrings.at("dsf::mobility::RoadNetwork::changeStreetNLanesById") + .c_str()) + .def("changeStreetNLanesByName", + &dsf::mobility::RoadNetwork::changeStreetNLanesByName, + pybind11::arg("name"), + pybind11::arg("nLanes"), + pybind11::arg("speedFactor") = std::nullopt, + dsf::g_docstrings.at("dsf::mobility::RoadNetwork::changeStreetNLanesByName") + .c_str()) .def("changeStreetCapacityById", &dsf::mobility::RoadNetwork::changeStreetCapacityById, pybind11::arg("streetId"), diff --git a/src/dsf/dsf.hpp b/src/dsf/dsf.hpp index 689da647..e6ac9f73 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 = 6; +static constexpr uint8_t DSF_VERSION_PATCH = 7; static auto const DSF_VERSION = std::format("{}.{}.{}", DSF_VERSION_MAJOR, DSF_VERSION_MINOR, DSF_VERSION_PATCH); diff --git a/src/dsf/mobility/RoadNetwork.cpp b/src/dsf/mobility/RoadNetwork.cpp index b4c5aeaa..84ca6500 100644 --- a/src/dsf/mobility/RoadNetwork.cpp +++ b/src/dsf/mobility/RoadNetwork.cpp @@ -936,7 +936,9 @@ namespace dsf::mobility { void RoadNetwork::setStreetStatusById(Id const streetId, RoadStatus const status) { try { - edge(streetId)->setStatus(status); + auto const& pStreet{edge(streetId)}; + pStreet->setStatus(status); + spdlog::info("Changed status of {} to {}", *pStreet, status); } catch (const std::out_of_range&) { throw std::out_of_range(std::format("Street with id {} not found", streetId)); } @@ -958,6 +960,36 @@ namespace dsf::mobility { nAffectedRoads.load(), streetName); } + void RoadNetwork::changeStreetNLanesById(Id const streetId, + int const nLanes, + std::optional const speedFactor) { + try { + edge(streetId)->changeNLanes(nLanes, speedFactor); + } catch (const std::out_of_range&) { + throw std::out_of_range(std::format("Street with id {} not found", streetId)); + } + } + void RoadNetwork::changeStreetNLanesByName(std::string const& streetName, + int const nLanes, + std::optional const speedFactor) { + std::atomic nAffectedRoads{0}; + std::for_each( + DSF_EXECUTION m_edges.cbegin(), + m_edges.cend(), + [this, &streetName, &nLanes, &speedFactor, &nAffectedRoads](auto const& pair) { + auto const& pStreet = pair.second; + if (pStreet->name().find(streetName) != std::string::npos) { + pStreet->changeNLanes(nLanes, speedFactor); + ++nAffectedRoads; + } + }); + spdlog::info( + "Changed number of lanes to {} for {} streets with name containing " + "\"{}\"", + nLanes, + nAffectedRoads.load(), + streetName); + } void RoadNetwork::changeStreetCapacityById(Id const streetId, double const factor) { try { auto const& pStreet{edge(streetId)}; diff --git a/src/dsf/mobility/RoadNetwork.hpp b/src/dsf/mobility/RoadNetwork.hpp index a6a883ee..2cf94c5c 100644 --- a/src/dsf/mobility/RoadNetwork.hpp +++ b/src/dsf/mobility/RoadNetwork.hpp @@ -207,6 +207,20 @@ namespace dsf::mobility { /// @param name The name to match /// @param status The status to set void setStreetStatusByName(std::string const& name, RoadStatus const status); + /// @brief Change the street's number of lanes by its id + /// @param streetId The id of the street + /// @param nLanes The new number of lanes + /// @param speedFactor Optional, The factor to multiply the max speed of the street + void changeStreetNLanesById(Id const streetId, + int const nLanes, + std::optional const speedFactor = std::nullopt); + /// @brief Change the street's number of lanes of all streets with the given name + /// @param name The name to match + /// @param nLanes The new number of lanes + /// @param speedFactor Optional, The factor to multiply the max speed of the street + void changeStreetNLanesByName(std::string const& name, + int const nLanes, + std::optional const speedFactor = std::nullopt); /// @brief Change the street's capacity by its id /// @param streetId The id of the street /// @param factor The factor to multiply the capacity by diff --git a/src/dsf/mobility/Street.cpp b/src/dsf/mobility/Street.cpp index 6ba5eb2e..cf2bf1b8 100644 --- a/src/dsf/mobility/Street.cpp +++ b/src/dsf/mobility/Street.cpp @@ -5,6 +5,30 @@ #include namespace dsf::mobility { + void Street::m_updateLaneMapping(int const nLanes) { + m_laneMapping.clear(); + switch (nLanes) { + case 1: + m_laneMapping.emplace_back(Direction::ANY); + break; + case 2: + m_laneMapping.emplace_back(Direction::RIGHTANDSTRAIGHT); + m_laneMapping.emplace_back(Direction::LEFT); + break; + case 3: + m_laneMapping.emplace_back(Direction::RIGHTANDSTRAIGHT); + m_laneMapping.emplace_back(Direction::STRAIGHT); + m_laneMapping.emplace_back(Direction::LEFT); + break; + default: + m_laneMapping.emplace_back(Direction::RIGHT); + for (auto i{1}; i < nLanes - 1; ++i) { + m_laneMapping.emplace_back(Direction::STRAIGHT); + } + m_laneMapping.emplace_back(Direction::LEFT); + break; + } + } Street::Street(Id id, std::pair nodePair, double length, @@ -27,27 +51,7 @@ namespace dsf::mobility { m_movingAgents{dsf::priority_queue, std::vector>, AgentComparator>()} { - switch (nLanes) { - case 1: - m_laneMapping.emplace_back(Direction::ANY); - break; - case 2: - m_laneMapping.emplace_back(Direction::RIGHTANDSTRAIGHT); - m_laneMapping.emplace_back(Direction::LEFT); - break; - case 3: - m_laneMapping.emplace_back(Direction::RIGHTANDSTRAIGHT); - m_laneMapping.emplace_back(Direction::STRAIGHT); - m_laneMapping.emplace_back(Direction::LEFT); - break; - default: - m_laneMapping.emplace_back(Direction::RIGHT); - for (auto i{1}; i < nLanes - 1; ++i) { - m_laneMapping.emplace_back(Direction::STRAIGHT); - } - m_laneMapping.emplace_back(Direction::LEFT); - break; - } + m_updateLaneMapping(nLanes); } auto Street::operator==(Street const& other) const -> bool { bool isEqual{true}; @@ -79,6 +83,29 @@ namespace dsf::mobility { assert(index < m_exitQueues.size()); m_exitQueues[index] = std::move(queue); } + void Street::changeNLanes(int const nLanes, std::optional const speedFactor) { + if (this->nExitingAgents() > 0) { + spdlog::warn("Changing number of lanes for {} which has {} exiting agents", + *this, + this->nExitingAgents()); + } + if (nLanes <= 0) { + throw std::invalid_argument("Number of lanes must be positive"); + } + if (nLanes == m_nLanes) { + return; + } + spdlog::info( + "Changing number of lanes for {} from {} to {}", *this, m_nLanes, nLanes); + m_capacity = static_cast(m_capacity * static_cast(nLanes) / m_nLanes); + m_transportCapacity = m_transportCapacity * nLanes / m_nLanes; + m_nLanes = nLanes; + if (speedFactor.has_value()) { + m_maxSpeed = m_maxSpeed * speedFactor.value(); + } + m_exitQueues.resize(m_nLanes); + m_updateLaneMapping(m_nLanes); + } void Street::enableCounter(std::string name, CounterPosition position) { if (m_counter.has_value()) { throw std::runtime_error( diff --git a/src/dsf/mobility/Street.hpp b/src/dsf/mobility/Street.hpp index 0d9af1db..936586bc 100644 --- a/src/dsf/mobility/Street.hpp +++ b/src/dsf/mobility/Street.hpp @@ -55,6 +55,10 @@ namespace dsf::mobility { CounterPosition m_counterPosition{CounterPosition::EXIT}; double m_stationaryWeight{1.0}; + /// @brief Update the street's lane mapping + /// @param nLanes The street's number of lanes + void m_updateLaneMapping(int const nLanes); + public: /// @brief Construct a new Street object /// @param id The street's id @@ -96,6 +100,12 @@ namespace dsf::mobility { weight > 0. ? m_stationaryWeight = weight : throw std::invalid_argument("Stationary weight must be positive"); } + /// @brief Change the number of lanes of the street. Usually if there is a construction site, you may want to + /// reduce the number of lanes and possibly the max speed. + /// @param nLanes The new number of lanes + /// @param speedFactor Optional, The factor to multiply the max speed of the street + void changeNLanes(int const nLanes, + std::optional const speedFactor = std::nullopt); /// @brief Enable a coil (dsf::Counter sensor) on the street /// @param name The name of the counter (default is "Coil_") /// @param position The position of the counter on the street (default is EXIT) diff --git a/src/dsf/utility/queue.hpp b/src/dsf/utility/queue.hpp index abe200fa..574b31d1 100644 --- a/src/dsf/utility/queue.hpp +++ b/src/dsf/utility/queue.hpp @@ -7,9 +7,15 @@ namespace dsf { template > class queue : public std::queue { public: + using std::queue::queue; typedef typename Container::iterator iterator; typedef typename Container::const_iterator const_iterator; + queue(queue&&) noexcept = default; + queue& operator=(queue&&) noexcept = default; + queue(const queue&) = delete; + queue& operator=(const queue&) = delete; + // c is a protected member of std::queue, which is the underlying container iterator begin() { return this->c.begin(); } iterator end() { return this->c.end(); } @@ -25,10 +31,16 @@ namespace dsf { typename Compare = std::less> class priority_queue : public std::priority_queue { public: + using std::priority_queue::priority_queue; typedef typename Container::iterator iterator; typedef typename Container::const_iterator const_iterator; - // c is a protected member of std::queue, which is the underlying container + priority_queue(priority_queue&&) noexcept = default; + priority_queue& operator=(priority_queue&&) noexcept = default; + priority_queue(const priority_queue&) = delete; + priority_queue& operator=(const priority_queue&) = delete; + + // c is a protected member of std::priority_queue, which is the underlying container iterator begin() { return this->c.begin(); } iterator end() { return this->c.end(); } const_iterator begin() const { return this->c.begin(); } diff --git a/test/mobility/Test_graph.cpp b/test/mobility/Test_graph.cpp index bff2e472..0a69944e 100644 --- a/test/mobility/Test_graph.cpp +++ b/test/mobility/Test_graph.cpp @@ -1548,3 +1548,139 @@ TEST_CASE("allPathsTo with closed roads") { CHECK_FALSE(pathMap.contains(1)); } } + +TEST_CASE("Change Street Lanes") { + Road::setMeanVehicleLength(5.); + + SUBCASE("changeStreetNLanesById") { + GIVEN("A network with multiple streets") { + RoadNetwork graph{}; + graph.addNode(0, dsf::geometry::Point(0.0, 0.0)); + graph.addNode(1, dsf::geometry::Point(1.0, 0.0)); + graph.addNode(2, dsf::geometry::Point(2.0, 0.0)); + + Street s01(10, std::make_pair(0, 1), 100.0, 20.0, 2); + Street s12(11, std::make_pair(1, 2), 150.0, 25.0, 3); + graph.addStreets(s01, s12); + + auto const* pStreet01 = graph.street(0, 1); + REQUIRE(pStreet01 != nullptr); + auto const initialCapacity = (*pStreet01)->capacity(); + auto const initialMaxSpeed = (*pStreet01)->maxSpeed(); + + WHEN("Lanes are increased for street by id") { + graph.changeStreetNLanesById(10, 4); + + THEN("The street's properties are updated correctly") { + CHECK_EQ((*pStreet01)->nLanes(), 4); + CHECK_EQ((*pStreet01)->capacity(), initialCapacity * 2); + CHECK_EQ((*pStreet01)->maxSpeed(), initialMaxSpeed); + CHECK_EQ((*pStreet01)->exitQueues().size(), 4); + } + } + + WHEN("Lanes are changed with speed factor") { + graph.changeStreetNLanesById(10, 1, 0.6); + + THEN("Both lanes and speed are updated") { + CHECK_EQ((*pStreet01)->nLanes(), 1); + CHECK_EQ((*pStreet01)->maxSpeed(), doctest::Approx(initialMaxSpeed * 0.6)); + } + } + + WHEN("Invalid street id is provided") { + THEN("Exception is thrown") { + CHECK_THROWS_AS(graph.changeStreetNLanesById(999, 2), std::out_of_range); + } + } + + WHEN("Invalid number of lanes is provided") { + THEN("Exception is thrown") { + CHECK_THROWS_AS(graph.changeStreetNLanesById(10, 0), std::invalid_argument); + CHECK_THROWS_AS(graph.changeStreetNLanesById(10, -1), std::invalid_argument); + } + } + } + } + + SUBCASE("changeStreetNLanesByName") { + GIVEN("A network with named streets") { + RoadNetwork graph{}; + graph.addNode(0, dsf::geometry::Point(0.0, 0.0)); + graph.addNode(1, dsf::geometry::Point(1.0, 0.0)); + graph.addNode(2, dsf::geometry::Point(2.0, 0.0)); + graph.addNode(3, dsf::geometry::Point(1.0, 1.0)); + + Street s01(10, std::make_pair(0, 1), 100.0, 20.0, 2, "Main Street"); + Street s12(11, std::make_pair(1, 2), 150.0, 25.0, 3, "Main Street"); + Street s03(12, std::make_pair(0, 3), 120.0, 15.0, 1, "Side Road"); + Street s32(13, std::make_pair(3, 2), 130.0, 18.0, 2, "Side Road"); + graph.addStreets(s01, s12, s03, s32); + + auto const* pMainStreet1 = graph.street(0, 1); + auto const* pMainStreet2 = graph.street(1, 2); + auto const* pSideRoad1 = graph.street(0, 3); + auto const* pSideRoad2 = graph.street(3, 2); + + REQUIRE(pMainStreet1 != nullptr); + REQUIRE(pMainStreet2 != nullptr); + REQUIRE(pSideRoad1 != nullptr); + REQUIRE(pSideRoad2 != nullptr); + + auto const initialMainSpeed1 = (*pMainStreet1)->maxSpeed(); + auto const initialMainSpeed2 = (*pMainStreet2)->maxSpeed(); + auto const initialSideSpeed1 = (*pSideRoad1)->maxSpeed(); + auto const initialSideSpeed2 = (*pSideRoad2)->maxSpeed(); + + WHEN("All streets with 'Main' in name are changed") { + graph.changeStreetNLanesByName("Main", 1); + + THEN("Only Main Streets are affected") { + CHECK_EQ((*pMainStreet1)->nLanes(), 1); + CHECK_EQ((*pMainStreet2)->nLanes(), 1); + CHECK_EQ((*pSideRoad1)->nLanes(), 1); // unchanged + CHECK_EQ((*pSideRoad2)->nLanes(), 2); // unchanged + CHECK_EQ((*pMainStreet1)->maxSpeed(), initialMainSpeed1); + CHECK_EQ((*pMainStreet2)->maxSpeed(), initialMainSpeed2); + } + } + + WHEN("Streets are changed by name with speed factor") { + graph.changeStreetNLanesByName("Side", 3, 0.8); + + THEN("Only Side Roads are affected with both changes") { + CHECK_EQ((*pSideRoad1)->nLanes(), 3); + CHECK_EQ((*pSideRoad2)->nLanes(), 3); + CHECK_EQ((*pMainStreet1)->nLanes(), 2); // unchanged + CHECK_EQ((*pMainStreet2)->nLanes(), 3); // unchanged + CHECK_EQ((*pSideRoad1)->maxSpeed(), doctest::Approx(initialSideSpeed1 * 0.8)); + CHECK_EQ((*pSideRoad2)->maxSpeed(), doctest::Approx(initialSideSpeed2 * 0.8)); + CHECK_EQ((*pMainStreet1)->maxSpeed(), initialMainSpeed1); // unchanged + CHECK_EQ((*pMainStreet2)->maxSpeed(), initialMainSpeed2); // unchanged + } + } + + WHEN("No streets match the name pattern") { + graph.changeStreetNLanesByName("NonExistent", 5); + + THEN("No streets are changed") { + CHECK_EQ((*pMainStreet1)->nLanes(), 2); + CHECK_EQ((*pMainStreet2)->nLanes(), 3); + CHECK_EQ((*pSideRoad1)->nLanes(), 1); + CHECK_EQ((*pSideRoad2)->nLanes(), 2); + } + } + + WHEN("Partial name match is used") { + graph.changeStreetNLanesByName("Street", 4); + + THEN("All streets with 'Street' in name are changed") { + CHECK_EQ((*pMainStreet1)->nLanes(), 4); + CHECK_EQ((*pMainStreet2)->nLanes(), 4); + CHECK_EQ((*pSideRoad1)->nLanes(), 1); // unchanged + CHECK_EQ((*pSideRoad2)->nLanes(), 2); // unchanged + } + } + } + } +} diff --git a/test/mobility/Test_street.cpp b/test/mobility/Test_street.cpp index 6a431222..7dbb1fc7 100644 --- a/test/mobility/Test_street.cpp +++ b/test/mobility/Test_street.cpp @@ -92,6 +92,79 @@ TEST_CASE("Street") { } } } + SUBCASE("changeNLanes") { + GIVEN("A street with 2 lanes") { + Street street{1, std::make_pair(0, 1), 100.0, 20.0, 2}; + int const initialCapacity = street.capacity(); + double const initialTransportCapacity = street.transportCapacity(); + double const initialMaxSpeed = street.maxSpeed(); + + WHEN("Number of lanes is increased to 4") { + street.changeNLanes(4); + THEN("All properties scale correctly") { + CHECK_EQ(street.nLanes(), 4); + CHECK_EQ(street.capacity(), initialCapacity * 2); + CHECK_EQ(street.transportCapacity(), initialTransportCapacity * 2); + CHECK_EQ(street.maxSpeed(), initialMaxSpeed); + CHECK_EQ(street.exitQueues().size(), 4); + CHECK_EQ(street.laneMapping().size(), 4); + } + } + + WHEN("Number of lanes is decreased to 1") { + street.changeNLanes(1); + THEN("All properties scale correctly") { + CHECK_EQ(street.nLanes(), 1); + CHECK_EQ(street.capacity(), initialCapacity / 2); + CHECK_EQ(street.transportCapacity(), initialTransportCapacity / 2); + CHECK_EQ(street.maxSpeed(), initialMaxSpeed); + CHECK_EQ(street.exitQueues().size(), 1); + CHECK_EQ(street.laneMapping().size(), 1); + } + } + + WHEN("Number of lanes is kept the same") { + street.changeNLanes(2); + THEN("Nothing changes") { + CHECK_EQ(street.nLanes(), 2); + CHECK_EQ(street.capacity(), initialCapacity); + CHECK_EQ(street.transportCapacity(), initialTransportCapacity); + CHECK_EQ(street.maxSpeed(), initialMaxSpeed); + } + } + + WHEN("Number of lanes changes with speed factor") { + street.changeNLanes(1, 0.5); + THEN("Speed is reduced by the factor") { + CHECK_EQ(street.nLanes(), 1); + CHECK_EQ(street.maxSpeed(), doctest::Approx(initialMaxSpeed * 0.5)); + } + } + + WHEN("Invalid number of lanes is provided") { + THEN("Exception is thrown") { + CHECK_THROWS_AS(street.changeNLanes(0), std::invalid_argument); + CHECK_THROWS_AS(street.changeNLanes(-1), std::invalid_argument); + } + } + } + + GIVEN("A street with 3 lanes") { + Street street{2, std::make_pair(1, 2), 150.0, 25.0, 3}; + + WHEN("Lanes change from 3 to 6 with speed factor 1.2") { + double const initialMaxSpeed = street.maxSpeed(); + street.changeNLanes(6, 1.2); + THEN("Capacity doubles and speed increases") { + CHECK_EQ(street.nLanes(), 6); + CHECK_EQ(street.capacity(), + static_cast(std::ceil((150.0 * 6) / Road::meanVehicleLength()))); + CHECK_EQ(street.maxSpeed(), doctest::Approx(initialMaxSpeed * 1.2)); + CHECK_EQ(street.exitQueues().size(), 6); + } + } + } + } SUBCASE("addAgent") { Agent a1{0, nullptr, 0}; a1.setFreeTime(5);