From eff3e66304d271c3467eecea5d73cf08ac7c13e7 Mon Sep 17 00:00:00 2001 From: pushkarevv Date: Mon, 2 Mar 2026 21:02:36 +0300 Subject: [PATCH 1/6] add a* solver and update infrastructure --- .gitignore | 20 +- Dockerfile | 4 +- models/src/models.cpp | 2 +- solver/factory/src/solver_factory.cpp | 4 +- solver/main/src/main.cpp | 3 +- solver/solvers/include/solvers/astar_solver.h | 14 ++ solver/solvers/src/astar_solver.cpp | 210 ++++++++++++++++++ 7 files changed, 251 insertions(+), 6 deletions(-) create mode 100644 solver/solvers/include/solvers/astar_solver.h create mode 100644 solver/solvers/src/astar_solver.cpp diff --git a/.gitignore b/.gitignore index 3fc50cc..fd075d2 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,22 @@ MODULE.bazel.lock /third_party/*/lib # Distable toolchain temporaly -/toolchain \ No newline at end of file +/toolchain + +# macOS +.DS_Store + +# LaTeX build artifacts +*.aux +*.bbl +*.blg +*.fdb_latexmk +*.fls +*.log +*.run.xml +*.synctex.gz +*.toc +*-blx.bib + +# MCAP recordings (heavy files) +data/*.mcap \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 4c055ec..8005ec8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM amd64/ubuntu:24.10 AS mapf-base +FROM ubuntu:24.04 AS mapf-base WORKDIR /tmp @@ -85,4 +85,4 @@ RUN wget https://github.com/foxglove/mcap/releases/download/releases%2Fmcap-cli% WORKDIR /mapf ENTRYPOINT ["/bin/zsh", "-lc"] -CMD ["trap : TERM INT; sleep infinity & wait"] \ No newline at end of file +CMD ["trap : TERM INT; sleep infinity & wait"] diff --git a/models/src/models.cpp b/models/src/models.cpp index 26dd969..a0c56ae 100644 --- a/models/src/models.cpp +++ b/models/src/models.cpp @@ -7,7 +7,7 @@ namespace mapf::models { AgentState::AgentState(AgentId agent_id, graph::NodeId node_id) : agent_id{std::move(agent_id)}, node_id{std::move(node_id)} {} - + m AgentState::AgentState(proto::AgentState agent_state_proto) : agent_id{std::move(agent_state_proto.agent_id())}, node_id{std::move(agent_state_proto.node_id())} {} diff --git a/solver/factory/src/solver_factory.cpp b/solver/factory/src/solver_factory.cpp index c0dfc98..6c73d6d 100644 --- a/solver/factory/src/solver_factory.cpp +++ b/solver/factory/src/solver_factory.cpp @@ -1,5 +1,6 @@ #include "factory/solver_factory.h" +#include "solvers/astar_solver.h" #include "solvers/bfs_solver.h" #include @@ -11,6 +12,7 @@ SolverFactory::SolverFactory() { factory_map_[#NAME] = []() { return std::make_shared(__VA_ARGS__); } REGISTER(bfs_solver, BFSSolver); + REGISTER(astar_solver, AStarSolver); #undef REGISTER } @@ -39,4 +41,4 @@ SolverFactory::SolverFactoryMap::const_iterator SolverFactory::cend() const { return factory_map_.cend(); } -} // namespace mapf::solver \ No newline at end of file +} // namespace mapf::solver diff --git a/solver/main/src/main.cpp b/solver/main/src/main.cpp index 9f98761..b61ed2f 100644 --- a/solver/main/src/main.cpp +++ b/solver/main/src/main.cpp @@ -1,8 +1,9 @@ -#include "models/models.h" +об#include "models/models.h" #include "factory/solver_factory.h" #include +#include #include #include #include diff --git a/solver/solvers/include/solvers/astar_solver.h b/solver/solvers/include/solvers/astar_solver.h new file mode 100644 index 0000000..faaee48 --- /dev/null +++ b/solver/solvers/include/solvers/astar_solver.h @@ -0,0 +1,14 @@ +#pragma once + +#include "solvers/solver_base.h" + +#include "models/models.h" + +namespace mapf::solver { + +struct AStarSolver : public SolverBase { + models::MAPFSolution FindSolution(const models::MAPFProblem& mapf_problem) const final; +}; + +} // namespace mapf::solver + diff --git a/solver/solvers/src/astar_solver.cpp b/solver/solvers/src/astar_solver.cpp new file mode 100644 index 0000000..39df6dc --- /dev/null +++ b/solver/solvers/src/astar_solver.cpp @@ -0,0 +1,210 @@ +#include "solvers/astar_solver.h" + +#include "solvers/bfs_solver.h" + +#include +#include +#include +#include +#include +#include + +namespace mapf::solver { + +namespace { + +using AgentGoalDistances = std::unordered_map>; + +AgentGoalDistances BuildGoalDistances(const models::MAPFProblem& mapf_problem) { + std::unordered_map> reverse_adjacency; + reverse_adjacency.reserve(mapf_problem.graph.nodes.size()); + for (const auto& edge : mapf_problem.graph.edges) { + reverse_adjacency[edge.to_node_id].emplace_back(edge.from_node_id); + } + + AgentGoalDistances goal_distances; + goal_distances.reserve(mapf_problem.agent_tasks.size()); + + for (const auto& [agent_id, agent_task] : mapf_problem.agent_tasks) { + std::unordered_map distances; + distances.reserve(mapf_problem.graph.nodes.size()); + + std::queue bfs_queue; + const graph::NodeId goal_node = agent_task.endpoints.to_node_id; + distances[goal_node] = 0; + bfs_queue.push(goal_node); + + while (!bfs_queue.empty()) { + const graph::NodeId current_node = bfs_queue.front(); + bfs_queue.pop(); + const uint64_t current_dist = distances[current_node]; + + auto reverse_it = reverse_adjacency.find(current_node); + if (reverse_it == reverse_adjacency.end()) { + continue; + } + + for (const graph::NodeId prev_node : reverse_it->second) { + if (distances.contains(prev_node)) { + continue; + } + distances[prev_node] = current_dist + 1; + bfs_queue.push(prev_node); + } + } + + goal_distances.emplace(agent_id, std::move(distances)); + } + + return goal_distances; +} + +uint64_t Heuristic( + const models::AgentStates& state, const AgentGoalDistances& goal_distances) { + uint64_t lower_bound = 0; + + for (const auto& [agent_id, agent_state] : state) { + auto distances_it = goal_distances.find(agent_id); + if (distances_it == goal_distances.end()) { + return std::numeric_limits::max(); + } + + auto agent_dist_it = distances_it->second.find(agent_state.node_id); + if (agent_dist_it == distances_it->second.end()) { + return std::numeric_limits::max(); + } + + lower_bound = std::max(lower_bound, agent_dist_it->second); + } + + return lower_bound; +} + +models::MAPFSolution BuildSolutionFromParents( + const models::MAPFProblem& mapf_problem, + models::AgentStates current_state, + const std::unordered_map& parent) { + std::unordered_map paths; + paths.reserve(mapf_problem.agent_tasks.size()); + for (const auto& [agent_id, _] : mapf_problem.agent_tasks) { + paths[agent_id] = {current_state[agent_id].node_id}; + } + + auto parent_it = parent.find(current_state); + while (parent_it != parent.end()) { + const auto& prev_state = parent_it->second; + for (const auto& [agent_id, _] : mapf_problem.agent_tasks) { + paths[agent_id].emplace_back(prev_state.at(agent_id).node_id); + } + current_state = prev_state; + parent_it = parent.find(current_state); + } + + models::MAPFSolution solution; + solution.agent_paths.reserve(paths.size()); + for (auto& [agent_id, path] : paths) { + std::reverse(path.begin(), path.end()); + solution.agent_paths.emplace(agent_id, models::AgentPath(agent_id, std::move(path))); + } + return solution; +} + +} // namespace + +models::MAPFSolution AStarSolver::FindSolution(const models::MAPFProblem& mapf_problem) const { + models::AgentStates start_state; + start_state.reserve(mapf_problem.agent_tasks.size()); + for (const auto& [agent_id, agent_task] : mapf_problem.agent_tasks) { + start_state.emplace( + agent_id, models::AgentState(agent_id, agent_task.endpoints.from_node_id)); + } + + models::AgentStates finish_state; + finish_state.reserve(mapf_problem.agent_tasks.size()); + for (const auto& [agent_id, agent_task] : mapf_problem.agent_tasks) { + finish_state.emplace( + agent_id, models::AgentState(agent_id, agent_task.endpoints.to_node_id)); + } + + const auto goal_distances = BuildGoalDistances(mapf_problem); + + struct OpenNode { + uint64_t f_score; + uint64_t g_score; + models::AgentStates state; + }; + + auto cmp = [](const OpenNode& lhs, const OpenNode& rhs) { + if (lhs.f_score != rhs.f_score) { + return lhs.f_score > rhs.f_score; + } + return lhs.g_score > rhs.g_score; + }; + std::priority_queue, decltype(cmp)> open_set(cmp); + + std::unordered_map cost_to_come; + std::unordered_map parent; + + const uint64_t start_h = Heuristic(start_state, goal_distances); + if (start_h == std::numeric_limits::max()) { + return {}; + } + open_set.push(OpenNode{start_h, 0, start_state}); + cost_to_come[start_state] = 0; + + while (!open_set.empty()) { + auto current = std::move(open_set.top()); + open_set.pop(); + + auto current_cost_it = cost_to_come.find(current.state); + if (current_cost_it == cost_to_come.end() || current.g_score != current_cost_it->second) { + continue; + } + + if (current.state == finish_state) { + return BuildSolutionFromParents(mapf_problem, std::move(current.state), parent); + } + + std::unordered_map individual_moves; + individual_moves.reserve(mapf_problem.agent_tasks.size()); + for (const auto& [agent_id, _] : mapf_problem.agent_tasks) { + const graph::NodeId current_node = current.state.at(agent_id).node_id; + const auto& neighbors = mapf_problem.graph.GetNeighbours(current_node); + individual_moves[agent_id] = neighbors; + individual_moves[agent_id].emplace(current_node); + } + + const auto joint_moves = GenerateJointMoves(individual_moves); + for (const auto& joint_move : joint_moves) { + models::AgentStates next_state; + next_state.reserve(joint_move.size()); + for (const auto& [agent_id, node_id] : joint_move) { + next_state.emplace(agent_id, models::AgentState(agent_id, node_id)); + } + + if (HasCollision(current.state, next_state)) { + continue; + } + + const uint64_t tentative_g = current.g_score + 1; + auto existing_cost_it = cost_to_come.find(next_state); + if (existing_cost_it != cost_to_come.end() && tentative_g >= existing_cost_it->second) { + continue; + } + + const uint64_t h = Heuristic(next_state, goal_distances); + if (h == std::numeric_limits::max()) { + continue; + } + + cost_to_come[next_state] = tentative_g; + parent[next_state] = current.state; + open_set.push(OpenNode{tentative_g + h, tentative_g, std::move(next_state)}); + } + } + + return {}; +} + +} // namespace mapf::solver + From bdf8d1c3915e314549207622af52561562283d61 Mon Sep 17 00:00:00 2001 From: pushkarevv Date: Mon, 2 Mar 2026 22:13:25 +0300 Subject: [PATCH 2/6] add benchmarks comparing bfs vs a* --- .bazelversion | 1 + .gitignore | 2 +- models/src/models.cpp | 2 +- solver/BUILD.bazel | 4 +- solver/solvers/include/solvers/astar_solver.h | 4 + solver/solvers/include/solvers/bfs_solver.h | 4 + solver/solvers/src/astar_solver.cpp | 3 + solver/solvers/src/bfs_solver.cpp | 3 + solver/solvers/tests/test_solvers.cpp | 0 solver/tests/test_solvers.cpp | 198 +++++++++++++++++- 10 files changed, 216 insertions(+), 5 deletions(-) create mode 100644 .bazelversion delete mode 100644 solver/solvers/tests/test_solvers.cpp diff --git a/.bazelversion b/.bazelversion new file mode 100644 index 0000000..0e79152 --- /dev/null +++ b/.bazelversion @@ -0,0 +1 @@ +8.1.1 diff --git a/.gitignore b/.gitignore index fd075d2..10a9a2c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -# Ignore BAZEL files +yj# Ignore BAZEL files MODULE.bazel.lock /bazel-* diff --git a/models/src/models.cpp b/models/src/models.cpp index a0c56ae..26dd969 100644 --- a/models/src/models.cpp +++ b/models/src/models.cpp @@ -7,7 +7,7 @@ namespace mapf::models { AgentState::AgentState(AgentId agent_id, graph::NodeId node_id) : agent_id{std::move(agent_id)}, node_id{std::move(node_id)} {} - m + AgentState::AgentState(proto::AgentState agent_state_proto) : agent_id{std::move(agent_state_proto.agent_id())}, node_id{std::move(agent_state_proto.node_id())} {} diff --git a/solver/BUILD.bazel b/solver/BUILD.bazel index 9db8b4f..1be0e00 100644 --- a/solver/BUILD.bazel +++ b/solver/BUILD.bazel @@ -1,4 +1,4 @@ -load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library") +load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library", "cc_test") package(default_visibility = ["//visibility:public"]) @@ -35,7 +35,7 @@ cc_binary( cc_test( name = "solvers_tests", - size = "small", + size = "large", srcs = glob(["tests/*.cpp"]), deps = [ ":solvers", diff --git a/solver/solvers/include/solvers/astar_solver.h b/solver/solvers/include/solvers/astar_solver.h index faaee48..897467c 100644 --- a/solver/solvers/include/solvers/astar_solver.h +++ b/solver/solvers/include/solvers/astar_solver.h @@ -8,6 +8,10 @@ namespace mapf::solver { struct AStarSolver : public SolverBase { models::MAPFSolution FindSolution(const models::MAPFProblem& mapf_problem) const final; + size_t GetNodesExpanded() const { return nodes_expanded_; } + + private: + mutable size_t nodes_expanded_ = 0; }; } // namespace mapf::solver diff --git a/solver/solvers/include/solvers/bfs_solver.h b/solver/solvers/include/solvers/bfs_solver.h index fa885fd..8b39ccd 100644 --- a/solver/solvers/include/solvers/bfs_solver.h +++ b/solver/solvers/include/solvers/bfs_solver.h @@ -17,6 +17,10 @@ bool HasCollision( struct BFSSolver : public SolverBase { models::MAPFSolution FindSolution(const models::MAPFProblem& mapf_problem) const final; + size_t GetNodesExpanded() const { return nodes_expanded_; } + + private: + mutable size_t nodes_expanded_ = 0; }; } // namespace mapf::solver diff --git a/solver/solvers/src/astar_solver.cpp b/solver/solvers/src/astar_solver.cpp index 39df6dc..4713873 100644 --- a/solver/solvers/src/astar_solver.cpp +++ b/solver/solvers/src/astar_solver.cpp @@ -152,9 +152,12 @@ models::MAPFSolution AStarSolver::FindSolution(const models::MAPFProblem& mapf_p open_set.push(OpenNode{start_h, 0, start_state}); cost_to_come[start_state] = 0; + nodes_expanded_ = 0; + while (!open_set.empty()) { auto current = std::move(open_set.top()); open_set.pop(); + ++nodes_expanded_; auto current_cost_it = cost_to_come.find(current.state); if (current_cost_it == cost_to_come.end() || current.g_score != current_cost_it->second) { diff --git a/solver/solvers/src/bfs_solver.cpp b/solver/solvers/src/bfs_solver.cpp index b8b891b..956cd01 100644 --- a/solver/solvers/src/bfs_solver.cpp +++ b/solver/solvers/src/bfs_solver.cpp @@ -73,9 +73,12 @@ models::MAPFSolution BFSSolver::FindSolution(const models::MAPFProblem& mapf_pro open_set.push(start_state); cost_to_come[start_state] = 0; + nodes_expanded_ = 0; + while (!open_set.empty()) { auto current_state = open_set.front(); open_set.pop(); + ++nodes_expanded_; if (current_state == finish_state) { std::unordered_map paths; diff --git a/solver/solvers/tests/test_solvers.cpp b/solver/solvers/tests/test_solvers.cpp deleted file mode 100644 index e69de29..0000000 diff --git a/solver/tests/test_solvers.cpp b/solver/tests/test_solvers.cpp index 534675d..f632a98 100644 --- a/solver/tests/test_solvers.cpp +++ b/solver/tests/test_solvers.cpp @@ -1,3 +1,4 @@ +#include "solvers/astar_solver.h" #include "solvers/bfs_solver.h" #include "graph/graph.h" @@ -7,10 +8,90 @@ #include +#include +#include + using namespace mapf::graph; using namespace mapf::models; using namespace mapf::solver; +// Builds a width x height grid graph with bidirectional edges +Graph BuildGridGraph(uint32_t width, uint32_t height) { + Nodes nodes; + Edges edges; + for (uint32_t y = 0; y < height; ++y) { + for (uint32_t x = 0; x < width; ++x) { + NodeId id = y * width + x; + nodes.emplace(id, Node(id)); + if (x + 1 < width) { + NodeId right = id + 1; + edges.emplace(Edge(id, right)); + edges.emplace(Edge(right, id)); + } + if (y + 1 < height) { + NodeId down = id + width; + edges.emplace(Edge(id, down)); + edges.emplace(Edge(down, id)); + } + } + } + return Graph(std::move(nodes), std::move(edges)); +} + +// Runs both solvers on the same problem and collects metrics +struct BenchmarkResult { + size_t bfs_nodes_expanded; + size_t astar_nodes_expanded; + double bfs_time_ms; + double astar_time_ms; + size_t bfs_makespan; + size_t astar_makespan; +}; + +BenchmarkResult RunBenchmark(const MAPFProblem& problem) { + BenchmarkResult result{}; + + BFSSolver bfs; + AStarSolver astar; + + auto t0 = std::chrono::high_resolution_clock::now(); + auto bfs_solution = bfs.FindSolution(problem); + auto t1 = std::chrono::high_resolution_clock::now(); + auto astar_solution = astar.FindSolution(problem); + auto t2 = std::chrono::high_resolution_clock::now(); + + result.bfs_time_ms = + std::chrono::duration(t1 - t0).count(); + result.astar_time_ms = + std::chrono::duration(t2 - t1).count(); + + result.bfs_nodes_expanded = bfs.GetNodesExpanded(); + result.astar_nodes_expanded = astar.GetNodesExpanded(); + + if (!bfs_solution.agent_paths.empty()) { + result.bfs_makespan = bfs_solution.agent_paths.begin()->second.path.size() - 1; + } + if (!astar_solution.agent_paths.empty()) { + result.astar_makespan = astar_solution.agent_paths.begin()->second.path.size() - 1; + } + + return result; +} + +void PrintBenchmark(const std::string& name, const BenchmarkResult& r) { + std::cout << "\n--- " << name << " ---" << std::endl; + std::cout << " Nodes expanded: BFS=" << r.bfs_nodes_expanded + << " A*=" << r.astar_nodes_expanded + << " (A* is " << (r.bfs_nodes_expanded / std::max(r.astar_nodes_expanded, size_t(1))) + << "x fewer)" << std::endl; + std::cout << " Time (ms): BFS=" << r.bfs_time_ms + << " A*=" << r.astar_time_ms << std::endl; + std::cout << " Makespan: BFS=" << r.bfs_makespan + << " A*=" << r.astar_makespan << std::endl; +} + +// Correctness tests from starter code (4 nodes, 2 agents) + class MAPFProblemTest1 : public testing::Test { protected: MAPFProblemTest1() { @@ -48,7 +129,122 @@ TEST_F(MAPFProblemTest1, BFSSolver) { ASSERT_VALID_SOLUTION(problem, solver.FindSolution(problem)); } +TEST_F(MAPFProblemTest1, AStarSolver) { + auto solver = AStarSolver(); + ASSERT_VALID_SOLUTION(problem, solver.FindSolution(problem)); +} + TEST_F(MAPFProblemTest2, BFSSolver) { auto solver = BFSSolver(); ASSERT_VALID_SOLUTION(problem, solver.FindSolution(problem)); -} \ No newline at end of file +} + +TEST_F(MAPFProblemTest2, AStarSolver) { + auto solver = AStarSolver(); + ASSERT_VALID_SOLUTION(problem, solver.FindSolution(problem)); +} + +// Benchmark: small grid, minimal difference expected +TEST(Benchmark, Grid3x3_2Agents) { + auto graph = BuildGridGraph(3, 3); + AgentTasks tasks{ + {0, {0, {0, 8}}}, + {1, {1, {8, 0}}}, + }; + MAPFProblem problem(std::move(graph), std::move(tasks)); + + BFSSolver bfs; + AStarSolver astar; + ASSERT_VALID_SOLUTION(problem, bfs.FindSolution(problem)); + ASSERT_VALID_SOLUTION(problem, astar.FindSolution(problem)); + + auto r = RunBenchmark(problem); + PrintBenchmark("3x3 grid, 2 agents", r); + + EXPECT_LE(astar.GetNodesExpanded(), bfs.GetNodesExpanded()); + EXPECT_EQ(r.bfs_makespan, r.astar_makespan); +} + +// Benchmark: corridor with a single side pocket (bottleneck) +// 0 — 1 — 2 — 3 — 4 — 5 — 6 — 7 +// | +// 8 +// Agents swap ends: 0->7 and 7->0. Only node 8 allows them to pass. +TEST(Benchmark, CorridorBottleneck_2Agents) { + Nodes nodes; + Edges edges; + for (uint32_t i = 0; i < 9; ++i) { + nodes.emplace(i, Node(i)); + } + // main corridor + for (uint32_t i = 0; i < 7; ++i) { + edges.emplace(Edge(i, i + 1)); + edges.emplace(Edge(i + 1, i)); + } + // side pocket at node 3 + edges.emplace(Edge(3, 8)); + edges.emplace(Edge(8, 3)); + auto graph = Graph(std::move(nodes), std::move(edges)); + + AgentTasks tasks{ + {0, {0, {0, 7}}}, + {1, {1, {7, 0}}}, + }; + MAPFProblem problem(std::move(graph), std::move(tasks)); + + BFSSolver bfs; + AStarSolver astar; + ASSERT_VALID_SOLUTION(problem, bfs.FindSolution(problem)); + ASSERT_VALID_SOLUTION(problem, astar.FindSolution(problem)); + + auto r = RunBenchmark(problem); + PrintBenchmark("corridor with bottleneck, 2 agents", r); + + EXPECT_LE(astar.GetNodesExpanded(), bfs.GetNodesExpanded()); + EXPECT_EQ(r.bfs_makespan, r.astar_makespan); +} + +// Benchmark: 3 agents, larger state space +TEST(Benchmark, Grid4x4_3Agents) { + auto graph = BuildGridGraph(4, 4); + AgentTasks tasks{ + {0, {0, {0, 15}}}, + {1, {1, {3, 12}}}, + {2, {2, {12, 3}}}, + }; + MAPFProblem problem(std::move(graph), std::move(tasks)); + + BFSSolver bfs; + AStarSolver astar; + ASSERT_VALID_SOLUTION(problem, bfs.FindSolution(problem)); + ASSERT_VALID_SOLUTION(problem, astar.FindSolution(problem)); + + auto r = RunBenchmark(problem); + PrintBenchmark("4x4 grid, 3 agents", r); + + EXPECT_LE(astar.GetNodesExpanded(), bfs.GetNodesExpanded()); + EXPECT_EQ(r.bfs_makespan, r.astar_makespan); +} + +// Benchmark: 4 agents, all doing diagonal swaps — hardest case +TEST(Benchmark, Grid4x4_4Agents) { + auto graph = BuildGridGraph(4, 4); + AgentTasks tasks{ + {0, {0, {0, 15}}}, + {1, {1, {15, 0}}}, + {2, {2, {3, 12}}}, + {3, {3, {12, 3}}}, + }; + MAPFProblem problem(std::move(graph), std::move(tasks)); + + BFSSolver bfs; + AStarSolver astar; + ASSERT_VALID_SOLUTION(problem, bfs.FindSolution(problem)); + ASSERT_VALID_SOLUTION(problem, astar.FindSolution(problem)); + + auto r = RunBenchmark(problem); + PrintBenchmark("4x4 grid, 4 agents", r); + + EXPECT_LE(astar.GetNodesExpanded(), bfs.GetNodesExpanded()); + EXPECT_EQ(r.bfs_makespan, r.astar_makespan); +} From 7565d657a8e5d4b11e201ce2b27405b3c2e033f0 Mon Sep 17 00:00:00 2001 From: pushkarevv Date: Fri, 15 May 2026 10:00:00 +0300 Subject: [PATCH 3/6] chore: remove foxglove/MCAP recorder and simulator runtime The JSON exporter + Python visualizer pipeline (added in later commits) replaces the MCAP/Foxglove recording path. Removes the simulator binary, its RecordActor and the MCAP toolchain; swaps the Docker dev environment to Python + matplotlib for the experiment plots. --- Dockerfile | 11 +- MODULE.bazel | 1 - foxglove/BUILD.bazel | 15 - foxglove/proto/foxglove.proto | 157 --------- simulator/BUILD.bazel | 74 ---- simulator/actors/include/actors/actor.h | 32 -- .../actors/include/actors/agents_actor.h | 48 --- .../actors/include/actors/record_actor.h | 50 --- .../actors/include/actors/solver_actor.h | 23 -- simulator/actors/src/actor.cpp | 15 - simulator/actors/src/agents_actor.cpp | 102 ------ simulator/actors/src/record_actor.cpp | 333 ------------------ simulator/actors/src/solver_actor.cpp | 26 -- simulator/context/include/context/context.h | 20 -- simulator/context/src/context.cpp | 11 - simulator/event/include/event/event.h | 19 - simulator/event/include/event/event_bus.h | 50 --- simulator/event/include/event/event_queue.h | 29 -- simulator/event/src/event.cpp | 9 - simulator/event/src/event_bus.cpp | 9 - simulator/event/src/event_queue.cpp | 27 -- .../launcher/include/launcher/launcher.h | 33 -- simulator/launcher/src/launcher.cpp | 25 -- simulator/main/src/main.cpp | 116 ------ .../include/messages/agent_move_message.h | 17 - .../include/messages/mapf_problem_message.h | 15 - .../include/messages/mapf_solution_message.h | 15 - simulator/messages/include/messages/message.h | 14 - .../messages/request_agent_states_message.h | 11 - .../messages/response_agent_states_message.h | 16 - simulator/messages/src/agent_move_message.cpp | 9 - .../messages/src/mapf_problem_message.cpp | 10 - .../messages/src/mapf_solution_message.cpp | 10 - .../src/response_agent_states_message.cpp | 11 - 34 files changed, 7 insertions(+), 1356 deletions(-) delete mode 100644 foxglove/BUILD.bazel delete mode 100644 foxglove/proto/foxglove.proto delete mode 100644 simulator/BUILD.bazel delete mode 100644 simulator/actors/include/actors/actor.h delete mode 100644 simulator/actors/include/actors/agents_actor.h delete mode 100644 simulator/actors/include/actors/record_actor.h delete mode 100644 simulator/actors/include/actors/solver_actor.h delete mode 100644 simulator/actors/src/actor.cpp delete mode 100644 simulator/actors/src/agents_actor.cpp delete mode 100644 simulator/actors/src/record_actor.cpp delete mode 100644 simulator/actors/src/solver_actor.cpp delete mode 100644 simulator/context/include/context/context.h delete mode 100644 simulator/context/src/context.cpp delete mode 100644 simulator/event/include/event/event.h delete mode 100644 simulator/event/include/event/event_bus.h delete mode 100644 simulator/event/include/event/event_queue.h delete mode 100644 simulator/event/src/event.cpp delete mode 100644 simulator/event/src/event_bus.cpp delete mode 100644 simulator/event/src/event_queue.cpp delete mode 100644 simulator/launcher/include/launcher/launcher.h delete mode 100644 simulator/launcher/src/launcher.cpp delete mode 100644 simulator/main/src/main.cpp delete mode 100644 simulator/messages/include/messages/agent_move_message.h delete mode 100644 simulator/messages/include/messages/mapf_problem_message.h delete mode 100644 simulator/messages/include/messages/mapf_solution_message.h delete mode 100644 simulator/messages/include/messages/message.h delete mode 100644 simulator/messages/include/messages/request_agent_states_message.h delete mode 100644 simulator/messages/include/messages/response_agent_states_message.h delete mode 100644 simulator/messages/src/agent_move_message.cpp delete mode 100644 simulator/messages/src/mapf_problem_message.cpp delete mode 100644 simulator/messages/src/mapf_solution_message.cpp delete mode 100644 simulator/messages/src/response_agent_states_message.cpp diff --git a/Dockerfile b/Dockerfile index 8005ec8..14e06c3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -77,11 +77,14 @@ RUN wget https://github.com/bazelbuild/buildtools/releases/download/v7.3.1/build && chmod +x buildifier-linux-${TARGETARCH} \ && mv buildifier-linux-${TARGETARCH} /usr/bin/buildifier -# Install MCAP CLI +# Python + matplotlib for the experiment plots (viz/plot_*.py). -RUN wget https://github.com/foxglove/mcap/releases/download/releases%2Fmcap-cli%2Fv0.0.50/mcap-linux-${TARGETARCH} \ - && chmod +x mcap-linux-${TARGETARCH} \ - && mv mcap-linux-${TARGETARCH} /usr/bin/mcap +RUN apt-get update -q \ + && apt-get install -yq --no-install-recommends \ + python3 \ + python3-matplotlib \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean WORKDIR /mapf ENTRYPOINT ["/bin/zsh", "-lc"] diff --git a/MODULE.bazel b/MODULE.bazel index 171ab60..3d74183 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -10,7 +10,6 @@ bazel_dep(name = "boost.describe", version = "1.83.0") bazel_dep(name = "boost.program_options", version = "1.83.0") bazel_dep(name = "googletest", version = "1.15.2") bazel_dep(name = "google_benchmark", version = "1.8.5") -bazel_dep(name = "mcap", version = "1.4.0") bazel_dep(name = "platforms", version = "0.0.10") bazel_dep(name = "protobuf", version = "29.0") bazel_dep(name = "rules_cc", version = "0.0.16") diff --git a/foxglove/BUILD.bazel b/foxglove/BUILD.bazel deleted file mode 100644 index 2638872..0000000 --- a/foxglove/BUILD.bazel +++ /dev/null @@ -1,15 +0,0 @@ -package(default_visibility = ["//visibility:public"]) - -proto_library( - name = "foxglove_proto", - srcs = glob(["proto/*.proto"]), - deps = [ - "@protobuf//:duration_proto", - "@protobuf//:timestamp_proto", - ], -) - -cc_proto_library( - name = "foxglove_cc_proto", - deps = [":foxglove_proto"], -) diff --git a/foxglove/proto/foxglove.proto b/foxglove/proto/foxglove.proto deleted file mode 100644 index 88a7d97..0000000 --- a/foxglove/proto/foxglove.proto +++ /dev/null @@ -1,157 +0,0 @@ -syntax = "proto3"; - -import "google/protobuf/duration.proto"; -import "google/protobuf/timestamp.proto"; - -package foxglove; - -message KeyValuePair { - optional string key = 1; - optional string value = 2; -} - -message Vector2 { - optional double x = 1; - optional double y = 2; -} - -message Vector3 { - optional double x = 1; - optional double y = 2; - optional double z = 3; -} - -message Point2 { - optional double x = 1; - optional double y = 2; -} - -message Point3 { - optional double x = 1; - optional double y = 2; - optional double z = 3; -} - -message Quaternion { - optional double x = 1; - optional double y = 2; - optional double z = 3; - optional double w = 4; -} - -message Pose { - optional Point3 position = 1; - optional Quaternion orientation = 2; -} - -message Color { - optional float r = 1; - optional float g = 2; - optional float b = 3; - optional float a = 4; -} - -message ArrowPrimitive { - optional Pose pose = 1; - optional double shaft_length = 2; - optional double shaft_diameter = 3; - optional double head_length = 4; - optional double head_diameter = 5; - optional Color color = 6; -} - -message CubePrimitive { - optional Pose pose = 1; - optional Vector3 size = 2; - optional Color color = 3; -} - -message CylinderPrimitive { - optional Pose pose = 1; - optional Vector3 size = 2; - optional double bottom_scale = 3; - optional double top_scale = 4; - optional Color color = 5; -} - -message LinePrimitive { - enum Type { - LINE_STRIP = 0; - LINE_LOOP = 1; - LINE_LIST = 2; - } - - optional Type type = 1; - optional Pose pose = 2; - optional double thickness = 3; - optional bool scale_invariant = 4; - repeated Point3 points = 5; - optional Color color = 6; - repeated Color colors = 7; - repeated fixed32 indices = 8; -} - -message ModelPrimitive { - optional Pose pose = 1; - optional Vector3 scale = 2; - optional Color color = 3; - optional bool override_color = 4; - optional string url = 5; - optional string media_type = 6; - optional bytes data = 7; -} - -message SpherePrimitive { - optional Pose pose = 1; - optional Vector3 size = 2; - optional Color color = 3; -} - -message TextPrimitive { - optional Pose pose = 1; - optional bool billboard = 2; - optional double font_size = 3; - optional bool scale_invariant = 4; - optional Color color = 5; - optional string text = 6; -} - -message TriangleListPrimitive { - optional Pose pose = 1; - repeated Point3 points = 2; - optional Color color = 3; - repeated Color colors = 4; - repeated fixed32 indices = 5; -} - -message SceneEntity { - optional google.protobuf.Timestamp timestamp = 1; - optional string frame_id = 2; - optional string id = 3; - optional google.protobuf.Duration lifetime = 4; - optional bool frame_locked = 5; - repeated KeyValuePair metadata = 6; - repeated ArrowPrimitive arrows = 7; - repeated CubePrimitive cubes = 8; - repeated SpherePrimitive spheres = 9; - repeated CylinderPrimitive cylinders = 10; - repeated LinePrimitive lines = 11; - repeated TriangleListPrimitive triangles = 12; - repeated TextPrimitive texts = 13; - repeated ModelPrimitive models = 14; -} - -message SceneEntityDeletion { - enum Type { - MATCHING_ID = 0; - ALL = 1; - } - optional google.protobuf.Timestamp timestamp = 1; - optional Type type = 2; - optional string id = 3; -} - -message SceneUpdate { - repeated SceneEntityDeletion deletions = 1; - repeated SceneEntity entities = 2; -} diff --git a/simulator/BUILD.bazel b/simulator/BUILD.bazel deleted file mode 100644 index aba90e9..0000000 --- a/simulator/BUILD.bazel +++ /dev/null @@ -1,74 +0,0 @@ -load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library") - -package(default_visibility = ["//visibility:public"]) - -cc_library( - name = "messages", - srcs = glob(["messages/src/*.cpp"]), - hdrs = glob(["messages/include/messages/*.h"]), - includes = ["messages/include"], - deps = [ - "//graph", - "//models", - ], -) - -cc_library( - name = "event", - srcs = glob(["event/src/*.cpp"]), - hdrs = glob(["event/include/event/*.h"]), - includes = ["event/include"], - deps = [ - ":messages", - ], -) - -cc_library( - name = "context", - srcs = glob(["context/src/*.cpp"]), - hdrs = glob(["context/include/context/*.h"]), - includes = ["context/include"], - deps = [ - ":event", - ], -) - -cc_library( - name = "actors", - srcs = glob(["actors/src/*.cpp"]), - hdrs = glob(["actors/include/actors/*.h"]), - includes = ["actors/include"], - deps = [ - ":context", - ":event", - ":messages", - "//foxglove:foxglove_cc_proto", - "//solver:solvers", - "@mcap", - ], -) - -cc_library( - name = "launcher", - srcs = glob(["launcher/src/*.cpp"]), - hdrs = glob(["launcher/include/launcher/*.h"]), - includes = ["launcher/include"], - deps = [ - ":actors", - ":context", - ":event", - ], -) - -cc_binary( - name = "main", - srcs = ["main/src/main.cpp"], - data = glob(["data/*"]), - deps = [ - ":actors", - ":launcher", - "//models", - "//solver:factory", - "@boost.program_options", - ], -) diff --git a/simulator/actors/include/actors/actor.h b/simulator/actors/include/actors/actor.h deleted file mode 100644 index fcbd918..0000000 --- a/simulator/actors/include/actors/actor.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include "context/context.h" - -#include "messages/message.h" - -#include - -namespace mapf::simulator { - -class Actor; - -using ActorPtr = std::shared_ptr; -using ActorRef = std::weak_ptr; - -class Actor : public std::enable_shared_from_this { - public: - explicit Actor(ContextPtr context); - - virtual ~Actor() = default; - - ActorRef GetActorRef(); - - virtual Actor& OnStart(); - - virtual Actor& OnFinish(); - - protected: - ContextPtr context_; -}; - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/actors/include/actors/agents_actor.h b/simulator/actors/include/actors/agents_actor.h deleted file mode 100644 index b5ecb4f..0000000 --- a/simulator/actors/include/actors/agents_actor.h +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once - -#include "actors/actor.h" - -#include "graph/graph.h" - -#include "messages/agent_move_message.h" -#include "messages/mapf_solution_message.h" -#include "messages/message.h" -#include "messages/request_agent_states_message.h" - -#include "models/models.h" - -#include -#include - -namespace mapf::simulator { - -class AgentsActor final : public Actor { - public: - AgentsActor(ContextPtr context, models::MAPFProblemPtr mapf_problem); - - AgentsActor& OnStart() final; - - const models::AgentState& GetAgentState(const models::AgentId& agent_id) const; - - private: - bool HasReachedGoal(const models::AgentId& agent_id) const; - - void HandleAgentMoveMessage(std::shared_ptr agent_move_message); - - void HandleMAPFSolutionMessage(std::shared_ptr mapf_solution_message); - - void HandleRequestAgentStatesMessage( - std::shared_ptr request_agent_states_message); - - void ScheduleNextMove(const models::AgentId& agent_id); - - models::MAPFProblemPtr mapf_problem_; - struct VersionedMapfSolution { - uint32_t version; - models::MAPFSolutionPtr mapf_solution; - } versioned_mapf_solution_; - models::AgentStatesPtr agent_states_; - std::unordered_map plan_progresses_; -}; - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/actors/include/actors/record_actor.h b/simulator/actors/include/actors/record_actor.h deleted file mode 100644 index 3ccbc1b..0000000 --- a/simulator/actors/include/actors/record_actor.h +++ /dev/null @@ -1,50 +0,0 @@ -#pragma once - -#include "actors/actor.h" - -#include "foxglove/proto/foxglove.pb.h" - -#include "graph/graph.h" - -#include "messages/response_agent_states_message.h" - -#include "models/models.h" - -#include - -#include -#include - -namespace mapf::simulator { - -class RecordActor final : public Actor { - public: - RecordActor( - ContextPtr context, std::filesystem::path scene_file, double period, graph::GraphPtr graph); - - ~RecordActor() final; - - RecordActor& OnStart() final; - - private: - void HandleResponseAgentStatesMessage( - std::shared_ptr response_agent_states_message); - - RecordActor& WriteGraph(); - - RecordActor& WriteAgentStates(const models::AgentStates& agent_states); - - foxglove::SceneUpdate& ResetSceneUpdate(); - - RecordActor& WriteSceneUpdate(); - - std::filesystem::path scene_file_; - double period_; - graph::GraphPtr graph_; - foxglove::SceneUpdate scene_update_; - double scene_update_ts_; - mcap::McapWriter writer_; - mcap::Channel channel_; -}; - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/actors/include/actors/solver_actor.h b/simulator/actors/include/actors/solver_actor.h deleted file mode 100644 index fc8635a..0000000 --- a/simulator/actors/include/actors/solver_actor.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include "actors/actor.h" - -#include "messages/mapf_problem_message.h" - -#include "solvers/solver_base.h" - -#include - -namespace mapf::simulator { - -class SolverActor final : public Actor { - public: - SolverActor(ContextPtr context, solver::SolverPtr solver); - - private: - void HandleMAPFProblemMessage(std::shared_ptr mapf_problem_message); - - solver::SolverPtr solver_; -}; - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/actors/src/actor.cpp b/simulator/actors/src/actor.cpp deleted file mode 100644 index c5f5d2d..0000000 --- a/simulator/actors/src/actor.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include "actors/actor.h" - -#include - -namespace mapf::simulator { - -Actor::Actor(ContextPtr context) : context_{std::move(context)} {} - -ActorRef Actor::GetActorRef() { return ActorRef(shared_from_this()); } - -Actor& Actor::OnStart() { return *this; } - -Actor& Actor::OnFinish() { return *this; } - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/actors/src/agents_actor.cpp b/simulator/actors/src/agents_actor.cpp deleted file mode 100644 index b94c1ac..0000000 --- a/simulator/actors/src/agents_actor.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "actors/agents_actor.h" - -#include "messages/mapf_problem_message.h" -#include "messages/response_agent_states_message.h" - -namespace mapf::simulator { - -AgentsActor::AgentsActor(ContextPtr context, models::MAPFProblemPtr mapf_problem) : - Actor{std::move(context)}, - mapf_problem_{std::move(mapf_problem)}, - versioned_mapf_solution_{.version = 0, .mapf_solution = nullptr}, - agent_states_{std::make_shared()} { - agent_states_->reserve(mapf_problem_->agent_tasks.size()); - plan_progresses_.reserve(mapf_problem_->agent_tasks.size()); - for (const auto& [agent_id, agent_task] : mapf_problem_->agent_tasks) { - agent_states_->emplace( - agent_id, models::AgentState(agent_id, agent_task.endpoints.from_node_id)); - plan_progresses_.emplace(agent_id, 0); - } - context_->event_bus - ->Subscribe([this](MessagePtr message) { - HandleAgentMoveMessage(std::static_pointer_cast(std::move(message))); - }) - .Subscribe([this](MessagePtr message) { - HandleMAPFSolutionMessage( - std::static_pointer_cast(std::move(message))); - }) - .Subscribe([this](MessagePtr message) { - HandleRequestAgentStatesMessage( - std::static_pointer_cast(std::move(message))); - }); -} - -AgentsActor& AgentsActor::OnStart() { - context_->event_bus->Publish( - context_->current_time, std::make_shared(mapf_problem_)); - return *this; -} - -const models::AgentState& AgentsActor::GetAgentState(const models::AgentId& agent_id) const { - return agent_states_->at(agent_id); -} - -bool AgentsActor::HasReachedGoal(const models::AgentId& agent_id) const { - return GetAgentState(agent_id).node_id - == mapf_problem_->agent_tasks[agent_id].endpoints.to_node_id; -} - -void AgentsActor::HandleAgentMoveMessage(std::shared_ptr agent_move_message) { - if (agent_move_message->plan_version != versioned_mapf_solution_.version) { - return; - } - const auto& agent_id = agent_move_message->agent_id; - plan_progresses_[agent_id] = agent_move_message->plan_step; - (*agent_states_)[agent_id] = models::AgentState( - agent_id, - versioned_mapf_solution_.mapf_solution->agent_paths[agent_id] - .path[agent_move_message->plan_step]); - ScheduleNextMove(agent_id); -} - -void AgentsActor::HandleMAPFSolutionMessage( - std::shared_ptr mapf_solution_message) { - ++versioned_mapf_solution_.version; - versioned_mapf_solution_.mapf_solution = std::move(mapf_solution_message->mapf_solution); - for (const auto& [agent_id, _] : mapf_problem_->agent_tasks) { - plan_progresses_[agent_id] = 0; - ScheduleNextMove(agent_id); - } -} - -void AgentsActor::HandleRequestAgentStatesMessage( - std::shared_ptr request_agent_states_message) { - auto agent_states = std::make_shared(); - bool all_finished = true; - for (const auto& [agent_id, _] : mapf_problem_->agent_tasks) { - agent_states->emplace(agent_id, GetAgentState(agent_id)); - all_finished &= HasReachedGoal(agent_id); - } - context_->event_bus->Publish( - context_->current_time, - std::make_shared(std::move(agent_states), all_finished)); -} - -void AgentsActor::ScheduleNextMove(const models::AgentId& agent_id) { - if (HasReachedGoal(agent_id)) { - return; - } - if (versioned_mapf_solution_.mapf_solution == nullptr) { - return; - } - const auto& plan = versioned_mapf_solution_.mapf_solution->agent_paths[agent_id].path; - if (plan_progresses_[agent_id] >= plan.size()) { - return; - } - double move_time = 1; // All agents speeds equals 1 for now - auto message = std::make_shared( - versioned_mapf_solution_.version, agent_id, plan_progresses_[agent_id] + 1); - context_->event_bus->Publish(context_->current_time + move_time, std::move(message)); -} - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/actors/src/record_actor.cpp b/simulator/actors/src/record_actor.cpp deleted file mode 100644 index 68a1b29..0000000 --- a/simulator/actors/src/record_actor.cpp +++ /dev/null @@ -1,333 +0,0 @@ -#define MCAP_IMPLEMENTATION - -#include "actors/record_actor.h" - -#include "geom/common.h" - -#include "messages/request_agent_states_message.h" - -#include -#include - -#include -#include -#include - -namespace { - -struct RGB { - float r, g, b; -}; - -RGB hsv2rgb(float h, float s, float v) { - float c = v * s; - float x = c * (1 - std::abs(std::fmod(h / 60.0f, 2) - 1)); - float m = v - c; - - float r, g, b; - if (h >= 0 && h < 60) { - r = c; - g = x; - b = 0; - } else if (h >= 60 && h < 120) { - r = x; - g = c; - b = 0; - } else if (h >= 120 && h < 180) { - r = 0; - g = c; - b = x; - } else if (h >= 180 && h < 240) { - r = 0; - g = x; - b = c; - } else if (h >= 240 && h < 300) { - r = x; - g = 0; - b = c; - } else { - r = c; - g = 0; - b = x; - } - - return RGB{.r = r + m, .g = g + m, .b = b + m}; -} - -google::protobuf::FileDescriptorSet BuildFileDescriptorSet( - const google::protobuf::Descriptor* top_level_descriptor) { - google::protobuf::FileDescriptorSet fd_set; - std::queue to_add; - to_add.push(top_level_descriptor->file()); - std::unordered_set seen_dependencies; - while (!to_add.empty()) { - const google::protobuf::FileDescriptor* next = to_add.front(); - to_add.pop(); - next->CopyTo(fd_set.add_file()); - for (int i = 0; i < next->dependency_count(); ++i) { - const auto& dep = next->dependency(i); - if (seen_dependencies.find(dep->name()) == seen_dependencies.end()) { - seen_dependencies.insert(dep->name()); - to_add.push(dep); - } - } - } - return fd_set; -} - -} // namespace - -namespace mapf::simulator { - -RecordActor::RecordActor( - ContextPtr context, std::filesystem::path scene_file, double period, graph::GraphPtr graph) : - Actor{std::move(context)}, - scene_file_{std::move(scene_file)}, - period_{period}, - graph_{std::move(graph)}, - scene_update_{}, - scene_update_ts_{context_->current_time} { - auto res = writer_.open(scene_file_.string(), mcap::McapWriterOptions("")); - if (!res.ok()) { - throw std::runtime_error(std::format("Failed to open file {}", scene_file_.string())); - } - mcap::Schema schema( - "foxglove.SceneUpdate", - "protobuf", - BuildFileDescriptorSet(foxglove::SceneUpdate::descriptor()).SerializeAsString()); - writer_.addSchema(schema); - channel_ = mcap::Channel( - "/foxglove/SceneUpdate", - "protobuf", - schema.id, - {{"protobuf_message_type", "foxglove.SceneUpdate"}}); - writer_.addChannel(channel_); - - context_->event_bus->Subscribe([this](MessagePtr message) { - HandleResponseAgentStatesMessage( - std::static_pointer_cast(std::move(message))); - }); -} - -RecordActor::~RecordActor() { - WriteSceneUpdate(); - writer_.close(); -} - -RecordActor& RecordActor::OnStart() { - WriteGraph(); - context_->event_bus->Publish( - context_->current_time, std::make_shared()); - return *this; -} - -void RecordActor::HandleResponseAgentStatesMessage( - std::shared_ptr response_agent_states_message) { - if (response_agent_states_message->agent_states == nullptr) { - return; - } - WriteAgentStates(*(response_agent_states_message->agent_states)); - if (!response_agent_states_message->all_finished) { - context_->event_bus->Publish( - context_->current_time + period_, std::make_shared()); - } -} - -RecordActor& RecordActor::WriteGraph() { - auto& scene_update = ResetSceneUpdate(); - - auto* node_entity = scene_update.add_entities(); - node_entity->set_frame_id("scene"); - node_entity->set_id("nodes"); - node_entity->set_frame_locked(true); - - auto* lifetime = node_entity->mutable_lifetime(); - lifetime->set_seconds(0); - lifetime->set_nanos(0); - - for (const auto& [_, node] : graph_->nodes) { - { - auto* cylinder = node_entity->add_cylinders(); - - auto* pose = cylinder->mutable_pose(); - auto* position = pose->mutable_position(); - position->set_x(node.pos.x); - position->set_y(node.pos.y); - position->set_z(0.0); - - auto* size = cylinder->mutable_size(); - size->set_x(0.2); - size->set_y(0.2); - size->set_z(0.05); - - auto* color = cylinder->mutable_color(); - color->set_r(0.7); - color->set_g(0.7); - color->set_b(0.7); - color->set_a(1.0); - } - - { - auto* text = node_entity->add_texts(); - text->set_text("Node " + std::to_string(node.id)); - text->set_billboard(true); - text->set_font_size(12); - text->set_scale_invariant(true); - - auto* color = text->mutable_color(); - color->set_r(255); - color->set_g(255); - color->set_b(255); - color->set_a(1.0); - - auto* text_pose = text->mutable_pose(); - auto* text_pos = text_pose->mutable_position(); - text_pos->set_x(node.pos.x); - text_pos->set_y(node.pos.y); - text_pos->set_z(0.06); - } - } - - auto* edge_entity = scene_update.add_entities(); - edge_entity->set_frame_id("scene"); - edge_entity->set_id("edges"); - edge_entity->set_frame_locked(true); - - lifetime = edge_entity->mutable_lifetime(); - lifetime->set_seconds(0); - lifetime->set_nanos(0); - - auto* lines = edge_entity->add_lines(); - lines->set_type(foxglove::LinePrimitive::LINE_LIST); - lines->set_thickness(0.05); - - auto* line_color = lines->mutable_color(); - line_color->set_r(0.3); - line_color->set_g(0.3); - line_color->set_b(0.3); - line_color->set_a(1.0); - - for (const auto& edge : graph_->edges) { - const auto& from_node = graph_->nodes.at(edge.from_node_id); - const auto& to_node = graph_->nodes.at(edge.to_node_id); - - auto* p1 = lines->add_points(); - p1->set_x(from_node.pos.x); - p1->set_y(from_node.pos.y); - p1->set_z(0.0); - - auto* p2 = lines->add_points(); - p2->set_x(to_node.pos.x); - p2->set_y(to_node.pos.y); - p2->set_z(0.0); - } - - return *this; -} - -RecordActor& RecordActor::WriteAgentStates(const models::AgentStates& agent_states) { - auto& scene_update = ResetSceneUpdate(); - - auto* deletion = scene_update.add_deletions(); - deletion->set_type(foxglove::SceneEntityDeletion::MATCHING_ID); - deletion->set_id("agents"); - - auto* agent_entity = scene_update.add_entities(); - agent_entity->set_frame_id("scene"); - agent_entity->set_id("agents"); - - auto* timestamp = agent_entity->mutable_timestamp(); - auto seconds = std::chrono::duration_cast( - std::chrono::duration(context_->current_time)); - auto nanos = std::chrono::duration_cast( - std::chrono::duration(context_->current_time) - - std::chrono::duration(seconds)); - timestamp->set_seconds(seconds.count()); - timestamp->set_nanos(nanos.count()); - - for (const auto& [_, agent] : agent_states) { - const auto& node = graph_->nodes.at(agent.node_id); - - { - auto* cylinder = agent_entity->add_cylinders(); - - auto* pose = cylinder->mutable_pose(); - auto* position = pose->mutable_position(); - position->set_x(node.pos.x); - position->set_y(node.pos.y); - position->set_z(0.05); - - auto* size = cylinder->mutable_size(); - size->set_x(0.3); - size->set_y(0.3); - size->set_z(0.05); - - auto* color = cylinder->mutable_color(); - float hue = std::fmod(agent.agent_id * 360.0f / agent_states.size(), 360.0f); - auto rgb = hsv2rgb(hue, 0.8f, 0.95f); - color->set_r(rgb.r); - color->set_g(rgb.g); - color->set_b(rgb.b); - color->set_a(1.0); - } - - { - auto* text = agent_entity->add_texts(); - text->set_text("Agent " + std::to_string(agent.agent_id)); - text->set_billboard(true); - text->set_font_size(16); - text->set_scale_invariant(true); - - auto* color = text->mutable_color(); - color->set_r(255); - color->set_g(255); - color->set_b(255); - color->set_a(1.0); - - auto* text_pose = text->mutable_pose(); - auto* text_pos = text_pose->mutable_position(); - text_pos->set_x(node.pos.x); - text_pos->set_y(node.pos.y); - text_pos->set_z(0.11); - } - } - - return *this; -} - -foxglove::SceneUpdate& RecordActor::ResetSceneUpdate() { - if (geom::Equal(scene_update_ts_, context_->current_time)) { - return scene_update_; - } - - WriteSceneUpdate(); - - scene_update_.Clear(); - scene_update_ts_ = context_->current_time; - - return scene_update_; -} - -RecordActor& RecordActor::WriteSceneUpdate() { - auto msg_ts = std::chrono::duration_cast( - std::chrono::duration(scene_update_ts_)) - .count(); - - std::string serialized; - scene_update_.SerializeToString(&serialized); - - mcap::Message msg; - msg.channelId = channel_.id; - msg.publishTime = msg_ts; - msg.logTime = msg_ts; - msg.data = reinterpret_cast(serialized.data()); - msg.dataSize = serialized.size(); - const auto res = writer_.write(msg); - if (!res.ok()) { - throw std::runtime_error("Failed to write MCAP"); - } - - return *this; -} - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/actors/src/solver_actor.cpp b/simulator/actors/src/solver_actor.cpp deleted file mode 100644 index c5cd665..0000000 --- a/simulator/actors/src/solver_actor.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#include "actors/solver_actor.h" - -#include "messages/mapf_problem_message.h" -#include "messages/mapf_solution_message.h" - -namespace mapf::simulator { - -SolverActor::SolverActor(ContextPtr context, solver::SolverPtr solver) : - Actor{std::move(context)}, solver_{std::move(solver)} { - context_->event_bus->Subscribe([this](MessagePtr message) { - HandleMAPFProblemMessage(std::static_pointer_cast(std::move(message))); - }); -} - -void SolverActor::HandleMAPFProblemMessage( - std::shared_ptr mapf_problem_message) { - if (mapf_problem_message->mapf_problem == nullptr) { - return; - } - const auto& mapf_problem = *(mapf_problem_message->mapf_problem); - auto solution_ptr = std::make_shared(solver_->FindSolution(mapf_problem)); - auto solution_message = std::make_shared(std::move(solution_ptr)); - context_->event_bus->Publish(context_->current_time, std::move(solution_message)); -} - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/context/include/context/context.h b/simulator/context/include/context/context.h deleted file mode 100644 index 5a10334..0000000 --- a/simulator/context/include/context/context.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "event/event_bus.h" - -#include - -namespace mapf::simulator { - -struct Context { - Context(); - - Context(EventBusPtr event_bus); - - EventBusPtr event_bus; - double current_time; -}; - -using ContextPtr = std::shared_ptr; - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/context/src/context.cpp b/simulator/context/src/context.cpp deleted file mode 100644 index c36d4e0..0000000 --- a/simulator/context/src/context.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "context/context.h" - -#include - -namespace mapf::simulator { - -Context::Context() : event_bus{nullptr}, current_time{0.0} {} - -Context::Context(EventBusPtr event_bus) : event_bus{std::move(event_bus)}, current_time{0.0} {} - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/event/include/event/event.h b/simulator/event/include/event/event.h deleted file mode 100644 index 81b01eb..0000000 --- a/simulator/event/include/event/event.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include -#include - -namespace mapf::simulator { - -using Action = std::function; - -struct Event { - Event(double timestamp, Action action); - - double timestamp; - Action action; -}; - -using EventPtr = std::shared_ptr; - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/event/include/event/event_bus.h b/simulator/event/include/event/event_bus.h deleted file mode 100644 index cf4a7e7..0000000 --- a/simulator/event/include/event/event_bus.h +++ /dev/null @@ -1,50 +0,0 @@ -#pragma once - -#include "event/event_queue.h" - -#include "messages/message.h" - -#include -#include -#include -#include -#include -#include - -namespace mapf::simulator { - -using MessageHandler = std::function; - -class EventBus { - public: - EventBus(EventQueuePtr event_queue); - - template MessageType> - EventBus& Subscribe(MessageHandler handler) { - subscribers_[std::type_index(typeid(MessageType))].emplace_back(std::move(handler)); - return *this; - } - - template MessageType> - EventBus& Publish(double timestamp, std::shared_ptr message) { - auto message_type_index = std::type_index(typeid(MessageType)); - if (!subscribers_.contains(message_type_index)) { - return *this; - } - auto callback = [this, message_type_index, message] { - for (const auto& handler : subscribers_.at(message_type_index)) { - handler(message); - } - }; - event_queue_->Push(std::make_shared(timestamp, std::move(callback))); - return *this; - } - - private: - EventQueuePtr event_queue_; - std::unordered_map> subscribers_; -}; - -using EventBusPtr = std::shared_ptr; - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/event/include/event/event_queue.h b/simulator/event/include/event/event_queue.h deleted file mode 100644 index 853e9f9..0000000 --- a/simulator/event/include/event/event_queue.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include "event/event.h" - -#include -#include -#include - -namespace mapf::simulator { - -class EventQueue { - public: - EventQueue& Push(EventPtr event); - - bool Empty() const; - - EventPtr Extract(); - - private: - struct EventCompare { - bool operator()(const EventPtr& lhs, const EventPtr& rhs) const; - }; - - std::priority_queue, EventCompare> event_queue_; -}; - -using EventQueuePtr = std::shared_ptr; - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/event/src/event.cpp b/simulator/event/src/event.cpp deleted file mode 100644 index b73c728..0000000 --- a/simulator/event/src/event.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "event/event.h" - -#include - -namespace mapf::simulator { - -Event::Event(double timestamp, Action action) : timestamp{timestamp}, action{std::move(action)} {} - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/event/src/event_bus.cpp b/simulator/event/src/event_bus.cpp deleted file mode 100644 index ffe158b..0000000 --- a/simulator/event/src/event_bus.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "event/event_bus.h" - -#include - -namespace mapf::simulator { - -EventBus::EventBus(EventQueuePtr event_queue) : event_queue_{std::move(event_queue)} {} - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/event/src/event_queue.cpp b/simulator/event/src/event_queue.cpp deleted file mode 100644 index 0083547..0000000 --- a/simulator/event/src/event_queue.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include "event/event_queue.h" - -#include - -namespace mapf::simulator { - -EventQueue& EventQueue::Push(EventPtr event) { - event_queue_.push(std::move(event)); - return *this; -} - -bool EventQueue::Empty() const { return event_queue_.empty(); } - -EventPtr EventQueue::Extract() { - if (Empty()) { - return nullptr; - } - auto event = event_queue_.top(); - event_queue_.pop(); - return event; -} - -bool EventQueue::EventCompare::operator()(const EventPtr& lhs, const EventPtr& rhs) const { - return lhs->timestamp > rhs->timestamp; -} - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/launcher/include/launcher/launcher.h b/simulator/launcher/include/launcher/launcher.h deleted file mode 100644 index 8a1d41f..0000000 --- a/simulator/launcher/include/launcher/launcher.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include "actors/actor.h" - -#include "event/event_queue.h" - -#include "context/context.h" - -#include -#include - -namespace mapf::simulator { - -class Launcher { - public: - Launcher(); - - template - std::shared_ptr CreateActor(Args&&... args) { - auto actor = std::make_shared(context_, std::forward(args)...); - actors_.emplace_back(actor); - return actor; - } - - Launcher& Run(); - - private: - std::vector actors_; - EventQueuePtr event_queue_; - ContextPtr context_; -}; - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/launcher/src/launcher.cpp b/simulator/launcher/src/launcher.cpp deleted file mode 100644 index 14c4499..0000000 --- a/simulator/launcher/src/launcher.cpp +++ /dev/null @@ -1,25 +0,0 @@ -#include "launcher/launcher.h" - -namespace mapf::simulator { - -Launcher::Launcher() : - actors_{}, - event_queue_{std::make_shared()}, - context_{std::make_shared(std::make_shared(event_queue_))} {} - -Launcher& Launcher::Run() { - for (auto& actor : actors_) { - actor->OnStart(); - } - while (!event_queue_->Empty()) { - auto event = event_queue_->Extract(); - context_->current_time = event->timestamp; - event->action(); - } - for (auto& actor : actors_) { - actor->OnFinish(); - } - return *this; -} - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/main/src/main.cpp b/simulator/main/src/main.cpp deleted file mode 100644 index 2c3fc63..0000000 --- a/simulator/main/src/main.cpp +++ /dev/null @@ -1,116 +0,0 @@ -#include "launcher/launcher.h" - -#include "actors/agents_actor.h" -#include "actors/record_actor.h" -#include "actors/solver_actor.h" - -#include "factory/solver_factory.h" - -#include "models/models.h" - -#include - -#include -#include -#include - -namespace po = boost::program_options; - -struct ProgramOptions { - static ProgramOptions Parse(int argc, char* argv[]) { - ProgramOptions program_options; - - auto options_description = po::options_description("Options"); - options_description.add_options()("help", "produce help message")( - "list-solvers,l", - po::bool_switch(&program_options.list_solvers)->default_value(false), - "list available solvers")( - "solver,s", - po::value(&program_options.solver_name), - "name of solver to be launched")( - "mapf-problem-file,i", - po::value(&program_options.mapf_problem_file), - "path to file with MAPF problem description")( - "scene-file,o", - po::value(&program_options.scene_file), - "path to MCAP Scene file")( - "record-frequency,f", - po::value(&program_options.record_frequency)->default_value(60), - "MCAP record frequency"); - - const char* workspace_dir_env = std::getenv("BUILD_WORKSPACE_DIRECTORY"); - std::filesystem::path workspace_dir = workspace_dir_env ? workspace_dir_env : ""; - if (workspace_dir.empty()) { - throw std::runtime_error("Got empty workspace dir"); - } - - po::variables_map variables_map; - po::store(po::parse_command_line(argc, argv, options_description), variables_map); - po::notify(variables_map); - - if (variables_map.count("help")) { - std::cout << options_description << std::endl; - std::exit(0); - } - - if (program_options.list_solvers) { - std::cout << "Available solvers:" << std::endl; - for (auto it = mapf::solver::SolverFactory::Instance().cbegin(); - it != mapf::solver::SolverFactory::Instance().cend(); - ++it) { - std::cout << std::string(4, ' ') << it->first << std::endl; - } - std::exit(0); - } - - if (!variables_map.count("solver")) { - throw std::runtime_error("the option '--solver' is required but missing"); - } - - if (mapf::solver::SolverFactory::Instance().find(program_options.solver_name) - == mapf::solver::SolverFactory::Instance().cend()) { - throw std::runtime_error( - std::format("No such solver in factory: {}", program_options.solver_name)); - } - - if (!variables_map.count("mapf-problem-file")) { - throw std::runtime_error("the option '--mapf-problem-file' is required but missing"); - } - - if (!variables_map.count("scene-file")) { - throw std::runtime_error("the option '--scene-file' is required but missing"); - } - - program_options.mapf_problem_file = workspace_dir / program_options.mapf_problem_file; - program_options.scene_file = workspace_dir / program_options.scene_file; - - return program_options; - } - - bool list_solvers; - std::string solver_name; - std::filesystem::path mapf_problem_file; - std::filesystem::path scene_file; - uint32_t record_frequency; -}; - -int main(int argc, char* argv[]) { - auto program_options = ProgramOptions::Parse(argc, argv); - - auto mapf_problem_ptr = std::make_shared( - mapf::models::LoadProtoFromFile( - program_options.mapf_problem_file)); - - auto launcher = mapf::simulator::Launcher(); - auto agents_actor = launcher.CreateActor(mapf_problem_ptr); - launcher.CreateActor( - program_options.scene_file, - 1.0 / program_options.record_frequency, - std::make_shared(mapf_problem_ptr->graph)); - launcher.CreateActor( - mapf::solver::SolverFactory::Instance().CreateSolver(program_options.solver_name)); - - launcher.Run(); - - return 0; -} \ No newline at end of file diff --git a/simulator/messages/include/messages/agent_move_message.h b/simulator/messages/include/messages/agent_move_message.h deleted file mode 100644 index ddc4819..0000000 --- a/simulator/messages/include/messages/agent_move_message.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include "messages/message.h" - -#include "models/models.h" - -namespace mapf::simulator { - -struct AgentMoveMessage final : public Message { - AgentMoveMessage(uint32_t plan_version, models::AgentId agent_id, uint32_t plan_step); - - uint32_t plan_version; - models::AgentId agent_id; - uint32_t plan_step; -}; - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/messages/include/messages/mapf_problem_message.h b/simulator/messages/include/messages/mapf_problem_message.h deleted file mode 100644 index d27033b..0000000 --- a/simulator/messages/include/messages/mapf_problem_message.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include "messages/message.h" - -#include "models/models.h" - -namespace mapf::simulator { - -struct MAPFProblemMessage final : public Message { - MAPFProblemMessage(models::MAPFProblemPtr mapf_problem); - - models::MAPFProblemPtr mapf_problem; -}; - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/messages/include/messages/mapf_solution_message.h b/simulator/messages/include/messages/mapf_solution_message.h deleted file mode 100644 index 17293a3..0000000 --- a/simulator/messages/include/messages/mapf_solution_message.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once - -#include "messages/message.h" - -#include "models/models.h" - -namespace mapf::simulator { - -struct MAPFSolutionMessage final : public Message { - MAPFSolutionMessage(models::MAPFSolutionPtr mapf_solution); - - models::MAPFSolutionPtr mapf_solution; -}; - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/messages/include/messages/message.h b/simulator/messages/include/messages/message.h deleted file mode 100644 index 7918828..0000000 --- a/simulator/messages/include/messages/message.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include -#include - -namespace mapf::simulator { - -struct Message { - virtual ~Message() = default; -}; - -using MessagePtr = std::shared_ptr; - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/messages/include/messages/request_agent_states_message.h b/simulator/messages/include/messages/request_agent_states_message.h deleted file mode 100644 index 208e55a..0000000 --- a/simulator/messages/include/messages/request_agent_states_message.h +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once - -#include "messages/message.h" - -namespace mapf::simulator { - -struct RequestAgentStatesMessage final : public Message { - RequestAgentStatesMessage() = default; -}; - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/messages/include/messages/response_agent_states_message.h b/simulator/messages/include/messages/response_agent_states_message.h deleted file mode 100644 index 90ce40e..0000000 --- a/simulator/messages/include/messages/response_agent_states_message.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include "messages/message.h" - -#include "models/models.h" - -namespace mapf::simulator { - -struct ResponseAgentStatesMessage final : public Message { - ResponseAgentStatesMessage(models::AgentStatesPtr agent_states, bool all_finished = false); - - models::AgentStatesPtr agent_states; - bool all_finished; -}; - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/messages/src/agent_move_message.cpp b/simulator/messages/src/agent_move_message.cpp deleted file mode 100644 index 665ddc7..0000000 --- a/simulator/messages/src/agent_move_message.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "messages/agent_move_message.h" - -namespace mapf::simulator { - -AgentMoveMessage::AgentMoveMessage( - uint32_t plan_version, models::AgentId agent_id, uint32_t plan_step) : - plan_version{plan_version}, agent_id{std::move(agent_id)}, plan_step{plan_step} {} - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/messages/src/mapf_problem_message.cpp b/simulator/messages/src/mapf_problem_message.cpp deleted file mode 100644 index 4da05dc..0000000 --- a/simulator/messages/src/mapf_problem_message.cpp +++ /dev/null @@ -1,10 +0,0 @@ -#include "messages/mapf_problem_message.h" - -#include - -namespace mapf::simulator { - -MAPFProblemMessage::MAPFProblemMessage(models::MAPFProblemPtr mapf_problem) : - mapf_problem{std::move(mapf_problem)} {} - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/messages/src/mapf_solution_message.cpp b/simulator/messages/src/mapf_solution_message.cpp deleted file mode 100644 index cb28f75..0000000 --- a/simulator/messages/src/mapf_solution_message.cpp +++ /dev/null @@ -1,10 +0,0 @@ -#include "messages/mapf_solution_message.h" - -#include - -namespace mapf::simulator { - -MAPFSolutionMessage::MAPFSolutionMessage(models::MAPFSolutionPtr mapf_solution) : - mapf_solution{std::move(mapf_solution)} {} - -} // namespace mapf::simulator \ No newline at end of file diff --git a/simulator/messages/src/response_agent_states_message.cpp b/simulator/messages/src/response_agent_states_message.cpp deleted file mode 100644 index 845d317..0000000 --- a/simulator/messages/src/response_agent_states_message.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "messages/response_agent_states_message.h" - -#include - -namespace mapf::simulator { - -ResponseAgentStatesMessage::ResponseAgentStatesMessage( - models::AgentStatesPtr agent_states, bool all_finished) : - agent_states{std::move(agent_states)}, all_finished{all_finished} {} - -} // namespace mapf::simulator \ No newline at end of file From b1702884c68b5d31585b261bb8dca7bbf3f9df74 Mon Sep 17 00:00:00 2001 From: pushkarevv Date: Fri, 15 May 2026 10:34:15 +0300 Subject: [PATCH 4/6] feat(solvers): add PIBT with deterministic hash tie-break --- data/mapf_problem_2.pb.txt | 498 ++++++++++++++++++ data/mapf_problem_4x4_3agents.pb.txt | 327 ++++++++++++ data/mapf_problem_4x4_4agents.pb.txt | 92 ++++ data/mapf_problem_corridor.pb.txt | 39 ++ solver/factory/src/solver_factory.cpp | 2 + solver/main/src/main.cpp | 2 +- .../solvers/include/solvers/goal_distances.h | 16 + solver/solvers/include/solvers/pibt_solver.h | 40 ++ solver/solvers/src/goal_distances.cpp | 52 ++ solver/solvers/src/pibt_solver.cpp | 249 +++++++++ solver/tests/test_solvers.cpp | 110 +++- 11 files changed, 1413 insertions(+), 14 deletions(-) create mode 100644 data/mapf_problem_2.pb.txt create mode 100644 data/mapf_problem_4x4_3agents.pb.txt create mode 100644 data/mapf_problem_4x4_4agents.pb.txt create mode 100644 data/mapf_problem_corridor.pb.txt create mode 100644 solver/solvers/include/solvers/goal_distances.h create mode 100644 solver/solvers/include/solvers/pibt_solver.h create mode 100644 solver/solvers/src/goal_distances.cpp create mode 100644 solver/solvers/src/pibt_solver.cpp diff --git a/data/mapf_problem_2.pb.txt b/data/mapf_problem_2.pb.txt new file mode 100644 index 0000000..ee5ff5d --- /dev/null +++ b/data/mapf_problem_2.pb.txt @@ -0,0 +1,498 @@ +graph { + nodes { + id: 0 + pos { + x: 0.0 + y: 0.0 + } + } + nodes { + id: 1 + pos { + x: 1.0 + y: 0.0 + } + } + nodes { + id: 2 + pos { + x: 4.0 + y: 0.0 + } + } + nodes { + id: 3 + pos { + x: 5.0 + y: 0.0 + } + } + nodes { + id: 4 + pos { + x: 0.0 + y: 1.0 + } + } + nodes { + id: 5 + pos { + x: 1.0 + y: 1.0 + } + } + nodes { + id: 6 + pos { + x: 4.0 + y: 1.0 + } + } + nodes { + id: 7 + pos { + x: 5.0 + y: 1.0 + } + } + nodes { + id: 8 + pos { + x: 0.0 + y: 2.0 + } + } + nodes { + id: 9 + pos { + x: 1.0 + y: 2.0 + } + } + nodes { + id: 10 + pos { + x: 2.0 + y: 2.0 + } + } + nodes { + id: 11 + pos { + x: 4.0 + y: 2.0 + } + } + nodes { + id: 12 + pos { + x: 5.0 + y: 2.0 + } + } + nodes { + id: 13 + pos { + x: 0.0 + y: 3.0 + } + } + nodes { + id: 14 + pos { + x: 1.0 + y: 3.0 + } + } + nodes { + id: 15 + pos { + x: 3.0 + y: 3.0 + } + } + nodes { + id: 16 + pos { + x: 4.0 + y: 3.0 + } + } + nodes { + id: 17 + pos { + x: 5.0 + y: 3.0 + } + } + nodes { + id: 18 + pos { + x: 0.0 + y: 4.0 + } + } + nodes { + id: 19 + pos { + x: 1.0 + y: 4.0 + } + } + nodes { + id: 20 + pos { + x: 4.0 + y: 4.0 + } + } + nodes { + id: 21 + pos { + x: 5.0 + y: 4.0 + } + } + nodes { + id: 22 + pos { + x: 0.0 + y: 5.0 + } + } + nodes { + id: 23 + pos { + x: 1.0 + y: 5.0 + } + } + nodes { + id: 24 + pos { + x: 4.0 + y: 5.0 + } + } + nodes { + id: 25 + pos { + x: 5.0 + y: 5.0 + } + } + edges { + from_node_id: 0 + to_node_id: 1 + } + edges { + from_node_id: 0 + to_node_id: 4 + } + edges { + from_node_id: 1 + to_node_id: 0 + } + edges { + from_node_id: 1 + to_node_id: 5 + } + edges { + from_node_id: 2 + to_node_id: 3 + } + edges { + from_node_id: 2 + to_node_id: 6 + } + edges { + from_node_id: 3 + to_node_id: 2 + } + edges { + from_node_id: 3 + to_node_id: 7 + } + edges { + from_node_id: 4 + to_node_id: 5 + } + edges { + from_node_id: 4 + to_node_id: 8 + } + edges { + from_node_id: 4 + to_node_id: 0 + } + edges { + from_node_id: 5 + to_node_id: 4 + } + edges { + from_node_id: 5 + to_node_id: 9 + } + edges { + from_node_id: 5 + to_node_id: 1 + } + edges { + from_node_id: 6 + to_node_id: 7 + } + edges { + from_node_id: 6 + to_node_id: 11 + } + edges { + from_node_id: 6 + to_node_id: 2 + } + edges { + from_node_id: 7 + to_node_id: 6 + } + edges { + from_node_id: 7 + to_node_id: 12 + } + edges { + from_node_id: 7 + to_node_id: 3 + } + edges { + from_node_id: 8 + to_node_id: 9 + } + edges { + from_node_id: 8 + to_node_id: 13 + } + edges { + from_node_id: 8 + to_node_id: 4 + } + edges { + from_node_id: 9 + to_node_id: 10 + } + edges { + from_node_id: 9 + to_node_id: 8 + } + edges { + from_node_id: 9 + to_node_id: 14 + } + edges { + from_node_id: 9 + to_node_id: 5 + } + edges { + from_node_id: 10 + to_node_id: 9 + } + edges { + from_node_id: 11 + to_node_id: 12 + } + edges { + from_node_id: 11 + to_node_id: 16 + } + edges { + from_node_id: 11 + to_node_id: 6 + } + edges { + from_node_id: 12 + to_node_id: 11 + } + edges { + from_node_id: 12 + to_node_id: 17 + } + edges { + from_node_id: 12 + to_node_id: 7 + } + edges { + from_node_id: 13 + to_node_id: 14 + } + edges { + from_node_id: 13 + to_node_id: 18 + } + edges { + from_node_id: 13 + to_node_id: 8 + } + edges { + from_node_id: 14 + to_node_id: 13 + } + edges { + from_node_id: 14 + to_node_id: 19 + } + edges { + from_node_id: 14 + to_node_id: 9 + } + edges { + from_node_id: 15 + to_node_id: 16 + } + edges { + from_node_id: 16 + to_node_id: 17 + } + edges { + from_node_id: 16 + to_node_id: 15 + } + edges { + from_node_id: 16 + to_node_id: 20 + } + edges { + from_node_id: 16 + to_node_id: 11 + } + edges { + from_node_id: 17 + to_node_id: 16 + } + edges { + from_node_id: 17 + to_node_id: 21 + } + edges { + from_node_id: 17 + to_node_id: 12 + } + edges { + from_node_id: 18 + to_node_id: 19 + } + edges { + from_node_id: 18 + to_node_id: 22 + } + edges { + from_node_id: 18 + to_node_id: 13 + } + edges { + from_node_id: 19 + to_node_id: 18 + } + edges { + from_node_id: 19 + to_node_id: 23 + } + edges { + from_node_id: 19 + to_node_id: 14 + } + edges { + from_node_id: 20 + to_node_id: 21 + } + edges { + from_node_id: 20 + to_node_id: 24 + } + edges { + from_node_id: 20 + to_node_id: 16 + } + edges { + from_node_id: 21 + to_node_id: 20 + } + edges { + from_node_id: 21 + to_node_id: 25 + } + edges { + from_node_id: 21 + to_node_id: 17 + } + edges { + from_node_id: 22 + to_node_id: 23 + } + edges { + from_node_id: 22 + to_node_id: 18 + } + edges { + from_node_id: 23 + to_node_id: 22 + } + edges { + from_node_id: 23 + to_node_id: 19 + } + edges { + from_node_id: 24 + to_node_id: 25 + } + edges { + from_node_id: 24 + to_node_id: 20 + } + edges { + from_node_id: 25 + to_node_id: 24 + } + edges { + from_node_id: 25 + to_node_id: 21 + } +} +agent_tasks { + agent_id: 0 + endpoints { + from_node_id: 0 + to_node_id: 25 + } +} +agent_tasks { + agent_id: 1 + endpoints { + from_node_id: 22 + to_node_id: 3 + } +} +agent_tasks { + agent_id: 2 + endpoints { + from_node_id: 9 + to_node_id: 11 + } +} +agent_tasks { + agent_id: 3 + endpoints { + from_node_id: 14 + to_node_id: 16 + } +} +agent_tasks { + agent_id: 4 + endpoints { + from_node_id: 12 + to_node_id: 8 + } +} +agent_tasks { + agent_id: 5 + endpoints { + from_node_id: 17 + to_node_id: 13 + } +} diff --git a/data/mapf_problem_4x4_3agents.pb.txt b/data/mapf_problem_4x4_3agents.pb.txt new file mode 100644 index 0000000..0302fcf --- /dev/null +++ b/data/mapf_problem_4x4_3agents.pb.txt @@ -0,0 +1,327 @@ +graph { + nodes { + id: 0 + pos { + x: 0.0 + y: 0.0 + } + } + nodes { + id: 1 + pos { + x: 1.0 + y: 0.0 + } + } + nodes { + id: 2 + pos { + x: 2.0 + y: 0.0 + } + } + nodes { + id: 3 + pos { + x: 3.0 + y: 0.0 + } + } + nodes { + id: 4 + pos { + x: 0.0 + y: 1.0 + } + } + nodes { + id: 5 + pos { + x: 1.0 + y: 1.0 + } + } + nodes { + id: 6 + pos { + x: 2.0 + y: 1.0 + } + } + nodes { + id: 7 + pos { + x: 3.0 + y: 1.0 + } + } + nodes { + id: 8 + pos { + x: 0.0 + y: 2.0 + } + } + nodes { + id: 9 + pos { + x: 1.0 + y: 2.0 + } + } + nodes { + id: 10 + pos { + x: 2.0 + y: 2.0 + } + } + nodes { + id: 11 + pos { + x: 3.0 + y: 2.0 + } + } + nodes { + id: 12 + pos { + x: 0.0 + y: 3.0 + } + } + nodes { + id: 13 + pos { + x: 1.0 + y: 3.0 + } + } + nodes { + id: 14 + pos { + x: 2.0 + y: 3.0 + } + } + nodes { + id: 15 + pos { + x: 3.0 + y: 3.0 + } + } + edges { + from_node_id: 0 + to_node_id: 1 + } + edges { + from_node_id: 0 + to_node_id: 4 + } + edges { + from_node_id: 1 + to_node_id: 2 + } + edges { + from_node_id: 1 + to_node_id: 0 + } + edges { + from_node_id: 1 + to_node_id: 5 + } + edges { + from_node_id: 2 + to_node_id: 3 + } + edges { + from_node_id: 2 + to_node_id: 1 + } + edges { + from_node_id: 2 + to_node_id: 6 + } + edges { + from_node_id: 3 + to_node_id: 2 + } + edges { + from_node_id: 3 + to_node_id: 7 + } + edges { + from_node_id: 4 + to_node_id: 5 + } + edges { + from_node_id: 4 + to_node_id: 8 + } + edges { + from_node_id: 4 + to_node_id: 0 + } + edges { + from_node_id: 5 + to_node_id: 6 + } + edges { + from_node_id: 5 + to_node_id: 4 + } + edges { + from_node_id: 5 + to_node_id: 9 + } + edges { + from_node_id: 5 + to_node_id: 1 + } + edges { + from_node_id: 6 + to_node_id: 7 + } + edges { + from_node_id: 6 + to_node_id: 5 + } + edges { + from_node_id: 6 + to_node_id: 10 + } + edges { + from_node_id: 6 + to_node_id: 2 + } + edges { + from_node_id: 7 + to_node_id: 6 + } + edges { + from_node_id: 7 + to_node_id: 11 + } + edges { + from_node_id: 7 + to_node_id: 3 + } + edges { + from_node_id: 8 + to_node_id: 9 + } + edges { + from_node_id: 8 + to_node_id: 12 + } + edges { + from_node_id: 8 + to_node_id: 4 + } + edges { + from_node_id: 9 + to_node_id: 10 + } + edges { + from_node_id: 9 + to_node_id: 8 + } + edges { + from_node_id: 9 + to_node_id: 13 + } + edges { + from_node_id: 9 + to_node_id: 5 + } + edges { + from_node_id: 10 + to_node_id: 11 + } + edges { + from_node_id: 10 + to_node_id: 9 + } + edges { + from_node_id: 10 + to_node_id: 14 + } + edges { + from_node_id: 10 + to_node_id: 6 + } + edges { + from_node_id: 11 + to_node_id: 10 + } + edges { + from_node_id: 11 + to_node_id: 15 + } + edges { + from_node_id: 11 + to_node_id: 7 + } + edges { + from_node_id: 12 + to_node_id: 13 + } + edges { + from_node_id: 12 + to_node_id: 8 + } + edges { + from_node_id: 13 + to_node_id: 14 + } + edges { + from_node_id: 13 + to_node_id: 12 + } + edges { + from_node_id: 13 + to_node_id: 9 + } + edges { + from_node_id: 14 + to_node_id: 15 + } + edges { + from_node_id: 14 + to_node_id: 13 + } + edges { + from_node_id: 14 + to_node_id: 10 + } + edges { + from_node_id: 15 + to_node_id: 14 + } + edges { + from_node_id: 15 + to_node_id: 11 + } +} +agent_tasks { + agent_id: 0 + endpoints { + from_node_id: 0 + to_node_id: 15 + } +} +agent_tasks { + agent_id: 1 + endpoints { + from_node_id: 3 + to_node_id: 12 + } +} +agent_tasks { + agent_id: 2 + endpoints { + from_node_id: 12 + to_node_id: 3 + } +} diff --git a/data/mapf_problem_4x4_4agents.pb.txt b/data/mapf_problem_4x4_4agents.pb.txt new file mode 100644 index 0000000..3d7e08c --- /dev/null +++ b/data/mapf_problem_4x4_4agents.pb.txt @@ -0,0 +1,92 @@ +graph { + nodes { id: 0 pos { x: 0.0 y: 0.0 } } + nodes { id: 1 pos { x: 1.0 y: 0.0 } } + nodes { id: 2 pos { x: 2.0 y: 0.0 } } + nodes { id: 3 pos { x: 3.0 y: 0.0 } } + nodes { id: 4 pos { x: 0.0 y: 1.0 } } + nodes { id: 5 pos { x: 1.0 y: 1.0 } } + nodes { id: 6 pos { x: 2.0 y: 1.0 } } + nodes { id: 7 pos { x: 3.0 y: 1.0 } } + nodes { id: 8 pos { x: 0.0 y: 2.0 } } + nodes { id: 9 pos { x: 1.0 y: 2.0 } } + nodes { id: 10 pos { x: 2.0 y: 2.0 } } + nodes { id: 11 pos { x: 3.0 y: 2.0 } } + nodes { id: 12 pos { x: 0.0 y: 3.0 } } + nodes { id: 13 pos { x: 1.0 y: 3.0 } } + nodes { id: 14 pos { x: 2.0 y: 3.0 } } + nodes { id: 15 pos { x: 3.0 y: 3.0 } } + + # horizontal edges + edges { from_node_id: 0 to_node_id: 1 } + edges { from_node_id: 1 to_node_id: 0 } + edges { from_node_id: 1 to_node_id: 2 } + edges { from_node_id: 2 to_node_id: 1 } + edges { from_node_id: 2 to_node_id: 3 } + edges { from_node_id: 3 to_node_id: 2 } + + edges { from_node_id: 4 to_node_id: 5 } + edges { from_node_id: 5 to_node_id: 4 } + edges { from_node_id: 5 to_node_id: 6 } + edges { from_node_id: 6 to_node_id: 5 } + edges { from_node_id: 6 to_node_id: 7 } + edges { from_node_id: 7 to_node_id: 6 } + + edges { from_node_id: 8 to_node_id: 9 } + edges { from_node_id: 9 to_node_id: 8 } + edges { from_node_id: 9 to_node_id: 10 } + edges { from_node_id: 10 to_node_id: 9 } + edges { from_node_id: 10 to_node_id: 11 } + edges { from_node_id: 11 to_node_id: 10 } + + edges { from_node_id: 12 to_node_id: 13 } + edges { from_node_id: 13 to_node_id: 12 } + edges { from_node_id: 13 to_node_id: 14 } + edges { from_node_id: 14 to_node_id: 13 } + edges { from_node_id: 14 to_node_id: 15 } + edges { from_node_id: 15 to_node_id: 14 } + + # vertical edges + edges { from_node_id: 0 to_node_id: 4 } + edges { from_node_id: 4 to_node_id: 0 } + edges { from_node_id: 4 to_node_id: 8 } + edges { from_node_id: 8 to_node_id: 4 } + edges { from_node_id: 8 to_node_id: 12 } + edges { from_node_id: 12 to_node_id: 8 } + + edges { from_node_id: 1 to_node_id: 5 } + edges { from_node_id: 5 to_node_id: 1 } + edges { from_node_id: 5 to_node_id: 9 } + edges { from_node_id: 9 to_node_id: 5 } + edges { from_node_id: 9 to_node_id: 13 } + edges { from_node_id: 13 to_node_id: 9 } + + edges { from_node_id: 2 to_node_id: 6 } + edges { from_node_id: 6 to_node_id: 2 } + edges { from_node_id: 6 to_node_id: 10 } + edges { from_node_id: 10 to_node_id: 6 } + edges { from_node_id: 10 to_node_id: 14 } + edges { from_node_id: 14 to_node_id: 10 } + + edges { from_node_id: 3 to_node_id: 7 } + edges { from_node_id: 7 to_node_id: 3 } + edges { from_node_id: 7 to_node_id: 11 } + edges { from_node_id: 11 to_node_id: 7 } + edges { from_node_id: 11 to_node_id: 15 } + edges { from_node_id: 15 to_node_id: 11 } +} +agent_tasks { + agent_id: 0 + endpoints { from_node_id: 0 to_node_id: 15 } +} +agent_tasks { + agent_id: 1 + endpoints { from_node_id: 15 to_node_id: 0 } +} +agent_tasks { + agent_id: 2 + endpoints { from_node_id: 3 to_node_id: 12 } +} +agent_tasks { + agent_id: 3 + endpoints { from_node_id: 12 to_node_id: 3 } +} diff --git a/data/mapf_problem_corridor.pb.txt b/data/mapf_problem_corridor.pb.txt new file mode 100644 index 0000000..2f936df --- /dev/null +++ b/data/mapf_problem_corridor.pb.txt @@ -0,0 +1,39 @@ +graph { + nodes { id: 0 pos { x: 0.0 y: 0.0 } } + nodes { id: 1 pos { x: 1.0 y: 0.0 } } + nodes { id: 2 pos { x: 2.0 y: 0.0 } } + nodes { id: 3 pos { x: 3.0 y: 0.0 } } + nodes { id: 4 pos { x: 4.0 y: 0.0 } } + nodes { id: 5 pos { x: 5.0 y: 0.0 } } + nodes { id: 6 pos { x: 6.0 y: 0.0 } } + nodes { id: 7 pos { x: 7.0 y: 0.0 } } + nodes { id: 8 pos { x: 3.0 y: 1.0 } } + + # main corridor + edges { from_node_id: 0 to_node_id: 1 } + edges { from_node_id: 1 to_node_id: 0 } + edges { from_node_id: 1 to_node_id: 2 } + edges { from_node_id: 2 to_node_id: 1 } + edges { from_node_id: 2 to_node_id: 3 } + edges { from_node_id: 3 to_node_id: 2 } + edges { from_node_id: 3 to_node_id: 4 } + edges { from_node_id: 4 to_node_id: 3 } + edges { from_node_id: 4 to_node_id: 5 } + edges { from_node_id: 5 to_node_id: 4 } + edges { from_node_id: 5 to_node_id: 6 } + edges { from_node_id: 6 to_node_id: 5 } + edges { from_node_id: 6 to_node_id: 7 } + edges { from_node_id: 7 to_node_id: 6 } + + # side pocket at node 3 + edges { from_node_id: 3 to_node_id: 8 } + edges { from_node_id: 8 to_node_id: 3 } +} +agent_tasks { + agent_id: 0 + endpoints { from_node_id: 0 to_node_id: 7 } +} +agent_tasks { + agent_id: 1 + endpoints { from_node_id: 7 to_node_id: 0 } +} diff --git a/solver/factory/src/solver_factory.cpp b/solver/factory/src/solver_factory.cpp index 6c73d6d..bb6dcff 100644 --- a/solver/factory/src/solver_factory.cpp +++ b/solver/factory/src/solver_factory.cpp @@ -2,6 +2,7 @@ #include "solvers/astar_solver.h" #include "solvers/bfs_solver.h" +#include "solvers/pibt_solver.h" #include @@ -13,6 +14,7 @@ SolverFactory::SolverFactory() { REGISTER(bfs_solver, BFSSolver); REGISTER(astar_solver, AStarSolver); + REGISTER(pibt_solver, PIBTSolver); #undef REGISTER } diff --git a/solver/main/src/main.cpp b/solver/main/src/main.cpp index b61ed2f..20c668b 100644 --- a/solver/main/src/main.cpp +++ b/solver/main/src/main.cpp @@ -1,4 +1,4 @@ -об#include "models/models.h" +#include "models/models.h" #include "factory/solver_factory.h" #include diff --git a/solver/solvers/include/solvers/goal_distances.h b/solver/solvers/include/solvers/goal_distances.h new file mode 100644 index 0000000..a97a12d --- /dev/null +++ b/solver/solvers/include/solvers/goal_distances.h @@ -0,0 +1,16 @@ +#pragma once + +#include "graph/graph.h" +#include "models/models.h" + +#include +#include + +namespace mapf::solver { + +using AgentGoalDistances = + std::unordered_map>; + +AgentGoalDistances BuildGoalDistances(const models::MAPFProblem& mapf_problem); + +} // namespace mapf::solver diff --git a/solver/solvers/include/solvers/pibt_solver.h b/solver/solvers/include/solvers/pibt_solver.h new file mode 100644 index 0000000..e5a9915 --- /dev/null +++ b/solver/solvers/include/solvers/pibt_solver.h @@ -0,0 +1,40 @@ +#pragma once + +#include "solvers/solver_base.h" + +#include "models/models.h" + +#include + +namespace mapf::solver { + +struct PIBTConfig { + enum class TieBreak { + Hash, + LowestId, + Random, + }; + enum class PriorityMode { + Dynamic, + Static, + }; + + TieBreak tie_break = TieBreak::Hash; + PriorityMode priority = PriorityMode::Dynamic; + uint64_t seed = 0xC0FFEEu; +}; + +struct PIBTSolver : public SolverBase { + PIBTSolver() = default; + explicit PIBTSolver(PIBTConfig config) : config_(config) {} + + models::MAPFSolution FindSolution(const models::MAPFProblem& mapf_problem) const final; + + size_t GetMakespan() const { return makespan_; } + + private: + PIBTConfig config_; + mutable size_t makespan_ = 0; +}; + +} // namespace mapf::solver diff --git a/solver/solvers/src/goal_distances.cpp b/solver/solvers/src/goal_distances.cpp new file mode 100644 index 0000000..fda1e56 --- /dev/null +++ b/solver/solvers/src/goal_distances.cpp @@ -0,0 +1,52 @@ +#include "solvers/goal_distances.h" + +#include +#include + +namespace mapf::solver { + +AgentGoalDistances BuildGoalDistances(const models::MAPFProblem& mapf_problem) { + std::unordered_map> reverse_adjacency; + reverse_adjacency.reserve(mapf_problem.graph.nodes.size()); + for (const auto& edge : mapf_problem.graph.edges) { + reverse_adjacency[edge.to_node_id].emplace_back(edge.from_node_id); + } + + AgentGoalDistances goal_distances; + goal_distances.reserve(mapf_problem.agent_tasks.size()); + + for (const auto& [agent_id, agent_task] : mapf_problem.agent_tasks) { + std::unordered_map distances; + distances.reserve(mapf_problem.graph.nodes.size()); + + std::queue bfs_queue; + const graph::NodeId goal_node = agent_task.endpoints.to_node_id; + distances[goal_node] = 0; + bfs_queue.push(goal_node); + + while (!bfs_queue.empty()) { + const graph::NodeId current_node = bfs_queue.front(); + bfs_queue.pop(); + const uint64_t current_dist = distances[current_node]; + + auto reverse_it = reverse_adjacency.find(current_node); + if (reverse_it == reverse_adjacency.end()) { + continue; + } + + for (const graph::NodeId prev_node : reverse_it->second) { + if (distances.contains(prev_node)) { + continue; + } + distances[prev_node] = current_dist + 1; + bfs_queue.push(prev_node); + } + } + + goal_distances.emplace(agent_id, std::move(distances)); + } + + return goal_distances; +} + +} // namespace mapf::solver diff --git a/solver/solvers/src/pibt_solver.cpp b/solver/solvers/src/pibt_solver.cpp new file mode 100644 index 0000000..b70cdfd --- /dev/null +++ b/solver/solvers/src/pibt_solver.cpp @@ -0,0 +1,249 @@ +#include "solvers/pibt_solver.h" + +#include "solvers/goal_distances.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace mapf::solver { + +namespace { + +using models::AgentId; +using graph::NodeId; + +constexpr uint64_t kUnreachable = std::numeric_limits::max(); + +uint64_t MixHash(uint64_t value, uint64_t step) { + uint64_t x = value * 0x9E3779B97F4A7C15ull + step * 0xD1B54A32D192ED03ull + 0x165667B19E3779F9ull; + x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ull; + x = (x ^ (x >> 27)) * 0x94D049BB133111EBull; + return x ^ (x >> 31); +} + +std::unordered_map DefaultInitialOffsets( + const models::MAPFProblem& mapf_problem, const AgentGoalDistances& goal_distances) { + struct AgentKey { + AgentId agent_id; + uint64_t start_distance; + }; + + std::vector agents; + agents.reserve(mapf_problem.agent_tasks.size()); + for (const auto& [agent_id, agent_task] : mapf_problem.agent_tasks) { + uint64_t start_distance = 0; + if (auto dist_it = goal_distances.find(agent_id); dist_it != goal_distances.end()) { + const auto& node_distances = dist_it->second; + auto node_it = node_distances.find(agent_task.endpoints.from_node_id); + start_distance = node_it != node_distances.end() ? node_it->second : 0; + } + agents.push_back(AgentKey{agent_id, start_distance}); + } + + std::sort(agents.begin(), agents.end(), [](const AgentKey& lhs, const AgentKey& rhs) { + if (lhs.start_distance != rhs.start_distance) { + return lhs.start_distance > rhs.start_distance; + } + return lhs.agent_id < rhs.agent_id; + }); + + std::unordered_map offsets; + offsets.reserve(agents.size()); + const double denominator = static_cast(agents.size() + 1); + for (size_t index = 0; index < agents.size(); ++index) { + offsets[agents[index].agent_id] = + static_cast(agents.size() - index) / denominator; + } + return offsets; +} + +struct PibtRun { + const models::MAPFProblem& problem; + const AgentGoalDistances& dist; + + std::unordered_map current; + std::unordered_map next; + std::unordered_map occupant_now; + std::unordered_map reserved; + + size_t step = 0; + PIBTConfig config; + std::mt19937 rng; + + uint64_t DistanceToGoal(AgentId agent_id, NodeId node_id) const { + auto dist_it = dist.find(agent_id); + if (dist_it == dist.end()) { + return kUnreachable; + } + auto node_it = dist_it->second.find(node_id); + return node_it != dist_it->second.end() ? node_it->second : kUnreachable; + } + + bool Decide(AgentId agent_id, std::optional pusher) { + const NodeId from_node = current.at(agent_id); + + std::vector candidates; + const auto& neighbours = problem.graph.GetNeighbours(from_node); + candidates.reserve(neighbours.size() + 1); + for (const NodeId neighbour : neighbours) { + candidates.push_back(neighbour); + } + candidates.push_back(from_node); + + if (config.tie_break == PIBTConfig::TieBreak::Random) { + std::shuffle(candidates.begin(), candidates.end(), rng); + std::stable_sort(candidates.begin(), candidates.end(), [&](NodeId lhs, NodeId rhs) { + return DistanceToGoal(agent_id, lhs) < DistanceToGoal(agent_id, rhs); + }); + } else { + std::sort(candidates.begin(), candidates.end(), [&](NodeId lhs, NodeId rhs) { + const uint64_t lhs_dist = DistanceToGoal(agent_id, lhs); + const uint64_t rhs_dist = DistanceToGoal(agent_id, rhs); + if (lhs_dist != rhs_dist) { + return lhs_dist < rhs_dist; + } + if (config.tie_break == PIBTConfig::TieBreak::LowestId) { + return lhs < rhs; + } + return MixHash(lhs, step) < MixHash(rhs, step); + }); + } + + for (const NodeId candidate : candidates) { + if (reserved.contains(candidate)) { + continue; + } + if (pusher.has_value() && candidate == current.at(*pusher)) { + continue; + } + + reserved.emplace(candidate, agent_id); + next.emplace(agent_id, candidate); + + auto occupant_it = occupant_now.find(candidate); + if (occupant_it != occupant_now.end()) { + const AgentId blocker = occupant_it->second; + if (blocker != agent_id && !next.contains(blocker)) { + + if (Decide(blocker, agent_id)) { + return true; + } + + reserved.erase(candidate); + next.erase(agent_id); + continue; + } + } + return true; + } + return false; + } +}; + +} + +models::MAPFSolution PIBTSolver::FindSolution(const models::MAPFProblem& mapf_problem) const { + makespan_ = 0; + + const auto goal_distances = BuildGoalDistances(mapf_problem); + + PibtRun run{mapf_problem, goal_distances, {}, {}, {}, {}}; + run.config = config_; + run.rng.seed(config_.seed); + + std::unordered_map goal; + std::unordered_map priority; + std::unordered_map paths; + + const auto offsets = DefaultInitialOffsets(mapf_problem, goal_distances); + + const size_t agent_count = mapf_problem.agent_tasks.size(); + run.current.reserve(agent_count); + goal.reserve(agent_count); + priority.reserve(agent_count); + paths.reserve(agent_count); + + for (const auto& [agent_id, agent_task] : mapf_problem.agent_tasks) { + const NodeId start_node = agent_task.endpoints.from_node_id; + run.current.emplace(agent_id, start_node); + goal.emplace(agent_id, agent_task.endpoints.to_node_id); + priority.emplace(agent_id, offsets.at(agent_id)); + paths.emplace(agent_id, graph::NodeIdsList{start_node}); + } + + const size_t max_timesteps = 64 * std::max(mapf_problem.graph.nodes.size(), 1); + + std::vector order; + order.reserve(agent_count); + for (const auto& [agent_id, _] : mapf_problem.agent_tasks) { + order.push_back(agent_id); + } + + bool solved = false; + for (size_t step = 0; step < max_timesteps; ++step) { + bool all_at_goal = true; + for (const auto& [agent_id, node_id] : run.current) { + if (node_id != goal.at(agent_id)) { + all_at_goal = false; + break; + } + } + if (all_at_goal) { + solved = true; + break; + } + + run.step = step; + run.next.clear(); + run.reserved.clear(); + run.occupant_now.clear(); + run.occupant_now.reserve(agent_count); + for (const auto& [agent_id, node_id] : run.current) { + run.occupant_now.emplace(node_id, agent_id); + } + + std::sort(order.begin(), order.end(), [&](AgentId lhs, AgentId rhs) { + return priority.at(lhs) > priority.at(rhs); + }); + + for (const AgentId agent_id : order) { + if (!run.next.contains(agent_id)) { + run.Decide(agent_id, std::nullopt); + } + } + + for (const auto& [agent_id, next_node] : run.next) { + run.current[agent_id] = next_node; + paths.at(agent_id).push_back(next_node); + } + + if (config_.priority == PIBTConfig::PriorityMode::Dynamic) { + for (auto& [agent_id, value] : priority) { + if (run.current.at(agent_id) == goal.at(agent_id)) { + value = offsets.at(agent_id); + } else { + value += 1.0; + } + } + } + } + + if (!solved) { + return {}; + } + + models::MAPFSolution solution; + solution.agent_paths.reserve(paths.size()); + for (auto& [agent_id, path] : paths) { + makespan_ = std::max(makespan_, path.size() - 1); + solution.agent_paths.emplace(agent_id, models::AgentPath(agent_id, std::move(path))); + } + return solution; +} + +} // namespace mapf::solver diff --git a/solver/tests/test_solvers.cpp b/solver/tests/test_solvers.cpp index f632a98..998cc8e 100644 --- a/solver/tests/test_solvers.cpp +++ b/solver/tests/test_solvers.cpp @@ -1,5 +1,6 @@ #include "solvers/astar_solver.h" #include "solvers/bfs_solver.h" +#include "solvers/pibt_solver.h" #include "graph/graph.h" @@ -15,7 +16,6 @@ using namespace mapf::graph; using namespace mapf::models; using namespace mapf::solver; -// Builds a width x height grid graph with bidirectional edges Graph BuildGridGraph(uint32_t width, uint32_t height) { Nodes nodes; Edges edges; @@ -38,7 +38,6 @@ Graph BuildGridGraph(uint32_t width, uint32_t height) { return Graph(std::move(nodes), std::move(edges)); } -// Runs both solvers on the same problem and collects metrics struct BenchmarkResult { size_t bfs_nodes_expanded; size_t astar_nodes_expanded; @@ -90,7 +89,6 @@ void PrintBenchmark(const std::string& name, const BenchmarkResult& r) { << " A*=" << r.astar_makespan << std::endl; } -// Correctness tests from starter code (4 nodes, 2 agents) class MAPFProblemTest1 : public testing::Test { protected: @@ -144,7 +142,6 @@ TEST_F(MAPFProblemTest2, AStarSolver) { ASSERT_VALID_SOLUTION(problem, solver.FindSolution(problem)); } -// Benchmark: small grid, minimal difference expected TEST(Benchmark, Grid3x3_2Agents) { auto graph = BuildGridGraph(3, 3); AgentTasks tasks{ @@ -165,23 +162,18 @@ TEST(Benchmark, Grid3x3_2Agents) { EXPECT_EQ(r.bfs_makespan, r.astar_makespan); } -// Benchmark: corridor with a single side pocket (bottleneck) -// 0 — 1 — 2 — 3 — 4 — 5 — 6 — 7 -// | -// 8 -// Agents swap ends: 0->7 and 7->0. Only node 8 allows them to pass. TEST(Benchmark, CorridorBottleneck_2Agents) { Nodes nodes; Edges edges; for (uint32_t i = 0; i < 9; ++i) { nodes.emplace(i, Node(i)); } - // main corridor + for (uint32_t i = 0; i < 7; ++i) { edges.emplace(Edge(i, i + 1)); edges.emplace(Edge(i + 1, i)); } - // side pocket at node 3 + edges.emplace(Edge(3, 8)); edges.emplace(Edge(8, 3)); auto graph = Graph(std::move(nodes), std::move(edges)); @@ -204,7 +196,6 @@ TEST(Benchmark, CorridorBottleneck_2Agents) { EXPECT_EQ(r.bfs_makespan, r.astar_makespan); } -// Benchmark: 3 agents, larger state space TEST(Benchmark, Grid4x4_3Agents) { auto graph = BuildGridGraph(4, 4); AgentTasks tasks{ @@ -226,7 +217,6 @@ TEST(Benchmark, Grid4x4_3Agents) { EXPECT_EQ(r.bfs_makespan, r.astar_makespan); } -// Benchmark: 4 agents, all doing diagonal swaps — hardest case TEST(Benchmark, Grid4x4_4Agents) { auto graph = BuildGridGraph(4, 4); AgentTasks tasks{ @@ -248,3 +238,97 @@ TEST(Benchmark, Grid4x4_4Agents) { EXPECT_LE(astar.GetNodesExpanded(), bfs.GetNodesExpanded()); EXPECT_EQ(r.bfs_makespan, r.astar_makespan); } + +TEST_F(MAPFProblemTest1, PIBTSolver) { + PIBTSolver solver; + auto solution = solver.FindSolution(problem); + ASSERT_FALSE(solution.agent_paths.empty()) << "PIBT found no solution"; + ASSERT_VALID_SOLUTION(problem, solution); +} + +TEST_F(MAPFProblemTest2, PIBTSolver_Limitation) { + PIBTSolver solver; + auto solution = solver.FindSolution(problem); + EXPECT_TRUE(solution.agent_paths.empty()) + << "PIBT is not expected to solve a tree (star graph)"; +} + +TEST(PIBT, Grid3x3_2Agents) { + auto graph = BuildGridGraph(3, 3); + AgentTasks tasks{ + {0, {0, {0, 8}}}, + {1, {1, {8, 0}}}, + }; + MAPFProblem problem(std::move(graph), std::move(tasks)); + + PIBTSolver solver; + auto solution = solver.FindSolution(problem); + ASSERT_FALSE(solution.agent_paths.empty()) << "PIBT found no solution"; + ASSERT_VALID_SOLUTION(problem, solution); + std::cout << "\n--- PIBT 3x3 grid, 2 agents ---\n makespan=" << solver.GetMakespan() + << std::endl; +} + +TEST(PIBT, CorridorBottleneck_2Agents_Limitation) { + Nodes nodes; + Edges edges; + for (uint32_t i = 0; i < 9; ++i) { + nodes.emplace(i, Node(i)); + } + for (uint32_t i = 0; i < 7; ++i) { + edges.emplace(Edge(i, i + 1)); + edges.emplace(Edge(i + 1, i)); + } + edges.emplace(Edge(3, 8)); + edges.emplace(Edge(8, 3)); + auto graph = Graph(std::move(nodes), std::move(edges)); + + AgentTasks tasks{ + {0, {0, {0, 7}}}, + {1, {1, {7, 0}}}, + }; + MAPFProblem problem(std::move(graph), std::move(tasks)); + + PIBTSolver solver; + auto solution = solver.FindSolution(problem); + EXPECT_TRUE(solution.agent_paths.empty()) + << "PIBT is not expected to solve a tree (corridor with dead ends)"; +} + +TEST(PIBT, Grid4x4_4Agents) { + auto graph = BuildGridGraph(4, 4); + AgentTasks tasks{ + {0, {0, {0, 15}}}, + {1, {1, {15, 0}}}, + {2, {2, {3, 12}}}, + {3, {3, {12, 3}}}, + }; + MAPFProblem problem(std::move(graph), std::move(tasks)); + + PIBTSolver solver; + auto solution = solver.FindSolution(problem); + ASSERT_FALSE(solution.agent_paths.empty()) << "PIBT found no solution"; + ASSERT_VALID_SOLUTION(problem, solution); + std::cout << "\n--- PIBT 4x4 grid, 4 agents ---\n makespan=" << solver.GetMakespan() + << std::endl; +} + +TEST(PIBT, Grid8x8_16Agents) { + auto graph = BuildGridGraph(8, 8); + AgentTasks tasks; + for (uint32_t k = 0; k < 16; ++k) { + tasks.emplace(k, AgentTask(k, {k, 63 - k})); + } + MAPFProblem problem(std::move(graph), std::move(tasks)); + + PIBTSolver solver; + auto t0 = std::chrono::high_resolution_clock::now(); + auto solution = solver.FindSolution(problem); + auto t1 = std::chrono::high_resolution_clock::now(); + + ASSERT_FALSE(solution.agent_paths.empty()) << "PIBT found no solution"; + ASSERT_VALID_SOLUTION(problem, solution); + std::cout << "\n--- PIBT 8x8 grid, 16 agents ---\n makespan=" << solver.GetMakespan() + << " time=" << std::chrono::duration(t1 - t0).count() << " ms" + << std::endl; +} From b41228e8bbcd39858542230fb84653548dbdb7df Mon Sep 17 00:00:00 2001 From: pushkarevv Date: Fri, 15 May 2026 11:08:42 +0300 Subject: [PATCH 5/6] feat(viz): JSON exporter and dependency-free Python visualizer --- README.md | 60 +++---- solver/BUILD.bazel | 11 ++ solver/tools/src/export_solution.cpp | 242 ++++++++++++++++++++++++++ viz/README.md | 52 ++++++ viz/data/arena.json | 50 ++++++ viz/make_figures.sh | 58 +++++++ viz/out/arena.html | 130 ++++++++++++++ viz/out/arena_trails.svg | 209 ++++++++++++++++++++++ viz/out/operating_envelope.svg | 42 +++++ viz/plot_envelope.py | 106 ++++++++++++ viz/visualize.py | 248 +++++++++++++++++++++++++++ 11 files changed, 1172 insertions(+), 36 deletions(-) create mode 100644 solver/tools/src/export_solution.cpp create mode 100644 viz/README.md create mode 100644 viz/data/arena.json create mode 100644 viz/make_figures.sh create mode 100644 viz/out/arena.html create mode 100644 viz/out/arena_trails.svg create mode 100644 viz/out/operating_envelope.svg create mode 100644 viz/plot_envelope.py create mode 100644 viz/visualize.py diff --git a/README.md b/README.md index 8de302a..d2cc9a2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # MAPF -Experiments in Multi-Agent Pathfinding +Experiments in Multi-Agent Pathfinding. + +The repository implements three solvers (BFS, A*, PIBT) on grid graphs, a benchmark +and ablation test suite, a JSON exporter, and a dependency-free Python visualizer that +turns exported solutions into an interactive HTML animation and an SVG trajectory image. ## Getting started @@ -66,62 +70,46 @@ Use paths, relative to MODULE directory. bazel run //solver:main -- -s bfs_solver -i data/mapf_problem_1.pb.txt -o data/mapf_solution_1.pb.txt ``` -## Running simulator +## Visualizing a solution -### Show information about simulator binary options +Solve an instance and write a JSON description (map, obstacles, every agent's path) suitable for the visualizer: ```sh -bazel run //simulator:main -- --help +bazel run //solver:export_solution -- \ + --width 32 --height 20 --agents 40 --obstacles \ + --solver pibt_solver --out viz/data/arena.json ``` -### List available solver names +Render an interactive HTML animation and a static SVG trajectory image: ```sh -bazel run //simulator:main -- --list-solvers +python3 viz/visualize.py viz/data/arena.json --out viz/out ``` -Simulator input is a file with text formated [MAPFProblem](models/proto/models.proto#L22) protobuf message. [See](data/mapf_problem_1.pb.txt) example of file structure. - -Simulator output is a MCAP file with serialized [Scene](scene/proto/scene.proto) protobuf message. - -Use paths, relative to MODULE directory. - -### Simulator run command example - -```sh -bazel run //simulator:main -- -s bfs_solver -i data/mapf_problem_1.pb.txt -o data/scene.mcap -``` +Open `viz/out/arena.html` in a browser for the animation, or use `viz/out/arena_trails.svg` as a static figure. ## Repository contents Guide to packages: -* __geom__: Package with geometry primitives (`Vec2`, floating point number comparators, etc.). See __geom/tests__ for usage examples, __geom/proto__ for available proto-definitions. - -* __graph__: Package with graph primitives (`Node`, `Edge`, `Endpoints`, etc.). See __graph/test__ for usage examples, __graph/proto__ for available proto-definitions. - -* __models__: Package with MAPF-problem specific primitives (`AgentState`, `AgentTask`, `AgentPath`, `MAPFProblem`, `MAPFSolution`, etc.). See __models/test__ for usage examples, __models/proto__ for available proto-definitions. +* __geom__: Geometry primitives (`Vec2`, floating point comparators). See __geom/tests__ for usage and __geom/proto__ for proto definitions. -* __scene__: Package with scene primitive, being recorded during simulation. See __models/proto__ for available proto-definitions. +* __graph__: Graph primitives (`Node`, `Edge`, `Endpoints`). See __graph/test__ for usage and __graph/proto__ for proto definitions. -* __simulator__: Package with simulator primitives. The simulator is implemented using a single-threaded [actor model](https://en.wikipedia.org/wiki/Actor_model). - - * __simulator/acotrs__: Available actors declarations, `Actor` interface is defined [here](simulator/actors/include/actors/actor.h). +* __models__: MAPF problem types (`AgentState`, `AgentTask`, `AgentPath`, `MAPFProblem`, `MAPFSolution`). See __models/test__ for usage and __models/proto__ for proto definitions. - * __simulator/context__: Global simulation context, holding current timestamp and `EventBus`, which is an object, that encapsulates interaction between different actors. - - * __simulator/event__: Event primitives, which encapsulates messages handling in runtime. +* __solver__: Package with available MAPF-problem solvers. - * __simulator/launcher__: Simulation launcher, running event loop. + * __solver/solvers__: Solver implementations (BFS, A*, PIBT). The `Solver` interface is defined [here](solver/solvers/include/solvers/solver_base.h). - * __simulator/main__: Simulator binary. See [CLI-options](simulator/main/src/main.cpp#L24) to determine available running modes. + * __solver/factory__: Factory with registered solvers. After registering a solver in [the factory constructor](solver/factory/src/solver_factory.cpp), it automatically becomes available in CLI options. - * __simulator/messages__: Available messages declarations. `Message` interface is defined [here](simulator/messages/include/messages/message.h). + * __solver/main__: Solver binary. See [CLI options](solver/main/src/main.cpp) for the supported flags. -* __solver__: Package with available MAPF-problem solvers. + * __solver/tools__: `export_solution` binary — solves an arena instance and writes a JSON file consumed by the visualizer. - * __solver/solvers__: Available solvers declarations, `Solver` interface is defined [here](solver/solvers/include/solvers/solver_base.h). + * __solver/tests__: GoogleTest correctness tests plus the benchmark, ablation and statistical experiments (`Experiment.OperatingEnvelope`, `Experiment.LargeScale`, `Experiment.Ablation`, `Experiment.Suboptimality`). - * __solver/factory__: Factory with registered solvers. After registering solver in [factory constructor](solver/factory/src/solver_factory.cpp#L11), it automatically becomes available in CLI-options. +* __viz__: Standard-library Python visualizer. `visualize.py` renders a JSON solution to HTML + SVG; `plot_envelope.py` renders the operating-envelope plot; `plot_suboptimality.py` renders the statistical box-plots. - * __solver/main__: Sover binary. See [CLI-options](solver/main/src/main.cpp#L17) to determine available running modes. \ No newline at end of file +* __data__: Example MAPF problems (text-format protobuf). diff --git a/solver/BUILD.bazel b/solver/BUILD.bazel index 1be0e00..2ac0aed 100644 --- a/solver/BUILD.bazel +++ b/solver/BUILD.bazel @@ -33,6 +33,17 @@ cc_binary( ], ) +cc_binary( + name = "export_solution", + srcs = ["tools/src/export_solution.cpp"], + deps = [ + ":factory", + ":solvers", + "//graph", + "//models", + ], +) + cc_test( name = "solvers_tests", size = "large", diff --git a/solver/tools/src/export_solution.cpp b/solver/tools/src/export_solution.cpp new file mode 100644 index 0000000..99ce0f9 --- /dev/null +++ b/solver/tools/src/export_solution.cpp @@ -0,0 +1,242 @@ + +// example: +// bazel run //solver:export_solution -- \ +// --width 32 --height 20 --agents 40 --obstacles --solver pibt_solver \ +// --out viz/data/arena.json +// output: +// { "width", "height", "solver", "makespan", "comp_time_ms", +// "obstacles": [[x,y], ...], +// "agents": [ { "id", "start":[x,y], "goal":[x,y], "path":[[x,y], ...] }, ... ] } + +#include "factory/solver_factory.h" + +#include "graph/graph.h" +#include "models/models.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using mapf::graph::Edge; +using mapf::graph::Edges; +using mapf::graph::Endpoints; +using mapf::graph::Graph; +using mapf::graph::Node; +using mapf::graph::NodeId; +using mapf::graph::Nodes; +using mapf::models::AgentId; +using mapf::models::AgentTask; +using mapf::models::AgentTasks; +using mapf::models::MAPFProblem; + +struct Config { + uint32_t width = 32; + uint32_t height = 20; + uint32_t agents = 40; + uint32_t seed = 42; + bool obstacles = false; + std::string solver = "pibt_solver"; + std::string out = "viz/data/demo.json"; +}; + +Config ParseArgs(int argc, char* argv[]) { + Config cfg; + for (int i = 1; i < argc; ++i) { + const std::string arg = argv[i]; + auto next = [&]() -> std::string { return (i + 1 < argc) ? argv[++i] : ""; }; + if (arg == "--width") { + cfg.width = static_cast(std::stoul(next())); + } else if (arg == "--height") { + cfg.height = static_cast(std::stoul(next())); + } else if (arg == "--agents") { + cfg.agents = static_cast(std::stoul(next())); + } else if (arg == "--seed") { + cfg.seed = static_cast(std::stoul(next())); + } else if (arg == "--solver") { + cfg.solver = next(); + } else if (arg == "--obstacles") { + cfg.obstacles = true; + } else if (arg == "--out") { + cfg.out = next(); + } else { + std::cerr << "Unknown argument: " << arg << "\n"; + } + } + return cfg; +} + +bool IsObstacle(const Config& cfg, uint32_t x, uint32_t y) { + if (!cfg.obstacles) { + return false; + } + constexpr uint32_t kBlock = 2; + constexpr uint32_t kSpacing = 6; + constexpr uint32_t kMargin = 3; + if (x < kMargin || y < kMargin || x + kMargin >= cfg.width || y + kMargin >= cfg.height) { + return false; + } + return (x % kSpacing < kBlock) && (y % kSpacing < kBlock); +} + +Graph BuildArena(const Config& cfg, std::vector& free_cells) { + Nodes nodes; + Edges edges; + auto id_of = [&](uint32_t x, uint32_t y) { return y * cfg.width + x; }; + + for (uint32_t y = 0; y < cfg.height; ++y) { + for (uint32_t x = 0; x < cfg.width; ++x) { + if (IsObstacle(cfg, x, y)) { + continue; + } + const NodeId id = id_of(x, y); + nodes.emplace(id, Node(id)); + free_cells.push_back(id); + } + } + for (uint32_t y = 0; y < cfg.height; ++y) { + for (uint32_t x = 0; x < cfg.width; ++x) { + if (IsObstacle(cfg, x, y)) { + continue; + } + const NodeId id = id_of(x, y); + if (x + 1 < cfg.width && !IsObstacle(cfg, x + 1, y)) { + edges.emplace(Edge(id, id_of(x + 1, y))); + edges.emplace(Edge(id_of(x + 1, y), id)); + } + if (y + 1 < cfg.height && !IsObstacle(cfg, x, y + 1)) { + edges.emplace(Edge(id, id_of(x, y + 1))); + edges.emplace(Edge(id_of(x, y + 1), id)); + } + } + } + return Graph(std::move(nodes), std::move(edges)); +} + +void WriteXY(std::ofstream& out, NodeId id, uint32_t width) { + out << "[" << (id % width) << "," << (id / width) << "]"; +} + +} // namespace + +int main(int argc, char* argv[]) { + const Config cfg = ParseArgs(argc, argv); + + std::vector free_cells; + Graph graph = BuildArena(cfg, free_cells); + + if (cfg.agents > free_cells.size()) { + std::cerr << "Not enough free cells (" << free_cells.size() << ") for " << cfg.agents + << " agents\n"; + return 1; + } + + std::mt19937 rng(cfg.seed); + std::vector starts = free_cells; + std::shuffle(starts.begin(), starts.end(), rng); + std::vector goals = free_cells; + std::shuffle(goals.begin(), goals.end(), rng); + + AgentTasks tasks; + for (uint32_t k = 0; k < cfg.agents; ++k) { + tasks.emplace(k, AgentTask(k, Endpoints(starts[k], goals[k]))); + } + MAPFProblem problem(std::move(graph), std::move(tasks)); + + auto solver = mapf::solver::SolverFactory::Instance().CreateSolver(cfg.solver); + if (!solver) { + std::cerr << "Unknown solver: " << cfg.solver << "\n"; + return 1; + } + + const auto t0 = std::chrono::high_resolution_clock::now(); + const auto solution = solver->FindSolution(problem); + const auto t1 = std::chrono::high_resolution_clock::now(); + const double comp_time_ms = std::chrono::duration(t1 - t0).count(); + + long long makespan = -1; + if (!solution.agent_paths.empty()) { + makespan = static_cast(solution.agent_paths.begin()->second.path.size()) - 1; + } + if (makespan < 0) { + std::cerr << "WARNING: solver '" << cfg.solver << "' found no solution for this instance\n"; + } + + std::filesystem::path out_path = cfg.out; + if (out_path.is_relative()) { + if (const char* workspace = std::getenv("BUILD_WORKSPACE_DIRECTORY")) { + out_path = std::filesystem::path(workspace) / out_path; + } + } + std::filesystem::create_directories(out_path.parent_path()); + + std::ofstream out(out_path); + if (!out.is_open()) { + std::cerr << "Failed to open output file: " << out_path << "\n"; + return 1; + } + + out << "{\n"; + out << " \"width\": " << cfg.width << ",\n"; + out << " \"height\": " << cfg.height << ",\n"; + out << " \"solver\": \"" << cfg.solver << "\",\n"; + out << " \"makespan\": " << makespan << ",\n"; + out << " \"comp_time_ms\": " << comp_time_ms << ",\n"; + + out << " \"obstacles\": ["; + bool first = true; + for (uint32_t y = 0; y < cfg.height; ++y) { + for (uint32_t x = 0; x < cfg.width; ++x) { + if (IsObstacle(cfg, x, y)) { + out << (first ? "" : ",") << "[" << x << "," << y << "]"; + first = false; + } + } + } + out << "],\n"; + + out << " \"agents\": [\n"; + std::vector ids; + ids.reserve(problem.agent_tasks.size()); + for (const auto& [id, _] : problem.agent_tasks) { + ids.push_back(id); + } + std::sort(ids.begin(), ids.end()); + + for (size_t idx = 0; idx < ids.size(); ++idx) { + const AgentId id = ids[idx]; + const auto& task = problem.agent_tasks.at(id); + out << " {\"id\": " << id << ", \"start\": "; + WriteXY(out, task.endpoints.from_node_id, cfg.width); + out << ", \"goal\": "; + WriteXY(out, task.endpoints.to_node_id, cfg.width); + out << ", \"path\": ["; + auto path_it = solution.agent_paths.find(id); + if (path_it != solution.agent_paths.end()) { + const auto& path = path_it->second.path; + for (size_t i = 0; i < path.size(); ++i) { + if (i != 0) { + out << ","; + } + WriteXY(out, path[i], cfg.width); + } + } + out << "]}" << (idx + 1 < ids.size() ? "," : "") << "\n"; + } + out << " ]\n"; + out << "}\n"; + out.close(); + + std::cout << "Wrote " << out_path << " (solver=" << cfg.solver << ", agents=" << cfg.agents + << ", makespan=" << makespan << ", comp_time=" << comp_time_ms << " ms)\n"; + return 0; +} diff --git a/viz/README.md b/viz/README.md new file mode 100644 index 0000000..a99c3e2 --- /dev/null +++ b/viz/README.md @@ -0,0 +1,52 @@ +# MAPF visualiser + +Two-step pipeline: a C++ tool runs a solver and dumps the solution to JSON; a +dependency-free Python script renders it. + +## 1. Export a solution to JSON (inside the Docker container) + +```sh +bazel run //solver:export_solution -- \ + --width 32 --height 20 --agents 40 --obstacles \ + --solver pibt_solver --seed 42 \ + --out viz/data/arena.json +``` + +Flags: +| flag | meaning | default | +|------|---------|---------| +| `--width`, `--height` | grid size | 32 × 20 | +| `--agents` | number of agents (random distinct start/goal) | 40 | +| `--obstacles` | add rectangular pillar obstacles (arena look) | off | +| `--solver` | any registered solver (`pibt_solver`, `astar_solver`, `bfs_solver`) | `pibt_solver` | +| `--seed` | RNG seed for the instance | 42 | +| `--out` | output JSON path (relative to repo root) | `viz/data/demo.json` | + +## 2. Render (on the host, Python 3 standard library only) + +```sh +python3 viz/visualize.py viz/data/arena.json --out viz/out +``` + +Produces: +* `viz/out/arena.html` — interactive animation: HUD (solver / agents / makespan / + comp_time / time step), play–pause, time slider, speed, trails toggle. Open in + any browser. +* `viz/out/arena_trails.svg` — static "all trajectories" image for the report. + +## JSON schema + +```json +{ + "width": 32, "height": 20, "solver": "pibt_solver", + "makespan": 39, "comp_time_ms": 16.3, + "obstacles": [[x, y], ...], + "agents": [ + {"id": 0, "start": [x, y], "goal": [x, y], "path": [[x, y], ...]} + ] +} +``` + +Coordinates are grid cells; an agent's `path` lists its cell at every time step +(equal length for all agents — they move in lockstep), so position at step `t` +is `path[t]`. diff --git a/viz/data/arena.json b/viz/data/arena.json new file mode 100644 index 0000000..b2764e2 --- /dev/null +++ b/viz/data/arena.json @@ -0,0 +1,50 @@ +{ + "width": 32, + "height": 20, + "solver": "pibt_solver", + "makespan": 39, + "comp_time_ms": 16.3141, + "obstacles": [[6,6],[7,6],[12,6],[13,6],[18,6],[19,6],[24,6],[25,6],[6,7],[7,7],[12,7],[13,7],[18,7],[19,7],[24,7],[25,7],[6,12],[7,12],[12,12],[13,12],[18,12],[19,12],[24,12],[25,12],[6,13],[7,13],[12,13],[13,13],[18,13],[19,13],[24,13],[25,13]], + "agents": [ + {"id": 0, "start": [22,16], "goal": [15,3], "path": [[22,16],[21,16],[21,15],[21,14],[21,13],[21,12],[21,11],[20,11],[19,11],[18,11],[17,11],[17,10],[16,10],[15,10],[15,9],[15,8],[14,8],[15,8],[15,7],[15,6],[15,6],[15,5],[15,4],[15,3],[15,3],[15,3],[15,3],[15,3],[15,3],[15,3],[15,3],[15,3],[15,3],[15,3],[15,3],[15,3],[15,3],[15,3],[15,3],[15,3]]}, + {"id": 1, "start": [16,9], "goal": [10,7], "path": [[16,9],[15,9],[14,9],[13,9],[13,8],[12,8],[12,8],[11,8],[10,8],[10,7],[10,7],[10,7],[10,7],[10,7],[10,7],[10,7],[10,7],[10,8],[10,7],[10,7],[10,7],[10,7],[10,7],[10,7],[10,7],[10,7],[10,7],[10,7],[10,7],[10,7],[10,7],[10,7],[10,7],[10,7],[10,7],[10,7],[10,7],[10,7],[10,7],[10,7]]}, + {"id": 2, "start": [18,4], "goal": [10,16], "path": [[18,4],[18,5],[17,5],[17,6],[17,7],[16,7],[15,7],[15,8],[15,9],[14,9],[13,9],[12,9],[11,9],[10,9],[10,10],[10,11],[10,12],[10,13],[10,14],[10,15],[10,16],[10,16],[10,16],[10,16],[10,16],[10,16],[10,16],[10,16],[10,16],[10,16],[10,16],[9,16],[9,17],[10,17],[10,16],[10,16],[10,16],[10,16],[10,16],[10,16]]}, + {"id": 3, "start": [0,17], "goal": [23,9], "path": [[0,17],[0,16],[1,16],[1,15],[2,15],[3,15],[4,15],[4,14],[5,14],[5,13],[5,12],[5,11],[5,10],[5,9],[6,9],[7,9],[8,9],[9,9],[10,9],[11,9],[12,9],[13,9],[14,9],[15,9],[16,9],[17,9],[18,9],[19,9],[20,9],[21,9],[22,9],[23,9],[23,9],[23,9],[23,9],[23,9],[23,9],[23,9],[23,9],[23,9]]}, + {"id": 4, "start": [18,9], "goal": [10,8], "path": [[18,9],[17,9],[16,9],[15,9],[15,8],[14,8],[13,8],[12,8],[11,8],[10,8],[10,8],[10,8],[10,8],[10,8],[10,8],[10,8],[10,8],[10,9],[10,8],[10,8],[10,8],[10,8],[10,8],[10,8],[10,8],[10,8],[10,8],[10,8],[10,8],[10,8],[10,8],[10,8],[10,8],[10,8],[10,8],[10,8],[10,8],[10,8],[10,8],[10,8]]}, + {"id": 5, "start": [19,15], "goal": [0,12], "path": [[19,15],[18,15],[17,15],[16,15],[15,15],[14,15],[13,15],[12,15],[11,15],[11,14],[10,14],[9,14],[8,14],[7,14],[6,14],[5,14],[5,13],[5,12],[4,12],[3,12],[2,12],[1,12],[0,12],[0,12],[0,12],[0,12],[0,12],[0,12],[0,12],[0,12],[0,12],[0,12],[0,12],[0,12],[0,12],[0,12],[0,12],[0,12],[0,12],[0,12]]}, + {"id": 6, "start": [8,6], "goal": [5,10], "path": [[8,6],[8,7],[8,8],[8,9],[8,10],[7,10],[6,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,9],[5,8],[5,9],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10],[5,10]]}, + {"id": 7, "start": [5,1], "goal": [17,0], "path": [[5,1],[5,0],[6,0],[7,0],[8,0],[9,0],[10,0],[11,0],[12,0],[13,0],[14,0],[15,0],[16,0],[17,0],[17,0],[17,0],[17,1],[17,0],[17,0],[17,0],[17,0],[17,0],[17,0],[17,0],[17,0],[17,0],[17,0],[17,0],[17,0],[17,0],[17,0],[17,0],[17,0],[17,0],[17,0],[17,0],[17,0],[17,0],[17,0],[17,0]]}, + {"id": 8, "start": [11,18], "goal": [4,11], "path": [[11,18],[11,17],[11,16],[11,15],[11,14],[10,14],[9,14],[9,13],[8,13],[8,12],[8,12],[8,13],[9,13],[8,13],[8,12],[8,11],[7,11],[6,11],[5,11],[4,11],[4,11],[4,11],[4,11],[4,11],[4,11],[4,11],[4,11],[4,11],[4,11],[4,11],[4,11],[4,11],[4,11],[4,11],[4,11],[4,11],[4,11],[4,11],[4,11],[4,11]]}, + {"id": 9, "start": [0,13], "goal": [8,0], "path": [[0,13],[0,12],[0,11],[1,11],[1,10],[1,9],[2,9],[3,9],[4,9],[4,8],[4,7],[4,6],[5,6],[5,5],[5,4],[5,3],[6,3],[7,3],[8,3],[8,2],[8,1],[8,0],[8,0],[8,0],[8,0],[8,0],[8,0],[8,0],[8,0],[8,0],[8,0],[8,0],[8,0],[8,0],[8,0],[8,0],[8,0],[8,0],[8,0],[8,0]]}, + {"id": 10, "start": [27,12], "goal": [7,2], "path": [[27,12],[27,11],[27,10],[27,9],[27,8],[26,8],[25,8],[24,8],[23,8],[22,8],[22,7],[21,7],[21,6],[20,6],[20,5],[20,4],[19,4],[18,4],[18,3],[18,2],[17,2],[16,2],[15,2],[14,2],[13,2],[12,2],[11,2],[10,2],[9,2],[8,2],[7,2],[7,2],[7,2],[7,2],[7,2],[7,2],[7,2],[7,2],[7,2],[7,2]]}, + {"id": 11, "start": [20,2], "goal": [9,12], "path": [[20,2],[19,2],[18,2],[17,2],[16,2],[16,3],[16,4],[16,5],[16,6],[16,7],[15,7],[15,8],[14,8],[13,8],[12,8],[12,9],[12,10],[12,11],[11,11],[10,11],[9,11],[9,12],[9,12],[9,12],[9,12],[9,12],[9,12],[9,12],[9,12],[9,12],[9,12],[9,12],[9,12],[9,12],[9,12],[9,12],[9,12],[9,12],[9,12],[9,12]]}, + {"id": 12, "start": [10,13], "goal": [5,14], "path": [[10,13],[10,14],[9,14],[8,14],[7,14],[6,14],[7,14],[8,14],[9,14],[8,14],[7,14],[6,14],[5,14],[5,14],[5,14],[4,14],[5,14],[5,14],[5,14],[5,14],[5,14],[5,14],[5,14],[5,14],[5,14],[5,14],[5,14],[5,14],[5,14],[5,14],[5,14],[5,14],[5,14],[5,14],[5,14],[5,14],[5,14],[5,14],[5,14],[5,14]]}, + {"id": 13, "start": [21,1], "goal": [20,3], "path": [[21,1],[20,1],[20,2],[20,2],[20,3],[20,3],[20,3],[20,3],[20,3],[20,3],[20,3],[20,3],[20,3],[20,3],[20,3],[20,3],[20,3],[20,3],[20,3],[20,3],[20,3],[21,3],[21,2],[20,2],[20,3],[20,3],[20,3],[20,3],[20,3],[20,3],[20,3],[20,3],[20,3],[20,3],[20,3],[20,3],[20,3],[20,3],[20,3],[20,3]]}, + {"id": 14, "start": [20,4], "goal": [31,18], "path": [[20,4],[21,4],[21,5],[22,5],[23,5],[23,6],[23,7],[23,8],[23,9],[23,10],[24,10],[24,11],[23,11],[23,12],[23,13],[23,14],[24,14],[25,14],[26,14],[26,15],[27,15],[28,15],[28,16],[29,16],[30,16],[31,16],[31,17],[31,18],[31,18],[31,18],[31,18],[31,18],[31,18],[31,18],[31,18],[31,18],[31,18],[31,18],[31,18],[31,18]]}, + {"id": 15, "start": [20,12], "goal": [10,11], "path": [[20,12],[20,11],[19,11],[18,11],[17,11],[16,11],[15,11],[14,11],[13,11],[12,11],[11,11],[10,11],[10,11],[10,11],[10,11],[10,12],[9,12],[9,11],[10,11],[9,11],[9,10],[9,11],[10,11],[10,11],[10,11],[10,11],[10,11],[10,11],[10,11],[10,11],[10,11],[10,11],[10,11],[10,11],[10,11],[10,11],[10,11],[10,11],[10,11],[10,11]]}, + {"id": 16, "start": [29,13], "goal": [8,18], "path": [[29,13],[29,14],[29,15],[29,16],[28,16],[27,16],[26,16],[25,16],[25,17],[24,17],[24,18],[23,18],[22,18],[21,18],[20,18],[19,18],[18,18],[17,18],[16,18],[15,18],[14,18],[13,18],[12,18],[11,18],[10,18],[9,18],[8,18],[8,18],[8,18],[8,18],[8,18],[8,18],[8,18],[8,18],[8,18],[8,18],[8,18],[8,18],[8,18],[8,18]]}, + {"id": 17, "start": [8,0], "goal": [15,1], "path": [[8,0],[9,0],[10,0],[10,1],[11,1],[12,1],[13,1],[14,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1],[15,1]]}, + {"id": 18, "start": [15,12], "goal": [15,14], "path": [[15,12],[15,13],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14],[15,14]]}, + {"id": 19, "start": [18,19], "goal": [20,17], "path": [[18,19],[19,19],[19,18],[20,18],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17],[20,17]]}, + {"id": 20, "start": [0,9], "goal": [12,18], "path": [[0,9],[0,10],[1,10],[2,10],[3,10],[3,11],[4,11],[5,11],[6,11],[7,11],[8,11],[8,12],[8,13],[8,14],[8,15],[9,15],[10,15],[11,15],[12,15],[12,16],[12,17],[12,18],[12,19],[12,18],[12,18],[12,18],[12,18],[12,18],[12,18],[12,18],[12,18],[12,18],[12,18],[12,18],[12,18],[12,18],[12,18],[12,18],[12,18],[12,18]]}, + {"id": 21, "start": [4,19], "goal": [14,18], "path": [[4,19],[5,19],[6,19],[6,18],[7,18],[8,18],[9,18],[10,18],[11,18],[12,18],[13,18],[14,18],[14,18],[14,18],[14,18],[14,18],[14,18],[14,18],[14,18],[14,18],[14,19],[14,18],[14,18],[14,18],[14,18],[14,18],[14,18],[14,18],[14,18],[14,18],[14,18],[14,18],[14,18],[14,18],[14,18],[14,18],[14,18],[14,18],[14,18],[14,18]]}, + {"id": 22, "start": [4,10], "goal": [9,16], "path": [[4,10],[4,11],[4,12],[4,13],[5,13],[5,14],[6,14],[7,14],[8,14],[8,15],[8,16],[9,16],[9,16],[9,16],[9,16],[9,16],[9,16],[9,16],[9,16],[9,16],[9,16],[9,16],[9,16],[9,16],[9,16],[9,16],[9,16],[9,16],[9,16],[9,16],[9,16],[8,16],[8,16],[8,15],[8,16],[9,16],[9,16],[9,16],[9,16],[9,16]]}, + {"id": 23, "start": [1,3], "goal": [6,4], "path": [[1,3],[2,3],[3,3],[4,3],[5,3],[6,3],[6,4],[6,4],[6,4],[6,4],[6,4],[6,4],[6,4],[6,4],[6,4],[6,4],[6,4],[6,4],[6,4],[6,4],[6,4],[6,4],[6,4],[6,4],[5,4],[5,3],[5,4],[6,4],[6,4],[6,4],[6,4],[6,4],[6,4],[6,4],[6,4],[6,4],[6,4],[6,4],[6,4],[6,4]]}, + {"id": 24, "start": [7,10], "goal": [22,3], "path": [[7,10],[7,9],[7,8],[8,8],[9,8],[10,8],[11,8],[11,7],[11,6],[11,5],[12,5],[12,4],[13,4],[13,3],[14,3],[15,3],[16,3],[17,3],[17,3],[18,3],[19,3],[20,3],[21,3],[22,3],[22,3],[22,3],[22,3],[22,3],[22,3],[22,3],[22,3],[22,3],[22,3],[22,3],[22,3],[22,3],[22,3],[22,3],[22,3],[22,3]]}, + {"id": 25, "start": [6,1], "goal": [30,16], "path": [[6,1],[7,1],[8,1],[9,1],[10,1],[10,2],[11,2],[11,3],[11,4],[12,4],[13,4],[13,5],[14,5],[15,5],[15,6],[15,7],[15,8],[15,9],[15,10],[15,11],[15,12],[16,12],[17,12],[17,13],[17,14],[18,14],[18,15],[18,16],[19,16],[20,16],[21,16],[22,16],[23,16],[24,16],[25,16],[26,16],[27,16],[28,16],[29,16],[30,16]]}, + {"id": 26, "start": [7,4], "goal": [8,9], "path": [[7,4],[8,4],[8,5],[8,6],[8,7],[8,8],[8,9],[8,9],[8,9],[8,9],[8,9],[8,9],[8,9],[8,9],[8,9],[8,9],[9,9],[9,8],[8,8],[8,9],[8,9],[8,9],[8,9],[8,9],[8,9],[8,9],[8,9],[8,9],[8,9],[8,9],[8,9],[8,9],[8,9],[8,9],[8,9],[8,9],[8,9],[8,9],[8,9],[8,9]]}, + {"id": 27, "start": [5,4], "goal": [20,0], "path": [[5,4],[6,4],[7,4],[7,3],[8,3],[9,3],[10,3],[10,2],[11,2],[11,1],[11,0],[12,0],[13,0],[14,0],[15,0],[16,0],[17,0],[18,0],[19,0],[20,0],[20,0],[20,0],[20,0],[20,0],[20,0],[20,0],[20,0],[20,0],[20,0],[20,0],[20,0],[20,0],[20,0],[20,0],[20,0],[20,0],[20,0],[20,0],[20,0],[20,0]]}, + {"id": 28, "start": [11,10], "goal": [11,14], "path": [[11,10],[11,11],[11,12],[11,13],[11,13],[11,14],[11,14],[11,14],[11,14],[12,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14],[11,14]]}, + {"id": 29, "start": [5,17], "goal": [29,16], "path": [[5,17],[5,16],[6,16],[7,16],[8,16],[9,16],[10,16],[11,16],[12,16],[13,16],[14,16],[15,16],[16,16],[17,16],[18,16],[19,16],[20,16],[21,16],[22,16],[22,15],[22,16],[23,16],[24,16],[25,16],[26,16],[27,16],[28,16],[29,16],[29,16],[29,16],[29,16],[29,16],[29,16],[29,16],[29,16],[29,16],[29,16],[29,16],[29,15],[29,16]]}, + {"id": 30, "start": [16,19], "goal": [16,19], "path": [[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19],[16,19]]}, + {"id": 31, "start": [26,10], "goal": [11,7], "path": [[26,10],[26,9],[26,8],[25,8],[24,8],[23,8],[22,8],[21,8],[20,8],[19,8],[18,8],[17,8],[16,8],[15,8],[14,8],[13,8],[12,8],[11,8],[11,7],[11,7],[11,7],[11,7],[11,7],[11,7],[11,7],[11,7],[11,7],[11,7],[11,7],[11,7],[11,7],[11,7],[11,7],[11,7],[11,7],[11,7],[11,7],[11,7],[11,7],[11,7]]}, + {"id": 32, "start": [18,5], "goal": [10,6], "path": [[18,5],[17,5],[16,5],[15,5],[14,5],[13,5],[12,5],[11,5],[10,5],[10,6],[10,6],[10,6],[10,6],[10,6],[10,6],[10,6],[10,6],[10,6],[9,6],[10,6],[10,6],[10,6],[10,6],[10,6],[10,6],[10,6],[10,5],[10,6],[10,6],[10,6],[10,6],[10,6],[10,6],[10,6],[10,6],[10,6],[10,6],[10,6],[10,6],[10,6]]}, + {"id": 33, "start": [20,14], "goal": [0,4], "path": [[20,14],[19,14],[18,14],[17,14],[16,14],[16,13],[16,12],[16,11],[15,11],[14,11],[14,10],[13,10],[13,9],[12,9],[11,9],[11,8],[11,7],[10,7],[10,6],[10,5],[10,4],[9,4],[8,4],[7,4],[6,4],[5,4],[4,4],[3,4],[2,4],[1,4],[0,4],[0,4],[0,4],[0,4],[0,4],[0,4],[0,4],[0,4],[0,4],[0,4]]}, + {"id": 34, "start": [22,4], "goal": [7,3], "path": [[22,4],[22,3],[21,3],[20,3],[19,3],[18,3],[17,3],[16,3],[15,3],[14,3],[13,3],[12,3],[11,3],[10,3],[9,3],[8,3],[7,3],[7,2],[7,3],[7,3],[7,3],[7,3],[7,3],[7,3],[7,3],[7,3],[7,3],[7,3],[7,3],[7,3],[7,3],[7,3],[7,3],[7,3],[7,3],[7,3],[7,3],[7,3],[7,3],[7,3]]}, + {"id": 35, "start": [5,15], "goal": [22,16], "path": [[5,15],[6,15],[7,15],[8,15],[9,15],[10,15],[11,15],[11,15],[11,16],[12,16],[13,16],[14,16],[15,16],[16,16],[17,16],[18,16],[19,16],[20,16],[21,16],[21,16],[21,17],[21,16],[22,16],[22,16],[22,16],[22,16],[22,16],[22,16],[22,16],[22,16],[22,16],[22,17],[22,16],[22,16],[22,16],[22,16],[22,16],[22,16],[22,16],[22,16]]}, + {"id": 36, "start": [10,18], "goal": [16,7], "path": [[10,18],[10,17],[10,16],[11,16],[12,16],[13,16],[14,16],[14,15],[15,15],[16,15],[16,14],[16,13],[16,12],[16,11],[16,10],[16,9],[16,8],[16,7],[16,7],[16,7],[16,7],[16,7],[16,7],[16,7],[16,7],[16,7],[16,7],[16,7],[16,7],[16,7],[16,7],[16,7],[16,7],[16,7],[16,7],[16,7],[16,7],[16,7],[16,7],[16,7]]}, + {"id": 37, "start": [24,8], "goal": [25,4], "path": [[24,8],[25,8],[25,8],[25,9],[25,8],[25,8],[25,9],[26,9],[25,9],[25,8],[26,8],[26,7],[26,6],[26,5],[25,5],[25,4],[25,4],[25,4],[25,4],[25,4],[25,4],[25,4],[25,4],[25,4],[25,4],[25,4],[25,4],[25,4],[25,4],[25,4],[25,4],[25,4],[25,4],[25,4],[25,4],[25,4],[25,4],[25,4],[25,4],[25,4]]}, + {"id": 38, "start": [30,1], "goal": [9,6], "path": [[30,1],[30,2],[29,2],[28,2],[28,3],[28,4],[28,5],[27,5],[26,5],[25,5],[24,5],[23,5],[22,5],[21,5],[21,5],[20,5],[19,5],[18,5],[17,5],[16,5],[15,5],[14,5],[13,5],[12,5],[11,5],[11,6],[10,6],[9,6],[9,6],[9,6],[9,6],[9,6],[9,6],[9,6],[9,6],[9,6],[9,6],[9,6],[9,6],[9,6]]}, + {"id": 39, "start": [30,5], "goal": [6,16], "path": [[30,5],[30,6],[29,6],[29,7],[29,8],[28,8],[27,8],[26,8],[26,9],[26,10],[26,11],[25,11],[24,11],[23,11],[23,12],[23,13],[23,14],[23,15],[23,16],[22,16],[21,16],[20,16],[19,16],[18,16],[17,16],[16,16],[15,16],[14,16],[13,16],[12,16],[11,16],[10,16],[9,16],[8,16],[7,16],[6,16],[6,16],[6,16],[6,16],[6,16]]} + ] +} diff --git a/viz/make_figures.sh b/viz/make_figures.sh new file mode 100644 index 0000000..1e1afbf --- /dev/null +++ b/viz/make_figures.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Regenerate every report figure (one consistent style) into the docs fig/ folder. +# +# Run from the mapf repo root: +# bash viz/make_figures.sh +# +# Produces, in $FIG_DIR: +# arena_trails.png -- static trajectory view (from the SVG) +# arena_anim.png -- one animation frame (screenshot of the HTML) +# operating_envelope.png -- runtime-vs-agents plot +# +# Requirements (already used during development, all on macOS): +# * Docker running (for the C++ exporter) +# * python3 (standard library only) +# * qlmanage + magick (ImageMagick) for SVG->PNG +# * Google Chrome for the animation screenshot +set -euo pipefail + +FIG_DIR="${FIG_DIR:-../../docs/fig}" +mkdir -p "$FIG_DIR" viz/data viz/out +FIG_DIR="$(cd "$FIG_DIR" && pwd)" +echo "Figures -> $FIG_DIR" + +# 1) Solve an arena instance with PIBT and export it to JSON (inside Docker). +docker compose exec mapf zsh -lc 'cd /mapf && bazel run //solver:export_solution -- \ + --width 32 --height 20 --agents 40 --obstacles --solver pibt_solver \ + --seed 42 --out viz/data/arena.json' + +# 2) Render the HTML animation and the trajectory SVG (host, stdlib only). +python3 viz/visualize.py viz/data/arena.json --out viz/out + +# 3) Render the operating-envelope plot to SVG. +python3 viz/plot_envelope.py --out viz/out/operating_envelope.svg + +# Helper: SVG -> trimmed PNG via Quick Look + ImageMagick. +svg2png() { # $1 = svg, $2 = out png + local tmp="/tmp/$(basename "$1").png" + rm -f "$tmp" + qlmanage -t -s 1500 -o /tmp "$1" >/dev/null 2>&1 + magick "$tmp" -fuzz 3% -trim +repage "$2" +} + +svg2png viz/out/arena_trails.svg "$FIG_DIR/arena_trails.png" +svg2png viz/out/operating_envelope.svg "$FIG_DIR/operating_envelope.png" + +# 4) Screenshot the HTML animation (an early frame) for the static figure. +CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" +if [ -x "$CHROME" ]; then + "$CHROME" --headless=new --disable-gpu --hide-scrollbars \ + --virtual-time-budget=3000 --window-size=1100,760 \ + --screenshot="$FIG_DIR/arena_anim.png" \ + "file://$(pwd)/viz/out/arena.html" >/dev/null 2>&1 +else + echo "WARN: Chrome not found; open viz/out/arena.html and screenshot it manually." +fi + +echo "Done. Figures written to $FIG_DIR:" +ls -1 "$FIG_DIR" diff --git a/viz/out/arena.html b/viz/out/arena.html new file mode 100644 index 0000000..a50a190 --- /dev/null +++ b/viz/out/arena.html @@ -0,0 +1,130 @@ + + + + +MAPF pibt_solver 32x20 + + + +
+
+ solved by pibt_solver + agents: + map: 32×20 + makespan: + comp_time: 16.3 ms + time step: +
+ +
+ + + + +
+
+ + + diff --git a/viz/out/arena_trails.svg b/viz/out/arena_trails.svg new file mode 100644 index 0000000..32e91a5 --- /dev/null +++ b/viz/out/arena_trails.svg @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/viz/out/operating_envelope.svg b/viz/out/operating_envelope.svg new file mode 100644 index 0000000..c8a9183 --- /dev/null +++ b/viz/out/operating_envelope.svg @@ -0,0 +1,42 @@ + + + +0.1 + +1 + +10 + +100 + +1000 + +0 + +50 + +100 + +150 + +200 + + +number of agents +runtime (ms, log scale) + + + + + + + + + + + +A* becomes intractable beyond ~4 agents + +A* (optimal) +PIBT + \ No newline at end of file diff --git a/viz/plot_envelope.py b/viz/plot_envelope.py new file mode 100644 index 0000000..b068092 --- /dev/null +++ b/viz/plot_envelope.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 + +#usage: python3 viz/plot_envelope.py --out viz/out/operating_envelope.svg + + +import argparse +import math +import os + + +ASTAR = [(2, 1.04), (3, 31.85), (4, 410.27)] +PIBT = [(4, 0.224), (16, 1.305), (32, 2.642), (50, 10.874), (100, 25.558), (200, 138.711)] + +W, H = 760, 460 +L, R, T, B = 78, 24, 24, 56 +PW, PH = W - L - R, H - T - B +X_MAX = 210.0 +LOG_MIN, LOG_MAX = -1.0, 3.0 + + +def x_px(agents): + return L + (agents / X_MAX) * PW + + +def y_px(ms): + lv = max(LOG_MIN, min(LOG_MAX, math.log10(ms))) + return T + (LOG_MAX - lv) / (LOG_MAX - LOG_MIN) * PH + + +def polyline(points, color, dash=False): + pts = " ".join(f"{x_px(a):.1f},{y_px(t):.1f}" for a, t in points) + d = ' stroke-dasharray="6 5"' if dash else "" + return f'' + + +def markers(points, color): + return "\n".join( + f'' + for a, t in points + ) + + +def build_svg(): + s = [] + s.append( + f'' + ) + s.append(f'') + + for e in range(int(LOG_MIN), int(LOG_MAX) + 1): + y = y_px(10 ** e) + s.append(f'') + label = {-1: "0.1", 0: "1", 1: "10", 2: "100", 3: "1000"}[e] + s.append(f'{label}') + + + for a in [0, 50, 100, 150, 200]: + x = x_px(a) + s.append(f'') + s.append(f'{a}') + + s.append(f'') + s.append(f'') + + + s.append(f'' + f'number of agents') + s.append(f'runtime (ms, log scale)') + + astar_color, pibt_color = "#d1495b", "#2b6cf0" + s.append(polyline(ASTAR, astar_color)) + s.append(markers(ASTAR, astar_color)) + s.append(polyline(PIBT, pibt_color)) + s.append(markers(PIBT, pibt_color)) + + s.append(f'A* becomes ' + f'intractable beyond ~4 agents') + + + lx, ly = L + PW - 132, T + 16 + s.append(f'') + s.append(f'A* (optimal)') + s.append(f'PIBT') + + s.append("") + return "\n".join(s) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--out", default="viz/out/operating_envelope.svg") + args = ap.parse_args() + os.makedirs(os.path.dirname(args.out), exist_ok=True) + with open(args.out, "w", encoding="utf-8") as f: + f.write(build_svg()) + print(f"wrote {args.out}") + + +if __name__ == "__main__": + main() diff --git a/viz/visualize.py b/viz/visualize.py new file mode 100644 index 0000000..1a85e57 --- /dev/null +++ b/viz/visualize.py @@ -0,0 +1,248 @@ +#!/usr/bin/env python3 +"""vizualizer + +reads json produced by `solver:export_solution` and emits: + * .html -- self-contained interactive animation (canvas, HUD, + play/pause, time slider, speed, trails toggle). + * _trails.svg -- static "all trajectories" image for the report. + +usage: python3 viz/visualize.py viz/data/arena.json --out viz/out +""" + +import argparse +import json +import os + + +def make_colors(n): + """Evenly spaced, visually distinct HSL colours.""" + return [f"hsl({int(i * 360 / max(n, 1)) % 360}, 72%, 52%)" for i in range(n)] + + +def cell_center(x, y, cell): + return (x * cell + cell / 2.0, y * cell + cell / 2.0) + + +def gen_svg(data, colors, cell=24): + w, h = data["width"], data["height"] + W, H = w * cell, h * cell + parts = [] + parts.append( + f'' + ) + parts.append(f'') + + for x in range(w + 1): + parts.append(f'') + for y in range(h + 1): + parts.append(f'') + + + for ox, oy in data.get("obstacles", []): + parts.append(f'') + + for i, agent in enumerate(data["agents"]): + path = agent["path"] + if not path: + continue + color = colors[i % len(colors)] + pts = " ".join(f"{cx:.1f},{cy:.1f}" for cx, cy in (cell_center(px, py, cell) for px, py in path)) + parts.append( + f'' + ) + + for i, agent in enumerate(data["agents"]): + color = colors[i % len(colors)] + sx, sy = cell_center(*agent["start"], cell) + gx, gy = agent["goal"] + parts.append(f'') + parts.append( + f'' + ) + + parts.append("") + return "\n".join(parts) + + +HTML_TEMPLATE = r""" + + + +__TITLE__ + + + +
+
+ solved by __SOLVER__ + agents: + map: __WIDTH__×__HEIGHT__ + makespan: + time step: + +
+ +
+ + + + +
+
+ + + +""" + + +def gen_html(data, colors): + return ( + HTML_TEMPLATE + .replace("__TITLE__", f"MAPF {data['solver']} {data['width']}x{data['height']}") + .replace("__SOLVER__", str(data["solver"])) + .replace("__WIDTH__", str(data["width"])) + .replace("__HEIGHT__", str(data["height"])) + .replace("__COMP__", f"{data.get('comp_time_ms', 0):.1f}") + .replace("__DATA__", json.dumps(data)) + .replace("__COLORS__", json.dumps(colors)) + ) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("input", help="JSON file from export_solution") + ap.add_argument("--out", default="viz/out", help="output directory") + args = ap.parse_args() + + with open(args.input, "r", encoding="utf-8") as f: + data = json.load(f) + + colors = make_colors(len(data["agents"])) + os.makedirs(args.out, exist_ok=True) + name = os.path.splitext(os.path.basename(args.input))[0] + + html_path = os.path.join(args.out, name + ".html") + svg_path = os.path.join(args.out, name + "_trails.svg") + with open(html_path, "w", encoding="utf-8") as f: + f.write(gen_html(data, colors)) + with open(svg_path, "w", encoding="utf-8") as f: + f.write(gen_svg(data, colors)) + + print(f"wrote {html_path}") + print(f"wrote {svg_path}") + print(f" {len(data['agents'])} agents, makespan {data.get('makespan')}, " + f"comp_time {data.get('comp_time_ms')} ms") + + +if __name__ == "__main__": + main() From 02d32c57dc8ef391d6d55a0eb8e44d316165f9d9 Mon Sep 17 00:00:00 2001 From: pushkarevv Date: Fri, 15 May 2026 11:46:23 +0300 Subject: [PATCH 6/6] feat(experiments): operating envelope, ablation, sub-optimality + SOC metric --- models/include/models/metrics.h | 13 + models/src/metrics.cpp | 34 ++ solver/tests/experiments.cpp | 576 ++++++++++++++++++++++++++++++++ viz/data/suboptimality.csv | 151 +++++++++ viz/out/suboptimality_hist.png | Bin 0 -> 45684 bytes viz/out/suboptimality_ratio.png | Bin 0 -> 54172 bytes viz/out/suboptimality_time.png | Bin 0 -> 45834 bytes viz/plot_suboptimality.py | 147 ++++++++ 8 files changed, 921 insertions(+) create mode 100644 models/include/models/metrics.h create mode 100644 models/src/metrics.cpp create mode 100644 solver/tests/experiments.cpp create mode 100644 viz/data/suboptimality.csv create mode 100644 viz/out/suboptimality_hist.png create mode 100644 viz/out/suboptimality_ratio.png create mode 100644 viz/out/suboptimality_time.png create mode 100644 viz/plot_suboptimality.py diff --git a/models/include/models/metrics.h b/models/include/models/metrics.h new file mode 100644 index 0000000..b5c3545 --- /dev/null +++ b/models/include/models/metrics.h @@ -0,0 +1,13 @@ +#pragma once + +#include "models/models.h" + +#include + +namespace mapf::models { + +size_t Makespan(const MAPFSolution& solution); + +size_t SumOfCosts(const MAPFSolution& solution); + +} // namespace mapf::models diff --git a/models/src/metrics.cpp b/models/src/metrics.cpp new file mode 100644 index 0000000..e0fe6aa --- /dev/null +++ b/models/src/metrics.cpp @@ -0,0 +1,34 @@ +#include "models/metrics.h" + +#include + +namespace mapf::models { + +size_t Makespan(const MAPFSolution& solution) { + if (solution.agent_paths.empty()) { + return 0; + } + const size_t path_len = solution.agent_paths.begin()->second.path.size(); + return path_len == 0 ? 0 : path_len - 1; +} + +size_t SumOfCosts(const MAPFSolution& solution) { + size_t total = 0; + for (const auto& [_, agent_path] : solution.agent_paths) { + const auto& path = agent_path.path; + if (path.empty()) { + continue; + } + const auto goal = path.back(); + size_t last_move = 0; + for (size_t i = 0; i + 1 < path.size(); ++i) { + if (path[i] != goal) { + last_move = i + 1; + } + } + total += last_move; + } + return total; +} + +} // namespace mapf::models diff --git a/solver/tests/experiments.cpp b/solver/tests/experiments.cpp new file mode 100644 index 0000000..f2b335c --- /dev/null +++ b/solver/tests/experiments.cpp @@ -0,0 +1,576 @@ +#include "solvers/astar_solver.h" +#include "solvers/bfs_solver.h" +#include "solvers/pibt_solver.h" + +#include "graph/graph.h" +#include "models/metrics.h" +#include "models/models.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace mapf::graph; +using namespace mapf::models; +using namespace mapf::solver; + +namespace { + +Graph MakeGrid(uint32_t width, uint32_t height) { + Nodes nodes; + Edges edges; + for (uint32_t y = 0; y < height; ++y) { + for (uint32_t x = 0; x < width; ++x) { + NodeId id = y * width + x; + nodes.emplace(id, Node(id)); + if (x + 1 < width) { + edges.emplace(Edge(id, id + 1)); + edges.emplace(Edge(id + 1, id)); + } + if (y + 1 < height) { + edges.emplace(Edge(id, id + width)); + edges.emplace(Edge(id + width, id)); + } + } + } + return Graph(std::move(nodes), std::move(edges)); +} + +Graph MakeCorridorWithPocket() { + Nodes nodes; + Edges edges; + for (uint32_t i = 0; i < 9; ++i) { + nodes.emplace(i, Node(i)); + } + for (uint32_t i = 0; i < 7; ++i) { + edges.emplace(Edge(i, i + 1)); + edges.emplace(Edge(i + 1, i)); + } + edges.emplace(Edge(3, 8)); + edges.emplace(Edge(8, 3)); + return Graph(std::move(nodes), std::move(edges)); +} + +MAPFProblem MakeRandomGridProblem( + uint32_t width, uint32_t height, uint32_t num_agents, uint32_t seed) { + auto graph = MakeGrid(width, height); + + std::vector ids(static_cast(width) * height); + std::iota(ids.begin(), ids.end(), 0u); + + std::mt19937 rng(seed); + std::vector starts = ids; + std::shuffle(starts.begin(), starts.end(), rng); + std::vector goals = ids; + std::shuffle(goals.begin(), goals.end(), rng); + + AgentTasks tasks; + for (uint32_t k = 0; k < num_agents; ++k) { + tasks.emplace(k, AgentTask(k, Endpoints(starts[k], goals[k]))); + } + return MAPFProblem(std::move(graph), std::move(tasks)); +} + +struct RunResult { + bool solved = false; + double time_ms = 0.0; + long long makespan = -1; + long long soc = -1; + long long nodes = -1; +}; + +long long MakespanOf(const MAPFSolution& solution) { + if (solution.agent_paths.empty()) { + return -1; + } + return static_cast(mapf::models::Makespan(solution)); +} + +long long SocOf(const MAPFSolution& solution) { + if (solution.agent_paths.empty()) { + return -1; + } + return static_cast(mapf::models::SumOfCosts(solution)); +} + +template +RunResult TimeSolver(Solver& solver, const MAPFProblem& problem) { + RunResult result; + auto t0 = std::chrono::high_resolution_clock::now(); + auto solution = solver.FindSolution(problem); + auto t1 = std::chrono::high_resolution_clock::now(); + result.time_ms = std::chrono::duration(t1 - t0).count(); + result.solved = !solution.agent_paths.empty(); + result.makespan = MakespanOf(solution); + result.soc = SocOf(solution); + return result; +} + +RunResult RunBFS(const MAPFProblem& problem) { + BFSSolver solver; + auto result = TimeSolver(solver, problem); + result.nodes = static_cast(solver.GetNodesExpanded()); + return result; +} + +RunResult RunAStar(const MAPFProblem& problem) { + AStarSolver solver; + auto result = TimeSolver(solver, problem); + result.nodes = static_cast(solver.GetNodesExpanded()); + return result; +} + +RunResult RunPIBT(const MAPFProblem& problem) { + PIBTSolver solver; + return TimeSolver(solver, problem); +} + +std::string Cell(const RunResult& r, bool show_nodes) { + if (!r.solved) { + return " -- "; + } + char buf[80]; + if (show_nodes) { + std::snprintf(buf, sizeof(buf), "%8lld %9.2f %4lld %4lld", r.nodes, r.time_ms, + r.makespan, r.soc); + } else { + std::snprintf(buf, sizeof(buf), "%9.3f %4lld %4lld", r.time_ms, r.makespan, r.soc); + } + return buf; +} + +} // namespace + +TEST(Experiment, OperatingEnvelope) { + std::printf("\n"); + std::printf("================================================================================\n"); + std::printf(" TABLE A -- head-to-head on small instances\n"); + std::printf(" (nodes expanded / time ms / makespan / sum-of-costs)\n"); + std::printf("================================================================================\n"); + std::printf("%-22s | %-30s | %-30s | %-22s\n", "instance", "BFS", "A*", "PIBT"); + std::printf("%-22s | %8s %9s %4s %4s | %8s %9s %4s %4s | %9s %4s %4s\n", + "", "nodes", "time", "mksp", "soc", "nodes", "time", "mksp", "soc", + "time", "mksp", "soc"); + std::printf("--------------------------------------------------------------------------------\n"); + + struct SmallCase { + std::string name; + MAPFProblem problem; + }; + std::vector small_cases; + small_cases.push_back({"3x3 grid, 2 agents", + MAPFProblem(MakeGrid(3, 3), AgentTasks{{0, {0, {0, 8}}}, {1, {1, {8, 0}}}})}); + small_cases.push_back({"corridor+pocket, 2", MAPFProblem(MakeCorridorWithPocket(), + AgentTasks{{0, {0, {0, 7}}}, {1, {1, {7, 0}}}})}); + small_cases.push_back({"4x4 grid, 3 agents", + MAPFProblem(MakeGrid(4, 4), + AgentTasks{{0, {0, {0, 15}}}, {1, {1, {3, 12}}}, {2, {2, {12, 3}}}})}); + + for (auto& c : small_cases) { + auto bfs = RunBFS(c.problem); + auto astar = RunAStar(c.problem); + auto pibt = RunPIBT(c.problem); + std::printf("%-22s | %-30s | %-30s | %-22s\n", c.name.c_str(), Cell(bfs, true).c_str(), + Cell(astar, true).c_str(), Cell(pibt, false).c_str()); + if (pibt.solved && astar.solved && astar.makespan > 0) { + const double mksp_ratio = + static_cast(pibt.makespan) / static_cast(astar.makespan); + const double soc_ratio = + astar.soc > 0 + ? static_cast(pibt.soc) / static_cast(astar.soc) + : 0.0; + std::printf("%-22s PIBT/optimal: makespan = %.2fx, SOC = %.2fx\n", "", mksp_ratio, + soc_ratio); + } + } + + std::printf("\n"); + std::printf("================================================================================\n"); + std::printf(" TABLE B -- scalability: joint-state space |V|^n vs PIBT (open grids)\n"); + std::printf(" time ms / makespan / sum-of-costs\n"); + std::printf("================================================================================\n"); + std::printf("%-22s | %14s | %-30s | %-24s\n", "instance", "|V|^n (approx)", "A*", "PIBT"); + std::printf("%-22s | %14s | %9s %4s %5s %5s | %9s %4s %5s\n", + "", "", "time", "mksp", "soc", "ok", "time", "mksp", "soc"); + std::printf("--------------------------------------------------------------------------------\n"); + + struct ScaleCase { + std::string name; + uint32_t w, h, agents; + bool run_astar; + }; + std::vector scale_cases{ + {"4x4 grid, 4 agents", 4, 4, 4, true}, + {"8x8 grid, 16 agents", 8, 8, 16, false}, + {"8x8 grid, 32 agents", 8, 8, 32, false}, + {"16x16 grid, 50 agents", 16, 16, 50, false}, + {"16x16 grid, 100 agents", 16, 16, 100, false}, + {"32x32 grid, 200 agents", 32, 32, 200, false}, + }; + + for (const auto& sc : scale_cases) { + MAPFProblem problem = (sc.w == 4 && sc.h == 4 && sc.agents == 4) + ? MAPFProblem(MakeGrid(4, 4), AgentTasks{{0, {0, {0, 15}}}, {1, {1, {15, 0}}}, + {2, {2, {3, 12}}}, {3, {3, {12, 3}}}}) + : MakeRandomGridProblem(sc.w, sc.h, sc.agents, 12345u); + + const double log10_state_space = + static_cast(sc.agents) * std::log10(static_cast(sc.w) * sc.h); + + std::string astar_cell = " intractable "; + if (sc.run_astar) { + auto astar = RunAStar(problem); + char buf[80]; + std::snprintf(buf, sizeof(buf), "%9.2f %4lld %5lld %5s", astar.time_ms, + astar.makespan, astar.soc, astar.solved ? "yes" : "no"); + astar_cell = buf; + } + + auto pibt = RunPIBT(problem); + char pibt_cell[80]; + std::snprintf(pibt_cell, sizeof(pibt_cell), "%9.3f %4lld %5lld", pibt.time_ms, + pibt.makespan, pibt.soc); + + char statespace[32]; + std::snprintf(statespace, sizeof(statespace), "~10^%.0f", log10_state_space); + + std::printf("%-22s | %14s | %-30s | %-24s%s\n", sc.name.c_str(), statespace, + astar_cell.c_str(), pibt_cell, pibt.solved ? "" : " (DNF)"); + EXPECT_TRUE(pibt.solved) << "PIBT should solve open-grid instance: " << sc.name; + } + std::printf("================================================================================\n\n"); +} + +TEST(Experiment, LargeScale) { + constexpr uint32_t kWidth = 64, kHeight = 64; + constexpr int kTrials = 5; + + std::printf("\n"); + std::printf("================================================================================\n"); + std::printf(" TABLE C -- PIBT large-scale stress test on a %ux%u open grid (%u cells)\n", + kWidth, kHeight, kWidth * kHeight); + std::printf(" %d random instances per row\n", kTrials); + std::printf("================================================================================\n"); + std::printf("%-8s | %-9s | %-12s | %-12s | %-12s | %-12s\n", "agents", "success", + "time avg ms", "time max ms", "makespan avg", "soc avg"); + std::printf("--------------------------------------------------------------------------------\n"); + + for (uint32_t agents : {100u, 200u, 400u, 700u, 1000u}) { + int solved = 0; + double time_sum = 0.0, time_max = 0.0, makespan_sum = 0.0, soc_sum = 0.0; + for (int s = 0; s < kTrials; ++s) { + auto problem = MakeRandomGridProblem(kWidth, kHeight, agents, 1000u + s); + auto r = RunPIBT(problem); + if (r.solved) { + ++solved; + time_sum += r.time_ms; + time_max = std::max(time_max, r.time_ms); + makespan_sum += static_cast(r.makespan); + soc_sum += static_cast(r.soc); + } + } + const double time_avg = solved ? time_sum / solved : 0.0; + const double makespan_avg = solved ? makespan_sum / solved : 0.0; + const double soc_avg = solved ? soc_sum / solved : 0.0; + std::printf("%-8u | %5d / %-2d | %12.1f | %12.1f | %12.1f | %12.1f\n", agents, solved, + kTrials, time_avg, time_max, makespan_avg, soc_avg); + EXPECT_GT(solved, 0) << "PIBT should solve at least one " << agents << "-agent instance"; + } + std::printf("================================================================================\n\n"); +} + +// ablation study +namespace { + +MAPFProblem DiagonalSwap(uint32_t n) { + auto graph = MakeGrid(n, n); + auto id = [&](uint32_t x, uint32_t y) { return y * n + x; }; + AgentTasks tasks; + tasks.emplace(0, AgentTask(0, Endpoints(id(0, 0), id(n - 1, n - 1)))); + tasks.emplace(1, AgentTask(1, Endpoints(id(n - 1, n - 1), id(0, 0)))); + tasks.emplace(2, AgentTask(2, Endpoints(id(n - 1, 0), id(0, n - 1)))); + tasks.emplace(3, AgentTask(3, Endpoints(id(0, n - 1), id(n - 1, 0)))); + return MAPFProblem(std::move(graph), std::move(tasks)); +} + +MAPFProblem ColumnSwap(uint32_t n) { + auto graph = MakeGrid(n, n); + auto id = [&](uint32_t x, uint32_t y) { return y * n + x; }; + AgentTasks tasks; + uint32_t a = 0; + for (uint32_t y = 0; y < n; ++y) { + tasks.emplace(a, AgentTask(a, Endpoints(id(0, y), id(n - 1, y)))); + ++a; + tasks.emplace(a, AgentTask(a, Endpoints(id(n - 1, y), id(0, y)))); + ++a; + } + return MAPFProblem(std::move(graph), std::move(tasks)); +} + +std::vector RunConfigDetailed(const PIBTConfig& cfg, + const std::vector& problems) { + std::vector makespans; + makespans.reserve(problems.size()); + for (const auto& problem : problems) { + PIBTSolver solver(cfg); + const auto solution = solver.FindSolution(problem); + makespans.push_back( + solution.agent_paths.empty() + ? -1 + : static_cast(solution.agent_paths.begin()->second.path.size() - 1)); + } + return makespans; +} + +} // namespace + +TEST(Experiment, Ablation) { + std::vector problems; + problems.push_back(DiagonalSwap(4)); + problems.push_back(DiagonalSwap(6)); + problems.push_back(DiagonalSwap(8)); + problems.push_back(ColumnSwap(5)); + problems.push_back(ColumnSwap(6)); + problems.push_back(ColumnSwap(8)); + const size_t n_sym = problems.size(); + + for (uint32_t s = 1; s <= 15; ++s) { + problems.push_back(MakeRandomGridProblem(8, 8, 16, s)); + } + for (uint32_t s = 1; s <= 15; ++s) { + problems.push_back(MakeRandomGridProblem(10, 10, 25, 100 + s)); + } + for (uint32_t s = 1; s <= 10; ++s) { + problems.push_back(MakeRandomGridProblem(12, 12, 30, 200 + s)); + } + const size_t n_total = problems.size(); + const size_t n_rand = n_total - n_sym; + + using TB = PIBTConfig::TieBreak; + using PM = PIBTConfig::PriorityMode; + struct Row { + const char* name; + PIBTConfig cfg; + }; + const std::vector rows = { + {"hash + dynamic (ours)", {TB::Hash, PM::Dynamic, 0xC0FFEEu}}, + {"hash + static", {TB::Hash, PM::Static, 0xC0FFEEu}}, + {"random + dynamic", {TB::Random, PM::Dynamic, 0xC0FFEEu}}, + {"random + static", {TB::Random, PM::Static, 0xC0FFEEu}}, + {"lowest-id + dynamic", {TB::LowestId, PM::Dynamic, 0xC0FFEEu}}, + {"lowest-id + static", {TB::LowestId, PM::Static, 0xC0FFEEu}}, + }; + + std::vector> results; + for (const auto& row : rows) { + results.push_back(RunConfigDetailed(row.cfg, problems)); + } + + auto avg_makespan_all = [&](size_t c) { + double sum = 0.0; + for (long long m : results[c]) { + sum += static_cast(m); + } + return sum / static_cast(results[c].size()); + }; + + auto avg_makespan_solved = [&](size_t c) -> std::pair { + double sum = 0.0; + int solved = 0; + for (long long m : results[c]) { + if (m >= 0) { + sum += static_cast(m); + ++solved; + } + } + if (solved == 0) { + return {0.0, 0}; + } + return {sum / static_cast(solved), solved}; + }; + + std::printf("\n"); + std::printf("================================================================================\n"); + std::printf(" ABLATION -- PIBT tie-break x priority\n"); + std::printf(" %zu symmetric stressors + %zu random instances = %zu total\n", n_sym, n_rand, + n_total); + std::printf("================================================================================\n"); + std::printf("%-28s | %-15s | %-15s | %-15s | %s\n", "configuration", "symmetric", + "random", "overall", "avg mksp (N)"); + std::printf("--------------------------------------------------------------------------------\n"); + for (size_t c = 0; c < rows.size(); ++c) { + int sym = 0, rnd = 0; + for (size_t i = 0; i < n_total; ++i) { + const bool solved = results[c][i] >= 0; + if (i < n_sym) { + sym += solved; + } else { + rnd += solved; + } + } + const int overall = sym + rnd; + const double sym_pct = 100.0 * sym / static_cast(n_sym); + const double rnd_pct = 100.0 * rnd / static_cast(n_rand); + const double all_pct = 100.0 * overall / static_cast(n_total); + const auto [avg_mksp, n_solved] = avg_makespan_solved(c); + std::printf("%-28s | %2d/%-2zu (%5.1f%%) | %2d/%-2zu (%5.1f%%) | %2d/%-2zu (%5.1f%%) | %5.2f (N=%d)\n", + rows[c].name, sym, n_sym, sym_pct, rnd, n_rand, rnd_pct, overall, n_total, + all_pct, avg_mksp, n_solved); + } + std::printf("================================================================================\n"); + std::printf(" Same-instance comparison over all %zu instances\n", n_total); + std::printf(" (only the two configurations that solve every instance):\n"); + std::printf(" hash+dynamic (ours) = %.2f vs random+dynamic = %.2f\n", avg_makespan_all(0), + avg_makespan_all(2)); + std::printf("================================================================================\n\n"); + + for (size_t i = 0; i < n_total; ++i) { + EXPECT_GE(results[0][i], 0) << "ours (hash+dynamic) failed instance " << i; + } +} + +// pibt vs a* + +namespace { + +struct ZoneSpec { + const char* name; + uint32_t w; + uint32_t h; + uint32_t agents; + uint32_t seeds; +}; + +std::filesystem::path ResolveOutputPath(const std::filesystem::path& relative) { + if (relative.is_absolute()) { + return relative; + } + if (const char* workspace = std::getenv("BUILD_WORKSPACE_DIRECTORY")) { + return std::filesystem::path(workspace) / relative; + } + if (const char* test_undeclared = std::getenv("TEST_UNDECLARED_OUTPUTS_DIR")) { + return std::filesystem::path(test_undeclared) / relative.filename(); + } + return relative; +} + +} // namespace + +TEST(Experiment, Suboptimality) { + const std::vector zones = { + {"3x3_2agents", 3, 3, 2, 50}, + {"4x4_2agents", 4, 4, 2, 50}, + {"4x4_3agents", 4, 4, 3, 50}, + }; + + const std::filesystem::path out_path = ResolveOutputPath("viz/data/suboptimality.csv"); + std::filesystem::create_directories(out_path.parent_path()); + std::ofstream csv(out_path); + ASSERT_TRUE(csv.is_open()) << "could not open " << out_path; + + csv << "zone,width,height,agents,seed," + "astar_nodes,astar_time_ms,astar_makespan,astar_soc," + "pibt_time_ms,pibt_makespan,pibt_soc," + "makespan_ratio,soc_ratio\n"; + + struct ZoneStats { + std::vector makespan_ratio; + std::vector soc_ratio; + std::vector astar_time; + std::vector pibt_time; + }; + std::vector stats(zones.size()); + + int total_instances = 0; + int pibt_solved = 0; + + for (size_t zi = 0; zi < zones.size(); ++zi) { + const auto& z = zones[zi]; + for (uint32_t s = 1; s <= z.seeds; ++s) { + auto problem = MakeRandomGridProblem(z.w, z.h, z.agents, s); + auto astar = RunAStar(problem); + auto pibt = RunPIBT(problem); + + ++total_instances; + if (pibt.solved) { + ++pibt_solved; + } + ASSERT_TRUE(astar.solved) << "A* failed on " << z.name << " seed=" << s; + ASSERT_TRUE(pibt.solved) << "PIBT failed on " << z.name << " seed=" << s; + + const double mksp_ratio = astar.makespan > 0 + ? static_cast(pibt.makespan) / static_cast(astar.makespan) + : 1.0; + const double soc_ratio = astar.soc > 0 + ? static_cast(pibt.soc) / static_cast(astar.soc) + : 1.0; + + stats[zi].makespan_ratio.push_back(mksp_ratio); + stats[zi].soc_ratio.push_back(soc_ratio); + stats[zi].astar_time.push_back(astar.time_ms); + stats[zi].pibt_time.push_back(pibt.time_ms); + + csv << z.name << "," << z.w << "," << z.h << "," << z.agents << "," << s << "," + << astar.nodes << "," << astar.time_ms << "," << astar.makespan << "," + << astar.soc << "," << pibt.time_ms << "," << pibt.makespan << "," << pibt.soc + << "," << mksp_ratio << "," << soc_ratio << "\n"; + } + } + csv.close(); + + auto percentile = [](std::vector xs, double q) { + if (xs.empty()) return 0.0; + std::sort(xs.begin(), xs.end()); + const double pos = q * (static_cast(xs.size()) - 1.0); + const size_t lo = static_cast(pos); + const size_t hi = std::min(lo + 1, xs.size() - 1); + const double frac = pos - static_cast(lo); + return xs[lo] * (1.0 - frac) + xs[hi] * frac; + }; + auto mean = [](const std::vector& xs) { + if (xs.empty()) return 0.0; + double s = 0; + for (double x : xs) s += x; + return s / static_cast(xs.size()); + }; + + std::printf("\n"); + std::printf("================================================================================\n"); + std::printf(" TABLE D -- statistical PIBT vs A* (open grids, 50 random instances per zone)\n"); + std::printf("================================================================================\n"); + std::printf("CSV written to: %s\n", out_path.string().c_str()); + std::printf("--------------------------------------------------------------------------------\n"); + std::printf("%-13s | %s\n", "zone", + " makespan ratio (PIBT/A*) | SOC ratio | mean t ms"); + std::printf("%-13s | %s\n", "", + " median p75 max mean | median p75 max mean | A* PIBT"); + std::printf("--------------------------------------------------------------------------------\n"); + for (size_t zi = 0; zi < zones.size(); ++zi) { + const auto& s = stats[zi]; + std::printf("%-13s | %5.3f %5.3f %5.3f %5.3f | %5.3f %5.3f %5.3f %5.3f |" + " %7.2f %7.3f\n", + zones[zi].name, + percentile(s.makespan_ratio, 0.5), percentile(s.makespan_ratio, 0.75), + *std::max_element(s.makespan_ratio.begin(), s.makespan_ratio.end()), + mean(s.makespan_ratio), + percentile(s.soc_ratio, 0.5), percentile(s.soc_ratio, 0.75), + *std::max_element(s.soc_ratio.begin(), s.soc_ratio.end()), + mean(s.soc_ratio), + mean(s.astar_time), mean(s.pibt_time)); + } + std::printf("================================================================================\n\n"); + + EXPECT_EQ(pibt_solved, total_instances) + << "PIBT should solve every open-grid instance in these zones"; +} diff --git a/viz/data/suboptimality.csv b/viz/data/suboptimality.csv new file mode 100644 index 0000000..fd6fa0d --- /dev/null +++ b/viz/data/suboptimality.csv @@ -0,0 +1,151 @@ +zone,width,height,agents,seed,astar_nodes,astar_time_ms,astar_makespan,astar_soc,pibt_time_ms,pibt_makespan,pibt_soc,makespan_ratio,soc_ratio +3x3_2agents,3,3,2,1,10,0.655709,3,6,0.101708,3,5,1,0.833333 +3x3_2agents,3,3,2,2,16,0.868792,3,5,0.071167,3,4,1,0.8 +3x3_2agents,3,3,2,3,26,1.29829,4,8,0.076291,4,5,1,0.625 +3x3_2agents,3,3,2,4,5,0.32875,2,2,0.058833,2,2,1,1 +3x3_2agents,3,3,2,5,6,0.311167,2,4,0.058209,2,3,1,0.75 +3x3_2agents,3,3,2,6,16,0.8615,3,6,0.063709,3,4,1,0.666667 +3x3_2agents,3,3,2,7,6,0.352833,2,2,0.069208,2,2,1,1 +3x3_2agents,3,3,2,8,6,0.349833,2,4,0.057791,2,3,1,0.75 +3x3_2agents,3,3,2,9,14,0.762125,3,5,0.065708,3,4,1,0.8 +3x3_2agents,3,3,2,10,34,1.69529,4,8,0.0735,4,4,1,0.5 +3x3_2agents,3,3,2,11,5,0.268666,2,4,0.059083,2,3,1,0.75 +3x3_2agents,3,3,2,12,16,0.8635,3,5,0.067917,3,4,1,0.8 +3x3_2agents,3,3,2,13,8,0.44175,2,2,0.062291,2,2,1,1 +3x3_2agents,3,3,2,14,21,1.10546,4,7,0.074,4,7,1,1 +3x3_2agents,3,3,2,15,15,0.808083,3,6,0.06375,3,4,1,0.666667 +3x3_2agents,3,3,2,16,5,0.324625,2,4,0.073167,4,8,2,2 +3x3_2agents,3,3,2,17,29,1.53329,4,8,0.076792,4,6,1,0.75 +3x3_2agents,3,3,2,18,2,0.105792,1,1,0.050291,1,1,1,1 +3x3_2agents,3,3,2,19,9,0.4625,3,6,0.06525,3,5,1,0.833333 +3x3_2agents,3,3,2,20,4,0.24075,2,4,0.058833,2,4,1,1 +3x3_2agents,3,3,2,21,6,0.357,2,4,0.058875,2,3,1,0.75 +3x3_2agents,3,3,2,22,34,1.69379,4,8,0.073584,4,4,1,0.5 +3x3_2agents,3,3,2,23,6,0.353791,2,4,0.056709,2,4,1,1 +3x3_2agents,3,3,2,24,9,0.581958,3,6,0.06825,3,6,1,1 +3x3_2agents,3,3,2,25,3,0.182583,2,4,0.059417,2,4,1,1 +3x3_2agents,3,3,2,26,6,0.3245,2,4,0.056625,2,3,1,0.75 +3x3_2agents,3,3,2,27,14,0.722542,3,6,0.0635,3,4,1,0.666667 +3x3_2agents,3,3,2,28,6,0.33425,2,2,0.055917,2,2,1,1 +3x3_2agents,3,3,2,29,9,0.485542,3,6,0.072666,4,7,1.33333,1.16667 +3x3_2agents,3,3,2,30,2,0.096292,1,2,0.05125,1,2,1,1 +3x3_2agents,3,3,2,31,4,0.241792,2,4,0.058959,2,4,1,1 +3x3_2agents,3,3,2,32,11,0.682125,3,5,0.064458,3,5,1,1 +3x3_2agents,3,3,2,33,4,0.226666,2,4,0.057166,2,3,1,0.75 +3x3_2agents,3,3,2,34,14,0.755709,3,6,0.062542,3,4,1,0.666667 +3x3_2agents,3,3,2,35,5,0.30725,2,2,0.063958,2,2,1,1 +3x3_2agents,3,3,2,36,5,0.428125,2,4,0.07325,4,6,2,1.5 +3x3_2agents,3,3,2,37,4,0.243458,2,4,0.056041,2,3,1,0.75 +3x3_2agents,3,3,2,38,4,0.214958,2,4,0.060292,2,3,1,0.75 +3x3_2agents,3,3,2,39,5,0.387875,2,4,0.079458,2,3,1,0.75 +3x3_2agents,3,3,2,40,24,1.98867,4,7,0.146583,4,7,1,1 +3x3_2agents,3,3,2,41,1,0.052792,0,0,0.072583,0,0,1,1 +3x3_2agents,3,3,2,42,17,1.65442,3,6,0.098334,3,3,1,0.5 +3x3_2agents,3,3,2,43,4,0.444084,2,4,0.082416,2,4,1,1 +3x3_2agents,3,3,2,44,12,0.818834,3,6,0.064917,3,5,1,0.833333 +3x3_2agents,3,3,2,45,17,0.900583,3,6,0.06525,3,4,1,0.666667 +3x3_2agents,3,3,2,46,5,0.316042,2,4,0.058458,2,3,1,0.75 +3x3_2agents,3,3,2,47,29,1.38675,4,8,0.071833,4,7,1,0.875 +3x3_2agents,3,3,2,48,11,0.607625,3,6,0.065458,3,5,1,0.833333 +3x3_2agents,3,3,2,49,8,0.434291,2,2,0.056125,2,2,1,1 +3x3_2agents,3,3,2,50,2,0.114416,1,1,0.049708,1,1,1,1 +4x4_2agents,4,4,2,1,6,0.508958,3,6,0.08175,3,5,1,0.833333 +4x4_2agents,4,4,2,2,58,3.79421,5,10,0.098042,5,7,1,0.7 +4x4_2agents,4,4,2,3,22,1.54862,3,6,0.083125,3,4,1,0.666667 +4x4_2agents,4,4,2,4,16,1.00675,4,7,0.092292,4,7,1,1 +4x4_2agents,4,4,2,5,60,3.89162,5,10,0.109458,5,8,1,0.8 +4x4_2agents,4,4,2,6,25,1.72825,4,8,0.085,4,7,1,0.875 +4x4_2agents,4,4,2,7,67,3.88125,5,9,0.089167,5,6,1,0.666667 +4x4_2agents,4,4,2,8,14,0.7845,4,8,0.091,5,8,1.25,1 +4x4_2agents,4,4,2,9,6,0.444917,3,6,0.076875,3,6,1,1 +4x4_2agents,4,4,2,10,20,1.36883,3,6,0.076542,3,4,1,0.666667 +4x4_2agents,4,4,2,11,4,0.35975,2,4,0.071583,2,4,1,1 +4x4_2agents,4,4,2,12,22,1.35771,4,8,0.087166,4,7,1,0.875 +4x4_2agents,4,4,2,13,28,1.78075,4,7,0.085291,4,6,1,0.857143 +4x4_2agents,4,4,2,14,59,3.3665,5,10,0.0905,5,7,1,0.7 +4x4_2agents,4,4,2,15,8,0.699625,3,6,0.077208,3,5,1,0.833333 +4x4_2agents,4,4,2,16,43,2.53271,5,10,0.088959,5,9,1,0.9 +4x4_2agents,4,4,2,17,18,1.30612,4,8,0.084958,4,8,1,1 +4x4_2agents,4,4,2,18,1,0.033708,0,0,0.051625,0,0,1,1 +4x4_2agents,4,4,2,19,14,0.912,4,7,0.083417,4,7,1,1 +4x4_2agents,4,4,2,20,6,0.44175,2,4,0.067084,2,3,1,0.75 +4x4_2agents,4,4,2,21,23,1.57275,5,10,0.090958,5,10,1,1 +4x4_2agents,4,4,2,22,6,0.351667,2,4,0.064958,2,3,1,0.75 +4x4_2agents,4,4,2,23,10,0.666209,3,5,0.078709,3,5,1,1 +4x4_2agents,4,4,2,24,7,0.592541,3,6,0.078125,3,5,1,0.833333 +4x4_2agents,4,4,2,25,6,0.532792,3,6,0.085083,3,5,1,0.833333 +4x4_2agents,4,4,2,26,6,0.525333,3,6,0.078209,3,5,1,0.833333 +4x4_2agents,4,4,2,27,84,5.05854,5,10,0.100708,5,10,1,1 +4x4_2agents,4,4,2,28,4,0.334583,2,4,0.069292,2,4,1,1 +4x4_2agents,4,4,2,29,6,0.327917,2,4,0.0725,2,3,1,0.75 +4x4_2agents,4,4,2,30,15,1.03038,4,8,0.086833,4,8,1,1 +4x4_2agents,4,4,2,31,38,2.13383,5,10,0.089125,5,8,1,0.8 +4x4_2agents,4,4,2,32,57,3.36479,5,10,0.095417,5,10,1,1 +4x4_2agents,4,4,2,33,66,3.88721,5,10,0.092792,5,10,1,1 +4x4_2agents,4,4,2,34,16,0.992,4,7,0.084833,4,7,1,1 +4x4_2agents,4,4,2,35,33,2.04429,4,7,0.092417,4,6,1,0.857143 +4x4_2agents,4,4,2,36,16,0.949958,3,6,0.077375,3,4,1,0.666667 +4x4_2agents,4,4,2,37,24,1.57658,4,8,0.089375,4,7,1,0.875 +4x4_2agents,4,4,2,38,7,0.430125,2,2,0.066334,2,2,1,1 +4x4_2agents,4,4,2,39,55,3.08946,5,10,0.0905,5,6,1,0.6 +4x4_2agents,4,4,2,40,3,0.305542,2,4,0.089208,4,6,2,1.5 +4x4_2agents,4,4,2,41,30,1.98854,4,7,0.0855,4,6,1,0.857143 +4x4_2agents,4,4,2,42,55,3.27492,5,8,0.0905,5,7,1,0.875 +4x4_2agents,4,4,2,43,9,0.793,3,6,0.090417,4,7,1.33333,1.16667 +4x4_2agents,4,4,2,44,2,0.146584,1,2,0.060375,1,2,1,1 +4x4_2agents,4,4,2,45,4,0.238291,2,4,0.066875,2,3,1,0.75 +4x4_2agents,4,4,2,46,113,6.25713,6,12,0.139334,6,11,1,0.916667 +4x4_2agents,4,4,2,47,55,3.19429,5,10,0.096375,5,6,1,0.6 +4x4_2agents,4,4,2,48,31,1.81304,4,8,0.085792,4,5,1,0.625 +4x4_2agents,4,4,2,49,10,0.654083,2,2,0.067875,2,2,1,1 +4x4_2agents,4,4,2,50,30,1.74562,4,8,0.08325,4,5,1,0.625 +4x4_3agents,4,4,3,1,20,8.35033,3,8,0.108333,3,6,1,0.75 +4x4_3agents,4,4,3,2,247,85.2477,5,15,0.150709,5,11,1,0.733333 +4x4_3agents,4,4,3,3,66,22.1928,3,9,0.116,3,5,1,0.555556 +4x4_3agents,4,4,3,4,56,17.0703,4,12,0.122666,4,10,1,0.833333 +4x4_3agents,4,4,3,5,433,141.572,5,14,0.161209,6,14,1.2,1 +4x4_3agents,4,4,3,6,60,19.4684,4,11,0.136083,5,12,1.25,1.09091 +4x4_3agents,4,4,3,7,353,115.654,5,14,0.171292,5,10,1,0.714286 +4x4_3agents,4,4,3,8,30,8.13871,4,12,0.148958,6,15,1.5,1.25 +4x4_3agents,4,4,3,9,46,19.7395,4,12,0.211084,4,10,1,0.833333 +4x4_3agents,4,4,3,10,13,5.6215,3,9,0.14275,3,7,1,0.777778 +4x4_3agents,4,4,3,11,5,3.63496,2,6,0.127791,2,6,1,1 +4x4_3agents,4,4,3,12,26,10.2999,4,11,0.152125,6,13,1.5,1.18182 +4x4_3agents,4,4,3,13,87,38.033,4,11,0.174166,4,9,1,0.818182 +4x4_3agents,4,4,3,14,208,62.3144,5,14,0.142916,5,11,1,0.785714 +4x4_3agents,4,4,3,15,387,127.324,5,12,0.143292,5,10,1,0.833333 +4x4_3agents,4,4,3,16,142,45.2716,5,15,0.200458,5,13,1,0.866667 +4x4_3agents,4,4,3,17,44,20.556,4,11,0.136667,4,12,1,1.09091 +4x4_3agents,4,4,3,18,842,257.345,6,18,0.158125,6,12,1,0.666667 +4x4_3agents,4,4,3,19,36,12.9842,4,11,0.126,4,10,1,0.909091 +4x4_3agents,4,4,3,20,20,7.78346,2,4,0.101167,2,3,1,0.75 +4x4_3agents,4,4,3,21,130,54.66,5,15,0.1965,5,12,1,0.8 +4x4_3agents,4,4,3,22,6,1.93962,2,6,0.125041,4,7,2,1.16667 +4x4_3agents,4,4,3,23,32,11.2598,3,9,0.118,3,5,1,0.555556 +4x4_3agents,4,4,3,24,52,19.094,4,11,0.120417,4,9,1,0.818182 +4x4_3agents,4,4,3,25,16,6.41763,3,5,0.104791,3,5,1,1 +4x4_3agents,4,4,3,26,9,4.71837,3,9,0.114292,3,8,1,0.888889 +4x4_3agents,4,4,3,27,597,180.639,6,18,0.159708,6,16,1,0.888889 +4x4_3agents,4,4,3,28,24,7.33733,3,8,0.103375,3,7,1,0.875 +4x4_3agents,4,4,3,29,10,2.88258,2,6,0.086333,2,4,1,0.666667 +4x4_3agents,4,4,3,30,28,9.45225,4,12,0.113709,4,12,1,1 +4x4_3agents,4,4,3,31,108,31.285,5,12,0.162833,9,22,1.8,1.83333 +4x4_3agents,4,4,3,32,425,127.541,5,15,0.163541,5,14,1,0.933333 +4x4_3agents,4,4,3,33,262,86.2666,5,15,0.150292,5,13,1,0.866667 +4x4_3agents,4,4,3,34,48,14.0612,4,10,0.119,4,10,1,1 +4x4_3agents,4,4,3,35,128,40.2308,4,12,0.11775,4,8,1,0.666667 +4x4_3agents,4,4,3,36,561,160.298,6,18,0.162416,6,12,1,0.666667 +4x4_3agents,4,4,3,37,89,30.1815,4,12,0.1285,4,10,1,0.833333 +4x4_3agents,4,4,3,38,98,30.4462,4,12,0.121291,4,8,1,0.666667 +4x4_3agents,4,4,3,39,199,61.6785,5,14,0.134916,5,10,1,0.714286 +4x4_3agents,4,4,3,40,1092,311.171,6,18,0.173583,6,14,1,0.777778 +4x4_3agents,4,4,3,41,137,47.1386,4,11,0.125459,4,7,1,0.636364 +4x4_3agents,4,4,3,42,154,47.3439,5,15,0.139542,6,15,1.2,1 +4x4_3agents,4,4,3,43,21,8.67021,3,9,0.179833,4,8,1.33333,0.888889 +4x4_3agents,4,4,3,44,138,52.3066,4,12,0.131875,4,12,1,1 +4x4_3agents,4,4,3,45,5,1.13746,2,6,0.0855,2,4,1,0.666667 +4x4_3agents,4,4,3,46,869,254.471,6,18,0.159167,6,15,1,0.833333 +4x4_3agents,4,4,3,47,271,124.539,5,15,0.338166,5,8,1,0.533333 +4x4_3agents,4,4,3,48,90,35.9244,4,11,0.261875,4,8,1,0.727273 +4x4_3agents,4,4,3,49,16,18.6498,2,4,0.120209,2,3,1,0.75 +4x4_3agents,4,4,3,50,54,28.2162,4,11,0.154375,4,8,1,0.727273 diff --git a/viz/out/suboptimality_hist.png b/viz/out/suboptimality_hist.png new file mode 100644 index 0000000000000000000000000000000000000000..b671ed7564e581302f8269013075371a2e7f6771 GIT binary patch literal 45684 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 AO#lD@ literal 0 HcmV?d00001 diff --git a/viz/out/suboptimality_ratio.png b/viz/out/suboptimality_ratio.png new file mode 100644 index 0000000000000000000000000000000000000000..b39c012dd297261730b4757770fab20e516f5b00 GIT binary patch literal 54172 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