diff --git a/docs/exit-codes.md b/docs/exit-codes.md index e587d37b3b..0a15ca8963 100644 --- a/docs/exit-codes.md +++ b/docs/exit-codes.md @@ -24,9 +24,10 @@ termination which prevents the execution of further components. | **Code** | **Name** | **Meaning** | | -------- | -------- | ----------- | -| 10 | TRANSLATE_UNSOLVABLE | Translator proved task to be unsolvable. Currently not used | -| 11 | SEARCH_UNSOLVABLE | Task is provably unsolvable with current bound. Currently only used by hillclimbing search. See also [issue377](http://issues.fast-downward.org/issue377). | +| 10 | TRANSLATE_UNSOLVABLE | Translator proved task to be unsolvable. Currently not used. | +| 11 | SEARCH_UNSOLVABLE | Task is provably unsolvable. | | 12 | SEARCH_UNSOLVED_INCOMPLETE | Search ended without finding a solution. | +| 13 | SEARCH_UNSOLVABLE_WITHIN_BOUND | Task is provably unsolvable with current bound. | The third block (20-29) represents expected failures which prevent the execution of further components. diff --git a/driver/returncodes.py b/driver/returncodes.py index 3c544abd73..6c8f0325ce 100644 --- a/driver/returncodes.py +++ b/driver/returncodes.py @@ -14,6 +14,7 @@ TRANSLATE_UNSOLVABLE = 10 SEARCH_UNSOLVABLE = 11 SEARCH_UNSOLVED_INCOMPLETE = 12 +SEARCH_UNSOLVABLE_WITHIN_BOUND = 13 TRANSLATE_OUT_OF_MEMORY = 20 TRANSLATE_OUT_OF_TIME = 21 diff --git a/misc/tests/benchmarks/blocks-unsolvable/domain.pddl b/misc/tests/benchmarks/blocks-unsolvable/domain.pddl new file mode 100644 index 0000000000..f6cadcda32 --- /dev/null +++ b/misc/tests/benchmarks/blocks-unsolvable/domain.pddl @@ -0,0 +1,48 @@ +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;; 4 Op-blocks world +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(define (domain BLOCKS) + (:requirements :strips) + (:predicates (on ?x ?y) + (ontable ?x) + (clear ?x) + (handempty) + (holding ?x) + ) + + (:action pick-up + :parameters (?x) + :precondition (and (clear ?x) (ontable ?x) (handempty)) + :effect + (and (not (ontable ?x)) + (not (clear ?x)) + (not (handempty)) + (holding ?x))) + + (:action put-down + :parameters (?x) + :precondition (holding ?x) + :effect + (and (not (holding ?x)) + (clear ?x) + (handempty) + (ontable ?x))) + (:action stack + :parameters (?x ?y) + :precondition (and (holding ?x) (clear ?y)) + :effect + (and (not (holding ?x)) + (not (clear ?y)) + (clear ?x) + (handempty) + (on ?x ?y))) + (:action unstack + :parameters (?x ?y) + :precondition (and (on ?x ?y) (clear ?x) (handempty)) + :effect + (and (holding ?x) + (clear ?y) + (not (clear ?x)) + (not (handempty)) + (not (on ?x ?y))))) diff --git a/misc/tests/benchmarks/blocks-unsolvable/probBLOCKS-4-0.pddl b/misc/tests/benchmarks/blocks-unsolvable/probBLOCKS-4-0.pddl new file mode 100644 index 0000000000..1119ef2be9 --- /dev/null +++ b/misc/tests/benchmarks/blocks-unsolvable/probBLOCKS-4-0.pddl @@ -0,0 +1,7 @@ +(define (problem BLOCKS-4-0) +(:domain BLOCKS) +(:objects D B A C ) +(:INIT (CLEAR C) (CLEAR A) (CLEAR B) (CLEAR D) (ONTABLE C) (ONTABLE A) + (ONTABLE B) (ONTABLE D) (HANDEMPTY)) +(:goal (AND (ON D C) (ON C B) (ON B A) (ON A D))) +) diff --git a/misc/tests/test-exitcodes.py b/misc/tests/test-exitcodes.py index 21691cb3d8..5db736f09f 100644 --- a/misc/tests/test-exitcodes.py +++ b/misc/tests/test-exitcodes.py @@ -38,6 +38,7 @@ "strips": "miconic/s1-0.pddl", "axioms": "philosophers/p01-phil2.pddl", "cond-eff": "miconic-simpleadl/s1-0.pddl", + "unsolvable": "blocks-unsolvable/probBLOCKS-4-0.pddl", "large": "satellite/p25-HC-pfile5.pddl", } @@ -121,6 +122,36 @@ defaultdict(lambda: returncodes.SEARCH_UNSUPPORTED)), ("cond-eff", [], MERGE_AND_SHRINK, defaultdict(lambda: returncodes.SUCCESS)), + ("unsolvable", [], "astar(blind())", + defaultdict(lambda: returncodes.SEARCH_UNSOLVABLE)), + ("unsolvable", [], "astar(lmcut())", + defaultdict(lambda: returncodes.SEARCH_UNSOLVABLE)), + ("unsolvable", [], "lazy(single(lmcut()))", + defaultdict(lambda: returncodes.SEARCH_UNSOLVABLE)), + ("unsolvable", [], "eager(alt([single(const(infinity)),single(const(1))]))", + defaultdict(lambda: returncodes.SEARCH_UNSOLVABLE)), + ("unsolvable", [], "eager(type_based([const(infinity),const(1)]))", + defaultdict(lambda: returncodes.SEARCH_UNSOLVABLE)), + ("unsolvable", [], "eager(pareto([const(infinity),const(1)]))", + defaultdict(lambda: returncodes.SEARCH_UNSOLVABLE)), + ("unsolvable", [], "eager(tiebreaking([const(infinity),const(1)],unsafe_pruning=false))", + defaultdict(lambda: returncodes.SEARCH_UNSOLVABLE)), + ("unsolvable", [], "astar(blind(),bound=2)", + defaultdict(lambda: returncodes.SEARCH_UNSOLVABLE_WITHIN_BOUND)), + ("unsolvable", [], "ehc(blind())", + defaultdict(lambda: returncodes.SEARCH_UNSOLVED_INCOMPLETE)), + ("unsolvable", [], "eager(single(blind(),pref_only=true))", + defaultdict(lambda: returncodes.SEARCH_UNSOLVED_INCOMPLETE)), + ("unsolvable", [], "eager(pareto([blind()],pref_only=true))", + defaultdict(lambda: returncodes.SEARCH_UNSOLVED_INCOMPLETE)), + ("unsolvable", [], "eager(pareto([cg(),cea()]))", + defaultdict(lambda: returncodes.SEARCH_UNSOLVED_INCOMPLETE)), + ("unsolvable", [], "eager(tiebreaking([const(infinity),blind()]))", + defaultdict(lambda: returncodes.SEARCH_UNSOLVED_INCOMPLETE)), + ("unsolvable", [], "eager(alt([single(cg()),single(const(infinity))]))", + defaultdict(lambda: returncodes.SEARCH_UNSOLVED_INCOMPLETE)), + ("unsolvable", [], "lazy(type_based([cg(),const(infinity)]))", + defaultdict(lambda: returncodes.SEARCH_UNSOLVED_INCOMPLETE)), # We cannot set/enforce memory limits on Windows/macOS and thus expect # DRIVER_UNSUPPORTED as exit code in those cases. ("large", ["--search-memory-limit", "100M"], MERGE_AND_SHRINK, diff --git a/src/search/evaluator.cc b/src/search/evaluator.cc index bf8287690e..9da9741c6a 100644 --- a/src/search/evaluator.cc +++ b/src/search/evaluator.cc @@ -19,7 +19,7 @@ Evaluator::Evaluator( log(utils::get_log_for_verbosity(verbosity)) { } -bool Evaluator::dead_ends_are_reliable() const { +bool Evaluator::is_safe() const { return true; } diff --git a/src/search/evaluator.h b/src/search/evaluator.h index 81c4c46ddf..f2a0b18816 100644 --- a/src/search/evaluator.h +++ b/src/search/evaluator.h @@ -29,12 +29,12 @@ class Evaluator { virtual ~Evaluator() = default; /* - dead_ends_are_reliable should return true if the evaluator is - "safe", i.e., infinite estimates can be trusted. + is_safe returns true if the evaluator reports dead ends reliably, + i.e., if its infinite estimates can be trusted. The default implementation returns true. */ - virtual bool dead_ends_are_reliable() const; + virtual bool is_safe() const; /* get_path_dependent_evaluators should insert all path-dependent diff --git a/src/search/evaluators/combining_evaluator.cc b/src/search/evaluators/combining_evaluator.cc index 1f6d01b63c..46002a1f23 100644 --- a/src/search/evaluators/combining_evaluator.cc +++ b/src/search/evaluators/combining_evaluator.cc @@ -15,14 +15,14 @@ CombiningEvaluator::CombiningEvaluator( : Evaluator(false, false, false, description, verbosity), subevaluators(evals) { utils::verify_list_not_empty(evals, "evals"); - all_dead_ends_are_reliable = true; + all_subevaluators_are_safe = true; for (const shared_ptr &subevaluator : subevaluators) - if (!subevaluator->dead_ends_are_reliable()) - all_dead_ends_are_reliable = false; + if (!subevaluator->is_safe()) + all_subevaluators_are_safe = false; } -bool CombiningEvaluator::dead_ends_are_reliable() const { - return all_dead_ends_are_reliable; +bool CombiningEvaluator::is_safe() const { + return all_subevaluators_are_safe; } EvaluationResult CombiningEvaluator::compute_result( diff --git a/src/search/evaluators/combining_evaluator.h b/src/search/evaluators/combining_evaluator.h index 81d81c0b0d..211975f8c7 100644 --- a/src/search/evaluators/combining_evaluator.h +++ b/src/search/evaluators/combining_evaluator.h @@ -15,7 +15,7 @@ namespace combining_evaluator { */ class CombiningEvaluator : public Evaluator { std::vector> subevaluators; - bool all_dead_ends_are_reliable; + bool all_subevaluators_are_safe; protected: virtual int combine_values(const std::vector &values) = 0; public: @@ -24,19 +24,18 @@ class CombiningEvaluator : public Evaluator { const std::string &description, utils::Verbosity verbosity); /* - Note: dead_ends_are_reliable() is a state-independent method, so - it only returns true if all subevaluators report dead ends reliably. + Note: is_safe() is a state-independent method, so + it only returns true if all subevaluators are safe. Note that we could get more fine-grained information when considering of reliability for a given evaluated state. For - example, if we use h1 (unreliable) and h2 (reliable) and have a + example, if we use h1 (unsafe) and h2 (safe) and have a state where h1 is finite and h2 is infinite, then we can - *reliably* mark the state as a dead end. There is currently no + *safely* mark the state as a dead end. There is currently no way to exploit such state-based information, and hence we do not compute it. */ - - virtual bool dead_ends_are_reliable() const override; + virtual bool is_safe() const override; virtual EvaluationResult compute_result( EvaluationContext &eval_context) override; diff --git a/src/search/evaluators/const_evaluator.cc b/src/search/evaluators/const_evaluator.cc index e3f10e3c2d..7c89ab6b55 100644 --- a/src/search/evaluators/const_evaluator.cc +++ b/src/search/evaluators/const_evaluator.cc @@ -1,5 +1,7 @@ #include "const_evaluator.h" +#include "../evaluation_result.h" + #include "../plugins/plugin.h" using namespace std; @@ -15,6 +17,9 @@ EvaluationResult ConstEvaluator::compute_result(EvaluationContext &) { result.set_evaluator_value(value); return result; } +bool ConstEvaluator::is_safe() const { + return value < EvaluationResult::INFTY; +} class ConstEvaluatorFeature : public plugins::TypedFeature { diff --git a/src/search/evaluators/const_evaluator.h b/src/search/evaluators/const_evaluator.h index bb90ae737d..0a37249488 100644 --- a/src/search/evaluators/const_evaluator.h +++ b/src/search/evaluators/const_evaluator.h @@ -21,6 +21,7 @@ class ConstEvaluator : public Evaluator { virtual void get_path_dependent_evaluators( std::set &) override { } + virtual bool is_safe() const override; }; } diff --git a/src/search/evaluators/weighted_evaluator.cc b/src/search/evaluators/weighted_evaluator.cc index f549c0caed..f1294c80ba 100644 --- a/src/search/evaluators/weighted_evaluator.cc +++ b/src/search/evaluators/weighted_evaluator.cc @@ -19,8 +19,14 @@ WeightedEvaluator::WeightedEvaluator( weight(weight) { } -bool WeightedEvaluator::dead_ends_are_reliable() const { - return evaluator->dead_ends_are_reliable(); +bool WeightedEvaluator::is_safe() const { + if (weight == 0) { + return true; + } + if (weight == EvaluationResult::INFTY) { + return false; + } + return evaluator->is_safe(); } EvaluationResult WeightedEvaluator::compute_result( diff --git a/src/search/evaluators/weighted_evaluator.h b/src/search/evaluators/weighted_evaluator.h index 8a8ff45d41..3923b7bb83 100644 --- a/src/search/evaluators/weighted_evaluator.h +++ b/src/search/evaluators/weighted_evaluator.h @@ -19,7 +19,7 @@ class WeightedEvaluator : public Evaluator { const std::shared_ptr &eval, int weight, const std::string &description, utils::Verbosity verbosity); - virtual bool dead_ends_are_reliable() const override; + virtual bool is_safe() const override; virtual EvaluationResult compute_result( EvaluationContext &eval_context) override; virtual void get_path_dependent_evaluators( diff --git a/src/search/heuristics/cea_heuristic.cc b/src/search/heuristics/cea_heuristic.cc index bb7ebdd9b1..6ab561f04b 100644 --- a/src/search/heuristics/cea_heuristic.cc +++ b/src/search/heuristics/cea_heuristic.cc @@ -445,7 +445,7 @@ ContextEnhancedAdditiveHeuristic::~ContextEnhancedAdditiveHeuristic() { delete problem; } -bool ContextEnhancedAdditiveHeuristic::dead_ends_are_reliable() const { +bool ContextEnhancedAdditiveHeuristic::is_safe() const { return false; } diff --git a/src/search/heuristics/cea_heuristic.h b/src/search/heuristics/cea_heuristic.h index 2f91d532da..e816c3fd37 100644 --- a/src/search/heuristics/cea_heuristic.h +++ b/src/search/heuristics/cea_heuristic.h @@ -58,7 +58,7 @@ class ContextEnhancedAdditiveHeuristic : public Heuristic { const std::shared_ptr &transform, bool cache_estimates, const std::string &description, utils::Verbosity verbosity); ~ContextEnhancedAdditiveHeuristic(); - virtual bool dead_ends_are_reliable() const override; + virtual bool is_safe() const override; }; } diff --git a/src/search/heuristics/cg_heuristic.cc b/src/search/heuristics/cg_heuristic.cc index eb1a1a6872..cc5ec7c1b5 100644 --- a/src/search/heuristics/cg_heuristic.cc +++ b/src/search/heuristics/cg_heuristic.cc @@ -44,7 +44,7 @@ CGHeuristic::CGHeuristic( transition_graphs = factory.build_dtgs(); } -bool CGHeuristic::dead_ends_are_reliable() const { +bool CGHeuristic::is_safe() const { return false; } diff --git a/src/search/heuristics/cg_heuristic.h b/src/search/heuristics/cg_heuristic.h index 40b6a85d14..7e44cd8dbd 100644 --- a/src/search/heuristics/cg_heuristic.h +++ b/src/search/heuristics/cg_heuristic.h @@ -45,7 +45,7 @@ class CGHeuristic : public Heuristic { int max_cache_size, tasks::AxiomHandlingType axiom_hanlding, const std::shared_ptr &transform, bool cache_estimates, const std::string &description, utils::Verbosity verbosity); - virtual bool dead_ends_are_reliable() const override; + virtual bool is_safe() const override; }; } diff --git a/src/search/heuristics/hm_heuristic.cc b/src/search/heuristics/hm_heuristic.cc index f98ef6c088..ded8e7f9a2 100644 --- a/src/search/heuristics/hm_heuristic.cc +++ b/src/search/heuristics/hm_heuristic.cc @@ -27,7 +27,7 @@ HMHeuristic::HMHeuristic( generate_all_tuples(); } -bool HMHeuristic::dead_ends_are_reliable() const { +bool HMHeuristic::is_safe() const { return !task_properties::has_axioms(task_proxy) && !has_cond_effects; } diff --git a/src/search/heuristics/hm_heuristic.h b/src/search/heuristics/hm_heuristic.h index 9b34b6980f..6f1c1e6b77 100644 --- a/src/search/heuristics/hm_heuristic.h +++ b/src/search/heuristics/hm_heuristic.h @@ -67,7 +67,7 @@ class HMHeuristic : public Heuristic { bool cache_estimates, const std::string &description, utils::Verbosity verbosity); - virtual bool dead_ends_are_reliable() const override; + virtual bool is_safe() const override; }; } diff --git a/src/search/heuristics/relaxation_heuristic.cc b/src/search/heuristics/relaxation_heuristic.cc index 0e245f3f53..7bea8a1a92 100644 --- a/src/search/heuristics/relaxation_heuristic.cc +++ b/src/search/heuristics/relaxation_heuristic.cc @@ -110,7 +110,7 @@ RelaxationHeuristic::RelaxationHeuristic( } } -bool RelaxationHeuristic::dead_ends_are_reliable() const { +bool RelaxationHeuristic::is_safe() const { return !task_properties::has_axioms(task_proxy); } diff --git a/src/search/heuristics/relaxation_heuristic.h b/src/search/heuristics/relaxation_heuristic.h index 6e2523e1c8..c76f14a0e1 100644 --- a/src/search/heuristics/relaxation_heuristic.h +++ b/src/search/heuristics/relaxation_heuristic.h @@ -116,7 +116,7 @@ class RelaxationHeuristic : public Heuristic { const std::shared_ptr &transform, bool cache_estimates, const std::string &description, utils::Verbosity verbosity); - virtual bool dead_ends_are_reliable() const override; + virtual bool is_safe() const override; }; extern void add_relaxation_heuristic_options_to_feature( diff --git a/src/search/landmarks/landmark_cost_partitioning_heuristic.cc b/src/search/landmarks/landmark_cost_partitioning_heuristic.cc index a95c026f2d..66de47d0b2 100644 --- a/src/search/landmarks/landmark_cost_partitioning_heuristic.cc +++ b/src/search/landmarks/landmark_cost_partitioning_heuristic.cc @@ -78,7 +78,7 @@ int LandmarkCostPartitioningHeuristic::get_heuristic_value( } } -bool LandmarkCostPartitioningHeuristic::dead_ends_are_reliable() const { +bool LandmarkCostPartitioningHeuristic::is_safe() const { return true; } diff --git a/src/search/landmarks/landmark_cost_partitioning_heuristic.h b/src/search/landmarks/landmark_cost_partitioning_heuristic.h index 344a0a5774..ac9d2824ed 100644 --- a/src/search/landmarks/landmark_cost_partitioning_heuristic.h +++ b/src/search/landmarks/landmark_cost_partitioning_heuristic.h @@ -32,7 +32,7 @@ class LandmarkCostPartitioningHeuristic : public LandmarkHeuristic { CostPartitioningMethod cost_partitioning, bool alm, lp::LPSolverType lpsolver); - virtual bool dead_ends_are_reliable() const override; + virtual bool is_safe() const override; }; } diff --git a/src/search/landmarks/landmark_sum_heuristic.cc b/src/search/landmarks/landmark_sum_heuristic.cc index a239884cd6..3c7bfd3d77 100644 --- a/src/search/landmarks/landmark_sum_heuristic.cc +++ b/src/search/landmarks/landmark_sum_heuristic.cc @@ -103,7 +103,7 @@ int LandmarkSumHeuristic::get_heuristic_value(const State &ancestor_state) { return h; } -bool LandmarkSumHeuristic::dead_ends_are_reliable() const { +bool LandmarkSumHeuristic::is_safe() const { return dead_ends_reliable; } diff --git a/src/search/landmarks/landmark_sum_heuristic.h b/src/search/landmarks/landmark_sum_heuristic.h index b3ea0f7138..d70daf8114 100644 --- a/src/search/landmarks/landmark_sum_heuristic.h +++ b/src/search/landmarks/landmark_sum_heuristic.h @@ -29,7 +29,7 @@ class LandmarkSumHeuristic : public LandmarkHeuristic { const std::string &description, utils::Verbosity verbosity, tasks::AxiomHandlingType axioms); - virtual bool dead_ends_are_reliable() const override; + virtual bool is_safe() const override; }; } diff --git a/src/search/open_list.h b/src/search/open_list.h index dbedc7ba05..19bee62545 100644 --- a/src/search/open_list.h +++ b/src/search/open_list.h @@ -122,6 +122,11 @@ class OpenList { virtual bool is_dead_end(EvaluationContext &eval_context) const = 0; virtual bool is_reliable_dead_end( EvaluationContext &eval_context) const = 0; + /* + If for some solvable task no reachable goal state is ever inserted + into the open list then it is not complete, i.e. is_complete is false. + */ + virtual bool is_complete() const = 0; }; using StateOpenListEntry = StateID; diff --git a/src/search/open_lists/alternation_open_list.cc b/src/search/open_lists/alternation_open_list.cc index 0067bd8f0a..ac250e31f5 100644 --- a/src/search/open_lists/alternation_open_list.cc +++ b/src/search/open_lists/alternation_open_list.cc @@ -8,6 +8,7 @@ #include #include +#include #include using namespace std; @@ -37,6 +38,7 @@ class AlternationOpenList : public OpenList { virtual bool is_dead_end(EvaluationContext &eval_context) const override; virtual bool is_reliable_dead_end( EvaluationContext &eval_context) const override; + virtual bool is_complete() const override; }; template @@ -124,6 +126,14 @@ bool AlternationOpenList::is_reliable_dead_end( return false; } +template +bool AlternationOpenList::is_complete() const { + auto is_sublist_complete = [](const auto &sublist) { + return sublist->is_complete(); + }; + return ranges::any_of(open_lists, is_sublist_complete); +} + AlternationOpenListFactory::AlternationOpenListFactory( const vector> &sublists, int boost) : sublists(sublists), boost(boost) { diff --git a/src/search/open_lists/best_first_open_list.cc b/src/search/open_lists/best_first_open_list.cc index ad682f2f77..606b564f4e 100644 --- a/src/search/open_lists/best_first_open_list.cc +++ b/src/search/open_lists/best_first_open_list.cc @@ -36,6 +36,7 @@ class BestFirstOpenList : public OpenList { virtual bool is_dead_end(EvaluationContext &eval_context) const override; virtual bool is_reliable_dead_end( EvaluationContext &eval_context) const override; + virtual bool is_complete() const override; }; template @@ -93,7 +94,15 @@ bool BestFirstOpenList::is_dead_end( template bool BestFirstOpenList::is_reliable_dead_end( EvaluationContext &eval_context) const { - return is_dead_end(eval_context) && evaluator->dead_ends_are_reliable(); + return is_dead_end(eval_context) && evaluator->is_safe(); +} + +template +bool BestFirstOpenList::is_complete() const { + if (this->only_contains_preferred_entries()) { + return false; + } + return evaluator->is_safe(); } BestFirstOpenListFactory::BestFirstOpenListFactory( diff --git a/src/search/open_lists/epsilon_greedy_open_list.cc b/src/search/open_lists/epsilon_greedy_open_list.cc index 9774f8b4cd..25855ccabf 100644 --- a/src/search/open_lists/epsilon_greedy_open_list.cc +++ b/src/search/open_lists/epsilon_greedy_open_list.cc @@ -52,6 +52,7 @@ class EpsilonGreedyOpenList : public OpenList { virtual bool is_dead_end(EvaluationContext &eval_context) const override; virtual bool is_reliable_dead_end( EvaluationContext &eval_context) const override; + virtual bool is_complete() const override; virtual void get_path_dependent_evaluators( set &evals) override; virtual bool empty() const override; @@ -116,7 +117,15 @@ bool EpsilonGreedyOpenList::is_dead_end( template bool EpsilonGreedyOpenList::is_reliable_dead_end( EvaluationContext &eval_context) const { - return is_dead_end(eval_context) && evaluator->dead_ends_are_reliable(); + return is_dead_end(eval_context) && evaluator->is_safe(); +} + +template +bool EpsilonGreedyOpenList::is_complete() const { + if (this->only_contains_preferred_entries()) { + return false; + } + return evaluator->is_safe(); } template diff --git a/src/search/open_lists/pareto_open_list.cc b/src/search/open_lists/pareto_open_list.cc index 1018489389..b50b5983cc 100644 --- a/src/search/open_lists/pareto_open_list.cc +++ b/src/search/open_lists/pareto_open_list.cc @@ -54,6 +54,7 @@ class ParetoOpenList : public OpenList { virtual bool is_dead_end(EvaluationContext &eval_context) const override; virtual bool is_reliable_dead_end( EvaluationContext &eval_context) const override; + virtual bool is_complete() const override; }; template @@ -215,11 +216,22 @@ bool ParetoOpenList::is_reliable_dead_end( EvaluationContext &eval_context) const { for (const shared_ptr &evaluator : evaluators) if (eval_context.is_evaluator_value_infinite(evaluator.get()) && - evaluator->dead_ends_are_reliable()) + evaluator->is_safe()) return true; return false; } +template +bool ParetoOpenList::is_complete() const { + if (this->only_contains_preferred_entries()) { + return false; + } + auto is_evaluator_safe = [](const auto &evaluator) { + return evaluator->is_safe(); + }; + return ranges::any_of(evaluators, is_evaluator_safe); +} + ParetoOpenListFactory::ParetoOpenListFactory( const vector> &evals, bool state_uniform_selection, int random_seed, bool pref_only) diff --git a/src/search/open_lists/tiebreaking_open_list.cc b/src/search/open_lists/tiebreaking_open_list.cc index 8a50ad6e27..ce1aad8df5 100644 --- a/src/search/open_lists/tiebreaking_open_list.cc +++ b/src/search/open_lists/tiebreaking_open_list.cc @@ -48,6 +48,7 @@ class TieBreakingOpenList : public OpenList { virtual bool is_dead_end(EvaluationContext &eval_context) const override; virtual bool is_reliable_dead_end( EvaluationContext &eval_context) const override; + virtual bool is_complete() const override; }; template @@ -135,11 +136,33 @@ bool TieBreakingOpenList::is_reliable_dead_end( EvaluationContext &eval_context) const { for (const shared_ptr &evaluator : evaluators) if (eval_context.is_evaluator_value_infinite(evaluator.get()) && - evaluator->dead_ends_are_reliable()) + evaluator->is_safe()) return true; return false; } +template +bool TieBreakingOpenList::is_complete() const { + if (this->only_contains_preferred_entries()) { + return false; + } + assert(!evaluators.empty()); + if (evaluators[0]->is_safe()) { + return true; + } + // At this point we know that the first evaluator is unsafe. + if (allow_unsafe_pruning) { + return false; + } + /* + Even if the first evaluator is unsafe we can still ensure + completeness if (allow_unsafe_pruning is false and) at least + one other evaluator is safe. + */ + auto is_safe = [](const auto &evaluator) { return evaluator->is_safe(); }; + return ranges::any_of(evaluators, is_safe); +} + TieBreakingOpenListFactory::TieBreakingOpenListFactory( const vector> &evals, bool unsafe_pruning, bool pref_only) diff --git a/src/search/open_lists/type_based_open_list.cc b/src/search/open_lists/type_based_open_list.cc index 9b93d98013..774ec68c82 100644 --- a/src/search/open_lists/type_based_open_list.cc +++ b/src/search/open_lists/type_based_open_list.cc @@ -43,6 +43,7 @@ class TypeBasedOpenList : public OpenList { EvaluationContext &eval_context) const override; virtual void get_path_dependent_evaluators( set &evals) override; + virtual bool is_complete() const override; }; template @@ -119,7 +120,7 @@ template bool TypeBasedOpenList::is_reliable_dead_end( EvaluationContext &eval_context) const { for (const shared_ptr &evaluator : evaluators) { - if (evaluator->dead_ends_are_reliable() && + if (evaluator->is_safe() && eval_context.is_evaluator_value_infinite(evaluator.get())) return true; } @@ -134,6 +135,14 @@ void TypeBasedOpenList::get_path_dependent_evaluators( } } +template +bool TypeBasedOpenList::is_complete() const { + auto is_evaluator_safe = [](const auto &evaluator) { + return evaluator->is_safe(); + }; + return ranges::any_of(evaluators, is_evaluator_safe); +} + TypeBasedOpenListFactory::TypeBasedOpenListFactory( const vector> &evaluators, int random_seed) : evaluators(evaluators), random_seed(random_seed) { diff --git a/src/search/planner.cc b/src/search/planner.cc index 2f5addf422..739bf999c4 100644 --- a/src/search/planner.cc +++ b/src/search/planner.cc @@ -8,6 +8,7 @@ #include "utils/system.h" #include "utils/timer.h" +#include #include using namespace std; @@ -53,9 +54,25 @@ int main(int argc, const char **argv) { utils::g_log << "Search time: " << search_timer << endl; utils::g_log << "Total time: " << utils::g_timer << endl; - ExitCode exitcode = search_algorithm->found_solution() - ? ExitCode::SUCCESS - : ExitCode::SEARCH_UNSOLVED_INCOMPLETE; + SearchStatus search_status = search_algorithm->get_status(); + ExitCode exitcode; + switch (search_status) { + case SearchStatus::SOLVED: + exitcode = ExitCode::SUCCESS; + break; + case SearchStatus::TIMEOUT: + exitcode = ExitCode::SEARCH_UNSOLVED_INCOMPLETE; + break; + case SearchStatus::UNSOLVABLE: + exitcode = ExitCode::SEARCH_UNSOLVABLE; + break; + case SearchStatus::UNSOLVABLE_WITHIN_BOUND: + exitcode = ExitCode::SEARCH_UNSOLVABLE_WITHIN_BOUND; + break; + default: + assert(search_status == SearchStatus::FAILED); + exitcode = ExitCode::SEARCH_UNSOLVED_INCOMPLETE; + } exit_with(exitcode); } catch (const utils::ExitException &e) { /* To ensure that all destructors are called before the program exits, diff --git a/src/search/pruning/limited_pruning.cc b/src/search/pruning/limited_pruning.cc index 1124046464..86b78f824d 100644 --- a/src/search/pruning/limited_pruning.cc +++ b/src/search/pruning/limited_pruning.cc @@ -24,6 +24,10 @@ void LimitedPruning::initialize(const shared_ptr &task) { log << "pruning method: limited" << endl; } +bool LimitedPruning::is_safe() const { + return pruning_method->is_safe(); +} + void LimitedPruning::prune(const State &state, vector &op_ids) { if (is_pruning_disabled) { return; diff --git a/src/search/pruning/limited_pruning.h b/src/search/pruning/limited_pruning.h index 11b4c89d7f..1b41f3a445 100644 --- a/src/search/pruning/limited_pruning.h +++ b/src/search/pruning/limited_pruning.h @@ -24,6 +24,7 @@ class LimitedPruning : public PruningMethod { int expansions_before_checking_pruning_ratio, utils::Verbosity verbosity); virtual void initialize(const std::shared_ptr &) override; + virtual bool is_safe() const override; }; } diff --git a/src/search/pruning/null_pruning_method.h b/src/search/pruning/null_pruning_method.h index 934f167ba8..29d986ee49 100644 --- a/src/search/pruning/null_pruning_method.h +++ b/src/search/pruning/null_pruning_method.h @@ -12,6 +12,9 @@ class NullPruningMethod : public PruningMethod { virtual void initialize(const std::shared_ptr &) override; virtual void print_statistics() const override { } + virtual bool is_safe() const override { + return true; + } }; } diff --git a/src/search/pruning/stubborn_sets.h b/src/search/pruning/stubborn_sets.h index c3f1a8f923..51157cba70 100644 --- a/src/search/pruning/stubborn_sets.h +++ b/src/search/pruning/stubborn_sets.h @@ -58,6 +58,9 @@ class StubbornSets : public PruningMethod { public: explicit StubbornSets(utils::Verbosity verbosity); virtual void initialize(const std::shared_ptr &task) override; + virtual bool is_safe() const override { + return true; + } }; // Return the first unsatified condition, or FactPair::no_fact if there is none. diff --git a/src/search/pruning_method.h b/src/search/pruning_method.h index 861ed3affb..11078e54b9 100644 --- a/src/search/pruning_method.h +++ b/src/search/pruning_method.h @@ -37,6 +37,9 @@ class PruningMethod { virtual void initialize(const std::shared_ptr &task); void prune_operators(const State &state, std::vector &op_ids); virtual void print_statistics() const; + + // is_safe returns true if no solvable state can be pruned. + virtual bool is_safe() const = 0; }; extern void add_pruning_options_to_feature(plugins::Feature &feature); diff --git a/src/search/search_algorithm.cc b/src/search/search_algorithm.cc index cb37b479e7..d92d56814c 100644 --- a/src/search/search_algorithm.cc +++ b/src/search/search_algorithm.cc @@ -99,6 +99,22 @@ SearchStatus SearchAlgorithm::get_status() const { return status; } +SearchStatus SearchAlgorithm::get_finished_search_status() const { + if (found_solution()) { + return SOLVED; + } else if (is_unbounded() && is_complete_within_bound()) { + log << "Search terminated -- no plan exists!" << endl; + return UNSOLVABLE; + } else if (is_complete_within_bound()) { + log << "Search terminated -- no plan with cost " << bound - 1 + << " or less exists!" << endl; + return UNSOLVABLE_WITHIN_BOUND; + } else { + log << "Search terminated without finding a plan!" << endl; + return FAILED; + } +} + const Plan &SearchAlgorithm::get_plan() const { assert(solution_found); return plan; diff --git a/src/search/search_algorithm.h b/src/search/search_algorithm.h index 9ed2d571c3..d9da31dc07 100644 --- a/src/search/search_algorithm.h +++ b/src/search/search_algorithm.h @@ -12,6 +12,7 @@ #include "utils/logging.h" +#include #include namespace plugins { @@ -32,6 +33,8 @@ enum SearchStatus { IN_PROGRESS, TIMEOUT, FAILED, + UNSOLVABLE, + UNSOLVABLE_WITHIN_BOUND, SOLVED }; @@ -77,8 +80,14 @@ class SearchAlgorithm { virtual ~SearchAlgorithm(); virtual void print_statistics() const = 0; virtual void save_plan_if_necessary(); + /* + is_complete_within_bound returns true if the search algorithm finds + a plan within the bound if a plan exists. + */ + virtual bool is_complete_within_bound() const = 0; bool found_solution() const; SearchStatus get_status() const; + SearchStatus get_finished_search_status() const; const Plan &get_plan() const; void search(); const SearchStatistics &get_statistics() const { @@ -90,6 +99,9 @@ class SearchAlgorithm { int get_bound() { return bound; } + bool is_unbounded() const { + return bound == std::numeric_limits::max(); + } PlanManager &get_plan_manager() { return plan_manager; } diff --git a/src/search/search_algorithms/eager_search.cc b/src/search/search_algorithms/eager_search.cc index 99be531179..d8cc299cdf 100644 --- a/src/search/search_algorithms/eager_search.cc +++ b/src/search/search_algorithms/eager_search.cc @@ -117,10 +117,8 @@ SearchStatus EagerSearch::step() { optional node = get_next_node_to_expand(); if (!node.has_value()) { assert(open_list->empty()); - log << "Completely explored state space -- no solution!" << endl; - return FAILED; + return get_finished_search_status(); } - return expand(node.value()); } @@ -318,6 +316,10 @@ void EagerSearch::dump_search_space() const { search_space.dump(task_proxy); } +bool EagerSearch::is_complete_within_bound() const { + return open_list->is_complete() && pruning_method->is_safe(); +} + void EagerSearch::start_f_value_statistics(EvaluationContext &eval_context) { if (f_evaluator) { int f_value = eval_context.get_evaluator_value(f_evaluator.get()); diff --git a/src/search/search_algorithms/eager_search.h b/src/search/search_algorithms/eager_search.h index 9d3da156ad..825a38ad35 100644 --- a/src/search/search_algorithms/eager_search.h +++ b/src/search/search_algorithms/eager_search.h @@ -57,6 +57,7 @@ class EagerSearch : public SearchAlgorithm { virtual void print_statistics() const override; void dump_search_space() const; + virtual bool is_complete_within_bound() const override; }; extern void add_eager_search_options_to_feature( diff --git a/src/search/search_algorithms/enforced_hill_climbing_search.cc b/src/search/search_algorithms/enforced_hill_climbing_search.cc index ce50688c16..a02667c15e 100644 --- a/src/search/search_algorithms/enforced_hill_climbing_search.cc +++ b/src/search/search_algorithms/enforced_hill_climbing_search.cc @@ -106,7 +106,7 @@ void EnforcedHillClimbingSearch::initialize() { if (dead_end) { log << "Initial state is a dead end, no solution" << endl; - if (evaluator->dead_ends_are_reliable()) + if (evaluator->is_safe()) utils::exit_with(ExitCode::SEARCH_UNSOLVABLE); else utils::exit_with(ExitCode::SEARCH_UNSOLVED_INCOMPLETE); @@ -235,8 +235,7 @@ SearchStatus EnforcedHillClimbingSearch::ehc() { } } } - log << "No solution - FAILED" << endl; - return FAILED; + return get_finished_search_status(); } void EnforcedHillClimbingSearch::print_statistics() const { @@ -259,6 +258,10 @@ void EnforcedHillClimbingSearch::print_statistics() const { } } +bool EnforcedHillClimbingSearch::is_complete_within_bound() const { + return false; +} + class EnforcedHillClimbingSearchFeature : public plugins::TypedFeature< SearchAlgorithm, EnforcedHillClimbingSearch> { diff --git a/src/search/search_algorithms/enforced_hill_climbing_search.h b/src/search/search_algorithms/enforced_hill_climbing_search.h index afc49e5ce2..55b81f9871 100644 --- a/src/search/search_algorithms/enforced_hill_climbing_search.h +++ b/src/search/search_algorithms/enforced_hill_climbing_search.h @@ -64,6 +64,7 @@ class EnforcedHillClimbingSearch : public SearchAlgorithm { const std::string &description, utils::Verbosity verbosity); virtual void print_statistics() const override; + virtual bool is_complete_within_bound() const override; }; } diff --git a/src/search/search_algorithms/iterated_search.cc b/src/search/search_algorithms/iterated_search.cc index d6a0d39fa8..b1ff19c926 100644 --- a/src/search/search_algorithms/iterated_search.cc +++ b/src/search/search_algorithms/iterated_search.cc @@ -63,7 +63,7 @@ shared_ptr IteratedSearch::create_current_phase() { SearchStatus IteratedSearch::step() { shared_ptr current_search = create_current_phase(); if (!current_search) { - return found_solution() ? SOLVED : FAILED; + return get_finished_search_status(); } if (pass_bound && best_bound < current_search->get_bound()) { current_search->set_bound(best_bound); @@ -116,7 +116,7 @@ SearchStatus IteratedSearch::step_return_value() { return IN_PROGRESS; } else { log << "No solution found - stop searching" << endl; - return iterated_found_solution ? SOLVED : FAILED; + return get_finished_search_status(); } } } @@ -131,6 +131,34 @@ void IteratedSearch::save_plan_if_necessary() { // each successful search iteration. } +bool IteratedSearch::is_complete_within_bound() const { + /* + TODO + - return true if the first search algorithm is complete and its bound is + greater or equal to that of IteratedSearch, i.e. if it is complete + within the bound of IteratedSearch + - otherwise (the first search algorithm is not complete within + IteratedSearch's bound), return false if continue_on_fail == false + - otherwise (continue_on_fail == true and first search algorithm is not + complete within IteratedSearch's bound), return true if at least one + (other) search algorithm is complete within IteratedSearch's bound + - otherwise (continue_on_fail == true and all search algorithms are not + complete within IteratedSearch's bound), return false + + Before we solve the component interaction problem (issue559), we cannot + access the necessary information from within a constant function. + + Once we can implement this function we also want to change IteratedSearch + such it automatically stops once a complete search finds no solution. + In that case the continue_on_fail option would not really be useful + anymore and should be removed. (Result of discussion between Claudia, + Malte, and Gabi.) + */ + log << "Warning: the completeness check for IteratedSearch is not yet implemented." + << endl; + return false; +} + class IteratedSearchFeature : public plugins::TypedFeature { public: diff --git a/src/search/search_algorithms/iterated_search.h b/src/search/search_algorithms/iterated_search.h index 8a49f0ba38..202dd81162 100644 --- a/src/search/search_algorithms/iterated_search.h +++ b/src/search/search_algorithms/iterated_search.h @@ -36,6 +36,7 @@ class IteratedSearch : public SearchAlgorithm { virtual void save_plan_if_necessary() override; virtual void print_statistics() const override; + virtual bool is_complete_within_bound() const override; }; } diff --git a/src/search/search_algorithms/lazy_search.cc b/src/search/search_algorithms/lazy_search.cc index dd5afac207..c4c6f921ae 100644 --- a/src/search/search_algorithms/lazy_search.cc +++ b/src/search/search_algorithms/lazy_search.cc @@ -121,8 +121,7 @@ void LazySearch::generate_successors() { SearchStatus LazySearch::fetch_next_state() { if (open_list->empty()) { - log << "Completely explored state space -- no solution!" << endl; - return FAILED; + return get_finished_search_status(); } EdgeOpenListEntry next = open_list->remove_min(); @@ -234,4 +233,8 @@ void LazySearch::print_statistics() const { statistics.print_detailed_statistics(); search_space.print_statistics(); } + +bool LazySearch::is_complete_within_bound() const { + return open_list->is_complete(); +} } diff --git a/src/search/search_algorithms/lazy_search.h b/src/search/search_algorithms/lazy_search.h index ae0dbcba04..8d0743da91 100644 --- a/src/search/search_algorithms/lazy_search.h +++ b/src/search/search_algorithms/lazy_search.h @@ -58,6 +58,7 @@ class LazySearch : public SearchAlgorithm { const std::string &description, utils::Verbosity verbosity); virtual void print_statistics() const override; + virtual bool is_complete_within_bound() const override; }; } diff --git a/src/search/utils/system.cc b/src/search/utils/system.cc index 46a2438908..59f6eee3b9 100644 --- a/src/search/utils/system.cc +++ b/src/search/utils/system.cc @@ -19,6 +19,8 @@ const char *get_exit_code_message_reentrant(ExitCode exitcode) { return "Task is provably unsolvable."; case ExitCode::SEARCH_UNSOLVED_INCOMPLETE: return "Search stopped without finding a solution."; + case ExitCode::SEARCH_UNSOLVABLE_WITHIN_BOUND: + return "Task is provably unsolvable within the given bound."; case ExitCode::SEARCH_OUT_OF_MEMORY: return "Memory limit has been reached."; case ExitCode::SEARCH_OUT_OF_TIME: @@ -33,6 +35,7 @@ bool is_exit_code_error_reentrant(ExitCode exitcode) { case ExitCode::SUCCESS: case ExitCode::SEARCH_UNSOLVABLE: case ExitCode::SEARCH_UNSOLVED_INCOMPLETE: + case ExitCode::SEARCH_UNSOLVABLE_WITHIN_BOUND: case ExitCode::SEARCH_OUT_OF_MEMORY: case ExitCode::SEARCH_OUT_OF_TIME: return false; diff --git a/src/search/utils/system.h b/src/search/utils/system.h index bd50f550d9..04a20a79d2 100644 --- a/src/search/utils/system.h +++ b/src/search/utils/system.h @@ -37,8 +37,9 @@ enum class ExitCode { SUCCESS = 0, // 10-19: exit codes denoting no plan was found (without any error) - SEARCH_UNSOLVABLE = 11, // Task is provably unsolvable with given bound. - SEARCH_UNSOLVED_INCOMPLETE = 12, // Search ended without finding a solution. + SEARCH_UNSOLVABLE = 11, + SEARCH_UNSOLVED_INCOMPLETE = 12, + SEARCH_UNSOLVABLE_WITHIN_BOUND = 13, // 20-29: "expected" failures SEARCH_OUT_OF_MEMORY = 22,