diff --git a/src/dsf/bindings.cpp b/src/dsf/bindings.cpp index 0a636e5b..626fe35b 100644 --- a/src/dsf/bindings.cpp +++ b/src/dsf/bindings.cpp @@ -34,6 +34,12 @@ PYBIND11_MODULE(dsf_cpp, m) { .value("DOUBLE_TAIL", dsf::TrafficLightOptimization::DOUBLE_TAIL) .export_values(); + // Bind RoadStatus enum + pybind11::enum_(mobility, "RoadStatus") + .value("OPEN", dsf::mobility::RoadStatus::OPEN) + .value("CLOSED", dsf::mobility::RoadStatus::CLOSED) + .export_values(); + // Bind spdlog log level enum pybind11::enum_(m, "LogLevel") .value("TRACE", spdlog::level::trace) @@ -243,6 +249,30 @@ PYBIND11_MODULE(dsf_cpp, m) { pybind11::arg("cycleTime"), pybind11::arg("counter"), dsf::g_docstrings.at("dsf::mobility::RoadNetwork::makeTrafficLight").c_str()) + .def( + "setStreetStatusById", + &dsf::mobility::RoadNetwork::setStreetStatusById, + pybind11::arg("streetId"), + pybind11::arg("status"), + dsf::g_docstrings.at("dsf::mobility::RoadNetwork::setStreetStatusById").c_str()) + .def("setStreetStatusByName", + &dsf::mobility::RoadNetwork::setStreetStatusByName, + pybind11::arg("name"), + pybind11::arg("status"), + dsf::g_docstrings.at("dsf::mobility::RoadNetwork::setStreetStatusByName") + .c_str()) + .def("changeStreetCapacityById", + &dsf::mobility::RoadNetwork::changeStreetCapacityById, + pybind11::arg("streetId"), + pybind11::arg("factor"), + dsf::g_docstrings.at("dsf::mobility::RoadNetwork::changeStreetCapacityById") + .c_str()) + .def("changeStreetCapacityByName", + &dsf::mobility::RoadNetwork::changeStreetCapacityByName, + pybind11::arg("name"), + pybind11::arg("factor"), + dsf::g_docstrings.at("dsf::mobility::RoadNetwork::changeStreetCapacityByName") + .c_str()) .def("addCoil", &dsf::mobility::RoadNetwork::addCoil, pybind11::arg("streetId"), diff --git a/src/dsf/dsf.hpp b/src/dsf/dsf.hpp index a7154348..689da647 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 = 5; +static constexpr uint8_t DSF_VERSION_PATCH = 6; static auto const DSF_VERSION = std::format("{}.{}.{}", DSF_VERSION_MAJOR, DSF_VERSION_MINOR, DSF_VERSION_PATCH); diff --git a/src/dsf/mobility/Road.hpp b/src/dsf/mobility/Road.hpp index 4c63e98f..5e9a3c3a 100644 --- a/src/dsf/mobility/Road.hpp +++ b/src/dsf/mobility/Road.hpp @@ -2,6 +2,7 @@ #include "../base/Edge.hpp" +#include #include #include #include @@ -15,6 +16,10 @@ namespace dsf::mobility { TERTIARY = 3, RESIDENTIAL = 4, }; + enum class RoadStatus : std::uint8_t { + OPEN = 0, + CLOSED = 1, + }; /// @brief The Road class represents a road in the network. class Road : public Edge { @@ -29,6 +34,7 @@ namespace dsf::mobility { bool m_hasPriority = false; std::set m_forbiddenTurns; // Stores the forbidden turns (road ids) std::optional m_roadType{std::nullopt}; + RoadStatus m_roadStatus = RoadStatus::OPEN; public: /// @brief Construct a new Road object @@ -80,7 +86,10 @@ namespace dsf::mobility { void setForbiddenTurns(std::set const& forbiddenTurns); /// @brief Set the road type /// @param roadType The road type - inline void setRoadType(RoadType roadType) { m_roadType = roadType; } + inline void setRoadType(RoadType const roadType) { m_roadType = roadType; } + /// @brief Set the road status + /// @param status The road status + inline void setStatus(RoadStatus const status) { m_roadStatus = status; } /// @brief Get the length, in meters /// @return double The length, in meters @@ -111,6 +120,9 @@ namespace dsf::mobility { /// @brief Get the road type /// @return std::optional The road type inline auto roadType() const noexcept { return m_roadType; } + /// @brief Get the road status + /// @return RoadStatus The road status + inline auto roadStatus() const noexcept { return m_roadStatus; } /// @brief Get the road's turn direction given the previous road angle /// @param previousStreetAngle The angle of the previous road /// @return Direction The turn direction @@ -127,4 +139,25 @@ namespace dsf::mobility { virtual double nExitingAgents(Direction direction, bool normalizeOnNLanes) const = 0; virtual double density(bool normalized = false) const = 0; }; -} // namespace dsf::mobility \ No newline at end of file +} // namespace dsf::mobility + +template <> +struct std::formatter { + constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); } + template + auto format(dsf::mobility::RoadStatus const& status, FormatContext&& ctx) const { + std::string_view name; + switch (status) { + case dsf::mobility::RoadStatus::OPEN: + name = "OPEN"; + break; + case dsf::mobility::RoadStatus::CLOSED: + name = "CLOSED"; + break; + default: + name = "UNKNOWN"; + break; + } + return std::format_to(ctx.out(), "{}", name); + } +}; \ No newline at end of file diff --git a/src/dsf/mobility/RoadDynamics.hpp b/src/dsf/mobility/RoadDynamics.hpp index c769928e..8f750763 100644 --- a/src/dsf/mobility/RoadDynamics.hpp +++ b/src/dsf/mobility/RoadDynamics.hpp @@ -499,10 +499,10 @@ namespace dsf::mobility { pItinerary->setPath(path); auto const newSize{pItinerary->path().size()}; if (oldSize > 0 && newSize != oldSize) { - spdlog::warn("Path for itinerary {} changed size from {} to {}", - pItinerary->id(), - oldSize, - newSize); + spdlog::debug("Path for itinerary {} changed size from {} to {}", + pItinerary->id(), + oldSize, + newSize); } if (m_bCacheEnabled) { pItinerary->save(std::format("{}{}.ity", CACHE_FOLDER, pItinerary->id())); @@ -731,7 +731,7 @@ namespace dsf::mobility { auto const timeTolerance{m_timeToleranceFactor.value() * std::ceil(pStreet->length() / pStreet->maxSpeed())}; if (timeDiff > timeTolerance) { - spdlog::warn( + spdlog::debug( "Time-step {} - {} currently on {} ({} turn - Traffic Light? {}), " "has been still for more than {} seconds ({} seconds). Killing it.", this->time_step(), @@ -1302,7 +1302,7 @@ namespace dsf::mobility { m_nAddedAgents += nAgents; if (m_timeToleranceFactor.has_value() && !m_agents.empty()) { auto const nStagnantAgents{m_agents.size()}; - spdlog::warn( + spdlog::debug( "Removing {} stagnant agents that were not inserted since the previous call to " "addAgentsUniformly().", nStagnantAgents); @@ -1414,7 +1414,7 @@ namespace dsf::mobility { m_nAddedAgents += nAgents; if (m_timeToleranceFactor.has_value() && !m_agents.empty()) { auto const nStagnantAgents{m_agents.size()}; - spdlog::warn( + spdlog::debug( "Removing {} stagnant agents that were not inserted since the previous call to " "addAgentsRandomly().", nStagnantAgents); @@ -1520,9 +1520,9 @@ namespace dsf::mobility { // Check if destination is reachable from source auto const& itinerary = itineraryIt->second; if (!itinerary->path().contains(*srcId)) { - spdlog::warn("Destination {} not reachable from source {}. Skipping agent.", - *dstId, - *srcId); + spdlog::debug("Destination {} not reachable from source {}. Skipping agent.", + *dstId, + *srcId); --nAgents; continue; } diff --git a/src/dsf/mobility/RoadNetwork.cpp b/src/dsf/mobility/RoadNetwork.cpp index 213933d7..b4c5aeaa 100644 --- a/src/dsf/mobility/RoadNetwork.cpp +++ b/src/dsf/mobility/RoadNetwork.cpp @@ -802,7 +802,7 @@ namespace dsf::mobility { } if (priorityRoads.size() < 2) { - spdlog::warn("Node {}: unable to auto-assign road priorities", pNode->id()); + spdlog::warn("{}: unable to auto-assign road priorities", *pNode); return; } @@ -934,6 +934,59 @@ namespace dsf::mobility { addEdge(std::move(street)); } + void RoadNetwork::setStreetStatusById(Id const streetId, RoadStatus const status) { + try { + edge(streetId)->setStatus(status); + } catch (const std::out_of_range&) { + throw std::out_of_range(std::format("Street with id {} not found", streetId)); + } + } + void RoadNetwork::setStreetStatusByName(std::string const& streetName, + RoadStatus const status) { + std::atomic nAffectedRoads{0}; + std::for_each(DSF_EXECUTION m_edges.cbegin(), + m_edges.cend(), + [this, &streetName, &status, &nAffectedRoads](auto const& pair) { + auto const& pStreet = pair.second; + if (pStreet->name().find(streetName) != std::string::npos) { + pStreet->setStatus(status); + ++nAffectedRoads; + } + }); + spdlog::info("Set status {} to {} streets with name containing \"{}\"", + status, + nAffectedRoads.load(), + streetName); + } + void RoadNetwork::changeStreetCapacityById(Id const streetId, double const factor) { + try { + auto const& pStreet{edge(streetId)}; + auto const& currentCapacity{pStreet->capacity()}; + pStreet->setCapacity(std::ceil(currentCapacity * factor)); + } catch (const std::out_of_range&) { + throw std::out_of_range(std::format("Street with id {} not found", streetId)); + } + } + void RoadNetwork::changeStreetCapacityByName(std::string const& streetName, + double const factor) { + std::atomic nAffectedRoads{0}; + std::for_each(DSF_EXECUTION m_edges.cbegin(), + m_edges.cend(), + [this, &streetName, &factor, &nAffectedRoads](auto const& pair) { + auto const& pStreet = pair.second; + if (pStreet->name().find(streetName) != std::string::npos) { + auto const& currentCapacity = pStreet->capacity(); + pStreet->setCapacity(std::ceil(currentCapacity * factor)); + ++nAffectedRoads; + } + }); + spdlog::info( + "Changed capacity by factor {} to {} streets with name containing \"{}\"", + factor, + nAffectedRoads.load(), + streetName); + } + void RoadNetwork::setStreetStationaryWeights( std::unordered_map const& weights) { std::for_each(DSF_EXECUTION m_edges.cbegin(), diff --git a/src/dsf/mobility/RoadNetwork.hpp b/src/dsf/mobility/RoadNetwork.hpp index 42fb845d..a6a883ee 100644 --- a/src/dsf/mobility/RoadNetwork.hpp +++ b/src/dsf/mobility/RoadNetwork.hpp @@ -198,6 +198,24 @@ namespace dsf::mobility { requires is_street_v> && (is_street_v> && ...) void addStreets(T1&& street, Tn&&... streets); + + /// @brief Set the street's status by its id + /// @param streetId The id of the street + /// @param status The status to set + void setStreetStatusById(Id const streetId, RoadStatus const status); + /// @brief Set the street's status of all streets with the given name + /// @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 capacity by its id + /// @param streetId The id of the street + /// @param factor The factor to multiply the capacity by + void changeStreetCapacityById(Id const streetId, double const factor); + /// @brief Change the street's capacity of all streets with the given name + /// @param name The name to match + /// @param factor The factor to multiply the capacity by + void changeStreetCapacityByName(std::string const& name, double const factor); + /// @brief Set the streets' stationary weights /// @param streetWeights A map where the key is the street id and the value is the street stationary weight. If a street id is not present in the map, its stationary weight is set to 1.0. void setStreetStationaryWeights(std::unordered_map const& streetWeights); @@ -366,6 +384,10 @@ namespace dsf::mobility { // Explore all incoming edges (nodes that can reach currentNode) auto const& inEdges = node(currentNode)->ingoingEdges(); for (auto const& inEdgeId : inEdges) { + // Skip closed roads + if (edge(inEdgeId)->roadStatus() == RoadStatus::CLOSED) { + continue; + } Id neighborId = edge(inEdgeId)->source(); // Calculate the weight of the edge from neighbor to currentNode using the dynamics function @@ -471,6 +493,10 @@ namespace dsf::mobility { // Explore all incoming edges (nodes that can reach currentNode) auto const& inEdges = node(currentNode)->ingoingEdges(); for (auto const& inEdgeId : inEdges) { + // Skip closed roads + if (edge(inEdgeId)->roadStatus() == RoadStatus::CLOSED) { + continue; + } Id neighborId = edge(inEdgeId)->source(); // Calculate the weight of the edge from neighbor to currentNode using the dynamics function diff --git a/src/dsf/py.typed b/src/dsf/py.typed deleted file mode 100644 index e69de29b..00000000 diff --git a/test/mobility/Test_graph.cpp b/test/mobility/Test_graph.cpp index 4127a16d..bff2e472 100644 --- a/test/mobility/Test_graph.cpp +++ b/test/mobility/Test_graph.cpp @@ -1311,3 +1311,240 @@ TEST_CASE("ShortestPath") { CHECK_EQ(path[0], 0); } } + +TEST_CASE("RoadStatus") { + SUBCASE("setStreetStatusById") { + 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(0, std::make_pair(0, 1), 100.0); + Street s12(1, std::make_pair(1, 2), 100.0); + graph.addStreets(s01, s12); + + // Initially all streets are OPEN + CHECK_EQ(graph.edge(0)->roadStatus(), RoadStatus::OPEN); + CHECK_EQ(graph.edge(1)->roadStatus(), RoadStatus::OPEN); + + // Close street by id + graph.setStreetStatusById(0, RoadStatus::CLOSED); + CHECK_EQ(graph.edge(0)->roadStatus(), RoadStatus::CLOSED); + CHECK_EQ(graph.edge(1)->roadStatus(), RoadStatus::OPEN); + + // Re-open street + graph.setStreetStatusById(0, RoadStatus::OPEN); + CHECK_EQ(graph.edge(0)->roadStatus(), RoadStatus::OPEN); + } + + SUBCASE("setStreetStatusByName") { + 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(3.0, 0.0)); + + Street s01(0, std::make_pair(0, 1), 100.0, 13.8888, 1, "Main Street"); + Street s12(1, std::make_pair(1, 2), 100.0, 13.8888, 1, "Main Street"); + Street s23(2, std::make_pair(2, 3), 100.0, 13.8888, 1, "Side Road"); + graph.addStreets(s01, s12, s23); + + // Initially all streets are OPEN + CHECK_EQ(graph.edge(0)->roadStatus(), RoadStatus::OPEN); + CHECK_EQ(graph.edge(1)->roadStatus(), RoadStatus::OPEN); + CHECK_EQ(graph.edge(2)->roadStatus(), RoadStatus::OPEN); + + // Close all streets with name "Main Street" + graph.setStreetStatusByName("Main Street", RoadStatus::CLOSED); + CHECK_EQ(graph.edge(0)->roadStatus(), RoadStatus::CLOSED); + CHECK_EQ(graph.edge(1)->roadStatus(), RoadStatus::CLOSED); + CHECK_EQ(graph.edge(2)->roadStatus(), RoadStatus::OPEN); + + // Re-open Main Street + graph.setStreetStatusByName("Main Street", RoadStatus::OPEN); + CHECK_EQ(graph.edge(0)->roadStatus(), RoadStatus::OPEN); + CHECK_EQ(graph.edge(1)->roadStatus(), RoadStatus::OPEN); + } + + SUBCASE("setStreetStatusByName - partial match") { + 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(0, std::make_pair(0, 1), 100.0, 13.8888, 1, "Via Roma Nord"); + Street s12(1, std::make_pair(1, 2), 100.0, 13.8888, 1, "Via Roma Sud"); + graph.addStreets(s01, s12); + + // Close all streets containing "Roma" in the name + graph.setStreetStatusByName("Roma", RoadStatus::CLOSED); + CHECK_EQ(graph.edge(0)->roadStatus(), RoadStatus::CLOSED); + CHECK_EQ(graph.edge(1)->roadStatus(), RoadStatus::CLOSED); + } +} + +TEST_CASE("ShortestPath with closed roads") { + SUBCASE("Closed road forces alternative path") { + // Create a network: 0 -> 1 -> 2 + // | ^ + // +-> 3 ----+ + // Path 0->1->2 has length 200, path 0->3->2 has length 300 + // If we close 0->1, shortest path should go through 3 + 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(0, std::make_pair(0, 1), 100.0); + Street s12(1, std::make_pair(1, 2), 100.0); + Street s03(2, std::make_pair(0, 3), 150.0); + Street s32(3, std::make_pair(3, 2), 150.0); + graph.addStreets(s01, s12, s03, s32); + + // Initially, shortest path is 0 -> 1 -> 2 + auto pathMap = + graph.shortestPath(0, 2, [](auto const& pEdge) { return pEdge->length(); }); + REQUIRE(pathMap.contains(0)); + CHECK_EQ(pathMap.at(0)[0], 1); + + // Close street 0->1 + graph.setStreetStatusById(0, RoadStatus::CLOSED); + + // Now shortest path should be 0 -> 3 -> 2 + pathMap = graph.shortestPath(0, 2, [](auto const& pEdge) { return pEdge->length(); }); + REQUIRE(pathMap.contains(0)); + CHECK_EQ(pathMap.at(0).size(), 1); + CHECK_EQ(pathMap.at(0)[0], 3); + + REQUIRE(pathMap.contains(3)); + CHECK_EQ(pathMap.at(3).size(), 1); + CHECK_EQ(pathMap.at(3)[0], 2); + } + + SUBCASE("No path when all routes closed") { + // Create a linear network: 0 -> 1 -> 2 + 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(0, std::make_pair(0, 1), 100.0); + Street s12(1, std::make_pair(1, 2), 100.0); + graph.addStreets(s01, s12); + + // Close the middle street + graph.setStreetStatusById(1, RoadStatus::CLOSED); + + // No path should exist from 0 to 2 + auto pathMap = + graph.shortestPath(0, 2, [](auto const& pEdge) { return pEdge->length(); }); + CHECK(pathMap.empty()); + } + + SUBCASE("Reopening road restores path") { + 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(0, std::make_pair(0, 1), 100.0); + Street s12(1, std::make_pair(1, 2), 100.0); + graph.addStreets(s01, s12); + + // Close and then reopen + graph.setStreetStatusById(0, RoadStatus::CLOSED); + auto pathMap = + graph.shortestPath(0, 2, [](auto const& pEdge) { return pEdge->length(); }); + CHECK(pathMap.empty()); + + graph.setStreetStatusById(0, RoadStatus::OPEN); + pathMap = graph.shortestPath(0, 2, [](auto const& pEdge) { return pEdge->length(); }); + CHECK_FALSE(pathMap.empty()); + REQUIRE(pathMap.contains(0)); + CHECK_EQ(pathMap.at(0)[0], 1); + } +} + +TEST_CASE("allPathsTo with closed roads") { + SUBCASE("Closed road excluded from paths") { + // Create a network: 0 -> 1 -> 2 + // | ^ + // +-> 3 ----+ + 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(0, std::make_pair(0, 1), 100.0); + Street s12(1, std::make_pair(1, 2), 100.0); + Street s03(2, std::make_pair(0, 3), 150.0); + Street s32(3, std::make_pair(3, 2), 150.0); + graph.addStreets(s01, s12, s03, s32); + + // Close street 0->1 + graph.setStreetStatusById(0, RoadStatus::CLOSED); + + // allPathsTo should not include node 1 as next hop from 0 + auto pathMap = graph.allPathsTo(2, [](auto const& pEdge) { return pEdge->length(); }); + + REQUIRE(pathMap.contains(0)); + // Node 0 should only have node 3 as next hop (not 1, since 0->1 is closed) + CHECK_EQ(pathMap.at(0).size(), 1); + CHECK_EQ(pathMap.at(0)[0], 3); + + // Node 1 should still have a path to 2 via 1->2 (which is still open) + REQUIRE(pathMap.contains(1)); + CHECK_EQ(pathMap.at(1).size(), 1); + CHECK_EQ(pathMap.at(1)[0], 2); + } + + SUBCASE("All paths blocked to target") { + 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(0, std::make_pair(0, 1), 100.0); + Street s12(1, std::make_pair(1, 2), 100.0); + graph.addStreets(s01, s12); + + // Close the only path to node 2 + graph.setStreetStatusById(1, RoadStatus::CLOSED); + + auto pathMap = graph.allPathsTo(2, [](auto const& pEdge) { return pEdge->length(); }); + + // Node 0 should not have a path to 2 + CHECK_FALSE(pathMap.contains(0)); + // Node 1 should not have a path to 2 either + CHECK_FALSE(pathMap.contains(1)); + } + + SUBCASE("Closing road by name affects allPathsTo") { + 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(0, std::make_pair(0, 1), 100.0, 13.8888, 1, "Main Road"); + Street s12(1, std::make_pair(1, 2), 100.0, 13.8888, 1, "Main Road"); + Street s03(2, std::make_pair(0, 3), 150.0, 13.8888, 1, "Side Road"); + Street s32(3, std::make_pair(3, 2), 150.0, 13.8888, 1, "Side Road"); + graph.addStreets(s01, s12, s03, s32); + + // Close all "Main Road" streets + graph.setStreetStatusByName("Main Road", RoadStatus::CLOSED); + + auto pathMap = graph.allPathsTo(2, [](auto const& pEdge) { return pEdge->length(); }); + + // From node 0, only path should be via node 3 + REQUIRE(pathMap.contains(0)); + CHECK_EQ(pathMap.at(0).size(), 1); + CHECK_EQ(pathMap.at(0)[0], 3); + + // Node 1 should have no path to 2 (since 1->2 is closed) + CHECK_FALSE(pathMap.contains(1)); + } +}