mutable
A Database System for Research and Fast Prototyping
Loading...
Searching...
No Matches
HeuristicSearch.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm>
4#include <cmath>
5#include <functional>
6#include <iosfwd>
7#include <mutable/Options.hpp>
12#include <type_traits>
13#include <unordered_map>
14#include <vector>
15
16
17namespace m {
18
19/*----- forward declarations -----------------------------------------------------------------------------------------*/
20struct AdjacencyMatrix;
21
22namespace ai {
23
25{
26 budget_exhausted_exception(std::string message) : m::exception(std::move(message)) { }
27};
28
29
30/*======================================================================================================================
31 * Type Traits for Heuristic Search
32 *====================================================================================================================*/
33
34/*===== State ========================================================================================================*/
35
36/*----- State --------------------------------------------------------------------------------------------------------*/
37template<typename State>
39 std::is_class_v<State> and
40 std::is_class_v<typename State::base_type> and
41 std::movable<State> and
42 requires (const State &S, const State *parent) {
43 { S.g() } -> std::convertible_to<double>;
44 { S.decrease_g(parent, double(0)) } -> std::same_as<void>;
45 };
46
47/*----- partitioning -------------------------------------------------------------------------------------------------*/
48template<typename State, typename... Context>
50 requires (const State &S, Context&... ctx) {
51 { State::num_partitions(ctx...) } -> std::convertible_to<unsigned>;
52 { S.partition_id(ctx...) } -> std::convertible_to<unsigned>;
53 };
54
55
56/*===== Heuristic ====================================================================================================*/
57
58template<typename Heuristic, typename... Context>
61 requires (Heuristic &H, const typename Heuristic::state_type &S, Context&... ctx) {
62 { H.operator()(S, ctx...) } -> std::convertible_to<double>;
63 };
64
65template<typename Heurisitc>
66concept is_admissible = (Heurisitc::is_admissible);
67
68
69/*===== Search Algorithm =============================================================================================*/
70
71template<typename Search, typename... Context>
74 heuristic_search_heuristic<typename Search::heuristic_type, Context...> and
75 std::same_as<typename Search::state_type, typename Search::heuristic_type::state_type> and
76 requires (
77 Search &search,
78 typename Search::state_type state,
79 typename Search::heuristic_type &heuristic,
80 typename Search::expand_type ex,
81 Context&... ctx
82 ) {
83 { search.search(state, ex, heuristic, ctx...) };
84 };
85
86
87/*======================================================================================================================
88 * Heuristic Search Classes
89 *====================================================================================================================*/
90
92template<
94 typename Expand,
95 typename Heurisitc,
96 bool HasRegularQueue,
97 bool HasBeamQueue,
98 typename Config,
99 typename... Context
100>
102{
103 static_assert(HasRegularQueue or HasBeamQueue, "must have at least one kind of queue");
104
105 using state_type = State;
106 using expand_type = Expand;
107 static constexpr bool has_regular_queue = HasRegularQueue;
108 static constexpr bool has_beam_queue = HasBeamQueue;
109
110 static constexpr bool detect_duplicates = true;
111 static constexpr bool enable_cost_based_pruning = Config::PerformCostBasedPruning;
112 static constexpr bool use_weighted_search = Config::PerformWeightedSearch;
113
114 private:
116 using pointer_type = void*;
118 struct comparator { bool operator()(pointer_type p_left, pointer_type p_right) const; };
121
122 /*----- Counters -------------------------------------------------------------------------------------------------*/
123#ifndef NDEBUG
124#define DEF_COUNTER(NAME) \
125 private: \
126 std::size_t num_##NAME##_ = 0; \
127 void inc_##NAME() { ++num_##NAME##_; } \
128 public: \
129 std::size_t num_##NAME() const { return num_##NAME##_; }
130#else
131#define DEF_COUNTER(NAME) \
132 void inc_##NAME() { } \
133 std::size_t num_##NAME() const { return 0; }
134#endif
135
136 DEF_COUNTER(new)
137 DEF_COUNTER(duplicates)
138 DEF_COUNTER(regular_to_beam)
139 DEF_COUNTER(none_to_beam)
140 DEF_COUNTER(discarded)
141 DEF_COUNTER(cheaper)
142 DEF_COUNTER(decrease_key)
143 DEF_COUNTER(pruned_by_cost)
144
145#undef DEF_COUNTER
146
147 public:
149 struct StateInfo
150 {
152 double h;
154 heap_type *queue;
156 typename heap_type::handle_type handle;
157
158 StateInfo() = delete;
159 StateInfo(double h, heap_type *queue) : h(h), queue(queue) { }
160 StateInfo(const StateInfo&) = delete;
161 StateInfo(StateInfo&&) = default;
162 StateInfo & operator=(StateInfo&&) = default;
163 };
164
165 using map_value_type = std::pair<const state_type, StateInfo>;
166 using map_type = std::unordered_map<
167 /* Key= */ state_type,
168 /* Mapped= */ StateInfo,
169 /* Hash= */ std::hash<state_type>,
170 /* KeyEqual= */ std::equal_to<state_type>,
171 /* Allocator= */ typename Config::template allocator_type<map_value_type>
172 >;
173
174 using partition_iterator = typename std::vector<map_type>::iterator;
175 using partition_const_iterator = typename std::vector<map_type>::const_iterator;
176 using partition_reverse_iterator = typename std::vector<map_type>::reverse_iterator;
177 using partition_const_reverse_iterator = typename std::vector<map_type>::const_reverse_iterator;
178
179 private:
180 /*----- Helper type to manage potentially partitioned states. ----------------------------------------------------*/
181 template<bool Partition>
183
184 template<>
185 struct Partitions<true>
186 {
187 std::vector<map_type> partitions_;
188
189 Partitions(Context&... context) : partitions_(state_type::num_partitions(context...)) { }
190 Partitions(const Partitions&) = delete;
191 Partitions(Partitions&&) = default;
192
194 if (Options::Get().statistics) {
195 std::cout << partitions_.size() << " partitions:";
196 for (auto &P : partitions_)
197 std::cout << "\n " << P.size();
198 std::cout << std::endl;
199 }
200 }
201
202 map_type & operator()(const state_type &state, Context&... context) {
203 M_insist(state.partition_id(context...) < partitions_.size(), "index out of bounds");
204 return partitions_[state.partition_id(context...)];
205 }
206 const map_type & operator()(const state_type &state, Context&... context) const {
207 M_insist(state.partition_id(context...) < partitions_.size(), "index out of bounds");
208 return partitions_[state.partition_id(context...)];
209 }
210
211 std::size_t size() const {
212 std::size_t n = 0;
213 for (auto &P : partitions_)
214 n += P.size();
215 return n;
216 }
217
218 void clear() {
219 for (auto &P : partitions_)
220 P.clear();
221 }
222
225 partition_const_iterator begin() const { return partitions_.begin(); }
226 partition_const_iterator end() const { return partitions_.end(); }
227 partition_const_iterator cbegin() const { return partitions_.begin(); }
228 partition_const_iterator cend() const { return partitions_.end(); }
235 };
236
237 template<>
238 struct Partitions<false>
239 {
241
242 Partitions(Context&...) { }
243 Partitions(const Partitions&) = delete;
244 Partitions(Partitions&&) = default;
245
246 map_type & operator()(const state_type&, Context&...) { return states_; }
247 const map_type & operator()(const state_type&, Context&...) const { return states_; }
248
249 std::size_t size() const { return states_.size(); }
250
251 void clear() { states_.clear(); }
252 };
253
256
258 // map_type states_;
259
262
264 double least_path_cost = std::numeric_limits<double>::infinity();
265
267 float weighting_factor_ = 1.f;
268
269 public:
270 StateManager(Context&... context) : partitions_(context...) { }
271 StateManager(const StateManager&) = delete;
274
287
290
291 std::size_t num_states_seen() const { return partitions_.size(); }
292 std::size_t num_states_in_regular_queue() const { return regular_queue_.size(); }
293 std::size_t num_states_in_beam_queue() const { return beam_queue_.size(); }
294
296 float weighting_factor() const { return weighting_factor_; }
297
299 float weighting_factor(float new_factor) {
300 M_insist(new_factor >= 0.f, "factor must not be negative");
301 M_insist(new_factor != 0.f, "factor must not be 0, use heuristic zero for Dijkstra`s search algorithm");
302 return std::exchange(weighting_factor_, new_factor);
303 }
304
306 void update_least_path_cost(double cost) {
307 M_insist(cost < least_path_cost, "cost must be strictly less than least_path_cost for updating");
308 if (Options::Get().statistics)
309 std::cout << "found cheaper path with least_path_cost = " << cost << std::endl;
310 least_path_cost = cost;
311 }
312
313 template<bool ToBeamQueue>
314 void push(state_type state, double h, Context&... context) {
315 static_assert(not ToBeamQueue or HasBeamQueue, "ToBeamQueue implies HasBeamQueue");
316 static_assert(ToBeamQueue or HasRegularQueue, "not ToBeamQueue implies HasRegularQueue");
317
318 if constexpr (enable_cost_based_pruning) {
319 /* Early pruning: if the current state is already more costly than the cheapest complete path found so far,
320 * we can safely ignore that state. Additionally, if the heuristic is admissible, we know that the
321 * remaining cost from the current state to the goal is *at least* `h`. Therefore, any complete path from
322 * start to goal that contains this state has cost at least `g + h`.
323 *
324 * For cost-based pruning, the weighting factor must not be considered since it renders the *f*-values
325 * *inadmissible*. Therefore, for weighted search we have to divide `h` by the weighting factor before
326 * pruning. Note, that the weighting factor was initially applied when computing `h` of a state for adding
327 * it to the priority queue. */
328 const auto min_path_cost = [&]() {
329 if constexpr (is_admissible<Heurisitc>) {
330 if constexpr (use_weighted_search)
331 return state.g() + h / weighting_factor();
332 else
333 return state.g() + h;
334 } else {
335 return state.g();
336 }
337 }();
338 if (min_path_cost >= least_path_cost) [[unlikely]] {
339 inc_pruned_by_cost();
340 return;
341 } else if (expand_type::is_goal(state, context...)) [[unlikely]] {
342 update_least_path_cost(state.g());
343 }
344 }
345
346 auto &Q = ToBeamQueue ? beam_queue_ : regular_queue_;
347 auto &P = partition(state, context...);
348
349 if constexpr (detect_duplicates) {
350 if (auto it = P.find(state); it == P.end()) [[likely]] {
351 /*----- Entirely new state, never seen before. -----*/
352 it = P.emplace_hint(it, std::move(state), StateInfo(h, &Q));
353 /*----- Enqueue state, obtain handle, and add to `StateInfo`. -----*/
354 it->second.handle = Q.push(&*it);
355 inc_new();
356 } else {
357 /*----- Duplicate, seen before. -----*/
358 M_insist(it->second.h == h, "must not have a different heuristic value for the same state");
359 inc_duplicates();
360
361 if (ToBeamQueue and it->second.queue == &regular_queue_) {
362 /*----- The state is in the regular queue and needs to be moved to the beam queue. -----*/
363 if constexpr (HasRegularQueue)
364 it->second.queue->erase(it->second.handle); // erase from regular queue
365 if (state.g() < it->first.g())
366 inc_cheaper();
367 it->first.decrease_g(state.parent(), state.g()); // update *g* value
368 it->second.handle = beam_queue_.push(&*it); // add to beam queue and update handle
369 it->second.queue = &beam_queue_; // update queue
370 inc_decrease_key();
371 if constexpr (HasRegularQueue)
372 inc_regular_to_beam();
373 else
374 inc_none_to_beam();
375 } else if (state.g() >= it->first.g()) [[likely]] {
376 /*----- The state wasn't reached on a cheaper path and hence cannot produce better solutions. -----*/
377 inc_discarded();
378 return; // XXX is it safe to not add the state to any queue?
379 } else {
380 /*----- The state was reached on a cheaper path. We must reconsider the state. -----*/
381 M_insist(state.g() < it->first.g(), "the state was reached on a cheaper path");
382 inc_cheaper();
383 it->first.decrease_g(state.parent(), state.g()); // decrease value of *g*
384 if (it->second.queue == nullptr) {
385 /*----- The state is currently not present in a queue. -----*/
386 it->second.handle = Q.push(&*it); // add to dedicated queue and update handle
387 it->second.queue = &Q; // update queue
388 } else {
389 /*----- Update the state's entry in the queue. -----*/
390 M_insist(it->second.queue == &Q, "the state must already be in its destinated queue");
391 Q.increase(it->second.handle); // we need to *increase* because the heap is a max-heap
392 inc_decrease_key();
393 }
394 }
395 }
396 } else {
397 const auto new_g = state.g();
398 auto [it, res] = P.try_emplace(std::move(state), StateInfo(h, &Q));
399 Q.push(&*it);
400 if (res) {
401 inc_new();
402 } else {
403 inc_duplicates();
404 if (new_g < it->first.g())
405 inc_cheaper();
406 }
407 }
408 }
409
410 void push_regular_queue(state_type state, double h, Context&... context) {
411 if constexpr (detect_duplicates) {
412 if constexpr (HasRegularQueue) {
413 push<false>(std::move(state), h, context...);
414 } else {
415 if constexpr (enable_cost_based_pruning) {
416 /* Early pruning: if the current state is already more costly than the cheapest complete path found
417 * so far, we can safely ignore that state. Additionally, if the heuristic is admissible, we know
418 * that the remaining cost from the current state to the goal is *at least* `h`. Therefore, any
419 * complete path from start to goal that contains this state has cost at least `g + h`.
420 *
421 * For cost-based pruning, the weighting factor must not be considered since it renders the
422 * *f*-values *inadmissible*. Therefore, for weighted search we have to divide `h` by the weighting
423 * factor before pruning. Note, that the weighting factor was initially applied when computing `h`
424 * of a state for adding it to the priority queue. */
425 const auto min_path_cost = [&]() {
426 if constexpr (is_admissible<Heurisitc>) {
427 if constexpr (use_weighted_search)
428 return state.g() + h / weighting_factor();
429 else
430 return state.g() + h;
431 } else {
432 return state.g();
433 }
434 }();
435 if (min_path_cost >= least_path_cost) [[unlikely]] {
436 inc_pruned_by_cost();
437 return;
438 } else if (expand_type::is_goal(state, context...)) [[unlikely]] {
439 update_least_path_cost(state.g());
440 }
441 }
442
443 auto &P = partition(state, context...);
444 /*----- There is no regular queue. Only update the state's mapping. -----*/
445 if (auto it = P.find(state); it == P.end()) {
446 /*----- This is a new state. Simply create a new entry. -----*/
447 it = P.emplace_hint(it, std::move(state), StateInfo(h, &regular_queue_));
448 inc_new();
449 /* We must not add the state to the regular queue, but we must remember that the state can still be
450 * "moved" to the beam queue. */
451 } else {
452 /*----- This is a duplicate state. Check whether it has lower cost *g*. -----*/
453 M_insist(it->second.h == h, "must not have a different heuristic value for the same state");
454 inc_duplicates();
455 if (state.g() < it->first.g()) {
456 inc_cheaper();
457 it->first.decrease_g(state.parent(), state.g());
458 if (it->second.queue == &beam_queue_) { // if state is currently in a queue
459 it->second.queue->increase(it->second.handle); // *increase* because max-heap
460 inc_decrease_key();
461 }
462 } else {
463 inc_discarded();
464 }
465 }
466 }
467 } else {
468 push<not HasRegularQueue>(std::move(state), h, context...);
469 }
470 }
471
472 void push_beam_queue(state_type state, double h, Context&... context) {
473 push<true>(std::move(state), h, context...);
474 }
475
476 bool is_regular_queue_empty() const { return not HasRegularQueue or regular_queue_.empty(); }
477 bool is_beam_queue_empty() const { return not HasBeamQueue or beam_queue_.empty(); }
479
480 std::pair<const state_type&, double> pop() {
481 M_insist(not queues_empty());
482 pointer_type ptr = nullptr;
483 if (HasBeamQueue and not beam_queue_.empty()) {
484 ptr = beam_queue_.top();
485 beam_queue_.pop();
486 } else if (HasRegularQueue and not regular_queue_.empty()) {
487 ptr = regular_queue_.top();
488 regular_queue_.pop();
489 }
490 M_insist(ptr, "ptr must have been set, the queues must not have been empty");
491
492 auto &entry = *static_cast<typename map_type::value_type*>(ptr);
493 entry.second.queue = nullptr; // remove from queue
494 return { entry.first, entry.second.h };
495 }
496
497 typename map_type::iterator find(const state_type &state, Context&... context) {
498 return partition(state, context...).find(state);
499 }
500 typename map_type::iterator end(const state_type &state, Context&... context) {
501 return partition(state, context...).end();
502 }
503 typename map_type::const_iterator find(const state_type &state, Context&... context) const {
504 return partition(state, context...).find(state);
505 }
506 typename map_type::const_iterator end(const state_type &state, Context&... context) const {
507 return partition(state, context...).end();
508 }
509
510 void clear() {
511 least_path_cost = std::numeric_limits<double>::infinity();
512 if constexpr (HasRegularQueue)
513 regular_queue_.clear();
514 if constexpr (HasBeamQueue)
515 beam_queue_.clear();
516 // states_.clear();
517 partitions_.clear();
518 }
519
520 void print_counters(std::ostream &out) const {
521#define X(NAME) num_##NAME() << " " #NAME
522 out << X(new) ", " << X(duplicates) ", ";
523 if (HasRegularQueue and HasBeamQueue)
524 out << X(regular_to_beam) ", ";
525 if (HasBeamQueue)
526 out << X(none_to_beam) ", ";
527 out << X(discarded) ", " << X(cheaper) ", " << X(decrease_key) ", " << X(pruned_by_cost);
528#undef X
529 }
530
531 map_type & partition(const state_type &state, Context&... context) { return partitions_(state, context...); }
532 const map_type & partition(const state_type &state, Context&... context) const {
533 return partitions_(state, context...);
534 }
535
536 friend std::ostream & operator<<(std::ostream &out, const StateManager &SM) {
537#ifndef NDEBUG
538 SM.print_counters(out);
539 out << ", ";
540#endif
541 out << SM.partitions_.size() << " seen, " << SM.regular_queue_.size() << " currently in regular queue, "
542 << SM.beam_queue_.size() << " currently in beam queue";
543 return out;
544 }
545
546 void dump(std::ostream &out) const { out << *this << std::endl; }
547 void dump() const { dump(std::cerr); }
548};
549
550template<
551 heuristic_search_state State,
552 typename Expand,
553 typename Heurisitc,
554 bool HasRegularQueue,
555 bool HasBeamQueue,
556 typename Config,
557 typename... Context
558>
561{
562 auto left = static_cast<typename map_type::value_type*>(p_left);
563 auto right = static_cast<typename map_type::value_type*>(p_right);
564 /* Compare greater than (`operator >`) to turn max-heap into min-heap. */
565 return left->first.g() + left->second.h > right->first.g() + right->second.h;
566}
567
568template<typename Config>
570 std::is_class_v<Config> and
571 requires { { Config::PerformCostBasedPruning } -> std::convertible_to<bool>; } and
572 requires { { Config::PerformWeightedSearch } -> std::convertible_to<bool>; } and
573 std::integral<decltype(Config::BeamWidth::num)> and
574 std::integral<decltype(Config::BeamWidth::den)> and
575 requires { { Config::Lazy } -> std::convertible_to<bool>; } and
576 requires { { Config::IsMonotone } -> std::convertible_to<bool>; } and
577 requires { { Config::PerformAnytimeSearch} -> std::convertible_to<bool>; };
578
581template<SearchConfigConcept StaticConfig>
583{
585
588
593
597 OptField<StaticConfig::PerformCostBasedPruning, double> upper_bound = std::numeric_limits<double>::quiet_NaN();
598
602
604 OptField<StaticConfig::PerformAnytimeSearch, uint64_t> expansion_budget = std::numeric_limits<uint64_t>::max();
605};
606
607template<typename state_type, typename... Context>
608concept has_mark = requires (state_type state, Context... context) { state.reset_marked(state, context...); };
609
612template<
614 typename Expand,
615 typename Heuristic,
616 SearchConfigConcept StaticConfig,
617 typename... Context
618>
619requires heuristic_search_heuristic<Heuristic, Context...>
621{
622 using state_type = State;
623 using expand_type = Expand;
624 using heuristic_type = Heuristic;
625 using static_search_config = StaticConfig;
626
628 static constexpr bool use_weighted_search = StaticConfig::PerformWeightedSearch;
630 static constexpr float beam_width = StaticConfig::BeamWidth::num / StaticConfig::BeamWidth::den;
632 static constexpr bool use_beam_search = StaticConfig::BeamWidth::num != 0;
634 static constexpr bool use_dynamic_beam_sarch = use_beam_search and beam_width < 1.f;
636 static constexpr bool is_lazy = StaticConfig::Lazy;
638 static constexpr bool is_monotone = StaticConfig::IsMonotone;
640 static constexpr float BEAM_FACTOR = .2f;
642 static constexpr bool use_anytime_search = StaticConfig::PerformAnytimeSearch;
643
644 using callback_t = std::function<void(state_type, double)>;
645
647 /* State= */ State,
648 /* Expand= */ Expand,
649 /* Heurisitc= */ Heuristic,
650 /* HasRegularQueue= */ not (use_beam_search and is_monotone),
651 /* HasBeamQueue= */ use_beam_search,
652 /* Config= */ StaticConfig,
653 /* Context...= */ Context...
654 >;
655
656 private:
658 float weighting_factor_ = 1.;
659
660#ifndef NDEBUG
661#define DEF_COUNTER(NAME) \
662 private: \
663 std::size_t num_##NAME##_ = 0; \
664 void inc_##NAME() { ++num_##NAME##_; } \
665 public: \
666 std::size_t num_##NAME() const { return num_##NAME##_; }
667#else
668#define DEF_COUNTER(NAME) \
669 void inc_##NAME() { } \
670 std::size_t num_##NAME() const { return 0; }
671#endif
672
673 DEF_COUNTER(cached_heuristic_value)
674
675#undef DEF_COUNTER
676
678 struct weighted_state
679 {
680 state_type state;
681 double h;
682
683 weighted_state(state_type state, double h) : state(std::move(state)), h(h) { }
684 weighted_state(const weighted_state&) = delete;
685 weighted_state(weighted_state&&) = default;
686 weighted_state & operator=(weighted_state&&) = default;
687
688 bool operator<(const weighted_state &other) const { return this->weight() < other.weight(); }
689 bool operator>(const weighted_state &other) const { return this->weight() > other.weight(); }
690 bool operator<=(const weighted_state &other) const { return this->weight() <= other.weight(); }
691 bool operator>=(const weighted_state &other) const { return this->weight() >= other.weight(); }
692
693 private:
694 double weight() const { return state.g() + h; }
695
696 };
697
700
702 std::vector<weighted_state> candidates;
703
704 public:
705 explicit genericAStar(Context&... context)
706 : state_manager_(context...)
707 {
708 if constexpr (use_beam_search) {
709 if constexpr (not use_dynamic_beam_sarch)
710 candidates.reserve(beam_width + 1);
711 }
712 }
713
714 genericAStar(const genericAStar&) = delete;
716
718
719 const state_manager_type & state_manager () const { return state_manager_; }
720
722 float weighting_factor() const { return weighting_factor_; }
723
725 float weighting_factor(float new_factor) {
726 M_insist(new_factor >= 0.f, "factor must not be negative");
727 M_insist(new_factor != 0.f, "weight must not be 0, use heuristic zero for Dijkstra`s search algorithm");
728 return std::exchange(weighting_factor_, new_factor);
729 }
730
736 const State & search(state_type initial_state, expand_type expand, heuristic_type &heuristic,
737 const SearchConfiguration<StaticConfig> &config, Context&... context);
738
740 void clear() {
741 state_manager_.clear();
742 candidates.clear();
743 }
744
745 private:
746 /*------------------------------------------------------------------------------------------------------------------
747 * Helper methods
748 *----------------------------------------------------------------------------------------------------------------*/
749
750 /* Try to add the successor state `s` to the beam. If the weight of `s` is already higher than the highest weight
751 * in the beam, immediately bypass the beam. */
752 void beam(state_type state, double h, Context&... context) {
753 auto &top = candidates.front();
754#ifndef NDEBUG
755 M_insist(std::is_heap(candidates.begin(), candidates.end()), "candidates must always be a max-heap");
756 for (auto &elem : candidates)
757 M_insist(top >= elem, "the top candidate at the front must be no less than any other candidate");
758#endif
759 if (candidates.size() < beam_width) {
760 /* There is still space in the candidates, so simply add the state to the heap. */
761 candidates.emplace_back(std::move(state), h);
762 if (candidates.size() == beam_width) // heapify when filled
763 std::make_heap(candidates.begin(), candidates.end());
764 } else if (state.g() + h >= top.state.g() + top.h) {
765 /* The state has higher g+h than the top of the candidates heap, so bypass the candidates immediately. */
766 state_manager_.push_regular_queue(std::move(state), h, context...);
767 } else {
768 /* The state has less g+h than the top of candidates heap. Pop the current top and insert the state into
769 * the candidates heap. */
770 M_insist(candidates.size() == beam_width);
771 M_insist(std::is_heap(candidates.begin(), candidates.end()));
772 candidates.emplace_back(std::move(state), h);
773 std::pop_heap(candidates.begin(), candidates.end());
774 weighted_state worst_candidate = std::move(candidates.back()); // extract worst candidate
775 candidates.pop_back();
776 M_insist(std::is_heap(candidates.begin(), candidates.end()));
777 M_insist(candidates.size() == beam_width);
778#ifndef NDEBUG
779 for (auto &elem : candidates)
780 M_insist(worst_candidate >= elem, "worst candidate must be no less than any other candidate");
781#endif
782 state_manager_.push_regular_queue(std::move(worst_candidate.state), worst_candidate.h, context...); // move to regular
783 }
784 };
785
786 /* Compute a beam of dynamic (relative) size from the set of successor states. */
787 void beam_dynamic(expand_type &expand, Context&... context) {
788 std::sort(candidates.begin(), candidates.end());
789 const std::size_t num_beamed = std::ceil(candidates.size() * BEAM_FACTOR);
790 M_insist(not candidates.size() or num_beamed, "if the state has successors, at least one must be in the beam");
791 auto it = candidates.begin();
792 /*----- Add states in the beam to the beam queue. -----*/
793 for (auto end = it + num_beamed; it != end; ++it) {
794 if constexpr (has_mark<state_type, Context...>)
795 expand.reset_marked(it->state, context...);
796 state_manager_.push_beam_queue(std::move(it->state), it->h, context...);
797 }
798 /*----- Add remaining states to the regular queue. -----*/
799 for (auto end = candidates.end(); it != end; ++it)
800 state_manager_.push_regular_queue(std::move(it->state), it->h, context...);
801 };
802
804 void for_each_successor_lazily(callback_t &&callback, const state_type &state, heuristic_type &heuristic,
805 expand_type &expand, Context&... context)
806 {
807 /*----- Evaluate heuristic lazily by using heurisitc value of current state. -----*/
808 double h_current_state;
809 if (auto it = state_manager_.find(state, context...); it == state_manager_.end(state, context...)) {
810 if constexpr (use_weighted_search)
811 h_current_state = weighting_factor() * heuristic(state, context...);
812 else
813 h_current_state = heuristic(state, context...);
814 } else {
815 inc_cached_heuristic_value();
816 h_current_state = it->second.h; // use cached `h`
817 }
818 expand(state, [&callback, h_current_state](state_type successor) {
819 callback(std::move(successor), h_current_state);
820 }, context...);
821 };
822
824 void for_each_successor_eagerly(callback_t &&callback, const state_type &state, heuristic_type &heuristic,
825 expand_type &expand, Context&... context)
826 {
827 /*----- Evaluate heuristic eagerly. -----*/
828 expand(state, [this, callback=std::move(callback), &state, &heuristic, &context...](state_type successor) {
829 if (auto it = state_manager_.find(successor, context...); it == state_manager_.end(state, context...)) {
830 if constexpr (use_weighted_search) {
831 const double h = weighting_factor() * heuristic(successor, context...);
832 callback(std::move(successor), h);
833 } else {
834 const double h = heuristic(successor, context...);
835 callback(std::move(successor), h);
836 }
837 } else {
838 inc_cached_heuristic_value();
839 callback(std::move(successor), it->second.h); // use cached `h`
840 }
841 }, context...);
842 };
843
845 void for_each_successor(callback_t &&callback, const state_type &state, heuristic_type &heuristic,
846 expand_type &expand, Context&... context)
847 {
848 if constexpr (is_lazy)
849 for_each_successor_lazily(std::move(callback), state, heuristic, expand, context...);
850 else
851 for_each_successor_eagerly(std::move(callback), state, heuristic, expand, context...);
852 };
853
855 void explore_state(const state_type &state, heuristic_type &heuristic, expand_type &expand, Context&... context) {
856 if constexpr (use_dynamic_beam_sarch) {
857 /*----- Add all successors to candidates. Only best `BEAM_FACTOR` will be added to beam queue. -----*/
858 candidates.clear();
859 for_each_successor([this](state_type successor, double h) {
860 candidates.emplace_back(std::move(successor), h);
861 }, state, heuristic, expand, context...);
862 beam_dynamic(expand, context...); // order by g+h and partition into beam and regular states
863 } else if constexpr (use_beam_search) {
864 /*----- Keep only `beam_width` best successors in `candidates`, rest is added to regular queue. ----*/
865 candidates.clear();
866 for_each_successor([this, &context...](state_type successor, double h) {
867 beam(std::move(successor), h, context...); // try to add to candidates
868 }, state, heuristic, expand, context...);
869 /*----- The states remaining in `candidates` are within the beam. -----*/
870 for (auto &s : candidates) {
871 if constexpr (has_mark<state_type, Context...>)
872 expand.reset_marked(s.state, context...);
873 state_manager_.push_beam_queue(std::move(s.state), s.h, context...);
874 }
875 } else {
876 /*----- Have only regular queue. -----*/
877 for_each_successor([this, &context...](state_type successor, double h) {
878 state_manager_.push_regular_queue(std::move(successor), h, context...);
879 }, state, heuristic, expand, context...);
880 }
881 };
882
883 public:
884 friend std::ostream & operator<<(std::ostream &out, const genericAStar &AStar) {
885 return out << AStar.state_manager_ << ", used cached heuristic value " << AStar.num_cached_heuristic_value()
886 << " times";
887 }
888
889 void dump(std::ostream &out) const { out << *this << std::endl; }
890 void dump() const { dump(std::cerr); }
891};
892
893template<
894 heuristic_search_state State,
895 typename Expand,
896 typename Heuristic,
897 SearchConfigConcept StaticConfig,
898 typename... Context
899>
900requires heuristic_search_heuristic<Heuristic, Context...>
902 state_type initial_state,
903 expand_type expand,
904 heuristic_type &heuristic,
906 Context&... context
907) {
908 if constexpr (use_weighted_search) {
909 /* For weighted search, set the weight to the desired weighting factor. */
910 weighting_factor(config.weighting_factor);
911 state_manager_.weighting_factor(config.weighting_factor);
912 }
913
914 if constexpr (StaticConfig::PerformCostBasedPruning) {
915 if (not std::isnan(config.upper_bound)) {
916 /* Initialize the least path cost. Note, that the given upper bound could be *exactly* the weight of the
917 * shortest path. To not prune the goal reached on that shortest path, we increase the upper bound *slightly*.
918 * More precisely, we increase the upper bound to the next representable value in `double`. */
919 state_manager_.update_least_path_cost(
920 std::nextafter(config.upper_bound, std::numeric_limits<double>::infinity())
921 );
922 }
923 }
924
925 /* Lambda function to assure that the budgeted number of expansions is met when using Anytime A*. */
926 auto have_budget = [budget=config.expansion_budget]() mutable -> bool {
927 if constexpr (use_anytime_search) {
928 if (budget) {
929 --budget;
930 return true;
931 } else {
932 /* Expansion budget exhausted. The search did not terminate with a goal state. */
933 throw budget_exhausted_exception("no goal state found with given expansion budget");
934 }
935 } else {
936 return true;
937 }
938 };
939
940 /* Initialize queue with initial state. */
941 state_manager_.template push<use_beam_search and is_monotone>(std::move(initial_state), 0, context...);
942
943 /* Run work list algorithm. */
944 while (not state_manager_.queues_empty() and have_budget()) {
945 M_insist(not (is_monotone and use_beam_search) or not state_manager_.is_beam_queue_empty(),
946 "the beam queue must not run empty with beam search on a monotone search space");
947 auto top = state_manager_.pop();
948 const state_type &state = top.first;
949
950 if (expand_type::is_goal(state, context...))
951 return state;
952
953 explore_state(state, heuristic, expand, context...);
954 }
955
956 throw std::logic_error("goal state unreachable from provided initial state");
957}
958
959}
960
961}
#define DEF_COUNTER(NAME)
and(sizeof(T)==4) U64x1 reinterpret_to_U64(m
Definition: WasmAlgo.cpp:266
#define M_insist(...)
Definition: macro.hpp:129
‍mutable namespace
Definition: Backend.hpp:10
and
Definition: enum_ops.hpp:12
STL namespace.
static Options & Get()
Return a reference to the single Options instance.
Definition: Options.cpp:9
Implements a small and efficient set over integers in the range of 0 to 63 (including).
Definition: ADT.hpp:26
Relies on the rules of aggregate initialization
OptField< StaticConfig::PerformCostBasedPruning, std::vector< std::pair< Subproblem, Subproblem > > > initial_plan
Initial plan for the query.
OptField< StaticConfig::PerformCostBasedPruning, double > upper_bound
Upper bound on the cost of plans to consider.
OptField< StaticConfig::PerformAnytimeSearch, uint64_t > expansion_budget
Budget for the maximum number of expansions.
SearchConfiguration(SearchConfiguration &&)=default
OptField< StaticConfig::PerformWeightedSearch, float > weighting_factor
The weighting factor for the heuristic.
const map_type & operator()(const state_type &, Context &...) const
Partitions(const Partitions &)=delete
map_type & operator()(const state_type &, Context &...)
partition_const_iterator end() const
partition_const_reverse_iterator rend() const
partition_const_iterator cbegin() const
partition_const_reverse_iterator crend() const
partition_const_iterator cend() const
partition_const_reverse_iterator rbegin() const
Partitions(const Partitions &)=delete
partition_reverse_iterator rbegin()
partition_const_iterator begin() const
partition_const_reverse_iterator crbegin() const
map_type & operator()(const state_type &state, Context &... context)
const map_type & operator()(const state_type &state, Context &... context) const
‍comparator for map entries based on states' g + h value
bool operator()(pointer_type p_left, pointer_type p_right) const
Tracks states and their presence in queues.
bool is_regular_queue_empty() const
void update_least_path_cost(double cost)
Update the least_path_cost to cost.
StateManager & operator=(StateManager &&)=default
(new) DEF_COUNTER(duplicates) DEF_COUNTER(regular_to_beam) DEF_COUNTER(none_to_beam) DEF_COUNTER(discarded) DEF_COUNTER(cheaper) DEF_COUNTER(decrease_key) DEF_COUNTER(pruned_by_cost) public std::pair< const state_type, StateInfo > map_value_type
partition_const_reverse_iterator partitions_crbegin() const
partition_reverse_iterator partitions_rbegin()
void push_regular_queue(state_type state, double h, Context &... context)
StateManager(StateManager &&)=default
partition_const_iterator partitions_begin() const
map_type::iterator end(const state_type &state, Context &... context)
heap_type regular_queue_
‍map of all states ever explored, mapping state to its info
partition_const_reverse_iterator partitions_rbegin() const
typename std::vector< map_type >::const_reverse_iterator partition_const_reverse_iterator
partition_reverse_iterator partitions_rend()
bool is_beam_queue_empty() const
StateManager(Context &... context)
std::size_t num_states_seen() const
void dump(std::ostream &out) const
partition_iterator partitions_end()
static constexpr bool use_weighted_search
range< partition_iterator > partitions()
static constexpr bool detect_duplicates
partition_const_reverse_iterator partitions_rend() const
typename std::vector< map_type >::const_iterator partition_const_iterator
double least_path_cost
‍the cost of the cheapest, complete path found yet; can be used for additional pruning
partition_iterator partitions_begin()
partition_const_reverse_iterator partitions_crend() const
static constexpr bool has_regular_queue
partition_const_iterator partitions_end() const
partition_const_iterator partitions_cend() const
map_type::const_iterator end(const state_type &state, Context &... context) const
range< partition_const_iterator > partitions() const
typename Config::template heap_type< pointer_type, typename Config::template compare< comparator > > heap_type
‍the type of heap to implement the priority queues
static constexpr bool has_beam_queue
map_type & partition(const state_type &state, Context &... context)
StateManager(const StateManager &)=delete
Partitions< supports_partitioning< State, Context... > > partitions_
‍map of all states ever explored, mapping state to its info; partitioned by state partition id
typename std::vector< map_type >::reverse_iterator partition_reverse_iterator
typename std::vector< map_type >::iterator partition_iterator
std::unordered_map< state_type, StateInfo, std::hash< state_type >, std::equal_to< state_type >, typename Config::template allocator_type< map_value_type > > map_type
const map_type & partition(const state_type &state, Context &... context) const
float weighting_factor() const
Returns the weighting factor for the heuristic value.
void * pointer_type
‍type for a pointer to an entry in the map of states
std::size_t num_states_in_regular_queue() const
float weighting_factor_
‍the weighting factor for the heuistic value of a state
partition_const_iterator partitions_cbegin() const
map_type::const_iterator find(const state_type &state, Context &... context) const
void push(state_type state, double h, Context &... context)
friend std::ostream & operator<<(std::ostream &out, const StateManager &SM)
std::size_t num_states_in_beam_queue() const
void print_counters(std::ostream &out) const
static constexpr bool enable_cost_based_pruning
map_type::iterator find(const state_type &state, Context &... context)
float weighting_factor(float new_factor)
Set the weighting factor for the heuristic value and returns the old value.
std::pair< const state_type &, double > pop()
void push_beam_queue(state_type state, double h, Context &... context)
budget_exhausted_exception(std::string message)
Implements a generic A* search algorithm.
void explore_state(const state_type &state, heuristic_type &heuristic, expand_type &expand, Context &... context)
Explores the given state.
std::function< void(state_type, double)> callback_t
genericAStar(genericAStar &&)=default
const state_manager_type & state_manager() const
float weighting_factor() const
Returns the weighting factor for the heuristic value.
friend std::ostream & operator<<(std::ostream &out, const genericAStar &AStar)
genericAStar & operator=(genericAStar &&)=default
float weighting_factor(float new_factor)
Set the weighting factor for the heuristic value and returns the old value.
void for_each_successor_lazily(callback_t &&callback, const state_type &state, heuristic_type &heuristic, expand_type &expand, Context &... context)
Expands the given state by lazily evaluating the heuristic function.
const State & search(state_type initial_state, expand_type expand, heuristic_type &heuristic, const SearchConfiguration< StaticConfig > &config, Context &... context)
Search for a path from the given initial_state to a goal state.
std::vector< weighted_state > candidates
‍candidates for the beam
DEF_COUNTER(cached_heuristic_value) struct weighted_state
A state plus its heuristic value.
StaticConfig static_search_config
void beam(state_type state, double h, Context &... context)
state_manager_type state_manager_
‍the search's state manager, tracking heuristic value, checking for duplicates, and ordering by f-val...
void dump(std::ostream &out) const
void for_each_successor_eagerly(callback_t &&callback, const state_type &state, heuristic_type &heuristic, expand_type &expand, Context &... context)
Expands the given state by eagerly evaluating the heuristic function.
void for_each_successor(callback_t &&callback, const state_type &state, heuristic_type &heuristic, expand_type &expand, Context &... context)
Expands the given state according to is_lazy.
void beam_dynamic(expand_type &expand, Context &... context)
genericAStar(const genericAStar &)=delete
void clear()
Resets the state of the search.
genericAStar(Context &... context)