diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a7e10cbf9a..0b03543ca3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,7 +69,7 @@ jobs: - name: Configure shell: bash - run: ./configure + run: ./configure --enable-experimental - name: Build shell: bash @@ -233,6 +233,7 @@ jobs: shell: bash run: > cmake -S "$GITHUB_WORKSPACE" -B "$GITHUB_WORKSPACE/build-ninja" + -DGECODE_ENABLE_EXPERIMENTAL=ON -G Ninja -DCMAKE_BUILD_TYPE=$BUILD_TYPE -DGECODE_ENABLE_QT=OFF @@ -496,6 +497,7 @@ jobs: -DGECODE_ENABLE_MPFR=OFF -DGECODE_BUILD_SHARED=${{ matrix.build_shared }} -DGECODE_BUILD_STATIC=${{ matrix.build_static }} + -DGECODE_ENABLE_EXPERIMENTAL=${{ matrix.build_shared }} - name: Build shell: pwsh @@ -607,6 +609,7 @@ jobs: -DGECODE_ENABLE_MPFR=OFF \ -DGECODE_BUILD_SHARED=OFF \ -DGECODE_BUILD_STATIC=ON \ + -DGECODE_ENABLE_EXPERIMENTAL=ON \ -DGECODE_ENABLE_EXAMPLES=OFF \ -DGECODE_ENABLE_FLATZINC=OFF @@ -675,7 +678,7 @@ jobs: shell: msys2 {0} run: | export PATH="$UV_DIR:$PATH" - CC=gcc CXX=g++ ./configure --with-host-os=Windows --disable-qt --disable-gist --disable-mpfr + CC=gcc CXX=g++ ./configure --with-host-os=Windows --disable-qt --disable-gist --disable-mpfr --enable-experimental - name: Build shell: msys2 {0} diff --git a/CMakeLists.txt b/CMakeLists.txt index 145bc33f87..f3ea030be1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -107,6 +107,7 @@ endif() set(GECODE_ENABLE_GIST "${GECODE_ENABLE_GIST_DEFAULT}" CACHE STRING "Enable Gist (AUTO, ON, or OFF)") set_property(CACHE GECODE_ENABLE_GIST PROPERTY STRINGS AUTO ON OFF) option(GECODE_ENABLE_CPPROFILER "Enable CPProfiler support" ON) +option(GECODE_ENABLE_EXPERIMENTAL "Enable experimental preview APIs" OFF) option(GECODE_ENABLE_CBS "Enable counting-based search support" OFF) set(GECODE_ENABLE_EXAMPLES_DEFAULT ${PROJECT_IS_TOP_LEVEL}) option(GECODE_ENABLE_EXAMPLES "Build examples" ${GECODE_ENABLE_EXAMPLES_DEFAULT}) @@ -692,6 +693,9 @@ endif() if(GECODE_ENABLE_CPPROFILER) set(GECODE_HAS_CPPROFILER "/**/") endif() +if(GECODE_ENABLE_EXPERIMENTAL) + set(GECODE_HAS_EXPERIMENTAL "/**/") +endif() if(GECODE_ENABLE_CBS) set(GECODE_HAS_CBS "/**/") endif() @@ -1815,6 +1819,7 @@ include(FeatureSummary) foreach(gecode_option IN ITEMS GECODE_BUILD_SHARED GECODE_BUILD_STATIC GECODE_ENABLE_THREAD GECODE_ENABLE_CPPROFILER GECODE_ENABLE_CBS + GECODE_ENABLE_EXPERIMENTAL GECODE_ENABLE_EXAMPLES GECODE_INSTALL GECODE_ENABLE_SEARCH GECODE_ENABLE_INT_VARS GECODE_ENABLE_SET_VARS GECODE_ENABLE_FLOAT_VARS GECODE_ENABLE_MINIMODEL GECODE_ENABLE_DRIVER diff --git a/Makefile.in b/Makefile.in index 75ab6033ca..c8a0955da7 100755 --- a/Makefile.in +++ b/Makefile.in @@ -269,19 +269,25 @@ SEARCHSRC0 = \ cpprofiler/tracer worker-control SEARCHHDR0 = \ statistics.hpp stop.hpp options.hpp cutoff.hpp \ - support.hh worker.hh worker-control.hh exception.hpp engine.hpp base.hpp \ + support.hh heuristic.hh \ + worker.hh worker-control.hh exception.hpp engine.hpp base.hpp \ nogoods.hh nogoods.hpp build.hpp traits.hpp sebs.hpp \ seq/path.hh seq/path.hpp seq/dfs.hh seq/dfs.hpp \ - seq/bab.hh seq/bab.hpp seq/lds.hh seq/lds.hpp \ + seq/bab.hh seq/bab.hpp seq/astar.hh seq/cabs.hh seq/choice-expansion.hh \ + seq/lds.hh seq/lds.hpp \ seq/rbs.hh seq/rbs.hpp seq/dead.hh \ seq/pbs.hh seq/pbs.hpp \ par/path.hh par/path.hpp par/engine.hh par/engine.hpp \ par/dfs.hh par/dfs.hpp par/bab.hh par/bab.hpp \ - par/pbs.hh par/pbs.hpp \ - dfs.hpp bab.hpp lds.hpp rbs.hpp pbs.hpp \ + par/pbs.hh par/pbs.hpp par/frontier.hh \ + dfs.hpp bab.hpp astar.hpp cabs.hpp lds.hpp rbs.hpp pbs.hpp \ relax.hh tracer.hpp trace-recorder.hpp \ cpprofiler/message.hpp cpprofiler/connector.hpp +ifeq "@enable_experimental@" "yes" +SEARCHSRC0 += astar cabs par/frontier +endif + SEARCHSRC = $(SEARCHSRC0:%=gecode/search/%.cpp) SEARCHHDR = gecode/search.hh $(SEARCHHDR0:%=gecode/search/%) SEARCHOBJ = $(SEARCHSRC:%.cpp=%$(OBJSUFFIX)) @@ -912,7 +918,8 @@ INTEXAMPLESRC0 = \ word-square crossword open-shop car-sequencing sat \ bin-packing knights tsp perfect-square schurs-lemma \ dominating-queens colored-matrix multi-bin-packing \ - qcp job-shop open-route common-pattern fifteen-puzzle + qcp job-shop open-route common-pattern fifteen-puzzle \ + tsptw INTEXAMPLEHDR = $(INTEXAMPLEHDR0:%=examples/%.hpp) \ diff --git a/changelog.in b/changelog.in index e14f52adfd..7b0016803e 100755 --- a/changelog.in +++ b/changelog.in @@ -56,6 +56,8 @@ # driver|iter|support|example|test|gist|flatzinc|other # What: bug|documentation|performance|new|removed|change # Rank: minor|major +# Experimental: true|false +# True entries appear under Experimental changes within each release. # Bug: # Issue: # Thanks: @@ -116,6 +118,30 @@ Rank: minor Honor propagation annotations in cumulative and unary scheduling bindings, including the unary specializations of cumulative constraints. +[ENTRY] +Module: search +What: new +Rank: major +Experimental: true +[DESCRIPTION] +Add experimental A* (AStar) and complete anytime beam search (CABS). +[MORE] +Both engines take an objective bound and an optional rank from +Search::Options::guidance. They support parallel search, dynamic worker +control, and portfolios with BAB. CABS also supports model-defined dominance. +Enable with --enable-experimental or GECODE_ENABLE_EXPERIMENTAL=ON; the +preview is disabled by default and its APIs may change in future releases. +The fifteen-puzzle and tsptw examples demonstrate both engines. + +[ENTRY] +Module: search +What: bug +Rank: minor +[DESCRIPTION] +Fix sequential portfolio search ignoring stop objects after a slice has +ended. An asset that stops by itself no longer stops the portfolio; it is +removed and the portfolio reports that it has stopped once all assets are done. + [ENTRY] Module: int What: new diff --git a/cmake/GecodeSources.cmake b/cmake/GecodeSources.cmake index be1a613b08..a74ecadedd 100644 --- a/cmake/GecodeSources.cmake +++ b/cmake/GecodeSources.cmake @@ -51,6 +51,14 @@ set(GECODE_SEARCH_SOURCES gecode/search/worker-control.cpp ) +if(GECODE_ENABLE_EXPERIMENTAL) + list(APPEND GECODE_SEARCH_SOURCES + gecode/search/astar.cpp + gecode/search/cabs.cpp + gecode/search/par/frontier.cpp + ) +endif() + set(GECODE_INT_SOURCES gecode/int/arithmetic.cpp gecode/int/arithmetic/mult.cpp diff --git a/configure b/configure index 42ff3918e9..29247a22a9 100755 --- a/configure +++ b/configure @@ -680,6 +680,7 @@ HAVEFLEX enable_cpprofiler LINKCPPROFILER enable_cbs +enable_experimental enable_gist enable_qt QTLIBS @@ -859,6 +860,7 @@ with_mpfr_include with_mpfr_lib enable_qt enable_gist +enable_experimental enable_cbs enable_cpprofiler enable_driver @@ -1531,6 +1533,7 @@ Optional Features: --enable-qt build with Qt support, requires Qt 5.15 or newer [default=yes] --enable-gist build Gecode Interactive Search Tool [default=yes] + --enable-experimental build experimental preview APIs [default=no] --enable-cbs build with support for counting-based search [default=no] --enable-cpprofiler build with support for CPProfiler [default=yes] @@ -3409,6 +3412,8 @@ GECODE_SOVERSION=${ac_gecode_soversion} + + @@ -13593,6 +13598,32 @@ printf "%s\n" "no" >&6; } fi + # Check whether --enable-experimental was given. +if test ${enable_experimental+y} +then : + enableval=$enable_experimental; +else case e in #( + e) enable_experimental=no ;; +esac +fi + + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether to build experimental preview APIs" >&5 +printf %s "checking whether to build experimental preview APIs... " >&6; } + case $enable_experimental in #( + yes) : + +printf "%s\n" "#define GECODE_HAS_EXPERIMENTAL /**/" >>confdefs.h + ;; #( + no) : + ;; #( + *) : + as_fn_error $? "--enable-experimental accepts only yes or no" "$LINENO" 5 ;; +esac + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $enable_experimental" >&5 +printf "%s\n" "$enable_experimental" >&6; } + + + # Check whether --enable-cbs was given. if test ${enable_cbs+y} then : diff --git a/configure.ac b/configure.ac index f5ffb8e044..5e0ea06b12 100644 --- a/configure.ac +++ b/configure.ac @@ -264,6 +264,7 @@ dnl ------------------------------------------------------------------ AC_GECODE_MPFR AC_GECODE_QT AC_GECODE_GIST +AC_GECODE_EXPERIMENTAL AC_GECODE_CBS AC_GECODE_CPPROFILER AC_GECODE_FLEXBISON diff --git a/docs/worker-control.md b/docs/worker-control.md index a1a0f26623..f6b3188d98 100644 --- a/docs/worker-control.md +++ b/docs/worker-control.md @@ -3,7 +3,8 @@ ## Purpose `Search::WorkerControl` lets an external scheduler change how many workers a -running DFS or BAB engine may use. A portfolio with a fixed thread budget can +running DFS or BAB engine may use. Experimental AStar and CABS engines also +support worker control. A portfolio with a fixed thread budget can move workers among its engines at runtime. The engine still starts with a fixed maximum: @@ -26,8 +27,8 @@ control.request(1); // Resume Gecode resolves the `threads` option when it constructs the engine. The result is the fixed worker capacity. A request must be between zero and that capacity, inclusive. Zero pauses the engine without discarding its search state. -This also applies when the resolved capacity is one: sequential DFS and BAB -wait at search boundaries until the control resumes them. +This also applies when the resolved capacity is one: sequential DFS, BAB, +AStar, and CABS wait at search boundaries until the control resumes them. Pausing requires a build with thread support. Without thread support, attaching an initially paused control or calling `request(0)` raises `Search::InvalidWorkerRequest`; controls must be used from one thread. @@ -80,8 +81,9 @@ An empty default-constructed handle means that worker adjustment is disabled. Calling `request` on an empty handle raises `Search::UninitializedWorkerControl`. -A shared identity can be bound to only one leaf engine at a time. It cannot be -shared by two DFS/BAB engines or attached directly to an enclosing meta-engine. +A shared identity can be bound to only one leaf engine at a time (DFS, BAB, +AStar, or CABS). It cannot be shared by two engines or attached directly to an +enclosing meta-engine. Ordinary reuse for a replacement engine after destruction is also rejected. These uses raise `Search::WorkerControlInUse`. diff --git a/doxygen/doxygen.conf.in b/doxygen/doxygen.conf.in index 02a6a25aef..82de20ae79 100755 --- a/doxygen/doxygen.conf.in +++ b/doxygen/doxygen.conf.in @@ -1007,6 +1007,7 @@ PREDEFINED = "GECODE_SET_EXPORT=" \ "GECODE_HAS_FLOAT_VARS=1" \ "GECODE_HAS_GIST=1" \ "GECODE_HAS_CPPROFILER=1" \ + "GECODE_HAS_EXPERIMENTAL=1" \ "GECODE_THREADS_PTHREADS=1" \ "GECODE_HAS_THREADS=1" \ "GECODE_HAS_MPFR=1" \ diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 6282beaeb8..f5c12bdcb9 100755 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -61,6 +61,7 @@ set(GECODE_EXAMPLE_SOURCES sudoku-advanced.cpp sudoku.cpp tsp.cpp + tsptw.cpp warehouses.cpp word-square.cpp) diff --git a/examples/fifteen-puzzle.cpp b/examples/fifteen-puzzle.cpp index af5edcdef5..16ef255fb2 100644 --- a/examples/fifteen-puzzle.cpp +++ b/examples/fifteen-puzzle.cpp @@ -34,8 +34,10 @@ #include #include #include +#include #include +#include #include #include #include @@ -52,6 +54,7 @@ class PuzzleOptions : public Options { public: enum { MODEL_OPEN, MODEL_FIXED }; enum { BRANCH_MIN, BRANCH_MANHATTAN }; + enum { SEARCH_BAB, SEARCH_ASTAR, SEARCH_CABS }; PuzzleOptions(void) : Options("FifteenPuzzle"), _scramble("scramble","number of random slides from the goal",12), @@ -65,6 +68,12 @@ class PuzzleOptions : public Options { branching(BRANCH_MIN,"min","try the lowest blank position first"); branching(BRANCH_MANHATTAN,"manhattan", "try the slide with least Manhattan distance first"); + search(SEARCH_BAB); + search(SEARCH_BAB,"bab","depth-first branch and bound"); +#ifdef GECODE_HAS_EXPERIMENTAL + search(SEARCH_ASTAR,"astar","A* with Manhattan distance"); + search(SEARCH_CABS,"cabs","complete anytime beam search"); +#endif solutions(0); seed(1); } @@ -222,6 +231,8 @@ post_goal(Home home, const IntVarArgs& tiles, IntVar length, * Each new board uses the same slide and goal constraints as the fixed * representation. Propagation determines how much of the path is constructed; * the initial Manhattan distance bounds the number of moves from below. + * With A* and CABS, the moves so far plus the Manhattan distance of the + * current board is the bound. * * The goal closes the path. Length counts states including the start, so * branch and bound minimizes moves by minimizing length minus one. @@ -285,6 +296,19 @@ class OpenPuzzle : public Script { result[j] = tiles[16*i+j]; return result; } + int + depth(void) const { + int i = 0; + while (i+1 < path.known_length() && path[i+1].assigned()) + i++; + return i; + } + int + bound(void) const { + const int g = depth(); + const int h = manhattan(values(state(g))); + return std::max(g+h,path.length().min()-1); + } void constrain(const Space& solution) override { const OpenPuzzle& best = static_cast(solution); @@ -363,6 +387,12 @@ class FixedPuzzle : public Script { i++; return i; } + int + bound(void) const { + const int g = depth(); + const int h = manhattan(values(state(g))); + return std::max(g+h,length.min()-1); + } void constrain(const Space& solution) override { const FixedPuzzle& best = static_cast(solution); @@ -377,6 +407,32 @@ class FixedPuzzle : public Script { } }; +template +static void +run(PuzzleOptions& opt) { + switch (opt.search()) { + case PuzzleOptions::SEARCH_BAB: + Script::run(opt); + break; +#ifdef GECODE_HAS_EXPERIMENTAL + case PuzzleOptions::SEARCH_ASTAR: + case PuzzleOptions::SEARCH_CABS: + { + Search::HeuristicGuidance guidance(Search::HeuristicGuidance::MINIMIZE, + [](const Space& home) -> std::int64_t { + return static_cast(home).bound(); + }); + opt.guidance(&guidance); + if (opt.search() == PuzzleOptions::SEARCH_ASTAR) + Script::run(opt); + else + Script::run(opt); + } + break; +#endif + } +} + /** \brief Main-function * \relates OpenPuzzle */ @@ -389,9 +445,9 @@ main(int argc, char* argv[]) { print_board(std::cout,initial_board(opt)); } if (opt.model() == PuzzleOptions::MODEL_OPEN) - Script::run(opt); + run(opt); else - Script::run(opt); + run(opt); return 0; } diff --git a/examples/tsptw.cpp b/examples/tsptw.cpp new file mode 100644 index 0000000000..6f6e7f5941 --- /dev/null +++ b/examples/tsptw.cpp @@ -0,0 +1,306 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main author: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#include +#include +#include + +#include + +using namespace Gecode; + +/// Instances for TSPTW +namespace { + + /// TSPTW instance + class Instance { + public: + /// Number of nodes, node 0 is the depot + int n; + /// Travel times + const int* d; + /// Ready and due time for each node + const int* w; + }; + + /// Instance rbg010a (Ascheuer) + const int PA_n = 11; + const int PA_d[PA_n*PA_n] = { + 0,0,0,0,0,0,0,0,0,0,0, + 71,0,85,85,85,96,85,85,97,85,77, + 50,65,0,77,77,62,77,77,62,77,74, + 64,74,88,0,88,80,88,88,82,88,87, + 44,54,68,68,0,60,68,68,62,68,67, + 51,75,51,51,51,0,51,51,80,51,65, + 53,64,78,78,78,68,0,78,69,78,76, + 51,67,69,69,69,75,69,0,75,69,63, + 43,61,71,71,71,43,71,71,0,71,68, + 53,64,78,78,78,68,78,78,69,0,80, + 42,66,52,52,52,70,52,52,70,52,0 + }; + const int PA_w[2*PA_n] = { + 0,9396, 0,865, 399,1299, 550,1450, 685,1585, 819,1719, + 1208,2108, 1412,2312, 1701,2330, 2413,3313, 3798,4698 + }; + + /// Instance n20w20.001 (Dumas et al.) + const int PB_n = 21; + const int PB_d[PB_n*PB_n] = { + 0,19,17,34,7,20,10,17,28,15,23,29,23,29,21,20,9,16,21,13,12, + 19,0,10,41,26,3,27,25,15,17,17,14,18,48,17,6,21,14,17,13,31, + 17,10,0,47,23,13,26,15,25,22,26,24,27,44,7,5,23,21,25,18,29, + 34,41,47,0,36,39,25,51,36,24,27,38,25,44,54,45,25,28,26,28,27, + 7,26,23,36,0,27,11,17,35,22,30,36,30,22,25,26,14,23,28,20,10, + 20,3,13,39,27,0,26,27,12,15,14,11,15,49,20,9,20,11,14,11,30, + 10,27,26,25,11,26,0,26,31,14,23,32,22,25,31,28,6,17,21,15,4, + 17,25,15,51,17,27,26,0,39,31,38,38,38,34,13,20,26,31,36,28,27, + 28,15,25,36,35,12,31,39,0,17,9,2,11,56,32,21,24,13,11,15,35, + 15,17,22,24,22,15,14,31,17,0,9,18,8,39,29,21,8,4,7,4,18, + 23,17,26,27,30,14,23,38,9,9,0,11,2,48,33,23,17,7,2,10,27, + 29,14,24,38,36,11,32,38,2,18,11,0,13,57,31,20,25,14,13,17,36, + 23,18,27,25,30,15,22,38,11,8,2,13,0,47,34,24,16,7,2,10,26, + 29,48,44,44,22,49,25,34,56,39,48,57,47,0,46,48,31,42,46,40,21, + 21,17,7,54,25,20,31,13,32,29,33,31,34,46,0,11,29,28,32,25,33, + 20,6,5,45,26,9,28,20,21,21,23,20,24,48,11,0,23,19,22,17,32, + 9,21,23,25,14,20,6,26,24,8,17,25,16,31,29,23,0,11,15,9,10, + 16,14,21,28,23,11,17,31,13,4,7,14,7,42,28,19,11,0,5,3,21, + 21,17,25,26,28,14,21,36,11,7,2,13,2,46,32,22,15,5,0,8,25, + 13,13,18,28,20,11,15,28,15,4,10,17,10,40,25,17,9,3,8,0,19, + 12,31,29,27,10,30,4,27,35,18,27,36,26,21,33,32,10,21,25,19,0 + }; + const int PB_w[2*PB_n] = { + 0,408, 62,68, 181,205, 306,324, 214,217, 51,61, + 102,129, 175,186, 250,263, 3,23, 21,49, 79,90, + 78,96, 140,154, 354,386, 42,63, 2,13, 24,42, + 20,33, 9,21, 275,300 + }; + + /// The instances + const Instance instances[] = { + {PA_n, PA_d, PA_w}, + {PB_n, PB_d, PB_w} + }; + /// Number of instances + const unsigned int n_instances = sizeof(instances)/sizeof(Instance); + +} + +/** + * \brief %Example: Travelling salesperson with time windows + * + * Starting and ending at the depot, visit every customer once such that + * service at each customer starts within its time window (waiting is + * allowed). Minimize the total travel time. The instances are from + * López-Ibáñez and + * Blum's collection. + * + * The tour is built from the depot one customer at a time. With the + * experimental heuristic search engines, the minimal total travel time + * is the bound. CABS also uses dominance: a partial tour dominates + * another if it has visited the same customers, ends at the same + * customer, and is neither later nor more expensive. + * + * \ingroup Example + */ +class TSPTW : public IntMinimizeScript { +protected: + /// The instance + const Instance& p; + /// Customer at each position of the tour + IntVarArray x; + /// Position of each customer + IntVarArray pos; + /// Travel time to each position, and back to the depot + IntVarArray c; + /// Start of service at each position + IntVarArray t; + /// Total travel time + IntVar total; +public: + /// Search engines + enum { + SEARCH_BAB, SEARCH_ASTAR, SEARCH_CABS + }; + /// The actual problem + TSPTW(const SizeOptions& opt) + : IntMinimizeScript(opt), p(instances[opt.size()]), + x(*this,p.n-1,1,p.n-1), pos(*this,p.n-1,0,p.n-2), + c(*this,p.n,0,Int::Limits::max), + t(*this,p.n-1,0,p.w[1]), total(*this,0,Int::Limits::max) { + int n = p.n; + IntArgs d(n*n,p.d), from(n), to(n), ready(n), due(n); + for (int i=0; i due[j])) + rel(*this, pos[j-1] < pos[k-1]); + element(*this, from, x[0], c[0]); + rel(*this, t[0] == max(element(ready,x[0]), c[0])); + for (int i=1; i 0) && (k == s.prefix()) && + (x[k-1].val() == s.x[k-1].val()) && (visited(k) == s.visited(k)) && + (t[k-1].val() <= s.t[k-1].val()) && + (prefix_cost(k) <= s.prefix_cost(k)); + } + /// Return a key for dominance + std::size_t key(void) const { + int k = prefix(); + std::size_t h = static_cast(visited(k)); + cmb_hash(h, k > 0 ? x[k-1].val() : 0); + return h; + } + /// Constructor for cloning \a s + TSPTW(TSPTW& s) : IntMinimizeScript(s), p(s.p) { + x.update(*this, s.x); + pos.update(*this, s.pos); + c.update(*this, s.c); + t.update(*this, s.t); + total.update(*this, s.total); + } + /// Copy during cloning + virtual Space* copy(void) { + return new TSPTW(*this); + } + /// Print solution + virtual void print(std::ostream& os) const { + os << "\tTour: 0"; + for (int i=0; i " << x[i]; + os << " -> 0" << std::endl + << "\tTravel time: " << total << std::endl; + } +}; + +/** \brief Main-function + * \relates TSPTW + */ +int +main(int argc, char* argv[]) { + SizeOptions opt("TSPTW"); + opt.size(0); + opt.solutions(0); + opt.search(TSPTW::SEARCH_BAB); + opt.search(TSPTW::SEARCH_BAB, "bab", "branch and bound"); +#ifdef GECODE_HAS_EXPERIMENTAL + opt.search(TSPTW::SEARCH_ASTAR, "astar", "A*"); + opt.search(TSPTW::SEARCH_CABS, "cabs", "complete anytime beam search"); +#endif + opt.parse(argc,argv); + if (opt.size() >= n_instances) { + std::cerr << "Error: size must be between 0 and " + << n_instances-1 << std::endl; + return 1; + } + switch (opt.search()) { + case TSPTW::SEARCH_BAB: + IntMinimizeScript::run(opt); + break; +#ifdef GECODE_HAS_EXPERIMENTAL + case TSPTW::SEARCH_ASTAR: + case TSPTW::SEARCH_CABS: + { + Search::HeuristicGuidance g(Search::HeuristicGuidance::MINIMIZE, + [](const Space& s) -> std::int64_t { + return static_cast(s).cost().min(); + }); + g.dominates = [](const Space& a, const Space& b) { + return static_cast(a) + .dominates(static_cast(b)); + }; + g.dominance_key = [](const Space& s) { + return static_cast(s).key(); + }; + opt.guidance(&g); + if (opt.search() == TSPTW::SEARCH_ASTAR) + IntMinimizeScript::run(opt); + else + IntMinimizeScript::run(opt); + } + break; +#endif + } + return 0; +} + +// STATISTICS: example-any diff --git a/gecode.m4 b/gecode.m4 index 666e3a1817..a78152fcc9 100755 --- a/gecode.m4 +++ b/gecode.m4 @@ -1417,6 +1417,31 @@ AC_DEFUN([AC_GECODE_GIST], fi ]) +dnl +dnl Macro: +dnl AC_GECODE_EXPERIMENTAL +dnl +dnl Description: +dnl Produces the configure switch --enable-experimental +dnl for compiling experimental preview APIs. +dnl +dnl Authors: +dnl Mikael Zayenz Lagerkvist +AC_DEFUN([AC_GECODE_EXPERIMENTAL], + [ + AC_ARG_ENABLE([experimental], + AS_HELP_STRING([--enable-experimental], + [build experimental preview APIs @<:@default=no@:>@]), + [], [enable_experimental=no]) + AC_MSG_CHECKING(whether to build experimental preview APIs) + AS_CASE([$enable_experimental], + [yes], [AC_DEFINE([GECODE_HAS_EXPERIMENTAL],[],[Whether experimental preview APIs are available])], + [no], [], + [AC_MSG_ERROR([--enable-experimental accepts only yes or no])]) + AC_MSG_RESULT([$enable_experimental]) + AC_SUBST([enable_experimental]) +]) + dnl dnl Macro: dnl AC_GECODE_CBS diff --git a/gecode/driver.hh b/gecode/driver.hh index 409968d979..75733bd80b 100755 --- a/gecode/driver.hh +++ b/gecode/driver.hh @@ -446,6 +446,10 @@ namespace Gecode { Driver::UnsignedIntOption _nogoods_limit; ///< Limit for no-good extraction Driver::DoubleOption _relax; ///< Probability to relax variable Driver::BoolOption _interrupt; ///< Whether to catch SIGINT +#ifdef GECODE_HAS_EXPERIMENTAL + /// Guidance for heuristic search + const Search::HeuristicGuidance* _guidance; +#endif //@} /// \name Execution options @@ -668,6 +672,16 @@ namespace Gecode { #endif //@} +#ifdef GECODE_HAS_EXPERIMENTAL + /// \name Heuristic search options + //@{ + /// Set guidance for heuristic search (not copied) + void guidance(const Search::HeuristicGuidance* g); + /// Return guidance for heuristic search + const Search::HeuristicGuidance* guidance(void) const; + //@} +#endif + #ifdef GECODE_HAS_GIST /// Helper class storing Gist inspectors class I_ { diff --git a/gecode/driver/options.cpp b/gecode/driver/options.cpp index 5c6c1fbf87..4d0d60b636 100755 --- a/gecode/driver/options.cpp +++ b/gecode/driver/options.cpp @@ -650,6 +650,9 @@ namespace Gecode { _interrupt("interrupt","whether to catch Ctrl-C (true) or not (false)", true), +#ifdef GECODE_HAS_EXPERIMENTAL + _guidance(nullptr), +#endif _mode("mode","how to execute script",SM_SOLUTION), _samples("samples","how many samples (time mode)",1), _iterations("iterations","iterations per sample (time mode)",1), diff --git a/gecode/driver/options.hpp b/gecode/driver/options.hpp index 2aa4c337c4..d87aeee3fc 100755 --- a/gecode/driver/options.hpp +++ b/gecode/driver/options.hpp @@ -326,6 +326,17 @@ namespace Gecode { return _threads.value(); } +#ifdef GECODE_HAS_EXPERIMENTAL + inline void + Options::guidance(const Search::HeuristicGuidance* g) { + _guidance = g; + } + inline const Search::HeuristicGuidance* + Options::guidance(void) const { + return _guidance; + } +#endif + inline void Options::c_d(unsigned int d) { _c_d.value(d); diff --git a/gecode/driver/script.hpp b/gecode/driver/script.hpp index ccbc287af0..1c8f707088 100755 --- a/gecode/driver/script.hpp +++ b/gecode/driver/script.hpp @@ -388,6 +388,9 @@ namespace Gecode { namespace Driver { so.stop = CombinedStop::create(o.node(),o.fail(), o.time(), o.restart_limit(), o.interrupt()); so.cutoff = createCutoff(o); +#ifdef GECODE_HAS_EXPERIMENTAL + so.guidance = o.guidance(); +#endif so.clone = false; so.nogoods_limit = o.nogoods() ? o.nogoods_limit() : 0U; if (o.interrupt()) @@ -491,6 +494,9 @@ namespace Gecode { namespace Driver { so.stop = CombinedStop::create(o.node(),o.fail(), o.time(), o.restart_limit(), o.interrupt()); so.cutoff = createCutoff(o); +#ifdef GECODE_HAS_EXPERIMENTAL + so.guidance = o.guidance(); +#endif so.nogoods_limit = o.nogoods() ? o.nogoods_limit() : 0U; if (o.interrupt()) CombinedStop::installCtrlHandler(true); @@ -553,6 +559,9 @@ namespace Gecode { namespace Driver { sok.stop = CombinedStop::create(o.node(),o.fail(), o.time(), o.restart_limit(), false); sok.cutoff = createCutoff(o); +#ifdef GECODE_HAS_EXPERIMENTAL + sok.guidance = o.guidance(); +#endif sok.nogoods_limit = o.nogoods() ? o.nogoods_limit() : 0U; { Meta e(s1,sok); diff --git a/gecode/search.hh b/gecode/search.hh index 13dfd9eec3..b5a061feab 100755 --- a/gecode/search.hh +++ b/gecode/search.hh @@ -47,6 +47,10 @@ #include #include #include +#ifdef GECODE_HAS_EXPERIMENTAL +#include +#include +#endif #include @@ -749,6 +753,9 @@ namespace Gecode { namespace Search { }; class Stop; +#ifdef GECODE_HAS_EXPERIMENTAL + class HeuristicGuidance; +#endif /** * \brief %Search engine options @@ -818,6 +825,14 @@ namespace Gecode { namespace Search { Cutoff* cutoff; /// Tracer object for tracing search SearchTracer* tracer; +#ifdef GECODE_HAS_EXPERIMENTAL + /// Guidance for heuristic search (AStar and CABS) + const HeuristicGuidance* guidance; + /// Maximal beam width for CABS (zero for no limit) + unsigned long int max_width; + /// Maximal number of stored nodes for CABS (zero for no limit) + unsigned long int frontier_limit; +#endif /// Default options GECODE_SEARCH_EXPORT static const Options def; /// Initialize with default values @@ -1152,6 +1167,128 @@ namespace Gecode { #include +#ifdef GECODE_HAS_EXPERIMENTAL +namespace Gecode { namespace Search { + + /** + * \brief Guidance for heuristic search engines (experimental) + * + * The bound is an optimistic objective value for a stable space: a lower + * bound when minimizing, an upper bound when maximizing, and the exact + * objective for a solution. It must agree with the model's constrain + * function. The optional rank orders nodes (lower first) but is never + * used for pruning. + * + * The optional dominance predicate returns whether every completion of + * the second space is matched by a completion of the first that is not + * worse. It must be transitive. CABS uses it to prune nodes in the same + * layer. If a dominance key is given, only nodes with equal keys are + * compared. + * + * Callbacks must only read the space passed to them. With several threads + * they can run concurrently. + * \ingroup TaskModelSearch + */ + class HeuristicGuidance { + public: + /// Objective direction + enum Sense { MINIMIZE, MAXIMIZE }; + /// Function evaluating a stable space + typedef std::function Evaluate; + /// Objective direction + Sense sense; + /// Optimistic objective bound (required) + Evaluate bound; + /// Node rank (optional, lower first) + Evaluate rank; + /// Whether the first space dominates the second (optional) + std::function dominates; + /// Dominance key (optional) + std::function dominance_key; + /// Initialize guidance + HeuristicGuidance(Sense s, Evaluate b, Evaluate r=Evaluate()) + : sense(s), bound(std::move(b)), rank(std::move(r)) {} + }; + +}} + +namespace Gecode { + + /** + * \brief A* search engine (experimental) + * + * Expands nodes in order of their bound, then their rank, using the + * guidance from Search::Options::guidance. Like BAB, next() returns + * improving solutions and exhausting the search proves the last + * solution optimal. + * + * All open nodes are stored as clones, so c_d and a_d are ignored. + * Tracing is not supported. + * \ingroup TaskModelSearch + */ + template + class AStar : public Search::Base { + public: + /// Initialize engine for space \a s and options \a o + AStar(T* s, const Search::Options& o=Search::Options::def); + /// Whether engine does best solution search + static const bool best = true; + }; + + /// Perform A* search for subclass \a T of space \a s and options \a o + template + T* astar(T* s, const Search::Options& o=Search::Options::def); + + /// Return an A* search engine builder + template + SEB astar(const Search::Options& o=Search::Options::def); + +} + +#include + +namespace Gecode { + + /** + * \brief Complete anytime beam search engine (experimental) + * + * Explores the tree layer by layer and keeps the best nodes of each + * layer, ordered by rank or, without rank, by bound. The first pass + * keeps one node per layer. If a pass discarded nodes that could + * improve the incumbent, the search restarts with twice the width. + * The guidance is taken from Search::Options::guidance. Like BAB, + * next() returns improving solutions and exhausting the search proves + * the last solution optimal. + * + * Search stops when the width would exceed Search::Options::max_width or + * the stored nodes would exceed Search::Options::frontier_limit. Nodes are + * stored as clones, so c_d and a_d are ignored. Tracing is not supported. + * Statistics::restart counts the restarts. + * \ingroup TaskModelSearch + */ + template + class CABS : public Search::Base { + public: + /// Initialize engine for space \a s and options \a o + CABS(T* s, const Search::Options& o=Search::Options::def); + /// Whether engine does best solution search + static const bool best = true; + }; + + /// Perform CABS search for subclass \a T of space \a s and options \a o + template + T* cabs(T* s, const Search::Options& o=Search::Options::def); + + /// Return a CABS search engine builder + template + SEB cabs(const Search::Options& o=Search::Options::def); + +} + +#include + +#endif + namespace Gecode { /** @@ -1349,6 +1486,11 @@ namespace Gecode { * * The outer options must not contain a worker control. Each builder can * instead supply a distinct control for its underlying engine. + * In parallel PBS, the outer thread count limits how many builders run, + * in array order; each builder's options set its own engine's thread count. + * + * With experimental search, AStar and CABS builders can be used + * together with BAB builders. */ PBS(T* s, SEBs& sebs, const Search::Options& o=Search::Options::def); /// Constrain future portfolio solutions to be better than \a b diff --git a/gecode/search/astar.cpp b/gecode/search/astar.cpp new file mode 100644 index 0000000000..9ed5eeab81 --- /dev/null +++ b/gecode/search/astar.cpp @@ -0,0 +1,247 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main authors: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + + +#include +#include +#include +#include + +#ifdef GECODE_HAS_EXPERIMENTAL + +#ifdef GECODE_HAS_THREADS +namespace Gecode { namespace Search { namespace Par { + + /// Parallel A* search + class AStar : public Frontier { + protected: + /// Open nodes + Queue queue; + bool open(void) const override { + return !queue.empty(); + } + Entry select(void) override { + return queue.pop(); + } + void submit(const Entry& e, bool) override { + if (!best.improves(e.bound)) { + stat.fail++; + delete e.space; + return; + } + Entry f = e; + f.order = order++; + queue.push(f); + } + bool advance(void) override { + return false; + } + void clear(void) override { + queue.clear(); + } + public: + /// Initialize for space \a s with options \a o + AStar(Space* s, const Options& o) + : Frontier(o), queue(Priority{guidance.sense,false}) { + initialize(s,o.clone); + } + /// Destructor + ~AStar(void) override { + terminate(); + } + }; + +}}} +#endif + +namespace Gecode { namespace Search { + + Engine* + astarengine(Space* s, const Options& o) { + Options e = o.expand(); + if (e.tracer != nullptr) + throw UnsupportedTracer("AStar::AStar"); + if ((e.guidance == nullptr) || !e.guidance->bound) + throw UninitializedGuidance("AStar::AStar"); +#ifdef GECODE_HAS_THREADS + if (e.threads > 1.0) + return new Par::AStar(s,e); +#endif + return new WorkerToEngine(s,e); + } + +}} + +namespace Gecode { namespace Search { namespace Seq { + + AStar::AStar(Space* s, const Options& o) + : opt(o), guidance(*o.guidance), queue(Priority{guidance.sense,false}), + order(0), status(SS_FAILED), depth(0), revision(0), counted(false), + best(guidance.sense) { + initialize(s,o.clone); + } + + void + AStar::initialize(Space* s, bool clone) { + if (s == nullptr) { + fail++; + return; + } + status = s->status(*this); + if (status == SS_FAILED) { + fail++; + if (!clone) + delete s; + return; + } + expansion.select(std::unique_ptr(clone ? s->clone() : s)); + } + + void + AStar::enqueue(std::unique_ptr s, unsigned long int d) { + std::int64_t b = guidance.bound(*s); + if (!best.improves(b)) { + fail++; + return; + } + std::int64_t r = guidance.rank ? guidance.rank(*s) : 0; + queue.push({s.release(),b,r,0,d,order++,best.revision}); + } + + Space* + AStar::publish(std::unique_ptr s) { + // Delete exhausted branchers before cloning the solution + (void) s->choice(); + std::int64_t b = guidance.bound(*s); + if (!best.improves(b)) + return nullptr; + Space* copy = s->clone(); + best.update(s.release(),b); + return copy; + } + + Space* + AStar::next(void) { + start(); + while (true) { + if (stop_seq(opt)) + return nullptr; + if (!expansion) { + if (queue.empty()) + return nullptr; + Entry e = queue.pop(); + std::unique_ptr s(e.space); + if (!best.improves(e.bound)) { + fail++; + continue; + } + expansion.select(std::move(s)); + depth = e.depth; + revision = e.revision; + counted = true; + } + if (!counted) { + // The root + node++; + stack_depth(depth); + counted = true; + if (status == SS_SOLVED) { + if (Space* s = publish(expansion.take())) + return s; + continue; + } + if (!best.improves(guidance.bound(expansion.space()))) { + fail++; + expansion.reset(); + continue; + } + } + ExpansionStep step = expansion.next(*this,depth+1,revision, + best.space.get(),best.revision); + if (step.state == ExpansionStep::EXHAUSTED) + continue; + switch (step.status) { + case SS_FAILED: + fail++; + break; + case SS_SOLVED: + if (Space* s = publish(std::move(step.space))) + return s; + break; + case SS_BRANCH: + enqueue(std::move(step.space),depth+1); + break; + default: + GECODE_NEVER; + } + } + } + + Statistics + AStar::statistics(void) const { + return *this; + } + + void + AStar::constrain(const Space& s) { + std::int64_t b = guidance.bound(s); + if (best.improves(b)) + best.update(s.clone(),b); + } + + void + AStar::reset(Space* s) { + std::unique_ptr replacement(s); + expansion.reset(); + queue.clear(); + best.reset(); + order = 0; + revision = 0; + depth = 0; + counted = false; + Worker::reset(); + initialize(s,false); + // The root is owned by the engine after initialize returns + (void) replacement.release(); + } + + NoGoods& + AStar::nogoods(void) { + return NoGoods::eng; + } + +}}} + +#endif + +// STATISTICS: search-other diff --git a/gecode/search/astar.hpp b/gecode/search/astar.hpp new file mode 100644 index 0000000000..544e0c6759 --- /dev/null +++ b/gecode/search/astar.hpp @@ -0,0 +1,93 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main authors: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifdef GECODE_HAS_EXPERIMENTAL +namespace Gecode { namespace Search { + + /// Create A* engine + GECODE_SEARCH_EXPORT Engine* + astarengine(Space* s, const Options& o); + + /// An A* engine builder + template + class AStarBuilder : public Builder { + using Builder::opt; + public: + /// The constructor + AStarBuilder(const Options& opt); + /// The actual build function + virtual Engine* operator() (Space* s) const; + }; + + template + inline + AStarBuilder::AStarBuilder(const Options& opt) + : Builder(opt,AStar::best) {} + + template + Engine* + AStarBuilder::operator() (Space* s) const { + return build(s,opt); + } + +}} + +namespace Gecode { + + template + inline + AStar::AStar(T* s, const Search::Options& o) + : Search::Base(Search::astarengine(s,o)) {} + + template + inline T* + astar(T* s, const Search::Options& o) { + AStar a(s,o); + T* l = nullptr; + while (T* n = a.next()) { + delete l; l = n; + } + return l; + } + + template + SEB + astar(const Search::Options& o) { + return new Search::AStarBuilder(o); + } + +} + +#endif + +// STATISTICS: search-other diff --git a/gecode/search/cabs.cpp b/gecode/search/cabs.cpp new file mode 100644 index 0000000000..8438f319a8 --- /dev/null +++ b/gecode/search/cabs.cpp @@ -0,0 +1,330 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main authors: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + + +#include +#include +#include +#include + +#ifdef GECODE_HAS_EXPERIMENTAL + +#ifdef GECODE_HAS_THREADS +namespace Gecode { namespace Search { namespace Par { + + /// Parallel complete anytime beam search + class CABS : public Frontier { + protected: + /// Order of nodes in a layer + Priority priority; + /// Root for restarting with a wider beam + std::unique_ptr root; + /// Nodes of the current layer, preferred node on top + Entries layer; + /// Candidates for the next layer + Beam candidates; + bool open(void) const override { + return !layer.empty(); + } + Entry select(void) override { + return layer.pop(); + } + void submit(const Entry& e, bool parent) override { + if (!best.improves(e.bound)) { + stat.fail++; + delete e.space; + return; + } + Entry f = e; + f.order = order++; + // Every other worker holds a parent and at most one child + unsigned long int stored = layer.entries() + candidates.size() + + n_pending + 2*(active-1) + (parent ? 1 : 0); + bool room = (opt.frontier_limit == 0UL) || (stored < opt.frontier_limit); + if (candidates.offer(f,guidance,room) == Beam::FULL) { + delete f.space; + limited = _stopped = true; + running = false; + } + } + bool advance(void) override { + if (!candidates.empty()) { + candidates.release(layer); + layer.sort(priority); + return true; + } + if (!candidates.improving(best)) + return false; + if (!candidates.widen(opt.max_width)) { + limited = _stopped = true; + return false; + } + stat.restart++; + std::unique_ptr t(new Task); + t->expansion.select(std::unique_ptr(root->clone())); + push(t.release()); + return true; + } + void clear(void) override { + layer.clear(); + candidates.clear(); + root.reset(); + } + void started(const Space& r) override { + root.reset(r.clone()); + } + public: + /// Initialize for space \a s with options \a o + CABS(Space* s, const Options& o) + : Frontier(o), priority{guidance.sense,static_cast(guidance.rank)}, + candidates(priority,guidance.dominates && guidance.dominance_key) { + batch = (o.frontier_limit == 0UL) ? 8 : 1; + keyed = guidance.dominates && guidance.dominance_key; + initialize(s,o.clone); + } + /// Destructor + ~CABS(void) override { + terminate(); + } + }; + +}}} +#endif + +namespace Gecode { namespace Search { + + Engine* + cabsengine(Space* s, const Options& o) { + Options e = o.expand(); + if (e.tracer != nullptr) + throw UnsupportedTracer("CABS::CABS"); + if ((e.guidance == nullptr) || !e.guidance->bound) + throw UninitializedGuidance("CABS::CABS"); +#ifdef GECODE_HAS_THREADS + if (e.threads > 1.0) + return new Par::CABS(s,e); +#endif + return new WorkerToEngine(s,e); + } + +}} + +namespace Gecode { namespace Search { namespace Seq { + + CABS::CABS(Space* s, const Options& o) + : opt(o), guidance(*o.guidance), + priority{guidance.sense,static_cast(guidance.rank)}, + candidates(priority,guidance.dominates && guidance.dominance_key), + order(0), status(SS_FAILED), depth(0), revision(0), counted(false), + best(guidance.sense), limited(false) { + initialize(s,o.clone); + } + + void + CABS::initialize(Space* s, bool clone) { + if (s == nullptr) { + fail++; + return; + } + status = s->status(*this); + if (status == SS_FAILED) { + fail++; + if (!clone) + delete s; + return; + } + std::unique_ptr r(clone ? s->clone() : nullptr); + expansion.select(std::unique_ptr((clone ? r.get() : s)->clone())); + root.reset(clone ? r.release() : s); + } + + bool + CABS::admit(std::unique_ptr s, unsigned long int d) { + std::int64_t b = guidance.bound(*s); + if (!best.improves(b)) { + fail++; + return true; + } + std::int64_t r = guidance.rank ? guidance.rank(*s) : 0; + std::size_t k = (guidance.dominates && guidance.dominance_key) ? + guidance.dominance_key(*s) : 0; + unsigned long int stored = layer.entries() + candidates.size() + + (expansion ? 1 : 0); + bool room = (opt.frontier_limit == 0UL) || (stored < opt.frontier_limit); + Entry e{s.get(),b,r,k,d,order++,best.revision}; + if (candidates.offer(e,guidance,room) == Beam::FULL) { + limited = _stopped = true; + return false; + } + (void) s.release(); + return true; + } + + Space* + CABS::publish(std::unique_ptr s) { + // Delete exhausted branchers before cloning the solution + (void) s->choice(); + std::int64_t b = guidance.bound(*s); + if (!best.improves(b)) + return nullptr; + Space* copy = s->clone(); + best.update(s.release(),b); + return copy; + } + + bool + CABS::advance(void) { + if (!candidates.empty()) { + candidates.release(layer); + layer.sort(priority); + return true; + } + if (!candidates.improving(best)) + return false; + if (!candidates.widen(opt.max_width)) { + limited = _stopped = true; + return false; + } + restart++; + expansion.select(std::unique_ptr(root->clone())); + status = SS_BRANCH; + depth = 0; + revision = 0; + counted = false; + return true; + } + + Space* + CABS::next(void) { + if (limited) { + _stopped = true; + return nullptr; + } + start(); + while (true) { + if (stop_seq(opt)) + return nullptr; + if (!expansion) { + if (layer.empty() && !advance()) + return nullptr; + if (!expansion) { + Entry e = layer.pop(); + std::unique_ptr s(e.space); + if (!best.improves(e.bound)) { + fail++; + continue; + } + expansion.select(std::move(s)); + depth = e.depth; + revision = e.revision; + counted = true; + } + } + if (!counted) { + // The root of a pass + node++; + stack_depth(depth); + counted = true; + if (status == SS_SOLVED) { + if (Space* s = publish(expansion.take())) + return s; + continue; + } + if (!best.improves(guidance.bound(expansion.space()))) { + fail++; + expansion.reset(); + continue; + } + } + ExpansionStep step = expansion.next(*this,depth+1,revision, + best.space.get(),best.revision); + if (step.state == ExpansionStep::EXHAUSTED) + continue; + switch (step.status) { + case SS_FAILED: + fail++; + break; + case SS_SOLVED: + if (Space* s = publish(std::move(step.space))) + return s; + break; + case SS_BRANCH: + if (!admit(std::move(step.space),depth+1)) + return nullptr; + break; + default: + GECODE_NEVER; + } + } + } + + Statistics + CABS::statistics(void) const { + return *this; + } + + void + CABS::constrain(const Space& s) { + std::int64_t b = guidance.bound(s); + if (best.improves(b)) + best.update(s.clone(),b); + } + + void + CABS::reset(Space* s) { + std::unique_ptr replacement(s); + expansion.reset(); + layer.clear(); + candidates.clear(); + root.reset(); + best.reset(); + order = 0; + revision = 0; + depth = 0; + counted = limited = false; + Worker::reset(); + initialize(s,false); + // The root is owned by the engine after initialize returns + (void) replacement.release(); + } + + NoGoods& + CABS::nogoods(void) { + return NoGoods::eng; + } + +}}} + +#endif + +// STATISTICS: search-other diff --git a/gecode/search/cabs.hpp b/gecode/search/cabs.hpp new file mode 100644 index 0000000000..96f7fd83df --- /dev/null +++ b/gecode/search/cabs.hpp @@ -0,0 +1,93 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main authors: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifdef GECODE_HAS_EXPERIMENTAL +namespace Gecode { namespace Search { + + /// Create CABS engine + GECODE_SEARCH_EXPORT Engine* + cabsengine(Space* s, const Options& o); + + /// A CABS engine builder + template + class CABSBuilder : public Builder { + using Builder::opt; + public: + /// The constructor + CABSBuilder(const Options& opt); + /// The actual build function + virtual Engine* operator() (Space* s) const; + }; + + template + inline + CABSBuilder::CABSBuilder(const Options& opt) + : Builder(opt,CABS::best) {} + + template + Engine* + CABSBuilder::operator() (Space* s) const { + return build(s,opt); + } + +}} + +namespace Gecode { + + template + inline + CABS::CABS(T* s, const Search::Options& o) + : Search::Base(Search::cabsengine(s,o)) {} + + template + inline T* + cabs(T* s, const Search::Options& o) { + CABS c(s,o); + T* l = nullptr; + while (T* n = c.next()) { + delete l; l = n; + } + return l; + } + + template + SEB + cabs(const Search::Options& o) { + return new Search::CABSBuilder(o); + } + +} + +#endif + +// STATISTICS: search-other diff --git a/gecode/search/exception.cpp b/gecode/search/exception.cpp index 7b36ff7f01..8d99738996 100644 --- a/gecode/search/exception.cpp +++ b/gecode/search/exception.cpp @@ -34,6 +34,13 @@ #include namespace Gecode { namespace Search { +#ifdef GECODE_HAS_EXPERIMENTAL + UninitializedGuidance::UninitializedGuidance(const char* l) + : Exception(l,"Heuristic guidance is missing") {} + + UnsupportedTracer::UnsupportedTracer(const char* l) + : Exception(l,"Search engine does not support tracing") {} +#endif UninitializedCutoff::UninitializedCutoff(const char* l) : Exception(l,"Cutoff for restart-based search is missing") {} diff --git a/gecode/search/exception.hpp b/gecode/search/exception.hpp index 26a6c0e6e9..e28c3fd9ce 100644 --- a/gecode/search/exception.hpp +++ b/gecode/search/exception.hpp @@ -80,6 +80,21 @@ namespace Gecode { namespace Search { /// Initialize with location \a l WorkerControlInUse(const char* l); }; +#ifdef GECODE_HAS_EXPERIMENTAL + /// %Exception: Missing guidance for heuristic search + class GECODE_SEARCH_EXPORT UninitializedGuidance : public Exception { + public: + /// Initialize with location \a l + UninitializedGuidance(const char* l); + }; + /// %Exception: Search engine does not support tracing + class GECODE_SEARCH_EXPORT UnsupportedTracer : public Exception { + public: + /// Initialize with location \a l + UnsupportedTracer(const char* l); + }; +#endif + //@} }} diff --git a/gecode/search/heuristic.hh b/gecode/search/heuristic.hh new file mode 100644 index 0000000000..87fde7c609 --- /dev/null +++ b/gecode/search/heuristic.hh @@ -0,0 +1,397 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main authors: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef GECODE_SEARCH_HEURISTIC_HH +#define GECODE_SEARCH_HEURISTIC_HH + +#include +#include +#include +#include +#include + +#ifdef GECODE_HAS_EXPERIMENTAL +namespace Gecode { namespace Search { + + /// Whether bound \a a is better than bound \a b for objective direction \a s + forceinline bool + better(HeuristicGuidance::Sense s, std::int64_t a, std::int64_t b) { + return (s == HeuristicGuidance::MINIMIZE) ? (a < b) : (a > b); + } + + /// Open node of a heuristic search engine + class Entry { + public: + /// The stable space (owned by the container holding the entry) + Space* space; + /// Bound and rank of the space + std::int64_t bound, rank; + /// Dominance key of the space + std::size_t key; + /// Depth of the node + unsigned long int depth; + /// Creation order, for breaking ties + unsigned long long int order; + /// Incumbent revision the space has been constrained with + unsigned long long int revision; + }; + + /// Order of open nodes: whether \a a is preferred to \a b + class Priority { + public: + /// Objective direction + HeuristicGuidance::Sense sense; + /// Whether to order by rank only (CABS with rank) + bool rank_only; + /// Test whether \a a is preferred to \a b + bool operator ()(const Entry& a, const Entry& b) const { + if (!rank_only && (a.bound != b.bound)) + return better(sense,a.bound,b.bound); + if (a.rank != b.rank) + return a.rank < b.rank; + return a.order < b.order; + } + }; + + /// Best solution found so far + class Incumbent { + public: + /// Objective direction + HeuristicGuidance::Sense sense; + /// The solution (if any) + std::unique_ptr space; + /// Its objective value + std::int64_t bound; + /// Incremented for every new incumbent + unsigned long long int revision; + /// Initialize without incumbent + Incumbent(HeuristicGuidance::Sense s) + : sense(s), bound(0), revision(0) {} + /// Whether a node with bound \a b can improve the incumbent + bool improves(std::int64_t b) const { + return !space || better(sense,b,bound); + } + /// Make \a s with objective value \a b the incumbent + void update(Space* s, std::int64_t b) { + space.reset(s); bound = b; revision++; + } + /// Remove incumbent + void reset(void) { + space.reset(); revision = 0; + } + }; + + /// Stack of entries owning their spaces + class Entries : public Support::DynamicStack { + public: + /// Initialize + Entries(void) : Support::DynamicStack(heap) {} + /// Delete all entries + void clear(void) { + while (!empty()) + delete pop().space; + } + /// Release memory + ~Entries(void) { clear(); } + /// Sort such that the preferred entry is on top + void sort(const Priority& p) { + if (entries() > 1) + std::sort(&(*this)[0],&(*this)[0]+entries(), + [&](const Entry& a, const Entry& b) { return p(b,a); }); + } + }; + + /// Priority queue of open nodes for A* + class Queue { + protected: + /// The entries as a binary heap with the preferred entry first + Entries entries; + /// The priority + Priority p; + public: + /// Initialize with priority \a p + Queue(const Priority& p0) : p(p0) {} + /// Whether the queue is empty + bool empty(void) const { return entries.empty(); } + /// Add entry \a e + void push(const Entry& e) { + entries.push(e); + Entry* f = &entries[0]; + std::push_heap(f,f+entries.entries(), + [this](const Entry& a, const Entry& b) { return p(b,a); }); + } + /// Remove and return the preferred entry + Entry pop(void) { + Entry* f = &entries[0]; + std::pop_heap(f,f+entries.entries(), + [this](const Entry& a, const Entry& b) { return p(b,a); }); + return entries.pop(); + } + /// Delete all entries + void clear(void) { entries.clear(); } + }; + + /** + * \brief Candidates for the next layer of CABS + * + * Keeps at most as many entries as the current width, with the least + * preferred one on top. + * Entries are stored in fixed slots so that slots with the same + * dominance key can be chained for dominance checks. + */ + class Beam { + public: + /// Result of offering an entry + enum Offer { + TAKEN, ///< The entry has been added or deleted + FULL ///< The entry needs a new slot but there is no room + }; + private: + /// Slot of an entry + class Slot { + public: + /// The entry + Entry e; + /// Position in heap (-1 for a free slot) + int pos; + /// Next and previous slot in bucket (or next free slot) + int next, prev; + }; + /// The slots + Support::DynamicArray slot; + /// Number of slots used so far and first free slot + int n_slot, free; + /// Heap of slot indices with the least preferred entry first + Support::DynamicArray pq; + /// Number of entries + int n; + /// First slot of each bucket for keyed dominance + Support::DynamicArray bucket; + /// Number of buckets (a power of two, zero if not keyed) + int n_bucket; + /// The priority + Priority p; + /// The current width + unsigned long int _width; + /// Best bound of discarded entries (if any) + std::optional discarded; + + /// Return bucket for key \a k + int index(std::size_t k) const { + return static_cast((k ^ (k >> 16)) & (n_bucket-1)); + } + /// Add slot \a s to its bucket + void link(int s) { + int b = index(slot[s].e.key); + slot[s].prev = -1; slot[s].next = bucket[b]; + if (bucket[b] >= 0) + slot[bucket[b]].prev = s; + bucket[b] = s; + } + /// Remove slot \a s from its bucket + void unlink(int s) { + Slot& x = slot[s]; + if (x.prev >= 0) + slot[x.prev].next = x.next; + else + bucket[index(x.e.key)] = x.next; + if (x.next >= 0) + slot[x.next].prev = x.prev; + } + /// Whether slot \a a must be above slot \a b in the heap + bool above(int a, int b) const { + return p(slot[b].e,slot[a].e); + } + /// Put slot \a s at heap position \a i + void place(int i, int s) { + pq[i] = s; slot[s].pos = i; + } + /// Restore heap order for slot at position \a i + void repair(int i) { + int s = pq[i]; + while ((i > 0) && above(s,pq[(i-1)/2])) { + place(i,pq[(i-1)/2]); i = (i-1)/2; + } + while (2*i+1 < n) { + int c = 2*i+1; + if ((c+1 < n) && above(pq[c+1],pq[c])) + c++; + if (!above(pq[c],s)) + break; + place(i,pq[c]); i = c; + } + place(i,s); + } + /// Add entry \a e + void add(const Entry& e) { + int s; + if (free >= 0) { + s = free; free = slot[s].next; + } else { + s = n_slot++; + } + slot[s].e = e; + if (n_bucket > 0) { + if (n >= n_bucket) { + n_bucket *= 2; + for (int b=0; b 0) + unlink(s); + slot[s].pos = -1; slot[s].next = free; free = s; + if (i < --n) { + place(i,pq[n]); repair(i); + } + } + /// Whether a dominance check between \a e and slot \a s is needed + bool comparable(const Entry& e, int s) const { + return (slot[s].pos >= 0) && ((n_bucket == 0) || (slot[s].e.key == e.key)); + } + /// Return first slot to check for dominance with \a e + int chain_first(const Entry& e) const { + return (n_bucket > 0) ? bucket[index(e.key)] : 0; + } + /// Return slot after \a s to check for dominance + int chain_next(int s) const { + if (n_bucket > 0) + return slot[s].next; + return (s+1 < n_slot) ? s+1 : -1; + } + public: + /// Initialize with priority \a p, using keys for dominance if \a keyed + Beam(const Priority& p0, bool keyed) + : slot(heap), n_slot(0), free(-1), pq(heap), n(0), bucket(heap), + n_bucket(keyed ? 16 : 0), p(p0), _width(1) { + for (int b=0; b ULONG_MAX/2) || ((m > 0UL) && (2*_width > m))) + return false; + _width *= 2; discarded.reset(); + return true; + } + /** + * \brief Offer entry \a e + * + * Deletes \a e if a candidate dominates it or if the beam is full and + * all candidates are preferred. Deletes candidates that \a e dominates + * and the least preferred candidate if \a e replaces it. Returns FULL + * without deleting \a e if \a e needs a new slot and \a room is false. + */ + Offer offer(const Entry& e, const HeuristicGuidance& g, bool room) { + if (g.dominates && (n > 0)) { + for (int s=chain_first(e); s>=0; s=chain_next(s)) + if (comparable(e,s) && g.dominates(*slot[s].e.space,*e.space)) { + delete e.space; + return TAKEN; + } + bool victim = false; + for (int s=chain_first(e); s>=0; ) { + int t = chain_next(s); + if (comparable(e,s) && g.dominates(*e.space,*slot[s].e.space)) { + remove(s); victim = true; + } + s = t; + } + if (victim) { + add(e); + return TAKEN; + } + } + if (static_cast(n) == _width) { + const Entry& worst = slot[pq[0]].e; + bool replace = p(e,worst); + std::int64_t b = replace ? worst.bound : e.bound; + if (!discarded || better(p.sense,b,*discarded)) + discarded = b; + if (replace) { + remove(pq[0]); add(e); + } else { + delete e.space; + } + return TAKEN; + } + if (!room) + return FULL; + add(e); + return TAKEN; + } + /// Move all entries to \a l + void release(Entries& l) { + for (int i=0; i + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + + +#include + +#if defined(GECODE_HAS_EXPERIMENTAL) && defined(GECODE_HAS_THREADS) +namespace Gecode { namespace Search { namespace Par { + + Frontier::Runner::Runner(Frontier& e, unsigned int i) + : Support::Runnable(false), engine(e), id(i) {} + + void + Frontier::Runner::run(void) { + engine.work(id); + } + + Support::Terminator* + Frontier::Runner::terminator(void) const { + return const_cast(this); + } + + void + Frontier::Runner::terminated(void) { + done.signal(); + } + + Frontier::Frontier(const Options& o) + : Search::Engine(o,static_cast(o.threads)), opt(o), + guidance(*o.guidance), best(guidance.sense), order(0), + pending(heap), n_pending(0), runners(nullptr), active(0), + running(false), shutdown(false), _stopped(false), limited(false), + batch(1), keyed(false) {} + + void + Frontier::started(const Space&) {} + + void + Frontier::push(Task* t) { + pending.push(t); n_pending++; + } + + void + Frontier::initialize(Space* s, bool clone) { + if (s == nullptr) { + stat.fail++; + return; + } + if (s->status(stat) == SS_FAILED) { + stat.fail++; + if (!clone) + delete s; + return; + } + started(*s); + std::unique_ptr t(new Task); + t->expansion.select(std::unique_ptr(clone ? s->clone() : s)); + push(t.release()); + } + + void + Frontier::launch(void) { + if (runners != nullptr) + return; + unsigned int n = static_cast(opt.threads); + runners = heap.alloc(n); + for (unsigned int i=0; idone.wait(); + for (unsigned int i=0; i(runners,n); + runners = nullptr; + { Support::Lock lock(mutex); shutdown = false; } + throw; + } + } + + void + Frontier::wake(void) { + if (worker_control.engaged()) { + WorkerControlAccess::signal_all(worker_control); + } else if (runners != nullptr) { + for (unsigned int i=0; i(opt.threads); i++) + runners[i]->wake.signal(); + } + } + + bool + Frontier::enabled(unsigned int id) { + return !worker_control.engaged() || + (id < WorkerControlAccess::requested(worker_control)); + } + + void + Frontier::terminate(void) { + if (runners == nullptr) + return; + { Support::Lock lock(mutex); shutdown = true; running = false; } + wake(); + unsigned int n = static_cast(opt.threads); + for (unsigned int i=0; idone.wait(); + for (unsigned int i=0; i(runners,n); + runners = nullptr; + } + + Frontier::~Frontier(void) { + terminate(); + while (n_pending > 0) { + delete pending.pop(); n_pending--; + } + } + + void + Frontier::publish(Space* s, std::int64_t b) { + if (!best.improves(b)) { + delete s; + return; + } + std::unique_ptr incumbent(s); + solution.reset(s->clone()); + best.update(incumbent.release(),b); + running = false; + } + + void + Frontier::work(unsigned int id) { + // The leased task and the incumbent used for constraining its children + Task* task = nullptr; + std::unique_ptr incumbent; + unsigned long long int revision = 0; + std::optional cutoff; + Worker local; + // Children and solution found since the last submission + Entry children[8]; + unsigned int n_children = 0, submitted = 0; + std::unique_ptr solved; + std::int64_t solved_bound = 0; + while (true) { + try { + bool notify = false; + { + Support::Lock lock(mutex); + bool idle = (n_pending == 0) && !open(); + bool enabled = this->enabled(id); + if (task != nullptr) { + stat += local; + local.reset(); + for (submitted=0; submitted(task->expansion)); + n_children = submitted = 0; + if (solved) + publish(solved.release(),solved_bound); + } + if (running && (opt.stop != nullptr) && opt.stop->stop(stat,opt)) { + _stopped = true; running = false; + } + if (task != nullptr) { + if (!task->expansion || !running || !enabled || shutdown) { + if (task->expansion) + push(task); + else + delete task; + task = nullptr; + active--; + // An enabled worker must check for termination + notify = (active == 0) && running && !enabled; + } + } + if (shutdown) { + incumbent.reset(); + return; + } + while (running && enabled && (task == nullptr)) { + if ((n_pending == 0) && !open() && (active == 0) && !advance()) + running = false; + if (!running || ((n_pending == 0) && !open())) + break; + if (n_pending > 0) { + task = pending.pop(); n_pending--; + } else { + Entry e = select(); + std::unique_ptr s(e.space); + if (!best.improves(e.bound)) { + stat.fail++; + if ((opt.stop != nullptr) && opt.stop->stop(stat,opt)) { + _stopped = true; running = false; + } + continue; + } + task = new Task; + task->expansion.select(std::move(s)); + task->depth = e.depth; + task->revision = e.revision; + task->counted = true; + } + active++; + } + if (task != nullptr) { + if (revision != best.revision) + incumbent.reset(); + if (best.space && (task->revision != best.revision) && !incumbent) + incumbent.reset(best.space->clone()); + revision = best.revision; + cutoff.reset(); + if (best.space) + cutoff = best.bound; + } else { + incumbent.reset(); + revision = 0; + } + notify = notify || (idle && ((n_pending > 0) || open())); + if (!running && (active == 0)) + changed.signal(); + } + if (notify) + wake(); + if (task == nullptr) { + if (worker_control.engaged()) + WorkerControlAccess::wait(worker_control,id); + else + runners[id]->wake.wait(); + continue; + } + if (!task->counted) { + local.node++; + local.stack_depth(task->depth); + task->counted = true; + } + // Limit the work between stop checks + for (unsigned int i=0; (i<32) && task->expansion; i++) { + Seq::ExpansionStep step = + task->expansion.next(local,task->depth+1,task->revision, + incumbent.get(),revision); + if (step.state == Seq::ExpansionStep::EXHAUSTED) + break; + if (step.status != SS_FAILED) { + if (step.status == SS_SOLVED) + (void) step.space->choice(); + std::int64_t b = guidance.bound(*step.space); + if (step.status == SS_SOLVED) { + if (!cutoff || better(guidance.sense,b,*cutoff)) { + solved = std::move(step.space); + solved_bound = b; + break; + } + continue; + } + if ((step.status == SS_BRANCH) && + (!cutoff || better(guidance.sense,b,*cutoff))) { + std::int64_t r = guidance.rank ? guidance.rank(*step.space) : 0; + std::size_t k = keyed ? guidance.dominance_key(*step.space) : 0; + children[n_children++] = + {step.space.release(),b,r,k,task->depth+1,0,revision}; + if (n_children == batch) + break; + continue; + } + } + local.fail++; + if (!enabled(id)) + break; + } + } catch (...) { + Support::Lock lock(mutex); + for (unsigned int i=submitted; i 0)) + continue; + if (error) + std::rethrow_exception(error); + return solution.release(); + } + } + + Search::Statistics + Frontier::statistics(void) const { + Support::Lock lock(mutex); + return stat; + } + + bool + Frontier::stopped(void) const { + Support::Lock lock(mutex); + return _stopped; + } + + void + Frontier::constrain(const Space& s) { + std::int64_t b = guidance.bound(s); + Support::Lock lock(mutex); + if (best.improves(b)) { + best.update(s.clone(),b); + solution.reset(); + } + } + + void + Frontier::reset(Space* s) { + std::unique_ptr replacement(s); + Support::Lock lock(mutex); + while (n_pending > 0) { + delete pending.pop(); n_pending--; + } + clear(); + best.reset(); + solution.reset(); + order = 0; + _stopped = limited = false; + error = nullptr; + stat.reset(); + initialize(s,false); + // The root is owned by the engine after initialize returns + (void) replacement.release(); + } + +}}} +#endif + +// STATISTICS: search-par diff --git a/gecode/search/par/frontier.hh b/gecode/search/par/frontier.hh new file mode 100644 index 0000000000..b0b370ca3e --- /dev/null +++ b/gecode/search/par/frontier.hh @@ -0,0 +1,173 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main authors: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + + +#ifndef GECODE_SEARCH_PAR_FRONTIER_HH +#define GECODE_SEARCH_PAR_FRONTIER_HH + +#include +#include +#include +#include + +#if defined(GECODE_HAS_EXPERIMENTAL) && defined(GECODE_HAS_THREADS) +namespace Gecode { namespace Search { namespace Par { + + /** + * \brief Parallel engine for heuristic search + * + * Workers lease a node, expand it outside the lock, and submit its + * children. Subclasses decide how open nodes are stored and selected. + * All members are protected by the mutex. + */ + class Frontier : public Search::Engine { + protected: + /// Node leased by a worker, or partially expanded node + class Task { + public: + /// The node with its remaining alternatives + Seq::ChoiceExpansion expansion; + /// Depth of the node + unsigned long int depth; + /// Incumbent revision the node is constrained with + unsigned long long int revision; + /// Whether the node has been counted + bool counted; + /// Initialize + Task(void) : depth(0), revision(0), counted(false) {} + }; + /// Thread running a worker + class Runner : public Support::Runnable, public Support::Terminator { + /// The engine + Frontier& engine; + /// Worker identifier + unsigned int id; + public: + /// Events for waking up and for termination + Support::Event wake, done; + /// Initialize + Runner(Frontier& e, unsigned int i); + /// Run worker + void run(void) override; + /// Return terminator + Support::Terminator* terminator(void) const override; + /// Signal termination + void terminated(void) override; + }; + /// Search options + Options opt; + /// Guidance + HeuristicGuidance guidance; + /// Best solution so far + Incumbent best; + /// Solution to be returned by next() + std::unique_ptr solution; + /// Number of created entries + unsigned long long int order; + /// Partially expanded nodes + Support::DynamicQueue pending; + /// Number of partially expanded nodes + unsigned int n_pending; + /// The threads (allocated on first next()) + Runner** runners; + /// Mutex protecting all data + mutable Support::Mutex mutex; + /// Signalled when search is idle + Support::Event changed; + /// Statistics + Search::Statistics stat; + /// Number of workers holding a task + unsigned int active; + /// Whether search is running, being shut down, stopped, or limited + bool running, shutdown, _stopped, limited; + /// First exception thrown by a worker + std::exception_ptr error; + + /// \name Open nodes (called with the mutex held) + //@{ + /// Whether there are open nodes + virtual bool open(void) const = 0; + /// Remove and return the next open node + virtual Entry select(void) = 0; + /// Add child \a e, or delete it; \a parent tells whether its parent is still leased + virtual void submit(const Entry& e, bool parent) = 0; + /// Called when all work is done, return whether there is new work + virtual bool advance(void) = 0; + /// Delete all open nodes + virtual void clear(void) = 0; + /// Called with the root \a r when search starts + virtual void started(const Space& r); + //@} + /// Number of children a worker collects before submitting + unsigned int batch; + /// Whether to compute dominance keys + bool keyed; + + /// Initialize with options \a o + Frontier(const Options& o); + /// Start search from root \a s + void initialize(Space* s, bool clone); + /// Add pending task \a t + void push(Task* t); + /// Stop all workers (must be called by subclass destructors) + void terminate(void); + /// Make \a s with bound \a b the incumbent, if it improves + void publish(Space* s, std::int64_t b); + /// Start the threads + void launch(void); + /// Wake up all workers + void wake(void); + /// Whether worker \a id is currently enabled + bool enabled(unsigned int id); + /// Run worker \a id + void work(unsigned int id); + public: + /// Destructor + ~Frontier(void) override; + /// Return next better solution (nullptr if none or stopped) + Space* next(void) override; + /// Return statistics + Search::Statistics statistics(void) const override; + /// Whether search has been stopped + bool stopped(void) const override; + /// Constrain future solutions to be better than \a s + void constrain(const Space& s) override; + /// Reset engine to restart at space \a s + void reset(Space* s) override; + }; + +}}} +#endif +#endif + +// STATISTICS: search-par diff --git a/gecode/search/seq/astar.hh b/gecode/search/seq/astar.hh new file mode 100644 index 0000000000..1241d28074 --- /dev/null +++ b/gecode/search/seq/astar.hh @@ -0,0 +1,95 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main authors: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + + +#ifndef GECODE_SEARCH_SEQ_ASTAR_HH +#define GECODE_SEARCH_SEQ_ASTAR_HH + +#include +#include +#include + +#ifdef GECODE_HAS_EXPERIMENTAL +namespace Gecode { namespace Search { namespace Seq { + + /// Sequential A* search + class AStar : public Worker { + private: + /// Search options + Options opt; + /// Guidance + HeuristicGuidance guidance; + /// Open nodes + Queue queue; + /// Number of created entries + unsigned long long int order; + /// Node being expanded + ChoiceExpansion expansion; + /// Status of the root node before it is counted + SpaceStatus status; + /// Depth of the node being expanded + unsigned long int depth; + /// Incumbent revision the node being expanded is constrained with + unsigned long long int revision; + /// Whether the node being expanded has been counted + bool counted; + /// Best solution so far + Incumbent best; + /// Add node \a s at depth \a d if it can improve the incumbent + void enqueue(std::unique_ptr s, unsigned long int d); + /// Make \a s the incumbent and return a copy, if it improves + Space* publish(std::unique_ptr s); + /// Start search from root \a s + void initialize(Space* s, bool clone); + public: + /// Initialize for space \a s with options \a o + AStar(Space* s, const Options& o); + /// Return next better solution (nullptr if none or stopped) + Space* next(void); + /// Return statistics + Statistics statistics(void) const; + /// Constrain future solutions to be better than \a b + void constrain(const Space& b); + /// Reset engine to restart at space \a s + void reset(Space* s); + /// Return no-goods (none) + NoGoods& nogoods(void); + }; + +}}} + +#endif + +#endif + +// STATISTICS: search-seq diff --git a/gecode/search/seq/cabs.hh b/gecode/search/seq/cabs.hh new file mode 100644 index 0000000000..301ed45822 --- /dev/null +++ b/gecode/search/seq/cabs.hh @@ -0,0 +1,105 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main authors: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + + +#ifndef GECODE_SEARCH_SEQ_CABS_HH +#define GECODE_SEARCH_SEQ_CABS_HH + +#include +#include +#include + +#ifdef GECODE_HAS_EXPERIMENTAL +namespace Gecode { namespace Search { namespace Seq { + + /// Sequential complete anytime beam search + class CABS : public Worker { + private: + /// Search options + Options opt; + /// Guidance + HeuristicGuidance guidance; + /// Order of nodes in a layer + Priority priority; + /// Root for restarting with a wider beam + std::unique_ptr root; + /// Nodes of the current layer, preferred node on top + Entries layer; + /// Candidates for the next layer + Beam candidates; + /// Number of created entries + unsigned long long int order; + /// Node being expanded + ChoiceExpansion expansion; + /// Status of the root node before it is counted + SpaceStatus status; + /// Depth of the node being expanded + unsigned long int depth; + /// Incumbent revision the node being expanded is constrained with + unsigned long long int revision; + /// Whether the node being expanded has been counted + bool counted; + /// Best solution so far + Incumbent best; + /// Whether search stopped at the width or frontier limit + bool limited; + /// Offer node \a s at depth \a d to the next layer, false if no room + bool admit(std::unique_ptr s, unsigned long int d); + /// Make \a s the incumbent and return a copy, if it improves + Space* publish(std::unique_ptr s); + /// Move to the next layer or pass, false if done + bool advance(void); + /// Start search from root \a s + void initialize(Space* s, bool clone); + public: + /// Initialize for space \a s with options \a o + CABS(Space* s, const Options& o); + /// Return next better solution (nullptr if none or stopped) + Space* next(void); + /// Return statistics + Statistics statistics(void) const; + /// Constrain future solutions to be better than \a b + void constrain(const Space& b); + /// Reset engine to restart at space \a s + void reset(Space* s); + /// Return no-goods (none) + NoGoods& nogoods(void); + }; + +}}} + +#endif + +#endif + +// STATISTICS: search-seq diff --git a/gecode/search/seq/choice-expansion.hh b/gecode/search/seq/choice-expansion.hh new file mode 100644 index 0000000000..150d681344 --- /dev/null +++ b/gecode/search/seq/choice-expansion.hh @@ -0,0 +1,144 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Main authors: + * Mikael Zayenz Lagerkvist + * + * Copyright: + * Mikael Zayenz Lagerkvist, 2026 + * + * This file is part of Gecode, the generic constraint + * development environment: + * http://www.gecode.dev + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + */ + +#ifndef GECODE_SEARCH_SEQ_CHOICE_EXPANSION_HH +#define GECODE_SEARCH_SEQ_CHOICE_EXPANSION_HH + +#include +#include +#include + +#ifdef GECODE_HAS_EXPERIMENTAL +namespace Gecode { namespace Search { namespace Seq { + + /// Result of expanding one alternative + class ExpansionStep { + public: + /// What the step produced + enum State { + CHILD, ///< A propagated child + TERMINAL, ///< The node itself, as it does not branch + EXHAUSTED ///< Nothing, all alternatives have been expanded + }; + /// The state + State state; + /// The child or node + std::unique_ptr space; + /// Status of the space + SpaceStatus status; + }; + + /// Node whose alternatives are expanded one at a time + class ChoiceExpansion { + private: + /// The node (declared before the choice so the choice is deleted first) + std::unique_ptr parent; + /// The choice of the node + std::unique_ptr choice; + /// Next alternative + unsigned int alternative; + public: + /// Initialize without node + ChoiceExpansion(void) : alternative(0) {} + /// Whether there is a node + explicit operator bool(void) const { return static_cast(parent); } + /// Return the node + Space& space(void) const { return *parent; } + /// Take stable node \a s + void select(std::unique_ptr s) { + assert(!parent && !choice); + parent = std::move(s); + alternative = 0; + } + /// Return the node before its choice has been computed + std::unique_ptr take(void) { + assert(!choice); + return std::move(parent); + } + /// Delete the node + void reset(void) { + choice.reset(); + parent.reset(); + alternative = 0; + } + /** + * \brief Commit and propagate the next alternative + * + * The child is constrained by \a incumbent if the node has been + * constrained with an older revision than \a revision. The last + * alternative reuses the node itself. + */ + ExpansionStep next(Worker& worker, unsigned long int depth, + unsigned long long int node_revision, + const Space* incumbent, + unsigned long long int revision) { + assert(parent); + if (!choice) { + SpaceStatus status = parent->status(worker); + if (status != SS_BRANCH) + return {ExpansionStep::TERMINAL,std::move(parent),status}; + choice.reset(parent->choice()); + alternative = 0; + } + if (alternative == choice->alternatives()) { + reset(); + return {ExpansionStep::EXHAUSTED,nullptr,SS_FAILED}; + } + std::unique_ptr child; + // Keeps the choice alive while committing the last alternative + std::unique_ptr last; + const Choice* c = choice.get(); + if (alternative+1 == choice->alternatives()) { + child = std::move(parent); + last = std::move(choice); + } else { + child.reset(parent->clone()); + } + child->commit(*c,alternative); + if ((incumbent != nullptr) && (node_revision != revision)) + child->constrain(*incumbent); + worker.node++; + worker.stack_depth(depth); + SpaceStatus status = child->status(worker); + alternative++; + return {ExpansionStep::CHILD,std::move(child),status}; + } + }; + +}}} + +#endif + +#endif + +// STATISTICS: search-seq diff --git a/gecode/search/seq/pbs.cpp b/gecode/search/seq/pbs.cpp index ec72050c7d..b26b7fbaf0 100644 --- a/gecode/search/seq/pbs.cpp +++ b/gecode/search/seq/pbs.cpp @@ -39,6 +39,7 @@ namespace Gecode { namespace Search { namespace Seq { PortfolioStop::stop(const Statistics& s, const Options& o) { if ((so != nullptr) && so->stop(s,o)) { ssi->done = false; + ssi->user = true; return true; } if (s.fail >= ssi->l) { diff --git a/gecode/search/seq/pbs.hh b/gecode/search/seq/pbs.hh index 6ed977b04a..d114daa368 100644 --- a/gecode/search/seq/pbs.hh +++ b/gecode/search/seq/pbs.hh @@ -43,6 +43,8 @@ namespace Gecode { namespace Search { namespace Seq { public: /// Whether search stopped because the slice is done bool done; + /// Whether search stopped because of the stop object + bool user; /// The current failure limit, incremented for each slice unsigned long long int l; }; @@ -111,6 +113,8 @@ namespace Gecode { namespace Search { namespace Seq { unsigned int cur; /// Whether a slave has been stopped bool slave_stop; + /// Whether a slave stopped by itself (for example, at a limit) + bool incomplete; public: /// Initialize PBS(Engine** slaves, Stop** stops, unsigned int n, diff --git a/gecode/search/seq/pbs.hpp b/gecode/search/seq/pbs.hpp index 59d66f43cc..1761ef8fef 100644 --- a/gecode/search/seq/pbs.hpp +++ b/gecode/search/seq/pbs.hpp @@ -82,8 +82,9 @@ namespace Gecode { namespace Search { namespace Seq { const Search::Options& opt) : stat(stat0), slice(opt.slice), slaves(heap.alloc(n)), n_slaves(n), cur(0), - slave_stop(false) { + slave_stop(false), incomplete(false) { ssi.done = false; + ssi.user = false; ssi.l = opt.slice; for (unsigned int i=0U; i 0) { + ssi.done = false; + ssi.user = false; if (Space* s = slaves[cur].next()) { // Constrain other slaves if (best) { @@ -108,7 +111,7 @@ namespace Gecode { namespace Search { namespace Seq { } return s; } - if (slaves[cur].stopped()) { + if (slaves[cur].stopped() && (ssi.done || ssi.user)) { if (ssi.done) { cur++; n_exhausted++; } else { @@ -116,7 +119,10 @@ namespace Gecode { namespace Search { namespace Seq { return nullptr; } } else { - // This slave is done, kill it after saving the statistics + // The slave is done or stopped by itself (then search is incomplete) + if (slaves[cur].stopped()) + incomplete = true; + // Kill the slave after saving the statistics stat += slaves[cur].statistics(); slaves[cur].~Slave(); slaves[cur] = slaves[--n_slaves]; @@ -132,6 +138,7 @@ namespace Gecode { namespace Search { namespace Seq { if (cur == n_slaves) cur = 0; } + slave_stop = incomplete; return nullptr; } diff --git a/gecode/support/config.hpp.in b/gecode/support/config.hpp.in index 9c9588878f..f434087688 100644 --- a/gecode/support/config.hpp.in +++ b/gecode/support/config.hpp.in @@ -67,6 +67,9 @@ /* Whether CPProfiler support available */ #undef GECODE_HAS_CPPROFILER +/* Whether experimental preview APIs are available */ +#undef GECODE_HAS_EXPERIMENTAL + /* Whether to build FLOAT variables */ #undef GECODE_HAS_FLOAT_VARS diff --git a/misc/genchangelog.py b/misc/genchangelog.py index 9bf2db440f..f8cba0b8ba 100644 --- a/misc/genchangelog.py +++ b/misc/genchangelog.py @@ -74,7 +74,6 @@ def main() -> int: whatorder = ["new", "removed", "change", "bug", "performance", "documentation"] - hastext = defaultdict(int) text = defaultdict(str) versions = "" @@ -89,16 +88,17 @@ def flush_current_release() -> None: body = body + f"\\section {sid} Changes in Version {version} ({date})\n\n" body = body + f"{info}\n\n" - for mod in modorder: - if hastext[mod]: - body = body + " - " + modclear[mod] + "\n" - hastext[mod] = 0 - for what in whatorder: - k = f"{mod}-{what}" - if text[k] != "": - body = body + " - " + whatclear[what] + "\n" - body = body + text[k] - text[k] = "" + for experimental in (False, True): + if experimental and any(text[(True, mod, what)] for mod in modorder for what in whatorder): + body += f"\\subsection {sid}_experimental Experimental changes\n\n" + for mod in modorder: + if any(text[(experimental, mod, what)] for what in whatorder): + body += " - " + modclear[mod] + "\n" + for what in whatorder: + k = (experimental, mod, what) + if text[k]: + body += " - " + whatclear[what] + "\n" + text[k] + text[k] = "" body = body + "\n\n" lines = sys.stdin.readlines() @@ -156,12 +156,20 @@ def flush_current_release() -> None: issue = "" thanks = "" author = "" + experimental = False desc = "" more = "" while idx < len(lines): l = lines[idx] idx += 1 + m = re.fullmatch(r"Experimental:[\t ]*(.*)\n?", l) + if m: + value = m.group(1).rstrip("\n") + if value not in {"true", "false"}: + print("genchangelog: invalid Experimental value " + repr(value), file=sys.stderr) + return 1 + experimental = value == "true" m = re.search(r"Module:[\t ]*(.*)$", l) if m: mod = m.group(1) @@ -214,7 +222,6 @@ def flush_current_release() -> None: more = more.replace("&", "\\&") more = re.sub(r"\n([ \t]*)\n", r"\n\1
\n", more) - hastext[mod] = 1 rb = rankclear.get(rank, "") if bug != "": rb = rb + f", bugzilla entry {bug}" @@ -238,7 +245,7 @@ def flush_current_release() -> None: ) moreinfocnt += 1 - k = f"{mod}-{what}" + k = (experimental, mod, what) text[k] = text[k] + f" - {desc} ({rb})\n" + more current = l continue diff --git a/misc/gencurrentchangelog.py b/misc/gencurrentchangelog.py index 4d5ff7424c..6e00f024f6 100644 --- a/misc/gencurrentchangelog.py +++ b/misc/gencurrentchangelog.py @@ -68,9 +68,25 @@ def main() -> int: ] whatorder = ["new", "removed", "change", "bug", "performance", "documentation"] - hastext = defaultdict(int) text = defaultdict(str) + def flush_current_release() -> None: + for experimental in (False, True): + if experimental and any(text[(True, mod, what)] for mod in modorder for what in whatorder): + sys.stdout.write("

Experimental changes

\n") + sys.stdout.write("
    \n") + for mod in modorder: + if any(text[(experimental, mod, what)] for what in whatorder): + sys.stdout.write(f"
  • {modclear[mod]}\n
      \n") + for what in whatorder: + k = (experimental, mod, what) + if text[k]: + sys.stdout.write(f"
    • {whatclear[what]}\n
        " + text[k] + "
    • \n") + sys.stdout.write("
  • \n") + sys.stdout.write("
\n") + if not any(text[(True, mod, what)] for mod in modorder for what in whatorder): + break + lines = sys.stdin.readlines() idx = 0 current = None @@ -91,20 +107,7 @@ def main() -> int: if re.match(r"^\[RELEASE\]", l): if not first: - sys.stdout.write("
    \n") - for mod in modorder: - if hastext[mod]: - sys.stdout.write(f"
  • {modclear[mod]}\n
      \n") - hastext[mod] = 0 - for what in whatorder: - k = f"{mod}-{what}" - if text[k] != "": - sys.stdout.write(f"
    • {whatclear[what]}\n
        ") - sys.stdout.write(text[k]) - sys.stdout.write("
    • \n") - text[k] = "" - sys.stdout.write("
  • \n") - sys.stdout.write("
\n") + flush_current_release() return 0 first = False @@ -135,6 +138,7 @@ def main() -> int: if re.match(r"^\[ENTRY\]", l): entry_number += 1 + experimental = False desc = "" bug = "" rank = "" @@ -149,6 +153,13 @@ def main() -> int: l = l.replace("%Gecode", "Gecode") if re.search(r"\[DESCRIPTION\]", l): break + m = re.fullmatch(r"Experimental:[\t ]*(.*)\n?", l) + if m: + value = m.group(1).rstrip("\n") + if value not in {"true", "false"}: + print("gencurrentchangelog: invalid Experimental value " + repr(value), file=sys.stderr) + return 1 + experimental = value == "true" m = re.search(r"Module:[\t ]*(.*)$", l) if m: mod = m.group(1) @@ -204,16 +215,19 @@ def main() -> int: ) return 1 - hastext[mod] = 1 rb = rankclear[rank] if bug != "": rb = rb + f", bugzilla entry {bug}" if thanks != "": rb = rb + f", thanks to {thanks}" - k = f"{mod}-{what}" + k = (experimental, mod, what) text[k] = text[k] + f"
  • {desc}({rb})
  • \n" continue + if not first: + flush_current_release() + return 0 + sys.stdout.write( """Initial release (2005-12-06) diff --git a/misc/gentxtchangelog.py b/misc/gentxtchangelog.py index 8cffd2b604..d1d4d085de 100644 --- a/misc/gentxtchangelog.py +++ b/misc/gentxtchangelog.py @@ -76,9 +76,24 @@ def main() -> int: whatorder = ["new", "removed", "change", "bug", "performance", "documentation"] - hastext = defaultdict(int) text = defaultdict(str) + def flush_current_release() -> None: + sys.stdout.write(f"Changes in Version {version} ({date})\n\n") + sys.stdout.write(f"Scope:{info}") + for experimental in (False, True): + if experimental and any(text[(True, mod, what)] for mod in modorder for what in whatorder): + sys.stdout.write("Experimental changes\n") + for mod in modorder: + if any(text[(experimental, mod, what)] for what in whatorder): + sys.stdout.write(f"- {modclear[mod]}\n") + for what in whatorder: + k = (experimental, mod, what) + if text[k]: + sys.stdout.write(f" - {whatclear[what]}\n" + text[k]) + text[k] = "" + sys.stdout.write("\n------------------------------------------------------------------------------\n\n") + lines = sys.stdin.readlines() idx = 0 current = None @@ -99,21 +114,7 @@ def main() -> int: if re.match(r"^\[RELEASE\]", l): if not first: - sys.stdout.write(f"Changes in Version {version} ({date})\n\n") - sys.stdout.write(f"Scope:{info}") - - for mod in modorder: - if hastext[mod]: - sys.stdout.write(f"- {modclear[mod]}\n") - hastext[mod] = 0 - for what in whatorder: - k = f"{mod}-{what}" - if text[k] != "": - sys.stdout.write(f" - {whatclear[what]}\n") - sys.stdout.write(text[k]) - text[k] = "" - - sys.stdout.write("\n------------------------------------------------------------------------------\n\n") + flush_current_release() first = False version = "" @@ -144,6 +145,7 @@ def main() -> int: continue if re.match(r"^\[ENTRY\]", l): + experimental = False desc = "" bug = "" issue = "" @@ -158,6 +160,13 @@ def main() -> int: l = l.replace("%Gecode", "Gecode") if re.search(r"\[DESCRIPTION\]", l): break + m = re.fullmatch(r"Experimental:[\t ]*(.*)\n?", l) + if m: + value = m.group(1).rstrip("\n") + if value not in {"true", "false"}: + print("gentxtchangelog: invalid Experimental value " + repr(value), file=sys.stderr) + return 1 + experimental = value == "true" m = re.search(r"Module:[\t ]*(.*)$", l) if m: mod = m.group(1) @@ -193,7 +202,6 @@ def main() -> int: if desc.endswith("\n"): desc = desc[:-1] - hastext[mod] = 1 rb = rankclear.get(rank, "") if bug != "": rb = rb + f", bugzilla entry {bug}" @@ -202,10 +210,13 @@ def main() -> int: if thanks != "": rb = rb + f", thanks to {thanks}" - k = f"{mod}-{what}" + k = (experimental, mod, what) text[k] = text[k] + f" - {desc}({rb})\n" continue + if not first and any(text.values()): + flush_current_release() + sys.stdout.write( """Initial release (2005-12-06) diff --git a/misc/tidy.py b/misc/tidy.py index b84a7d593e..1948ccdaf5 100644 --- a/misc/tidy.py +++ b/misc/tidy.py @@ -61,6 +61,7 @@ class Entry: module: str what: str rank: str + experimental: str bug: str issue: str thanks: str @@ -144,6 +145,7 @@ def parse_changelog() -> tuple[list[Release], list[Entry]]: "module": "", "what": "", "rank": "", + "experimental": "false", "bug": "", "issue": "", "thanks": "", @@ -153,6 +155,7 @@ def parse_changelog() -> tuple[list[Release], list[Entry]]: "module": "Module", "what": "What", "rank": "Rank", + "experimental": "Experimental", "bug": "Bug", "issue": "Issue", "thanks": "Thanks", @@ -233,6 +236,8 @@ def validate_changelog_entries(generated: str) -> list[str]: errors.append(f"line {entry.line}: unknown module {entry.module!r}") if entry.what not in WHATS: errors.append(f"line {entry.line}: unknown change type {entry.what!r}") + if entry.experimental not in {"true", "false"}: + errors.append(f"line {entry.line}: invalid Experimental value {entry.experimental!r}") if entry.rank not in RANKS: errors.append(f"line {entry.line}: unknown rank {entry.rank!r}") @@ -470,10 +475,48 @@ def validate_current_changelog() -> list[str]: return errors +def validate_experimental_changelog() -> list[str]: + errors = [] + release = "[RELEASE]\nVersion: 1.0.0\nDate: 2000-01-01\n[DESCRIPTION]\nFixture.\n" + + def entry(description: str, experimental: str = "", module: str = "search", what: str = "new") -> str: + field = f"Experimental: {experimental}\n" if experimental else "" + return (f"[ENTRY]\nModule: {module}\nWhat: {what}\nRank: minor\n" + + field + f"[DESCRIPTION]\n{description}\n") + + # Mixed input order checks separation while preserving module/type order. + source = release + entry("Preview search change.", "true", what="change") + source += entry("Ordinary omitted.") + entry("Preview kernel.", "true", "kernel") + source += entry("Ordinary false.", "false") + entry("Preview search addition.", "true") + older = release.replace("1.0.0", "0.9.0") + entry("Older ordinary.") + for generator in (GEN_CHANGELOG, ROOT / "misc" / "gentxtchangelog.py", GEN_CURRENT_CHANGELOG): + for suffix in ("", older): + result = run(["uv", "run", str(generator)], stdin=source + suffix) + output = result.stdout + markers = ["Ordinary omitted.", "Ordinary false.", "Experimental changes", + "Preview kernel.", "Preview search addition.", "Preview search change."] + positions = [output.find(marker) for marker in markers] + if result.returncode or min(positions) < 0 or positions != sorted(positions): + errors.append(f"{generator.name}: experimental separation/order regression") + if output.count("Experimental changes") != 1: + errors.append(f"{generator.name}: experimental heading leaked across releases") + omitted = run(["uv", "run", str(generator)], stdin=release + entry("Ordinary.")) + explicit = run(["uv", "run", str(generator)], stdin=release + entry("Ordinary.", "false")) + if omitted.returncode or explicit.returncode or omitted.stdout != explicit.stdout or "Experimental changes" in omitted.stdout: + errors.append(f"{generator.name}: false/omitted historical rendering differs") + for value in ("TRUE", "False", "yes", "1", "", "true "): + invalid = entry("Invalid.", "true").replace("Experimental: true", f"Experimental: {value}") + result = run(["uv", "run", str(generator)], stdin=release + invalid) + if result.returncode == 0 or "invalid Experimental value" not in result.stderr: + errors.append(f"{generator.name}: accepted invalid Experimental {value!r}") + return errors + + def check_changelog(*, fix: bool) -> int: generated = generate_changelog() errors = validate_changelog_entries(generated) errors.extend(validate_current_changelog()) + errors.extend(validate_experimental_changelog()) if is_tracked(CHANGELOG_HH): if CHANGELOG_HH.exists() and CHANGELOG_HH.read_text() == generated: diff --git a/test/search.cpp b/test/search.cpp index 70cf9d8cc0..29457efde1 100644 --- a/test/search.cpp +++ b/test/search.cpp @@ -44,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -83,6 +84,248 @@ namespace Test { using namespace Gecode; using namespace Gecode::Int; +#ifdef GECODE_HAS_EXPERIMENTAL + /// Explicit finite tree with prefix costs, admissible bounds, and ranks + class TreeSpace : public Space { + public: + /// Tree node + struct Node { + int cost, bound, rank, best; + std::vector children; + }; + typedef std::vector Graph; + std::shared_ptr graph; + bool maximize; + int vertex, ceiling; + /// Brancher choosing a child of the current node + class Decisions : public Brancher { + class Decision : public Choice { + public: + int vertex; + Decision(const Brancher& b, int v, unsigned int n) + : Choice(b,n), vertex(v) {} + void archive(Archive& e) const override { + Choice::archive(e); e << vertex << alternatives(); + } + }; + public: + Decisions(Home home) : Brancher(home) {} + Decisions(Space& home, Decisions& b) : Brancher(home,b) {} + bool status(const Space& s) const override { + return !static_cast(s).node().children.empty(); + } + const Choice* choice(Space& s) override { + const TreeSpace& t = static_cast(s); + return new Decision(*this,t.vertex, + static_cast(t.node().children.size())); + } + const Choice* choice(const Space&, Archive& e) override { + int v; unsigned int n; e >> v >> n; + return new Decision(*this,v,n); + } + ExecStatus commit(Space& s, const Choice& c, unsigned int a) override { + TreeSpace& t = static_cast(s); + t.vertex = (*t.graph)[static_cast(c).vertex].children[a]; + return t.node().cost >= t.ceiling ? ES_FAILED : ES_OK; + } + Actor* copy(Space& home) override { + return new (home) Decisions(home,*this); + } + size_t dispose(Space& home) override { + (void) Brancher::dispose(home); + return sizeof(*this); + } + }; + TreeSpace(std::shared_ptr g, bool max) + : graph(g), maximize(max), vertex(0), + ceiling(std::numeric_limits::max()) { + (void) new (*this) Decisions(*this); + } + TreeSpace(TreeSpace& s) + : Space(s), graph(s.graph), maximize(s.maximize), vertex(s.vertex), + ceiling(s.ceiling) {} + Space* copy(void) override { return new TreeSpace(*this); } + bool master(const MetaInfo&) override { return true; } + const Node& node(void) const { return (*graph)[vertex]; } + void constrain(const Space& s) override { + ceiling = static_cast(s).node().cost; + if (node().cost >= ceiling) fail(); + } + /// Guidance minimizing cost, or maximizing negated cost + static Gecode::Search::HeuristicGuidance + guidance(bool max, bool ranked, bool dominance, bool keyed) { + using Gecode::Search::HeuristicGuidance; + HeuristicGuidance g(max ? HeuristicGuidance::MAXIMIZE : + HeuristicGuidance::MINIMIZE, + [](const Space& s) -> std::int64_t { + const TreeSpace& t = static_cast(s); + return t.maximize ? -t.node().bound : t.node().bound; + }); + if (ranked) + g.rank = [](const Space& s) -> std::int64_t { + return static_cast(s).node().rank; + }; + if (dominance) + g.dominates = [](const Space& a, const Space& b) { + return static_cast(a).node().best <= + static_cast(b).node().best; + }; + if (keyed) + g.dominance_key = [](const Space& s) -> std::size_t { + return static_cast + (static_cast(s).node().best % 3); + }; + return g; + } + /// Return a random tree where all nodes above depth \a d branch + static std::shared_ptr + random(Gecode::Support::RandomGenerator& r, int d=1) { + Graph g; + g.push_back({0,0,0,0,{}}); + grow(g,0,0,d,r); + return std::make_shared(std::move(g)); + } + private: + static void grow(Graph& g, int v, int depth, int d, + Gecode::Support::RandomGenerator& r) { + if ((depth < d) || ((depth < 5) && (r(4U) != 0U))) { + unsigned int n = (depth < d ? 2U : 1U) + r(3U); + for (unsigned int i=0; i(g.size()); + g.push_back({g[v].cost+static_cast(r(5U)),0, + static_cast(r(10U)),0,{}}); + g[v].children.push_back(c); + grow(g,c,depth+1,d,r); + } + } + if (g[v].children.empty()) { + g[v].best = g[v].bound = g[v].cost; + } else { + g[v].best = std::numeric_limits::max(); + for (int c : g[v].children) + g[v].best = std::min(g[v].best,g[c].best); + g[v].bound = g[v].cost + + static_cast(r(static_cast(g[v].best-g[v].cost+1))); + } + } + }; + + /// Stop object raising its node limit on each stop + class Increasing : public Gecode::Search::Stop { + unsigned long long int limit, step; + public: + unsigned int stops; + Increasing(unsigned long long int s) : limit(s), step(s), stops(0) {} + bool stop(const Gecode::Search::Statistics& s, + const Gecode::Search::Options&) override { + if (s.node <= limit) + return false; + limit += step; + stops++; + return true; + } + }; + + /// Check that improving solutions reach the optimum unless search stops + template + static bool improving(Engine& e, const TreeSpace& root, + const Increasing* stop=nullptr) { + int last = std::numeric_limits::max(); + unsigned int stops = 0; + while (true) { + if (TreeSpace* s = e.next()) { + int cost = s->node().cost; + delete s; + if (cost >= last) + return false; + last = cost; + } else if ((stop != nullptr) && (stop->stops != stops)) { + // Resume after the stop object stopped search + stops = stop->stops; + } else { + break; + } + } + return e.stopped() || (last == (*root.graph)[0].best); + } + + /// Heuristic search engines on random trees + template class E> + class Heuristic : public Base { + public: + Heuristic(const std::string& name) + : Base("Search::"+name,TestTags::check()) {} + bool run(void) override { + for (bool max : {false,true}) + for (bool ranked : {false,true}) + for (bool dominance : {false,true}) + for (unsigned int threads : {1U,2U,4U}) + for (unsigned int step : {0U,1U,7U}) { +#ifndef GECODE_HAS_THREADS + if (threads > 1U) continue; +#endif + TreeSpace root(TreeSpace::random(_rand),max); + Gecode::Search::HeuristicGuidance g = + TreeSpace::guidance(max,ranked,dominance,_rand(2U) == 0U); + Increasing stop(step); + Gecode::Search::Options o; + o.threads = threads; + o.guidance = &g; + if (step > 0U) + o.stop = &stop; + if (_rand(3U) == 0U) + o.max_width = 1UL << _rand(3U); + E e(&root,o); + if (!improving(e,root,step > 0U ? &stop : nullptr)) + return false; + if ((o.max_width == 0UL) && e.stopped()) + return false; + } + return true; + } + }; + + Heuristic astar_test("AStar"); + Heuristic cabs_test("CABS"); + + /// Portfolio of BAB, A*, and CABS assets + class HeuristicPortfolio : public Base { + public: + HeuristicPortfolio(void) + : Base("Search::Heuristic::Portfolio",TestTags::check()) {} + bool run(void) override { + TreeSpace root(TreeSpace::random(_rand),false); + Gecode::Search::HeuristicGuidance g = + TreeSpace::guidance(false,true,false,false); + Gecode::Search::Options a; + a.threads = 1; + a.guidance = &g; + SEBs assets({bab(a),astar(a), + cabs(a)}); + Gecode::Search::Options o; +#ifdef GECODE_HAS_THREADS + o.threads = 3; +#endif + PBS e(&root,assets,o); + if (!improving(e,root)) + return false; + // A CABS asset stopping at its width limit must not stop the others + Gecode::Search::Options w(a); + w.max_width = 1; + SEBs limited({cabs(w),bab(a)}); + Gecode::Search::Options s; + PBS f(&root,limited,s); + int last = std::numeric_limits::max(); + while (TreeSpace* t = f.next()) { + last = t->node().cost; + delete t; + } + return last == (*root.graph)[0].best; + } + } heuristic_portfolio; +#endif + + /// Values for selecting branchers enum HowToBranch { HTB_NONE, ///< Do not branch @@ -584,11 +827,49 @@ namespace Test { } return ok && !engine.stopped() && (solutions == root.solutions()); } +#ifdef GECODE_HAS_EXPERIMENTAL + template class E> + bool heuristic(unsigned int node, unsigned int threads) { + Gecode::Search::WorkerControl control(node == 0U ? 0U : 1U); + Pause pause(control,node); + TreeSpace root(TreeSpace::random(_rand,4),false); + Gecode::Search::HeuristicGuidance g = + TreeSpace::guidance(false,true,false,false); + Gecode::Search::Options o; + o.threads = threads; + o.worker_control = control; + o.stop = &pause; + o.guidance = &g; + E engine(&root,o); + int last = std::numeric_limits::max(); + auto consume = [&] { + while (TreeSpace* s = engine.next()) { + last = s->node().cost; + delete s; + } + }; + auto result = std::async(std::launch::async,consume); + pause.reached.get_future().wait(); + bool ok = result.wait_for(std::chrono::milliseconds(20)) == + std::future_status::timeout; + control.request(threads); + result.get(); + return ok && !engine.stopped() && (last == (*root.graph)[0].best); + } +#endif + public: WorkerControlSequentialPause(void) : Base("Search::WorkerControl::SequentialPause", TestTags::check()) {} bool run(void) override { #ifdef GECODE_HAS_THREADS +#ifdef GECODE_HAS_EXPERIMENTAL + for (unsigned int node : {0U,1U}) + for (unsigned int threads : {1U,4U}) + if (!heuristic(node,threads) || + !heuristic(node,threads)) + return false; +#endif return check(0U) && check(1U) && check(0U) && check(1U); #else