From 674b8bda90b0d840e191c112962ffbf3fbef8b18 Mon Sep 17 00:00:00 2001 From: Erick Fuentes Date: Sat, 30 Dec 2023 00:21:22 -0500 Subject: [PATCH 1/3] get it turning over --- .gitignore | 2 + WORKSPACE | 7 + experimental/polytope/BUILD | 19 +++ experimental/polytope/polytope.cc | 235 ++++++++++++++++++++++++++++++ experimental/polytope/puzzle.cc | 82 +++++++++++ experimental/polytope/puzzle.hh | 20 +++ 6 files changed, 365 insertions(+) create mode 100644 experimental/polytope/BUILD create mode 100644 experimental/polytope/polytope.cc create mode 100644 experimental/polytope/puzzle.cc create mode 100644 experimental/polytope/puzzle.hh diff --git a/.gitignore b/.gitignore index 7132e400..72345074 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ bazel-* .dazel_run +.vscode/ + # Ignore temporary emacs files .#* ### Automatically added by Hedron's Bazel Compile Commands Extractor: https://github.com/hedronvision/bazel-compile-commands-extractor diff --git a/WORKSPACE b/WORKSPACE index d1a3b843..0297677f 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -246,6 +246,13 @@ http_archive( sha256 = "9bd940ecb810aa6af15372436731a8fb898abf170342acf95850e8ce2747eda8", ) +http_archive( + name = "json", + urls = ["https://github.com/nlohmann/json/archive/refs/tags/v3.11.3.zip"], + strip_prefix = "json-3.11.3", + sha256 = "04022b05d806eb5ff73023c280b68697d12b93e1b7267a0b22a1a39ec7578069" +) + # Hedron's Compile Commands Extractor for Bazel # https://github.com/hedronvision/bazel-compile-commands-extractor http_archive( diff --git a/experimental/polytope/BUILD b/experimental/polytope/BUILD new file mode 100644 index 00000000..da1e30e9 --- /dev/null +++ b/experimental/polytope/BUILD @@ -0,0 +1,19 @@ + +cc_library( + name = "puzzle", + hdrs = ["puzzle.hh"], + srcs = ["puzzle.cc"], + deps = [ + "//planning:a_star", + ] +) + +cc_binary( + name = "polytope", + srcs = ["polytope.cc"], + deps = [ + ":puzzle", + "@cxxopts", + "@json", + ] +) \ No newline at end of file diff --git a/experimental/polytope/polytope.cc b/experimental/polytope/polytope.cc new file mode 100644 index 00000000..dfbc7781 --- /dev/null +++ b/experimental/polytope/polytope.cc @@ -0,0 +1,235 @@ + +#include +#include +#include + +#include "cxxopts.hpp" +#include "experimental/polytope/puzzle.hh" +#include "nlohmann/json.hpp" + +using json = nlohmann::json; + +namespace robot::experimental::polytope { +namespace { +struct PuzzleSpec { + int id; + std::string type; + std::vector initial_state; + std::vector solution_state; + std::vector name_from_idx; + int num_wild_cards; +}; + +std::vector split_on(const std::string& str, const char delim) { + std::vector parts = {}; + auto part_start_iter = str.begin(); + for (auto iter = str.begin(); iter != str.end(); ++iter) { + if (*iter == delim) { + parts.push_back(std::string(part_start_iter, iter)); + part_start_iter = iter + 1; + } + } + parts.push_back(std::string(part_start_iter, str.end())); + return parts; +} + +Puzzle parse_puzzle_line(const std::string& name, const std::string& info_string) { + const auto quote_stripped = info_string.substr(1, info_string.size() - 2); + const auto double_quoted = std::regex_replace(quote_stripped, std::regex("'"), "\""); + const auto puzzle_info = json::parse(double_quoted); + std::unordered_map actions; + + for (const auto& [action_name, action_info] : puzzle_info.items()) { + std::vector permutation(action_info.begin(), action_info.end()); + // permutation is vector where each element is the index of the element in the new state + // compute the inverse permutation + std::vector inverse_permutation(permutation.size()); + for (int i = 0; i < static_cast(permutation.size()); ++i) { + inverse_permutation[permutation[i]] = i; + } + + const auto action = [permutation](const Puzzle::State& state) { + Puzzle::State new_state = state; + new_state.reserve(state.size()); + for (int i = 0; i < static_cast(state.size()); ++i) { + new_state[i] = state[permutation[i]]; + } + return new_state; + }; + + actions[action_name] = action; + + const auto inverse_action = [inverse_permutation](const Puzzle::State& state) { + Puzzle::State new_state = state; + new_state.reserve(state.size()); + for (int i = 0; i < static_cast(state.size()); ++i) { + new_state[i] = state[inverse_permutation[i]]; + } + return new_state; + }; + + actions["-" + action_name] = inverse_action; + } + + const auto puzzle = Puzzle{ + name, + actions, + }; + return puzzle; +} + +std::unordered_map load_puzzle_info( + const std::filesystem::path& puzzle_info_file) { + std::ifstream puzzle_info_stream(puzzle_info_file); + + std::unordered_map puzzles; + for (std::string line; std::getline(puzzle_info_stream, line);) { + const auto sep = line.find(','); + const auto name = line.substr(0, sep); + const auto info_string = line.substr(sep + 1); + if (name == "puzzle_type") { + // Skip the header + continue; + } + std::cout << name << std::endl; + puzzles[name] = parse_puzzle_line(name, info_string); + } + + return puzzles; +} + +std::tuple, std::vector, std::vector> parse_state( + const std::string& solution_str, const std::string& initial_str) { + std::vector solution_parts = split_on(solution_str, ';'); + std::vector initial_parts = split_on(initial_str, ';'); + + std::unordered_map idx_from_name; + for (const auto& part : solution_parts) { + if (idx_from_name.count(part) == 0) { + idx_from_name[part] = idx_from_name.size(); + } + } + + std::vector solution_state(solution_parts.size()); + std::vector initial_state(initial_parts.size()); + + for (int i = 0; i < static_cast(solution_parts.size()); ++i) { + solution_state[i] = idx_from_name[solution_parts[i]]; + initial_state[i] = idx_from_name[initial_parts[i]]; + } + + std::vector name_from_idx(idx_from_name.size()); + for (const auto& [name, idx] : idx_from_name) { + name_from_idx[idx] = name; + } + + return std::make_tuple(solution_state, initial_state, name_from_idx); +} + +std::optional parse_puzzle_line(const std::string& line) { + // Find the position of all commas in the line + std::vector parts = split_on(line, ','); + + if (parts[0] == "id") { + // Skip the header + return std::nullopt; + } + + const auto& [solution_state, initial_state, name_from_idx] = parse_state(parts[2], parts[3]); + + return std::make_optional(PuzzleSpec{ + .id = std::stoi(parts[0]), + .type = parts[1], + .initial_state = initial_state, + .solution_state = solution_state, + .name_from_idx = name_from_idx, + .num_wild_cards = std::stoi(parts[4]), + }); +} + +std::vector load_puzzles(const std::filesystem::path& puzzle_file) { + std::ifstream puzzle_stream(puzzle_file); + + std::vector puzzles; + for (std::string line; std::getline(puzzle_stream, line);) { + const auto result = parse_puzzle_line(line); + if (result.has_value()) { + puzzles.push_back(result.value()); + } + } + + return puzzles; +} +} // namespace + +void solve_puzzles(const std::filesystem::path& puzzle_info_file, + const std::filesystem::path& puzzle_file) { + const auto puzzle_infos = load_puzzle_info(puzzle_info_file); + const auto puzzles = load_puzzles(puzzle_file); + + for (const auto& spec : puzzles) { + const auto& puzzle = puzzle_infos.at(spec.type); + std::cout << "Puzzle " << spec.id << " (" << spec.type << "):" << std::endl; + std::cout << "initial state: "; + for (const auto& idx : spec.initial_state) { + std::cout << spec.name_from_idx[idx] << " "; + } + std::cout << std::endl; + std::cout << "solution_state: "; + for (const auto& idx : spec.solution_state) { + std::cout << spec.name_from_idx[idx] << " "; + } + std::cout << std::endl; + + const auto solution = + solve(puzzle, spec.initial_state, spec.solution_state, spec.num_wild_cards); + if (solution.has_value()) { + std::cout << " Solution: "; + std::vector state = spec.initial_state; + for (const auto& action : solution.value()) { + std::cout << action << " "; + state = puzzle.actions.at(action)(state); + } + std::cout << std::endl; + + std::cout << "solved state: "; + for (const auto& idx : state) { + std::cout << spec.name_from_idx[idx] << " "; + } + std::cout << std::endl; + + } else { + std::cout << " No solution found" << std::endl; + } + } +} +} // namespace robot::experimental::polytope + +int main(int argc, char** argv) { + cxxopts::Options options("polytope", "A tool solving Kaggle 2023 polytope problem"); + options.add_options()("puzzle_info", "Puzzle info file", cxxopts::value())( + "puzzles", "puzzle file", cxxopts::value())("h,help", "Print usage"); + + auto args = options.parse(argc, argv); + + if (args.count("help")) { + std::cout << options.help() << std::endl; + return 0; + } + + if (!args.count("puzzle_info")) { + std::cout << "Missing puzzle info file" << std::endl; + std::cout << options.help() << std::endl; + return 1; + } + + if (!args.count("puzzles")) { + std::cout << "Missing puzzle file" << std::endl; + std::cout << options.help() << std::endl; + return 1; + } + + robot::experimental::polytope::solve_puzzles(args["puzzle_info"].as(), + args["puzzles"].as()); + return 0; +} \ No newline at end of file diff --git a/experimental/polytope/puzzle.cc b/experimental/polytope/puzzle.cc new file mode 100644 index 00000000..ea82f544 --- /dev/null +++ b/experimental/polytope/puzzle.cc @@ -0,0 +1,82 @@ + +#include "experimental/polytope/puzzle.hh" + +#include "planning/a_star.hh" + +namespace std { +// Add a std::hash specialization for tuple> +template <> +struct hash>> { + size_t operator()(const std::tuple> &item) const { + hash string_hasher; + hash int_hasher; + const auto &[name, state] = item; + size_t out = string_hasher(name); + for (const auto &item : state) { + out ^= int_hasher(item) << 1; + } + return out; + } +}; +} // namespace std + +namespace robot::experimental::polytope { +namespace { +std::string compute_inverse_move(const std::string &move_name) { + if (move_name == "") { + return ""; + } else if (move_name.starts_with("-")) { + return move_name.substr(1); + } else { + return "-" + move_name; + } +} +} + +std::optional> solve(const Puzzle& puzzle, const Puzzle::State& initial_state, + const Puzzle::State& solution_state, const int num_wildcards) { + const auto initial_search_state = std::make_tuple(std::string(""), initial_state); + const auto successors_for_state = [&puzzle](const auto& search_state) { + const auto &[prev_move, prev_state] = search_state; + const auto inverse_move = compute_inverse_move(prev_move); + std::vector>> out; + for (const auto& [name, action] : puzzle.actions) { + if (name == inverse_move) { + continue; + } + out.push_back({ + .state = std::make_tuple(name, action(prev_state)), + .edge_cost = 1, + }); + } + return out; + }; + + const auto heuristic = []([[maybe_unused]] const auto &search_state) {return 0.0;}; + + const auto termination_check = [&solution_state, num_wildcards](const auto &search_state) { + int error_count = 0; + const auto &[move, state] = search_state; + for (int i = 0; i < static_cast(state.size()); ++i) { + if (state[i] != solution_state[i]) { + ++error_count; + } + } + return error_count <= num_wildcards; + }; + + const auto maybe_solution = planning::a_star(initial_search_state, successors_for_state, + heuristic, termination_check); + if (!maybe_solution.has_value()) { + return std::nullopt; + } + std::vector out; + for (const auto &search_state : maybe_solution->states) { + const auto &[move, state] = search_state; + if (move != "") { + out.push_back(move); + } + } + return out; +} +} \ No newline at end of file diff --git a/experimental/polytope/puzzle.hh b/experimental/polytope/puzzle.hh new file mode 100644 index 00000000..ee790a2a --- /dev/null +++ b/experimental/polytope/puzzle.hh @@ -0,0 +1,20 @@ + +#pragma once + +#include +#include +#include +#include +#include + +namespace robot::experimental::polytope { +struct Puzzle { + using State = std::vector; + using Move = std::function; + std::string name; + std::unordered_map actions; +}; + +std::optional> solve(const Puzzle& puzzle, const Puzzle::State& initial_state, + const Puzzle::State& solution_state, const int num_wildcards); +} // namespace robot::experimental::polytope \ No newline at end of file From 25a73fee115838ed6f9120cccf1e607410c144a6 Mon Sep 17 00:00:00 2001 From: Erick Fuentes Date: Sun, 31 Dec 2023 10:49:40 -0500 Subject: [PATCH 2/3] lint, do some optimization, add heuristic --- experimental/polytope/puzzle.cc | 50 ++++++++++++++++++++++++--------- experimental/polytope/puzzle.hh | 6 ++-- 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/experimental/polytope/puzzle.cc b/experimental/polytope/puzzle.cc index ea82f544..0aa53dc3 100644 --- a/experimental/polytope/puzzle.cc +++ b/experimental/polytope/puzzle.cc @@ -31,19 +31,33 @@ std::string compute_inverse_move(const std::string &move_name) { return "-" + move_name; } } -} +} // namespace -std::optional> solve(const Puzzle& puzzle, const Puzzle::State& initial_state, - const Puzzle::State& solution_state, const int num_wildcards) { +std::optional> solve(const Puzzle &puzzle, + const Puzzle::State &initial_state, + const Puzzle::State &solution_state, + const int num_wildcards) { const auto initial_search_state = std::make_tuple(std::string(""), initial_state); - const auto successors_for_state = [&puzzle](const auto& search_state) { - const auto &[prev_move, prev_state] = search_state; - const auto inverse_move = compute_inverse_move(prev_move); - std::vector>> out; - for (const auto& [name, action] : puzzle.actions) { - if (name == inverse_move) { + + std::unordered_map>> + valid_moves_from_move; + valid_moves_from_move[""] = {}; + for (const auto &key_and_move : puzzle.actions) { + valid_moves_from_move[""].push_back(key_and_move); + valid_moves_from_move[key_and_move.first] = {}; + const auto inverse_move = compute_inverse_move(key_and_move.first); + for (const auto &other_key_and_move : puzzle.actions) { + if (other_key_and_move.first == inverse_move) { continue; } + valid_moves_from_move[key_and_move.first].push_back(other_key_and_move); + } + } + + const auto successors_for_state = [valid_moves_from_move](const auto &search_state) { + const auto &[prev_move, prev_state] = search_state; + std::vector>> out; + for (const auto &[name, action] : valid_moves_from_move.at(prev_move)) { out.push_back({ .state = std::make_tuple(name, action(prev_state)), .edge_cost = 1, @@ -52,7 +66,17 @@ std::optional> solve(const Puzzle& puzzle, const Puzzle return out; }; - const auto heuristic = []([[maybe_unused]] const auto &search_state) {return 0.0;}; + const auto heuristic = [solution_state](const auto &search_state) { + int error_count = 0; + const auto &[move, state] = search_state; + for (int i = 0; i < static_cast(state.size()); ++i) { + if (state[i] != solution_state[i]) { + ++error_count; + } + } + // One move can fix up to 8 errors, so we divide by 8 + return error_count / 8.0; + }; const auto termination_check = [&solution_state, num_wildcards](const auto &search_state) { int error_count = 0; @@ -65,8 +89,8 @@ std::optional> solve(const Puzzle& puzzle, const Puzzle return error_count <= num_wildcards; }; - const auto maybe_solution = planning::a_star(initial_search_state, successors_for_state, - heuristic, termination_check); + const auto maybe_solution = + planning::a_star(initial_search_state, successors_for_state, heuristic, termination_check); if (!maybe_solution.has_value()) { return std::nullopt; } @@ -79,4 +103,4 @@ std::optional> solve(const Puzzle& puzzle, const Puzzle } return out; } -} \ No newline at end of file +} // namespace robot::experimental::polytope \ No newline at end of file diff --git a/experimental/polytope/puzzle.hh b/experimental/polytope/puzzle.hh index ee790a2a..5559bfd7 100644 --- a/experimental/polytope/puzzle.hh +++ b/experimental/polytope/puzzle.hh @@ -15,6 +15,8 @@ struct Puzzle { std::unordered_map actions; }; -std::optional> solve(const Puzzle& puzzle, const Puzzle::State& initial_state, - const Puzzle::State& solution_state, const int num_wildcards); +std::optional> solve(const Puzzle& puzzle, + const Puzzle::State& initial_state, + const Puzzle::State& solution_state, + const int num_wildcards); } // namespace robot::experimental::polytope \ No newline at end of file From 490481c62b7ccce1a65e660568695a15e170acb3 Mon Sep 17 00:00:00 2001 From: Erick Fuentes Date: Tue, 2 Jan 2024 14:27:08 -0500 Subject: [PATCH 3/3] add checks move to ida * --- experimental/polytope/BUILD | 4 +++- experimental/polytope/polytope.cc | 29 +++++++++++++++++++---------- experimental/polytope/puzzle.cc | 16 +++++++++------- planning/id_a_star.hh | 11 +++++++++-- 4 files changed, 40 insertions(+), 20 deletions(-) diff --git a/experimental/polytope/BUILD b/experimental/polytope/BUILD index da1e30e9..6550a668 100644 --- a/experimental/polytope/BUILD +++ b/experimental/polytope/BUILD @@ -4,7 +4,8 @@ cc_library( hdrs = ["puzzle.hh"], srcs = ["puzzle.cc"], deps = [ - "//planning:a_star", + "//planning:id_a_star", + "//common:check", ] ) @@ -15,5 +16,6 @@ cc_binary( ":puzzle", "@cxxopts", "@json", + "//common:check", ] ) \ No newline at end of file diff --git a/experimental/polytope/polytope.cc b/experimental/polytope/polytope.cc index dfbc7781..b05e6437 100644 --- a/experimental/polytope/polytope.cc +++ b/experimental/polytope/polytope.cc @@ -6,6 +6,7 @@ #include "cxxopts.hpp" #include "experimental/polytope/puzzle.hh" #include "nlohmann/json.hpp" +#include "common/check.hh" using json = nlohmann::json; @@ -41,29 +42,37 @@ Puzzle parse_puzzle_line(const std::string& name, const std::string& info_string for (const auto& [action_name, action_info] : puzzle_info.items()) { std::vector permutation(action_info.begin(), action_info.end()); + + for (int i = 0; i < static_cast(permutation.size()); ++i) { + const auto iter = std::find(permutation.begin(), permutation.end(), i); + CHECK(iter != permutation.end()); + } // permutation is vector where each element is the index of the element in the new state // compute the inverse permutation std::vector inverse_permutation(permutation.size()); for (int i = 0; i < static_cast(permutation.size()); ++i) { - inverse_permutation[permutation[i]] = i; + inverse_permutation.at(permutation.at(i)) = i; } - const auto action = [permutation](const Puzzle::State& state) { - Puzzle::State new_state = state; + const auto action = [permutation = std::move(permutation)](const Puzzle::State& state) { + CHECK(state.size() == permutation.size()); + Puzzle::State new_state; new_state.reserve(state.size()); + for (int i = 0; i < static_cast(state.size()); ++i) { - new_state[i] = state[permutation[i]]; + new_state.push_back(state.at(permutation.at(i))); } return new_state; }; actions[action_name] = action; - const auto inverse_action = [inverse_permutation](const Puzzle::State& state) { - Puzzle::State new_state = state; + const auto inverse_action = [inverse_permutation = std::move(inverse_permutation)](const Puzzle::State& state) { + CHECK(state.size() == inverse_permutation.size()); + Puzzle::State new_state; new_state.reserve(state.size()); for (int i = 0; i < static_cast(state.size()); ++i) { - new_state[i] = state[inverse_permutation[i]]; + new_state.push_back(state.at(inverse_permutation.at(i))); } return new_state; }; @@ -114,13 +123,13 @@ std::tuple, std::vector, std::vector> parse_s std::vector initial_state(initial_parts.size()); for (int i = 0; i < static_cast(solution_parts.size()); ++i) { - solution_state[i] = idx_from_name[solution_parts[i]]; - initial_state[i] = idx_from_name[initial_parts[i]]; + solution_state.at(i) = idx_from_name.at(solution_parts.at(i)); + initial_state.at(i) = idx_from_name.at(initial_parts.at(i)); } std::vector name_from_idx(idx_from_name.size()); for (const auto& [name, idx] : idx_from_name) { - name_from_idx[idx] = name; + name_from_idx.at(idx) = name; } return std::make_tuple(solution_state, initial_state, name_from_idx); diff --git a/experimental/polytope/puzzle.cc b/experimental/polytope/puzzle.cc index 0aa53dc3..b670fa3a 100644 --- a/experimental/polytope/puzzle.cc +++ b/experimental/polytope/puzzle.cc @@ -1,17 +1,17 @@ #include "experimental/polytope/puzzle.hh" -#include "planning/a_star.hh" +#include "planning/id_a_star.hh" +#include "common/check.hh" namespace std { // Add a std::hash specialization for tuple> template <> struct hash>> { size_t operator()(const std::tuple> &item) const { - hash string_hasher; hash int_hasher; - const auto &[name, state] = item; - size_t out = string_hasher(name); + const auto &[_, state] = item; + size_t out = 0; for (const auto &item : state) { out ^= int_hasher(item) << 1; } @@ -66,21 +66,23 @@ std::optional> solve(const Puzzle &puzzle, return out; }; - const auto heuristic = [solution_state](const auto &search_state) { + const auto heuristic = [&solution_state](const auto &search_state) -> double { int error_count = 0; const auto &[move, state] = search_state; + CHECK(state.size() == solution_state.size()); for (int i = 0; i < static_cast(state.size()); ++i) { if (state[i] != solution_state[i]) { ++error_count; } } // One move can fix up to 8 errors, so we divide by 8 - return error_count / 8.0; + return static_cast(error_count / 8.0 + 0.5); }; const auto termination_check = [&solution_state, num_wildcards](const auto &search_state) { int error_count = 0; const auto &[move, state] = search_state; + CHECK(state.size() == solution_state.size()); for (int i = 0; i < static_cast(state.size()); ++i) { if (state[i] != solution_state[i]) { ++error_count; @@ -90,7 +92,7 @@ std::optional> solve(const Puzzle &puzzle, }; const auto maybe_solution = - planning::a_star(initial_search_state, successors_for_state, heuristic, termination_check); + planning::id_a_star(initial_search_state, successors_for_state, heuristic, termination_check); if (!maybe_solution.has_value()) { return std::nullopt; } diff --git a/planning/id_a_star.hh b/planning/id_a_star.hh index 9d530c32..d343efcf 100644 --- a/planning/id_a_star.hh +++ b/planning/id_a_star.hh @@ -135,7 +135,7 @@ CostLimitedDFSResult cost_limited_dfs(const State &initial_state, } // We have successors to expand from this node. - const auto &next_successor = frame.successors->back(); + const auto next_successor = frame.successors->back(); frame.successors->pop_back(); const double next_cost_to_come = frame.cost_to_come + next_successor.edge_cost; const double next_est_cost_to_go = heuristic(next_successor.state); @@ -146,10 +146,17 @@ CostLimitedDFSResult cost_limited_dfs(const State &initial_state, continue; } + for (const auto &other_frame : stack) { + if (other_frame.state == next_successor.state) { + // We have already expanded this node, so we skip it. + continue; + } + } + num_nodes_visited += 1; // We have not yet found a solution, so we push the successor onto the stack. stack.push_back(StackFrame{ - .state = next_successor.state, + .state = std::move(next_successor.state), .cost_to_come = next_cost_to_come, .est_cost_to_go = next_est_cost_to_go, .successors = std::nullopt,