diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000000..fa6732e6cd --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.1) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +set(CMAKE_CXX_FLAGS "-O3") +set(CMAKE_CXX_FLAGS_RELEASE "-O3") + +project(pydownward) + +include_directories("${CMAKE_SOURCE_DIR}/src/search/") +include_directories("${CMAKE_SOURCE_DIR}/src/search/ext/") +include_directories("${CMAKE_SOURCE_DIR}/python") + + +file (GLOB SOURCE_FILES "src/search/*.cc" "src/search/*/*.cc") +file (GLOB HEADER_FILES "src/search/*.h*" "src/search/*/*.h*") +file (GLOB PYTHON_FILES "python/*.cc" "python/*.h") + +# Set up such that XCode organizes the files +#source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCE_FILES} ${HEADER_FILES} ${PYTHON_FILES} ) + +find_package(pybind11 REQUIRED) +pybind11_add_module(pydownward + ${SOURCE_FILES} + ${HEADER_FILES} + ${PYTHON_FILES} +) + +target_link_libraries(pydownward PUBLIC) + +install(TARGETS pydownward + COMPONENT python + LIBRARY DESTINATION "${PYTHON_LIBRARY_DIR}" + ) diff --git a/README_PYBIND.txt b/README_PYBIND.txt new file mode 100644 index 0000000000..f155cdc06d --- /dev/null +++ b/README_PYBIND.txt @@ -0,0 +1,62 @@ +To install pybind11, use + apt install python3-pybind11 + +To test the current pybind setup, proceed as follows: + +$ mkdir pybind-build +$ cd pybind-build +$ cmake .. -DPYTHON_LIBRARY_DIR="/try-pybindings" -DPYTHON_EXECUTABLE="/usr/bin/python3" + (or wherever your python3 lives) +$ make -j8 +$ make install +$ cd ../try-pybindings +$ ls +pydownward.cpython-310-x86_64-linux-gnu.so test.py +$ cat test.py + +Put some output.sas file into the directory. The test only runs enforced +hill-climbing, which has a high likelihood to NOT solve a task. It can for +example solve the first driverlog task, so if in doubt, use that one. + +Enjoy: +$ python -i test.py + + +Notes on the interface: + +* We hacked the code in some places + - We made Heuristic::compute_heuristic public. This might be necessary for the + trampoline classes but have not looked for alternatives. + - We added a constructor Heuristic(task) that fixes some options (see + create_dummy_options_for_python_binding in heuristic.cc). The long term plan + would be t not pass the options object into the constructor and instead pass + individual parameters that are then also all exposed through the interface. + - Similarly, we added temporary constructors for the FF heuristic, search + engines, and EHC search. + - We added a global function get_root_task to access the global task. We have + to think about how to expose the task. + +* The order of template parameters in py::class_<...> does not matter. Pybind + will figure out which parameter is which. We should agree on some default order. + For Heuristic, we use four parameters: + - Heuristic is the class to expose to Python + - std::shared_ptr is the holder that is internally used for + reference counting, we use shared pointers to make this compatible with our + other features. + - Evaluator is the base class + - HeuristicTrampoline is a class used to handle function calls of virtual + functions. If we inherit from Heuristic in Python, the trampoline class is + responsible for forwarding calls to the Python class. See + https://pybind11.readthedocs.io/en/stable/advanced/classes.html + +* Trampoline classes (EvaluatorTrampoline, HeuristicTrampoline) are needed to + derive classes in Python. They are needed for all classes in the hierarchy. For + example, if we wanted to allow users to inherit from FFHeuristic, it would also + need a trampoline class. They have to define all virtual methods that we want to + make available in Python on every level (FFHeuristicTrampoline would have to + repeat the definition of compute_heuristic). It is not clear if the trampoline + for Evaluator is needed as long as no virtual method from Evaluator is in the + interface. Probably yes, because the constructor counts. + +* py::make_iterator can be used to set up an iterable but py::keep_alive is + required to keep the iterator alive while it is used. diff --git a/python/test.cc b/python/test.cc new file mode 100644 index 0000000000..5b62806849 --- /dev/null +++ b/python/test.cc @@ -0,0 +1,107 @@ + #include +#include +#include +#include +#include + +#include "command_line.h" +#include "heuristic.h" +#include "search_engine.h" +#include "task_proxy.h" + +#include "search_engines/enforced_hill_climbing_search.h" +#include "heuristics/ff_heuristic.h" +#include "tasks/root_task.h" +#include "task_utils/task_properties.h" +#include "utils/logging.h" +#include "utils/system.h" +#include "utils/timer.h" + +namespace py = pybind11; + +//py::object test(const std::string &sas_file, const std::string &cmd_line) { +void read_task(const std::string &sas_file) { + std::ifstream task_file(sas_file); + tasks::read_root_task(task_file); +} + +class EvaluatorTrampoline : public Evaluator { +public: + using Evaluator::Evaluator; +}; + +class HeuristicTrampoline : public Heuristic { +public: + using Heuristic::Heuristic; + + virtual int compute_heuristic(const State &ancestor_state) override { + PYBIND11_OVERRIDE_PURE( + int, /* Return type */ + Heuristic, /* Parent class */ + compute_heuristic, /* Name of function in C++ (must match Python name) */ + ancestor_state /* Argument(s) */ + ); + } +}; + +void init_ff(py::module_ &m) { + py::options options; + options.disable_function_signatures(); + + py::class_, Heuristic>(m, "FFHeuristic") + .def(py::init>(), py::arg("task"), py::doc(R"delimiter( + FFHeuristic + bla bla bla synopsis + + Parameters + ---------- + :param AbstractTask task: optional task transformation for this heuristic +)delimiter")); +} + +void init_ehc(py::module_ &m) { + py::options options; + options.disable_function_signatures(); + + py::class_>(m, "EHCSearch") + .def(py::init>()) + .def("search", &enforced_hill_climbing_search::EnforcedHillClimbingSearch::search, py::doc("this has some effect")) + .def("found_solution", &enforced_hill_climbing_search::EnforcedHillClimbingSearch::found_solution) + .def("get_plan", &enforced_hill_climbing_search::EnforcedHillClimbingSearch::get_plan); +} + +PYBIND11_MODULE(pydownward, m) { + m.doc() = "Gabi's pybind11 example plugin"; // Optional module docstring + + py::options options; + options.disable_function_signatures(); + + m.def("read_task", &read_task, "Read the task from sas_file", py::arg("sas_file")="output.sas"); + + m.def("get_root_task", &tasks::get_root_task, "Get the root task"); + + py::class_>(m, "AbstractTask") + .def("get_operator_name", &AbstractTask::get_operator_name); + + py::class_(m, "OperatorID") + .def("get_index", &OperatorID::get_index); + + py::class_(m, "FactProxy") + .def("get_name", &FactProxy::get_name); + + py::class_(m, "State") + .def("__getitem__", [](State &self, unsigned index) + { return self[index]; }) + .def("__iter__", [](const State &s) { return py::make_iterator(begin(s), end(s)); }, py::keep_alive<0, 1>()); + + py::class_, EvaluatorTrampoline>(m, "Evaluator"); + + py::class_, Evaluator, HeuristicTrampoline>(m, "Heuristic") + .def(py::init>()) + .def("compute_heuristic", &Heuristic::compute_heuristic); + + std::vector> init_functions = {init_ff, init_ehc}; + for(auto f : init_functions) { + f(m); + } +} diff --git a/src/search/heuristic.cc b/src/search/heuristic.cc index 9030deb64e..89a1e313e4 100644 --- a/src/search/heuristic.cc +++ b/src/search/heuristic.cc @@ -7,6 +7,7 @@ #include "task_utils/task_properties.h" #include "tasks/cost_adapted_task.h" #include "tasks/root_task.h" +#include "utils/logging.h" #include #include @@ -22,6 +23,22 @@ Heuristic::Heuristic(const plugins::Options &opts) task_proxy(*task) { } + +plugins::Options create_dummy_options_for_python_binding() { + plugins::Options opts; + opts.set("verbosity", utils::Verbosity::NORMAL); + opts.set_unparsed_config("Evaluator created from Python"); + return opts; +} + +Heuristic::Heuristic(shared_ptr task) + : Evaluator(create_dummy_options_for_python_binding(), true, true, true), + heuristic_cache(HEntry(NO_VALUE, true)), + cache_evaluator_values(true), + task(task), + task_proxy(*task) { +} + Heuristic::~Heuristic() { } diff --git a/src/search/heuristic.h b/src/search/heuristic.h index 12e0770851..dd74dcf4c8 100644 --- a/src/search/heuristic.h +++ b/src/search/heuristic.h @@ -61,8 +61,6 @@ class Heuristic : public Evaluator { enum {DEAD_END = -1, NO_VALUE = -2}; - virtual int compute_heuristic(const State &ancestor_state) = 0; - /* Usage note: Marking the same operator as preferred multiple times is OK -- it will only appear once in the list of preferred @@ -74,8 +72,11 @@ class Heuristic : public Evaluator { public: explicit Heuristic(const plugins::Options &opts); + explicit Heuristic(std::shared_ptr task); virtual ~Heuristic() override; + virtual int compute_heuristic(const State &ancestor_state) = 0; + virtual void get_path_dependent_evaluators( std::set & /*evals*/) override { } diff --git a/src/search/heuristics/ff_heuristic.cc b/src/search/heuristics/ff_heuristic.cc index c782fee62f..015613df23 100644 --- a/src/search/heuristics/ff_heuristic.cc +++ b/src/search/heuristics/ff_heuristic.cc @@ -19,6 +19,23 @@ FFHeuristic::FFHeuristic(const plugins::Options &opts) } } +static plugins::Options mimic_options(shared_ptr task) { + plugins::Options opts; + opts.set("cache_estimates", true); + opts.set>("transform", task); + opts.set("verbosity", utils::Verbosity::NORMAL); + opts.set_unparsed_config("Manual FF"); + return opts; +} + +FFHeuristic::FFHeuristic(shared_ptr task) + : AdditiveHeuristic(mimic_options(task)), + relaxed_plan(task_proxy.get_operators().size(), false) { + if (log.is_at_least_normal()) { + log << "Initializing FF heuristic..." << endl; + } +} + void FFHeuristic::mark_preferred_operators_and_relaxed_plan( const State &state, PropID goal_id) { Proposition *goal = get_proposition(goal_id); diff --git a/src/search/heuristics/ff_heuristic.h b/src/search/heuristics/ff_heuristic.h index 5a7cad2ecf..6605237ddd 100644 --- a/src/search/heuristics/ff_heuristic.h +++ b/src/search/heuristics/ff_heuristic.h @@ -33,6 +33,7 @@ class FFHeuristic : public additive_heuristic::AdditiveHeuristic { virtual int compute_heuristic(const State &ancestor_state) override; public: explicit FFHeuristic(const plugins::Options &opts); + explicit FFHeuristic(std::shared_ptr task); }; } diff --git a/src/search/search_engine.cc b/src/search/search_engine.cc index 8a7b42cd93..1f1ae3d29d 100644 --- a/src/search/search_engine.cc +++ b/src/search/search_engine.cc @@ -40,6 +40,28 @@ successor_generator::SuccessorGenerator &get_successor_generator( return successor_generator; } +SearchEngine::SearchEngine(const std::string &description, OperatorCost cost_type, double max_time, int cost_bound) + : description(description), + status(IN_PROGRESS), + solution_found(false), + task(tasks::g_root_task), + task_proxy(*task), + log(utils::g_log), // TODO the options version can set the verbosity level + state_registry(task_proxy), + successor_generator(get_successor_generator(task_proxy, log)), + search_space(state_registry, log), + statistics(log), + cost_type(cost_type), + is_unit_cost(task_properties::is_unit_cost(task_proxy)), + max_time(max_time) { + if (cost_bound < 0) { + cerr << "error: negative cost bound " << cost_bound << endl; + utils::exit_with(ExitCode::SEARCH_INPUT_ERROR); + } + bound = cost_bound; + task_properties::print_variable_statistics(task_proxy); +} + SearchEngine::SearchEngine(const plugins::Options &opts) : description(opts.get_unparsed_config()), status(IN_PROGRESS), diff --git a/src/search/search_engine.h b/src/search/search_engine.h index 478fb39a36..08321f555c 100644 --- a/src/search/search_engine.h +++ b/src/search/search_engine.h @@ -61,6 +61,7 @@ class SearchEngine { int get_adjusted_cost(const OperatorProxy &op) const; public: SearchEngine(const plugins::Options &opts); + SearchEngine(const std::string &description, OperatorCost cost_type, double max_time, int cost_bound); virtual ~SearchEngine(); virtual void print_statistics() const = 0; virtual void save_plan_if_necessary(); diff --git a/src/search/search_engines/enforced_hill_climbing_search.cc b/src/search/search_engines/enforced_hill_climbing_search.cc index 7ae27218f2..ecd031d7c6 100644 --- a/src/search/search_engines/enforced_hill_climbing_search.cc +++ b/src/search/search_engines/enforced_hill_climbing_search.cc @@ -92,6 +92,39 @@ EnforcedHillClimbingSearch::EnforcedHillClimbingSearch( opts, use_preferred, preferred_usage)->create_edge_open_list(); } + +// TODO we do not support preferred operators, selection of preferred usage +// TODO verbosity fixed to NORMAL +EnforcedHillClimbingSearch::EnforcedHillClimbingSearch(const std::string &description, + int cost_type, double max_time, int cost_bound, + shared_ptr h) + : SearchEngine(description, OperatorCost(cost_type), max_time, cost_bound), + evaluator(h), + preferred_operator_evaluators(vector>()), + preferred_usage(PreferredUsage::RANK_PREFERRED_FIRST), + current_eval_context(state_registry.get_initial_state(), &statistics), + current_phase_start_g(-1), + num_ehc_phases(0), + last_num_expanded(-1) { + for (const shared_ptr &eval : preferred_operator_evaluators) { + eval->get_path_dependent_evaluators(path_dependent_evaluators); + } + evaluator->get_path_dependent_evaluators(path_dependent_evaluators); + + State initial_state = state_registry.get_initial_state(); + for (Evaluator *evaluator : path_dependent_evaluators) { + evaluator->notify_initial_state(initial_state); + } + use_preferred = find(preferred_operator_evaluators.begin(), + preferred_operator_evaluators.end(), evaluator) != + preferred_operator_evaluators.end(); + + plugins::Options options; + options.set("verbosity", utils::Verbosity::NORMAL); + open_list = create_ehc_open_list_factory( + options, use_preferred, preferred_usage)->create_edge_open_list(); +} + EnforcedHillClimbingSearch::~EnforcedHillClimbingSearch() { } diff --git a/src/search/search_engines/enforced_hill_climbing_search.h b/src/search/search_engines/enforced_hill_climbing_search.h index 49c9bbfc2e..ec813b301c 100644 --- a/src/search/search_engines/enforced_hill_climbing_search.h +++ b/src/search/search_engines/enforced_hill_climbing_search.h @@ -61,6 +61,9 @@ class EnforcedHillClimbingSearch : public SearchEngine { public: explicit EnforcedHillClimbingSearch(const plugins::Options &opts); + EnforcedHillClimbingSearch(const std::string &description, + int cost_type, double max_time, int cost_bound, + std::shared_ptr h); virtual ~EnforcedHillClimbingSearch() override; virtual void print_statistics() const override; diff --git a/src/search/tasks/root_task.cc b/src/search/tasks/root_task.cc index 2ed2241122..0ebc9db7e5 100644 --- a/src/search/tasks/root_task.cc +++ b/src/search/tasks/root_task.cc @@ -497,6 +497,11 @@ void read_root_task(istream &in) { g_root_task = make_shared(in); } +shared_ptr get_root_task() { + return g_root_task; +} + + class RootTaskFeature : public plugins::TypedFeature { public: RootTaskFeature() : TypedFeature("no_transform") { diff --git a/src/search/tasks/root_task.h b/src/search/tasks/root_task.h index 15961d1e87..6e1e3efca8 100644 --- a/src/search/tasks/root_task.h +++ b/src/search/tasks/root_task.h @@ -6,5 +6,7 @@ namespace tasks { extern std::shared_ptr g_root_task; extern void read_root_task(std::istream &in); + +extern std::shared_ptr get_root_task(); } #endif diff --git a/try-pybindings/test.py b/try-pybindings/test.py new file mode 100755 index 0000000000..81c324c7ce --- /dev/null +++ b/try-pybindings/test.py @@ -0,0 +1,25 @@ +#! /usr/bin/env python + +import pydownward + +class MyHeuristic(pydownward.Heuristic): + def __init__(self, task): + super().__init__(task) + self.ff = pydownward.FFHeuristic(task) + + def compute_heuristic(self, state): + ff = self.ff.compute_heuristic(state) + print(f"Returning h = {ff} for state: {[f.get_name() for f in state]}") + return ff + +pydownward.read_task("output.sas") +task = pydownward.get_root_task() + +h = MyHeuristic(task) +search = pydownward.EHCSearch("hallo", 0, 100.0, 100, h) +search.search() +assert search.found_solution() +op_id_plan = search.get_plan() +plan = [task.get_operator_name(op_id.get_index(), False) + for op_id in op_id_plan] +print(plan) \ No newline at end of file