diff --git a/Makefile.in b/Makefile.in index aa182f5cde..6d93b6e376 100755 --- a/Makefile.in +++ b/Makefile.in @@ -323,7 +323,9 @@ INTSRC0 = \ int-set.cpp var-imp/int.cpp var-imp/bool.cpp var/int.cpp \ var/bool.cpp array.cpp bool.cpp bool/eqv.cpp \ extensional/dfa.cpp extensional/tuple-set.cpp \ - extensional-regular.cpp extensional-tuple-set.cpp \ + extensional-regular.cpp extensional-open-regular.cpp \ + extensional-tuple-set.cpp \ + open-var-list.cpp \ dom.cpp rel.cpp precede.cpp element.cpp count.cpp \ arithmetic.cpp exec.cpp \ exec/when.cpp element/pair.cpp \ @@ -348,6 +350,7 @@ INTSRC0 = \ exception.cpp INTHDR0 = \ + open-var-list.hpp \ limits.hpp support-values.hh support-values.hpp \ idx-view.hh idx-view.hpp div.hh div.hpp \ exec.hh exec/when.hpp \ @@ -906,10 +909,11 @@ 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 + qcp job-shop open-route common-pattern fifteen-puzzle -INTEXAMPLEHDR = $(INTEXAMPLEHDR0:%=examples/%.hpp) +INTEXAMPLEHDR = $(INTEXAMPLEHDR0:%=examples/%.hpp) \ + examples/common-pattern.hh INTEXAMPLESRC = $(INTEXAMPLESRC0:%=examples/%.cpp) ifeq "@enable_examples@" "yes" INSTALLEXAMPLEHDR = $(INTEXAMPLEHDR) @@ -1119,7 +1123,7 @@ EXETARGETS = $(FLATZINCEXE) $(FLATZINCLAUNCHER) INTTESTSRC00 = \ channel arithmetic basic bool count dom distinct element extensional \ - rel linear gcc sorted unshare exec sequence \ + rel linear gcc sorted unshare exec sequence open-var-list \ mm-arithmetic mm-bool mm-lin mm-count mm-rel \ bin-packing order unary cumulative cumulatives circuit \ no-overlap precede inter-distance nvalues member min-distance diff --git a/changelog.in b/changelog.in index f40e012a94..d15c3fe4c7 100755 --- a/changelog.in +++ b/changelog.in @@ -92,6 +92,26 @@ Prefix embedded mimalloc symbols with gecode_ to avoid allocator conflicts. Mimalloc can substantially improve parallel search performance on some workloads. The system allocator is the default. +[ENTRY] +Module: int +What: new +Rank: major +[DESCRIPTION] +Add OpenIntVarList and OpenBoolVarList for lists whose length is decided +during propagation and search. +[MORE] +OpenVarList supports other variable types through a factory. +Regular-language and other constraints follow list growth. +Regular-language filtering excludes prefixes whose shortest accepting +continuation exceeds the maximum list length. +Branching uses standard variable and value selectors, with either incremental +list construction or a length decision before assigning values. +Factories and transitions run inside Space::NoGPI scopes by default; +callbacks can opt into global information with Space::UseGPI. +The open-route example introduces list construction and branching. +Common-pattern demonstrates constrained frequent subsequence mining, +and fifteen-puzzle compares open lists with fixed arrays. + [ENTRY] Module: kernel What: new diff --git a/cmake/GecodeSources.cmake b/cmake/GecodeSources.cmake index e762f19034..8e435b352e 100644 --- a/cmake/GecodeSources.cmake +++ b/cmake/GecodeSources.cmake @@ -83,7 +83,9 @@ set(GECODE_INT_SOURCES gecode/int/exec.cpp gecode/int/exec/when.cpp gecode/int/extensional-regular.cpp + gecode/int/extensional-open-regular.cpp gecode/int/extensional-tuple-set.cpp + gecode/int/open-var-list.cpp gecode/int/extensional/dfa.cpp gecode/int/extensional/tuple-set.cpp gecode/int/gcc.cpp @@ -429,6 +431,7 @@ set(GECODE_TEST_SOURCES test/int/no-overlap.cpp test/int/nvalues.cpp test/int/order.cpp + test/int/open-var-list.cpp test/int/precede.cpp test/int/rel.cpp test/int/sequence.cpp diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index a223ae844a..6282beaeb8 100755 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -13,6 +13,7 @@ set(GECODE_EXAMPLE_SOURCES car-sequencing.cpp cartesian-heart.cpp colored-matrix.cpp + common-pattern.cpp crew.cpp crossword.cpp crowded-chess.cpp @@ -22,6 +23,7 @@ set(GECODE_EXAMPLE_SOURCES donald.cpp efpa.cpp eq20.cpp + fifteen-puzzle.cpp golden-spiral.cpp golf.cpp golomb-ruler.cpp @@ -40,6 +42,7 @@ set(GECODE_EXAMPLE_SOURCES money.cpp multi-bin-packing.cpp nonogram.cpp + open-route.cpp open-shop.cpp ortho-latin.cpp partition.cpp diff --git a/examples/common-pattern.cpp b/examples/common-pattern.cpp new file mode 100644 index 0000000000..2910074ad6 --- /dev/null +++ b/examples/common-pattern.cpp @@ -0,0 +1,249 @@ +/* -*- 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 "common-pattern.hh" + +#include +#include + +using namespace Gecode; + +/// Sequence database and restrictions on the patterns to mine +class CommonPatternOptions : public Options { +protected: + Driver::StringValueOption _sequences, _file, _support, _required; + Driver::StringOption _format, _language; + Driver::UnsignedIntOption _min_length; + Driver::BoolOption _motif; + std::unique_ptr input; + std::vector required_items; + int threshold; +public: + CommonPatternOptions(void) + : Options("CommonPattern"), + _sequences("sequences","comma-separated sequences over A..Z","ABCBC,BABC,ABDC"), + _file("file","sequence database; one sequence per line",""), + _support("support","minimum supporting sequences, as a percentage","100"), + _required("required","items that must occur, comma-separated letters or IDs",""), + _format("format","database format",PatternMining::LETTERS), + _language("language","pattern language",PatternMining::DEFAULT), + _min_length("min-length","minimum pattern length",1), + _motif("motif","default language starts with A and ends with C",true), + threshold(0) { + _format.add(PatternMining::LETTERS,"letters"); + _format.add(PatternMining::INTEGERS,"integers"); + _format.add(PatternMining::SPMF,"spmf"); + _language.add(PatternMining::DEFAULT,"default","use -motif"); + _language.add(PatternMining::NONE,"none"); + _language.add(PatternMining::MOTIF,"motif"); + _language.add(PatternMining::RE10,"re10"); + _language.add(PatternMining::RE14,"re14"); + _language.add(PatternMining::RE2,"re2"); + add(_sequences); add(_file); add(_format); add(_support); + add(_min_length); add(_required); add(_language); add(_motif); + } + + /// Validate external input before constructing a space + void read(void) { + if (_min_length.value() < 1 || _min_length.value() > Int::Limits::max) + throw std::invalid_argument("Minimum length must be a positive Gecode integer"); + PatternMining::Sequences sequences; + const std::string filename(_file.value()); + if (filename.empty()) { + if (_format.value() != PatternMining::LETTERS) + throw std::invalid_argument("Integer and SPMF formats require -file"); + const std::string text(_sequences.value()); + if (text.empty() || text.back() == ',') + throw std::invalid_argument("Expected comma-separated nonempty A..Z sequences"); + std::istringstream stream(text); + std::string sequence; + while (std::getline(stream,sequence,',')) + sequences.push_back(PatternMining::read_sequence(sequence,PatternMining::LETTERS)); + } else { + std::ifstream stream(filename); + sequences = PatternMining::read_database(stream, + static_cast(_format.value())); + } + input.reset(new PatternMining::Database(sequences)); + threshold = PatternMining::minimum_support(_support.value(),input->ends.size()); + const std::string items(_required.value()); + if (!items.empty()) { + if (items.back() == ',') + throw std::invalid_argument("Empty required item"); + std::istringstream stream(items); + std::string item; + while (std::getline(stream,item,',')) { + const std::vector parsed = PatternMining::read_sequence(item, + letters() ? PatternMining::LETTERS : PatternMining::INTEGERS); + if (parsed.size() != 1) + throw std::invalid_argument("Required items must be single letters or IDs"); + required_items.push_back(parsed[0]); + } + } + } + + const PatternMining::Database& database(void) const { return *input; } + const std::vector& required(void) const { return required_items; } + int support(void) const { return threshold; } + int min_length(void) const { return static_cast(_min_length.value()); } + bool letters(void) const { return _format.value() == PatternMining::LETTERS; } + PatternMining::Language language(void) const { + if (_language.value() == PatternMining::DEFAULT) + return _motif.value() ? PatternMining::MOTIF : PatternMining::NONE; + return static_cast(_language.value()); + } +}; + +/** + * \brief %Example: constrained frequent subsequences of unknown length + * + * A pattern must occur in at least -support percent of the input sequences. + * Matching preserves order and may skip items; repeated items match distinct + * positions. Each supporting record counts once, including duplicate records. + * By default the inputs ABCBC, BABC, ABDC must all support the pattern, which + * must start with A and end with C. AC and ABC are feasible; ABC is longest. + * + * Ordinary length and regular constraints restrict the pattern, not the + * matching positions in the input records. By default, DFS enumerates every + * qualifying pattern; -search bab finds a longest one. + * + * An OpenIntVarList constructs pattern positions as needed, without an end + * marker or unused suffix variables. Pattern languages therefore describe + * just the pattern, without padding transitions. The private frequency + * constraint maintains supporting suffixes after the assigned prefix and + * removes items with insufficient support. The method follows Amina Kemmar, + * Samir Loudni, Yahia Lebbah, Patrice Boizumault, and Thierry Charnois, + * "PREFIX-PROJECTION Global Constraint for Sequential Pattern Mining", + * CP 2015, pp. 226-243, https://doi.org/10.1007/978-3-319-23219-5_17. + * This implementation also bounds the remaining pattern length by the lengths + * of the supporting suffixes. Projection can be combined with the pattern's + * length, item, and language restrictions. OpenRoute is the simpler + * introduction to open lists. + * + * The paper's Table 1 can be mined with: + * -sequences ABCBC,BABC,AB,BCD -support 50 -language none. + * -min-length and -required constrain length and item membership. -language + * also accepts motif (A, any items, C), re10, re14, re2 (expressions + * from the paper). Their letters mean item IDs A=1 through Z=26; integer data + * must use the corresponding mapping for these named expressions. + * + * -file reads one sequence per line in -format letters, integers, or spmf. + * Integer IDs are positive and preserved in output. SPMF records must have + * singleton itemsets: "1 -1 2 -1 3 -1 -2" is the sequence ABC. Multi-item + * itemsets are rejected because they define a different mining problem. + * No gap or time restrictions are supported. + * + * \ingroup Example + */ +class CommonPattern : public IntMaximizeScript { +protected: + OpenIntVarList pattern; + IntVar frequency; + PatternMining::Database database; + bool letters; +public: + enum { SEARCH_BAB, SEARCH_DFS }; + + CommonPattern(const CommonPatternOptions& opt) + : IntMaximizeScript(opt), database(opt.database()), letters(opt.letters()) { + pattern = OpenIntVarList(*this,database.domain(),database.max_length()); + rel(*this,pattern.length(),IRT_GQ,opt.min_length()); + frequency = PatternMining::frequent(*this,pattern,database,opt.support()); + if (opt.language() != PatternMining::NONE) + extensional(*this,pattern,PatternMining::language(opt.language(),database)); + if (!opt.required().empty()) { + REG alphabet(IntArgs(database.alphabet)); + for (int item : opt.required()) + extensional(*this,pattern,*alphabet + REG(item) + *alphabet); + } + branch(*this,pattern,INT_VAR_NONE(),INT_VAL_MIN()); + } + + virtual IntVar cost(void) const { return pattern.length(); } + + CommonPattern(CommonPattern& s) + : IntMaximizeScript(s), database(s.database), letters(s.letters) { + pattern.update(*this,s.pattern); + frequency.update(*this,s.frequency); + } + + virtual Space* copy(void) { return new CommonPattern(*this); } + + virtual void print(std::ostream& os) const { + os << "\t"; + for (int i=0; i('A'+item-1); + else + os << item; + } else { + os << pattern[i]; + } + } + os << " (length " << pattern.length() + << ", support " << frequency << '/' << database.ends.size() + << ")" << std::endl; + } +}; + +/** \brief Main-function + * \relates CommonPattern + */ +int +main(int argc, char* argv[]) { + CommonPatternOptions opt; + opt.solutions(0); + opt.search(CommonPattern::SEARCH_DFS); + opt.search(CommonPattern::SEARCH_BAB,"bab","find a longest pattern"); + opt.search(CommonPattern::SEARCH_DFS,"dfs","enumerate frequent patterns"); + opt.parse(argc,argv); + try { + opt.read(); + } catch (const std::invalid_argument& error) { + std::cerr << error.what() << std::endl; + return 1; + } + if (opt.search() == CommonPattern::SEARCH_DFS) + IntMaximizeScript::run(opt); + else + IntMaximizeScript::run(opt); + return 0; +} + +// STATISTICS: example-any diff --git a/examples/common-pattern.hh b/examples/common-pattern.hh new file mode 100644 index 0000000000..6524010652 --- /dev/null +++ b/examples/common-pattern.hh @@ -0,0 +1,455 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* + * Private modelling support for the common-pattern example. + * Copyright Mikael Zayenz Lagerkvist, 2026. + * Distributed under the MIT license; see LICENSE. + */ + +#ifndef GECODE_EXAMPLES_COMMON_PATTERN_HH +#define GECODE_EXAMPLES_COMMON_PATTERN_HH + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace PatternMining { + + using namespace Gecode; + typedef std::vector> Sequences; + + enum Format { LETTERS, INTEGERS, SPMF }; + enum Language { DEFAULT, NONE, MOTIF, RE10, RE14, RE2 }; + + /// Read positive integer items or A..Z (represented by item IDs 1..26) + inline std::vector + read_sequence(const std::string& line, Format format) { + std::vector sequence; + if (format == LETTERS) { + for (char a : line) { + if ((a < 'A') || (a > 'Z')) + throw std::invalid_argument("Letter sequences must contain A..Z only"); + sequence.push_back(a-'A'+1); + } + } else { + std::istringstream stream(line); + std::string token; + bool open_itemset = false, ended = false; + while (stream >> token) { + int item; + const auto parsed = std::from_chars(token.data(),token.data()+token.size(),item); + if ((parsed.ec != std::errc()) || (parsed.ptr != token.data()+token.size())) + throw std::invalid_argument("Invalid integer item"); + if (ended) + throw std::invalid_argument("Items after SPMF sequence end"); + if ((format == SPMF) && (item == -1)) { + if (!open_itemset) + throw std::invalid_argument("Empty SPMF itemset"); + open_itemset = false; + } else if ((format == SPMF) && (item == -2)) { + if (open_itemset) + throw std::invalid_argument("Missing SPMF itemset end"); + ended = true; + } else { + if ((item <= 0) || (item > Int::Limits::max) || + ((format == SPMF) && open_itemset)) + throw std::invalid_argument("Expected positive items; SPMF itemsets must be singletons"); + sequence.push_back(item); + open_itemset = true; + } + } + if (!stream.eof() || ((format == SPMF) && !ended)) + throw std::invalid_argument("Invalid sequence or missing SPMF sequence end"); + } + if (sequence.empty()) + throw std::invalid_argument("Empty sequence"); + return sequence; + } + + /// Read one sequence per line, retaining duplicate rows as distinct records + inline Sequences + read_database(std::istream& stream, Format format) { + Sequences input; + std::string line; + while (std::getline(stream,line)) { + if (!line.empty() && (line.back() == '\r')) + line.pop_back(); + if (line.empty() || ((format == SPMF) && + ((line[0] == '#') || (line[0] == '%') || (line[0] == '@')))) + continue; + input.push_back(read_sequence(line,format)); + } + if (!stream.eof() || input.empty()) + throw std::invalid_argument("Cannot read a nonempty sequence database"); + return input; + } + + /// Round the support percentage (at most six decimals) up to a record count + inline int + minimum_support(const std::string& percent, int sequences) { + unsigned long long numerator = 0, scale = 1; + bool decimal = false, digit = false; + for (char c : percent) { + if ((c == '.') && !decimal) { + decimal = true; + } else if ((c >= '0') && (c <= '9')) { + digit = true; + numerator = numerator*10 + (c-'0'); + if (decimal) + scale *= 10; + if ((numerator > 100000000ULL) || (scale > 1000000ULL)) + throw std::invalid_argument("Support needs at most six decimals"); + } else { + throw std::invalid_argument("Support must be a percentage in (0,100]"); + } + } + if (!digit || (numerator == 0) || (numerator > 100*scale)) + throw std::invalid_argument("Support must be a percentage in (0,100]"); + const unsigned long long denominator = 100*scale; + return static_cast((sequences*numerator+denominator-1)/denominator); + } + + /// Immutable input, with dense ranks internally and original IDs externally + class Database { + public: + /// Concatenated item ranks, exclusive record ends, and rank-to-ID mapping + IntSharedArray items, ends, alphabet; + /// Each item's last position per record, in decreasing position order + IntSharedArray last_positions, last_ends; + + explicit Database(const Sequences& input) { + if (input.empty()) + throw std::invalid_argument("Empty database"); + std::vector symbols; + for (const std::vector& sequence : input) { + if (sequence.empty() || + (sequence.size() >= static_cast(Int::Limits::max)-symbols.size())) + throw std::invalid_argument("Empty sequence or database too large"); + for (int item : sequence) { + if ((item <= 0) || (item > Int::Limits::max)) + throw std::invalid_argument("Item IDs must be positive Gecode integers"); + symbols.push_back(item); + } + } + items.init(static_cast(symbols.size())); + std::sort(symbols.begin(),symbols.end()); + symbols.erase(std::unique(symbols.begin(),symbols.end()),symbols.end()); + alphabet.init(static_cast(symbols.size())); + std::copy(symbols.begin(),symbols.end(),alphabet.begin()); + ends.init(static_cast(input.size())); + last_ends.init(static_cast(input.size())); + Region region; + int* seen = region.alloc(alphabet.size()); + std::fill(seen,seen+alphabet.size(),-1); + std::vector last; + int k = 0, i = 0; + for (const std::vector& sequence : input) { + const int start = k; + for (int item : sequence) + items[k++] = static_cast(std::lower_bound( + symbols.begin(),symbols.end(),item)-symbols.begin()); + ends[i] = k; + for (int j=k; j-- > start; ) + if (seen[items[j]] != i) { + seen[items[j]] = i; + last.push_back(j); + } + last_ends[i++] = static_cast(last.size()); + } + last_positions.init(static_cast(last.size())); + std::copy(last.begin(),last.end(),last_positions.begin()); + } + + IntSet domain(void) const { return IntSet(alphabet.begin(),alphabet.size()); } + + int max_length(void) const { + int bound = 0, start = 0; + for (int end : ends) { + bound = std::max(bound,end-start); + start = end; + } + return bound; + } + + /// Count supporting records, rather than embeddings within a record + int support(const std::vector& pattern) const { + int count = 0, start = 0; + for (int end : ends) { + size_t matched = 0; + for (int i=start; (i(database.ends.size())), + size(database.ends.size()), data(database), threshold(support) { + for (int i=0; i(p.size)), size(p.size), + data(p.data), threshold(p.threshold) { + std::copy(p.suffixes,p.suffixes+size,suffixes); + } + + /// Extend the prefix by one item and report whether enough records support it + bool advance(int item) { + const int rank = static_cast(std::lower_bound( + data.alphabet.begin(),data.alphabet.end(),item)-data.alphabet.begin()); + if ((rank == data.alphabet.size()) || (data.alphabet[rank] != item)) + return false; + int n = 0; + for (int i=0; i= threshold; + } + + int support(void) const { return size; } + + /** + * \brief Items occurring in at least threshold supporting suffixes + * + * Last occurrences count each item at most once per record. They are + * scanned in decreasing position order, so the scan stops at the suffix + * boundary. + */ + IntSet frequent(Region& region) const { + const int d = data.alphabet.size(); + int* count = region.alloc(d); + std::fill(count,count+d,0); + for (int i=0; i(d); + int n = 0; + for (int a=0; a= threshold) + values[n++] = data.alphabet[a]; + return IntSet(values,n); + } + + /// The threshold-th largest suffix length bounds every frequent extension + int remaining(Region& region) const { + int* sizes = region.alloc(size); + for (int i=0; i()); + return sizes[threshold-1]; + } + }; + + /** + * \brief Private prefix projection for a growing list + * + * Adapts ProjectSDB (Algorithm 1) and Filter-Prefix-Projection (Algorithm 2) + * from "PREFIX-PROJECTION Global Constraint for Sequential Pattern Mining", + * Amina Kemmar, Samir Loudni, Yahia Lebbah, Patrice Boizumault, and + * Thierry Charnois, CP 2015, https://doi.org/10.1007/978-3-319-23219-5_17 + * + * The open list replaces the termination marker. Suffix lengths bound how + * many more items can follow the assigned prefix. The projection advances + * only through the leading assigned positions. Items frequent in the + * supporting suffixes filter every remaining materialized position. + * Adding positions reuses the cached items and suffix-length bound until + * the assigned prefix grows. + * + * The support variable is bounded by the prefix's supporting-record count + * and becomes exact once the list is closed and fully assigned. + */ + class OpenProjection : public Propagator { + protected: + OpenIntVarList list; + Int::IntView length, frequency; + Projection projection; + IntSet available; + int consumed, subscribed; + /// Filtering depends on the assigned prefix, not on constructed positions + int cached_prefix, extension_bound; + + OpenProjection(Space& home, OpenIntVarList pattern, + const Database& database, int support, IntVar count) + : Propagator(home), list(pattern), length(pattern.length()), frequency(count), + projection(home,database,support), consumed(0), + subscribed(pattern.known_length()), cached_prefix(-1), extension_bound(0) { + home.notice(*this,AP_DISPOSE); + list.subscribe(home,*this); + length.subscribe(home,*this,Int::PC_INT_BND); + for (int i=0; i database.ends.size())) { + home.fail(); + return IntVar(); + } + IntVar count(home,support,database.ends.size()); + OpenProjection::post(home,pattern,database,support,count); + return count; + } + + /// Built-in motif and benchmark languages described in CommonPattern + inline REG + language(Language name, const Database& database) { + if (name == RE10) + return *REG(1) + REG(2) + (REG(2) | REG(3)) + *REG(4) + + REG(5) + *REG(6) + (REG(7) | REG(8)) + *REG(9); + if (name == RE14) + return *REG(1) + (REG(17) | (REG(2) + *REG(19) + + (REG(2) | REG(3)))) + *REG(4) + REG(5) + *(REG(9) | REG(19)) + + (REG(6) | REG(8)) + *REG(7) + REG(18); + REG alphabet(IntArgs(database.alphabet.size(),database.alphabet.begin())); + if (name == MOTIF) + return REG(1) + *alphabet + REG(3); + return (REG(19) | REG(20)) + alphabet + (REG(18) | REG(11)); + } + +} + +#endif diff --git a/examples/fifteen-puzzle.cpp b/examples/fifteen-puzzle.cpp new file mode 100644 index 0000000000..f4456905a2 --- /dev/null +++ b/examples/fifteen-puzzle.cpp @@ -0,0 +1,398 @@ +/* -*- 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 +#include +#include +#include +#include +#include + +using namespace Gecode; +using Board = std::array; + +/// Options for the initial scramble and bounded solution path. +class PuzzleOptions : public Options { + Driver::UnsignedIntOption _scramble; + Driver::UnsignedIntOption _limit; +public: + enum { MODEL_OPEN, MODEL_FIXED }; + enum { BRANCH_MIN, BRANCH_MANHATTAN }; + PuzzleOptions(void) + : Options("FifteenPuzzle"), + _scramble("scramble","number of random slides from the goal",12), + _limit("limit","maximum number of solution moves",12) { + add(_scramble); + add(_limit); + model(MODEL_OPEN); + model(MODEL_OPEN,"open","construct states as the path grows"); + model(MODEL_FIXED,"fixed","construct all bounded states initially"); + branching(BRANCH_MIN); + branching(BRANCH_MIN,"min","try the lowest blank position first"); + branching(BRANCH_MANHATTAN,"manhattan", + "try the slide with least Manhattan distance first"); + solutions(0); + seed(1); + } + unsigned int + scramble(void) const { + return _scramble.value(); + } + int + limit(void) const { + Int::Limits::check(16*(static_cast(_limit.value())+1), + "FifteenPuzzle::limit"); + return static_cast(_limit.value()); + } +}; + +static Board +goal(void) { + return {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,0}; +} + +static IntArgs +neighbors(int blank) { + IntArgs result; + if (blank >= 4) + result << blank-4; + if (blank % 4 != 0) + result << blank-1; + if (blank % 4 != 3) + result << blank+1; + if (blank < 12) + result << blank+4; + return result; +} + +static Board +initial_board(const PuzzleOptions& opt) { + Board board = goal(); + Rnd random(opt.seed()); + int blank = 15; + int previous = -1; + for (unsigned int i=0; i(choices.size()))]; + std::swap(board[blank],board[next]); + previous = blank; + blank = next; + } + return board; +} + +static int +manhattan(const Board& board) { + int distance = 0; + for (int i=0; i<16; i++) { + if (board[i] != 0) { + const int target = board[i]-1; + distance += std::abs(i/4-target/4) + std::abs(i%4-target%4); + } + } + return distance; +} + +static Board +values(const IntVarArgs& tiles) { + Board result; + for (int i=0; i<16; i++) + result[i] = tiles[i].val(); + return result; +} + +// The assigned preceding board changes only at the blank and the moved tile. +static int +manhattan_value(const IntVarArgs& before, int blank, IntVar next) { + int best = next.min(); + int best_change = std::numeric_limits::max(); + for (IntVarValues candidates(next); candidates(); ++candidates) { + const int position = candidates.val(); + const int target = before[position].val()-1; + const int change = + std::abs(blank/4-target/4) + std::abs(blank%4-target%4) - + std::abs(position/4-target/4) - std::abs(position%4-target%4); + if (change < best_change) { + best = position; + best_change = change; + } + } + return best; +} + +static void +print_board(std::ostream& os, const Board& board) { + for (int i=0; i<16; i++) { + os << std::setw(3) << board[i]; + if (i % 4 == 3) + os << '\n'; + } +} + +static void +post_start(Home home, const IntVarArgs& tiles, IntVar blank, + const Board& initial) { + for (int i=0; i<16; i++) + rel(home,tiles[i] == initial[i]); + const int zero = static_cast( + std::find(initial.begin(),initial.end(),0)-initial.begin()); + rel(home,blank == zero); +} + +// Swap the blank with an adjacent tile and preserve every other position. +static void +post_slide(Home home, + const IntVarArgs& before, IntVar old_blank, + const IntVarArgs& after, IntVar new_blank) { + static const TupleSet adjacent = [] { + TupleSet table(2); + for (int i=0; i<16; i++) { + for (int j : neighbors(i)) + table.add(IntArgs({i,j})); + } + table.finalize(); + return table; + }(); + extensional(home,IntVarArgs({old_blank,new_blank}),adjacent); + rel(home,element(after,old_blank) == element(before,new_blank)); + rel(home,element(after,new_blank) == 0); + for (int i=0; i<16; i++) { + rel(home,((old_blank != i) && (new_blank != i)) >> + (before[i] == after[i])); + } +} + +// The last board must be solved; reaching the goal forbids a longer path. +static void +post_goal(Home home, const IntVarArgs& tiles, IntVar length, + int states) { + const Board target = goal(); + BoolExpr matches(tiles[0] == target[0]); + for (int i=1; i<16; i++) + matches = matches && (tiles[i] == target[i]); + BoolVar solved = expr(home,matches); + rel(home,(length == states) >> solved); + rel(home,solved >> (length <= states)); +} + +/** + * \brief %Example: solve the 15-puzzle with an open list of states + * + * Start from a solvable board obtained by randomly sliding from the goal. + * Each path variable records the blank position in one board. The tile list + * holds sixteen variables per board. The transition on the path constructs and + * constrains the next board when that position is materialized. + * 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. + * + * The goal closes the path. Length counts states including the start, so + * branch and bound minimizes moves by minimizing length minus one. + * The independent move limit bounds search and need not equal the scramble. + * Defaults use twelve slides, seed one, and a twelve-move limit. Select + * -model fixed to compare with an array representation; -scramble, -seed, + * and -limit control the instance and search bound independently. + * -branching min (default) tries blank positions in numerical order; + * -branching manhattan tries the slide with the lowest resulting Manhattan + * distance, breaking ties by the lowest position. + * + * \ingroup Example + */ +class OpenPuzzle : public Script { + OpenIntVarList tiles; + OpenIntVarList path; +public: + OpenPuzzle(const PuzzleOptions& opt) + : Script(opt), + tiles(*this,IntSet(0,15),16*(opt.limit()+1)), + path(*this,IntSet(0,15), + [](Space& home, OpenIntVarList x, int i) { + static_cast(home).transition(x,i); + },opt.limit()+1) { + const Board initial = initial_board(opt); + rel(*this,tiles.length() == 16*path.length()); + IntVar start = path.get(*this,0); + post_start(*this,state(0),start,initial); + rel(*this,path.length() >= manhattan(initial)+1); + IntValBranch vals = INT_VAL_MIN(); + if (opt.branching() == PuzzleOptions::BRANCH_MANHATTAN) + vals = INT_VAL([](const Space& home, IntVar next, int i) { + const OpenPuzzle& s = static_cast(home); + return manhattan_value(s.state(i-1),s.path[i-1].val(),next); + }); + branch(*this,path,INT_VAR_NONE(),vals); + } + + void + transition(OpenIntVarList x, int i) { + tiles.materialize(*this,16*(i+1)); + if (failed()) + return; + if (i > 0) + post_slide(*this,state(i-1),x[i-1],state(i),x[i]); + post_goal(*this,state(i),path.length(),i+1); + } + + OpenPuzzle(OpenPuzzle& s) : Script(s) { + tiles.update(*this,s.tiles); + path.update(*this,s.path); + } + Space* + copy(void) override { + return new OpenPuzzle(*this); + } + IntVarArgs + state(int i) const { + IntVarArgs result(16); + for (int j=0; j<16; j++) + result[j] = tiles[16*i+j]; + return result; + } + void + constrain(const Space& solution) override { + const OpenPuzzle& best = static_cast(solution); + rel(*this,path.length() < best.path.length().val()); + } + void + print(std::ostream& os) const override { + os << "Moves: " << path.length().val()-1 << "\nBlank positions:"; + for (int i=0; i= manhattan(initial)+1); + IntValBranch vals = INT_VAL_MIN(); + if (opt.branching() == PuzzleOptions::BRANCH_MANHATTAN) + vals = INT_VAL([](const Space& home, IntVar next, int i) { + const FixedPuzzle& s = static_cast(home); + return manhattan_value(s.state(i-1),s.path[i-1].val(),next); + }); + branch(*this,path,INT_VAR_NONE(),vals, + [](const Space& home, IntVar, int i) { + const FixedPuzzle& s = static_cast(home); + const int g = s.depth(); + return values(s.state(g)) != goal() && i == g+1; + }); + } + + FixedPuzzle(FixedPuzzle& s) : Script(s) { + tiles.update(*this,s.tiles); + path.update(*this,s.path); + length.update(*this,s.length); + } + Space* + copy(void) override { + return new FixedPuzzle(*this); + } + IntVarArgs + state(int i) const { + IntVarArgs result(16); + for (int j=0; j<16; j++) + result[j] = tiles[16*i+j]; + return result; + } + int + depth(void) const { + int i = 0; + while (i+1 < path.size() && path[i+1].assigned()) + i++; + return i; + } + void + constrain(const Space& solution) override { + const FixedPuzzle& best = static_cast(solution); + rel(*this,length < best.length.val()); + } + void + print(std::ostream& os) const override { + os << "Moves: " << length.val()-1 << "\nBlank positions:"; + for (int i=0; i(opt); + else + Script::run(opt); + return 0; +} + +// STATISTICS: example-any diff --git a/examples/open-route.cpp b/examples/open-route.cpp new file mode 100644 index 0000000000..8bab74aaab --- /dev/null +++ b/examples/open-route.cpp @@ -0,0 +1,129 @@ +/* -*- 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 + +using namespace Gecode; + +/** + * \brief %Example: simple routes of unknown length + * + * Find all simple routes from node 0 to node 4 in this directed graph: + * 0 -> 1, 2, 4 + * 1 -> 2, 4 + * 2 -> 1, 3 + * 3 -> 2, 4 + * + * The graph contains cycles, but distinct node variables exclude revisits. + * A simple route uses at most the five nodes, so this finite bound preserves + * every solution while ensuring that search terminates. + * + * \ingroup Example + */ +class OpenRoute : public Script { +protected: + /// Number of graph nodes and maximal route length + static const int n = 5; + /// Node IDs in route order, including source and destination + OpenIntVarList route; +public: + /// Branching to use for the route + enum { + BRANCH_INCREMENTAL, ///< Assign the current prefix before extending it + BRANCH_LENGTH ///< Choose the complete route length first + }; + + /// Construct the model + OpenRoute(const Options& opt) + : Script(opt), route(*this,IntSet(0,n-1),n) { + // State 5 consumes the source node. Each remaining transition consumes + // the destination of a graph arc; only node 4 is an accepting state. + DFA paths(5,{ + {5,0,0}, + {0,1,1}, {0,2,2}, {0,4,4}, + {1,2,2}, {1,4,4}, + {2,1,1}, {2,3,3}, + {3,2,2}, {3,4,4} + },{4}); + extensional(*this,route,paths); + distinct(*this,route); + + // Ordinary brancher posting order gives length-first search when wanted. + if (opt.branching() == BRANCH_LENGTH) + branch(*this,route.length(),INT_VAL_MIN()); + branch(*this,route,INT_VAR_NONE(),INT_VAL_MIN()); + } + + /// Constructor for cloning \a s, including the space-local open list + OpenRoute(OpenRoute& s) : Script(s) { + route.update(*this,s.route); + } + + /// Perform copying during cloning + virtual Space* + copy(void) { + return new OpenRoute(*this); + } + + /// Print the materialized route (complete in a solution) + virtual void + print(std::ostream& os) const { + os << "\t"; + for (int i=0; i 0) + os << " -> "; + os << route[i]; + } + os << " (length " << route.length() << ")" << std::endl; + } +}; + +/** \brief Main-function + * \relates OpenRoute + */ +int +main(int argc, char* argv[]) { + Options opt("OpenRoute"); + opt.solutions(0); + opt.branching(OpenRoute::BRANCH_INCREMENTAL); + opt.branching(OpenRoute::BRANCH_INCREMENTAL,"incremental", + "Assign the current prefix before extending the route"); + opt.branching(OpenRoute::BRANCH_LENGTH,"length", + "Choose the complete route length first"); + opt.parse(argc,argv); + Script::run(opt); + return 0; +} + +// STATISTICS: example-any diff --git a/gecode/int.hh b/gecode/int.hh index 04bfb99d70..1e37b7a950 100755 --- a/gecode/int.hh +++ b/gecode/int.hh @@ -2424,8 +2424,352 @@ namespace Gecode { std::size_t hash(void) const; }; + /** + * \brief Space-local list of variables with an open length + * + * Variables can be appended until the list is closed. The length + * variable denotes the eventual list length. Propagation constructs the + * positions required by the minimum length. The known_length() query counts + * the constructed positions. Indexed access is limited to this prefix; + * get() constructs positions and constrains the eventual length to include + * the requested position. + * + * Handles in one Space share the same list. Updating during cloning + * preserves that sharing in the clone with separate mutable state. + * + * OpenIntVarList and OpenBoolVarList provide standard factories. + * Other variable types use a factory passed to OpenVarList, for + * example a function returning SetVar or FloatVar. Storage, cloning, and + * materialization are shared; constraint and branching overloads remain + * specific to the variable type. Var must provide the usual Gecode variable + * handle operations: default and copy construction, assignment, assigned(), + * varimp(), and update(). + * + * Operations require an initialized list belonging to the supplied space. + * Appended variables must also be initialized and belong to that space. + * + * A list can be used in an ordinary Space: + * \code + * class Model : public Space { + * public: + * OpenIntVarList x; + * Model(void) : x(*this,IntSet(0,2),3) { + * rel(*this,x.length(),IRT_GQ,1); + * distinct(*this,x); + * branch(*this,x); + * } + * Model(Model& s) : Space(s) { + * x.update(*this,s.x); + * } + * virtual Space* copy(void) { + * return new Model(*this); + * } + * }; + * \endcode + * + * The \ref CommonPattern "common-pattern example" finds constrained + * frequent subsequences across several inputs. The pattern length is a + * decision variable. The open list avoids allocating an array up to the + * input-length bound and encoding an unused suffix with a termination + * symbol. Prefix projection and regular constraints on the pattern + * language follow list growth. + * The \ref OpenRoute "open-route example" is a smaller introduction, + * including search and printing solutions. + * + * \ingroup TaskModelInt + */ + template + class OpenVarList : public LocalHandle { + private: + /// Space-local list implementation + class Object; + public: + /** + * \brief Function creating the variable at position \a i in \a home + * + * The function object is shared between clones. It must be deterministic + * and safe for concurrent calls. Captured data must be owned or shared + * and must not be space-local. Returned variables must belong to \a home + * and be initialized. + * + * Materialization calls the factory inside a Space::NoGPI scope. + * New propagators keep their information in the space and have zero + * AFC, so AFC-based branching does not count them. Identifiers and + * groups remain available. Group changes are local to the space; clones + * copy the current group. A callback-local + * Space::UseGPI scope enables global information and ordinary AFC for + * selected postings. Nested materialization starts a new NoGPI scope. + * Each scope restores the previous setting on exit, including + * exception unwinding. + */ + typedef std::function Factory; + /** + * \brief Function called after materializing position \a i + * + * The function object is shared between clones. It must be + * deterministic and safe for concurrent calls. Captured data must be + * owned or shared and must not be space-local; \a home and \a x refer + * to the current space. + * + * Calls from materialize() and direct append() run inside a + * Space::NoGPI scope with the same AFC and group behavior as Factory. + * A callback-local Space::UseGPI scope enables global information for + * selected postings. Nested materialize() or append() calls enter their + * own NoGPI scope and restore the callback's setting when they return. + */ + typedef std::function x, int i)> + Transition; + /// Construct an uninitialized list + OpenVarList(void); + /// Construct a list using \a create for each position + OpenVarList(Space& home, Factory create, + int max=Int::Limits::max); + /// Construct a list using \a create and transition function \a t + OpenVarList(Space& home, Factory create, Transition t, + int max=Int::Limits::max); + /// Update during cloning + void update(Space& home, OpenVarList& s); + /// Return the number of constructed positions (their values may be unassigned) + int known_length(void) const; + /// Return variable at position \a i, where 0 <= \a i < known_length() + Var operator [](int i) const; + /// Return the eventual-length variable + IntVar length(void) const; + /** + * \brief Append variable \a x + * + * Constrains the eventual length to include the new position and calls + * the transition function, if supplied, inside a Space::NoGPI scope. + * The appended position is available to the transition through indexed + * access. Appending beyond the maximum eventual length fails the space. + * + * Throws an exception of type Int::ArgumentSame if \a x is an + * unassigned variable already in the list. + */ + void append(Space& home, Var x); + /** + * \brief Materialize the first \a n variables + * + * The eventual length is constrained to be at least \a n. Positions + * required by its minimum are materialized, including any increase + * imposed by the transition function. + * + * Factory and transition calls run inside Space::NoGPI scopes by + * default. A callback-local Space::UseGPI scope enables global + * information for selected postings (see Factory). + */ + void materialize(Space& home, int n); + /** + * \brief Require and return the variable at position \a i + * + * Constrains the eventual length to be at least \a i+1 and materializes + * positions through \a i, using the Space::NoGPI scopes documented + * for materialize(). + * + * Returns an uninitialized variable if \a home is failed or + * materialization fails. Throws Int::OutOfLimits for a negative index, + * an index outside Int::Limits, or (in a nonfailed space) an index at + * least length().max(). Check for failure before using the result, + * for example with an integer list: + * \code + * IntVar y = x.get(home,i); + * if (home.failed()) + * return; + * rel(home,y,IRT_NQ,0); + * \endcode + */ + Var get(Space& home, int i); + /** + * \brief Close the list at its materialized length + * + * First materializes any positions required by the minimum eventual + * length, using the Space::NoGPI scopes documented for materialize(), + * then assigns the eventual length to the number of positions. + * The contained variables can remain unassigned. + */ + void close(Space& home); + /** + * \brief Subscribe \a p to the addition of positions + * + * This reports construction of positions. Subscribe separately to the + * length variable or contained variables for length or value changes. + */ + void subscribe(Space& home, Propagator& p); + /// Cancel the subscription of \a p to the addition of positions + void cancel(Space& home, Propagator& p); + /// Reschedule \a p for the addition of positions + void reschedule(Space& home, Propagator& p); + }; + + + /** + * \brief Open list of integer variables + * \ingroup TaskModelInt + */ + class OpenIntVarList : public OpenVarList { + public: + /** + * \brief Function returning the domain for position \a i + * + * Like Factory, it must be deterministic, safe for concurrent calls, and + * must not capture space-local data. Captured data must be owned or shared. + * The function object is shared between clones. + */ + typedef std::function Domain; + using OpenVarList::OpenVarList; + /// Construct an uninitialized list + GECODE_INT_EXPORT OpenIntVarList(void); + /// Share an integer list constructed through a variable factory + OpenIntVarList(const OpenVarList& x); + /// Construct a list with maximal length \a max and full domains + GECODE_INT_EXPORT + OpenIntVarList(Space& home, int max=Int::Limits::max); + /// Construct a list using domain \a d for every position + GECODE_INT_EXPORT + OpenIntVarList(Space& home, const IntSet& d, + int max=Int::Limits::max); + /// Construct a list using domain \a d and transition function \a t + GECODE_INT_EXPORT + OpenIntVarList(Space& home, const IntSet& d, Transition t, + int max=Int::Limits::max); + /// Construct a list using \a d to create each position + GECODE_INT_EXPORT + OpenIntVarList(Space& home, Domain d, + int max=Int::Limits::max); + /// Construct a list using \a d and transition function \a t + GECODE_INT_EXPORT + OpenIntVarList(Space& home, Domain d, Transition t, + int max=Int::Limits::max); + private: + /// Convert a position-dependent domain to a variable factory + static Factory factory(Domain d); + }; + + /** + * \brief Open list of Boolean variables + * \ingroup TaskModelInt + */ + class OpenBoolVarList : public OpenVarList { + public: + using OpenVarList::OpenVarList; + /// Construct an uninitialized list + GECODE_INT_EXPORT OpenBoolVarList(void); + /// Share a Boolean list constructed through a variable factory + OpenBoolVarList(const OpenVarList& x); + /// Construct a list with maximal length \a max and domains 0..1 + GECODE_INT_EXPORT + OpenBoolVarList(Space& home, int max=Int::Limits::max); + /// Construct a list with domains 0..1 and transition function \a t + GECODE_INT_EXPORT + OpenBoolVarList(Space& home, Transition t, + int max=Int::Limits::max); + }; + } +namespace Gecode { + + /** + * \brief Constrain all materialized variables in \a x to be distinct + * + * The constraint is value consistent and applies to variables added + * after posting. The propagation level \a ipl is ignored. + * + * \ingroup TaskModelIntDistinct + */ + GECODE_INT_EXPORT void + distinct(Home home, OpenIntVarList x, + IntPropLevel ipl=IPL_DEF); + + /** + * \brief Constrain consecutive materialized variables in \a x by \a irt + * + * The constraint applies to variables added after posting. + * + * \ingroup TaskModelIntRel + */ + GECODE_INT_EXPORT void + rel(Home home, OpenIntVarList x, IntRelType irt, + IntPropLevel ipl=IPL_DEF); + + /** + * \brief Post a sliding sequence constraint on \a x + * + * Each complete window of size \a q, including windows completed after + * posting, contains between \a l and \a u values from \a s. A list shorter + * than \a q has no complete windows and satisfies the constraint. + * + * \ingroup TaskModelIntSequence + */ + GECODE_INT_EXPORT void + sequence(Home home, OpenIntVarList x, const IntSet& s, + int q, int l, int u, IntPropLevel ipl=IPL_DEF); + + /** + * \brief Constrain the sum of every complete window in \a x + * + * Each complete window of size \a q, including windows completed after + * posting, has a sum between \a l and \a u. A list shorter than \a q has no + * complete windows and satisfies the constraint. + * + * \ingroup TaskModelIntSequence + */ + GECODE_INT_EXPORT void + slidingsum(Home home, OpenIntVarList x, + int q, int l, int u, IntPropLevel ipl=IPL_DEF); + + /** + * \brief Constrain \a y to the minimum of the eventual sequence \a x + * + * The eventual sequence must be nonempty. Bounds are enforced as positions + * appear; at least one variable must equal \a y when the list closes. + * + * \ingroup TaskModelIntArith + */ + GECODE_INT_EXPORT void + min(Home home, OpenIntVarList x, IntVar y, + IntPropLevel ipl=IPL_DEF); + + /** + * \brief Constrain \a y to the maximum of the eventual sequence \a x + * + * The eventual sequence must be nonempty. Bounds are enforced as positions + * appear; at least one variable must equal \a y when the list closes. + * + * \ingroup TaskModelIntArith + */ + GECODE_INT_EXPORT void + max(Home home, OpenIntVarList x, IntVar y, + IntPropLevel ipl=IPL_DEF); + + /** + * \brief Constrain \a s to precede \a t in the eventual sequence \a x + * + * Every occurrence of \a t requires an earlier occurrence of \a s; + * neither value is required to occur. The constraint applies to variables + * added after posting. The propagation level \a ipl is ignored. + * + * \ingroup TaskModelIntPrecede + */ + GECODE_INT_EXPORT void + precede(Home home, OpenIntVarList x, int s, int t, + IntPropLevel ipl=IPL_DEF); + + /** + * \brief Constrain successive values in \a c to precede each other in \a x + * + * The constraint applies to variables added after posting. + * + * \ingroup TaskModelIntPrecede + */ + GECODE_INT_EXPORT void + precede(Home home, OpenIntVarList x, const IntArgs& c, + IntPropLevel ipl=IPL_DEF); + +} + +#include + #include namespace Gecode { namespace Int { namespace Extensional { @@ -2841,6 +3185,34 @@ namespace Gecode { extensional(Home home, const BoolVarArgs& x, DFA d, IntPropLevel ipl=IPL_DEF); + /** + * \brief Post an open regular-language constraint + * + * While \a x is open, its materialized prefix must have a continuation + * accepted by \a d within the maximum length of \a x. Future variable domains + * and holes in the length domain are not used to filter these continuations. + * Once its length equals its materialized size, the complete list must be + * accepted by \a d. + * + * \ingroup TaskModelIntExt + */ + GECODE_INT_EXPORT void + extensional(Home home, OpenIntVarList x, DFA d, + IntPropLevel ipl=IPL_DEF); + + /** + * \brief Post an open regular-language constraint on Boolean variables + * + * As for integer lists, the materialized prefix must have an accepted + * continuation within its maximum length while open, and the complete + * list must be accepted when closed. + * + * \ingroup TaskModelIntExt + */ + GECODE_INT_EXPORT void + extensional(Home home, OpenBoolVarList x, DFA d, + IntPropLevel ipl=IPL_DEF); + /** \brief Post propagator for \f$x\in t\f$. * * \li Supports domain consistency (\a ipl = IPL_DOM, default) only. @@ -5810,6 +6182,70 @@ namespace Gecode { GECODE_INT_EXPORT void branch(Home home, IntVar x, IntValBranch vals, IntVarValPrint vvp=nullptr); + /** + * \brief Branch over the eventual integer list \a x + * + * Selects the first unassigned materialized variable and its minimum + * value. Once all materialized variables are assigned, tries closing the + * list before extending it by one position. + * \ingroup TaskModelIntBranch + */ + GECODE_INT_EXPORT void + branch(Home home, OpenIntVarList x); + /** + * \brief Branch over the eventual Boolean list \a x + * \ingroup TaskModelIntBranch + */ + GECODE_INT_EXPORT void + branch(Home home, OpenBoolVarList x); + /** + * \brief Branch over an open integer list with standard selectors + * + * Selectors consider all materialized positions accepted by the filter. + * Once no unassigned variable is accepted, the brancher tries closing at + * known_length() before requiring another position and resuming + * value selection. Filters and callbacks receive the position in the list. + * Variables rejected by the filter may remain unassigned in a solution. + * Without a practical bound on list length, search can be very expensive. + * + * To decide the length before list values, post an ordinary brancher on + * x.length() before the list brancher, for example: + * \code + * branch(home,x.length(),INT_VAL_MIN()); + * branch(home,x,INT_VAR_SIZE_MIN(),INT_VAL_MIN()); + * \endcode + * Branchers run in posting order. Once the length is assigned, propagation + * materializes that many positions before the list brancher selects values. + * + * Action and CHB selection require the complete list to be materialized + * when posting; otherwise Int::UnknownBranching is thrown. + * \ingroup TaskModelIntBranch + */ + GECODE_INT_EXPORT void + branch(Home home, OpenIntVarList x, + IntVarBranch vars, IntValBranch vals, + IntBranchFilter bf=nullptr, IntVarValPrint vvp=nullptr); + /// Branch over an open integer list with tie-breaking selectors + GECODE_INT_EXPORT void + branch(Home home, OpenIntVarList x, + TieBreak vars, IntValBranch vals, + IntBranchFilter bf=nullptr, IntVarValPrint vvp=nullptr); + /** + * \brief Branch over an open Boolean list with standard selectors + * + * The incremental branching, filter, and Action/CHB restrictions are as + * for integers. + * \ingroup TaskModelIntBranch + */ + GECODE_INT_EXPORT void + branch(Home home, OpenBoolVarList x, + BoolVarBranch vars, BoolValBranch vals, + BoolBranchFilter bf=nullptr, BoolVarValPrint vvp=nullptr); + /// Branch over an open Boolean list with tie-breaking selectors + GECODE_INT_EXPORT void + branch(Home home, OpenBoolVarList x, + TieBreak vars, BoolValBranch vals, + BoolBranchFilter bf=nullptr, BoolVarValPrint vvp=nullptr); /** * \brief Branch over \a x with variable selection \a vars and value selection \a vals * diff --git a/gecode/int/extensional-open-regular.cpp b/gecode/int/extensional-open-regular.cpp new file mode 100644 index 0000000000..e745fec7e9 --- /dev/null +++ b/gecode/int/extensional-open-regular.cpp @@ -0,0 +1,345 @@ +/* -*- 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 + +namespace Gecode { namespace Int { namespace Extensional { + + /// Shortest accepting suffix length from each state, or -1 if none exists + static SharedArray + accepting_distances(const DFA& dfa) { + const int k = dfa.n_states(); + SharedArray distance(k); + Region r; + int* first = r.alloc(k); + int* source = r.alloc(dfa.n_transitions()); + int* next = r.alloc(dfa.n_transitions()); + int* queue = r.alloc(k); + for (int s=0; s + class OpenRegular : public Propagator { + protected: + OpenVarList list; + IntView length; + DFA dfa; + SharedArray distance; + unsigned char* start; + int offset; + int subscribed; + + OpenRegular(Home home, + OpenVarList list0, DFA dfa0) + : Propagator(home), list(list0), + length(list0.length()), + dfa(dfa0), distance(accepting_distances(dfa0)), + start(static_cast(home).alloc(dfa.n_states())), + offset(0), subscribed(list0.known_length()) { + home.notice(*this,AP_DISPOSE); + for (int i=0; i(p.dfa.n_states())), + offset(p.offset), subscribed(p.subscribed) { + list.update(home,p.list); + length.update(home,p.length); + for (int i=0; i= 0) && (distance[s] <= remaining)) + return true; + return false; + } + + private: + /// Mark states supporting the remaining domains through the last layer + const unsigned char* + compute_backward_support(Region& r, int remaining) const { + const int n = list.known_length()-offset; + const int k = dfa.n_states(); + const unsigned long long cells = + static_cast(n+1) * + static_cast(k); + if (cells > + static_cast + ((std::numeric_limits::max)())) + throw OutOfLimits("Int::extensional"); + + unsigned char* backward = + r.alloc(static_cast(cells)); + for (size_t i=0; i(cells); i++) + backward[i] = 0; + + unsigned char* last = backward + static_cast(n)*k; + for (int s=0; s= 0) && (distance[s] <= remaining)) + last[s] = 1; + + for (int i=n; i--; ) { + unsigned char* in = backward + static_cast(i)*k; + unsigned char* out = in+k; + View x(list[offset+i]); + for (ViewValues v(x); v(); ++v) + for (DFA::Transitions t(dfa,v.val()); t(); ++t) + if (out[t.o_state()] != 0) + in[t.i_state()] = 1; + } + return backward; + } + + public: + virtual Actor* + copy(Space& home) { + return new (home) OpenRegular(home,*this); + } + + virtual PropCost + cost(const Space&, const ModEventDelta&) const { + return PropCost::linear(PropCost::HI,subscribed-offset); + } + + virtual void + reschedule(Space& home) { + length.reschedule(home,*this,PC_INT_BND); + list.reschedule(home,*this); + for (int i=offset; i(k); + unsigned char* next = r.alloc(k); + for (int s=0; s(dfa.n_symbols()); + int n_supported = 0; + const unsigned char* out = backward + static_cast(i+1)*k; + for (ViewValues v(x); v(); ++v) { + bool support = false; + for (DFA::Transitions t(dfa,v.val()); t() && !support; ++t) + support = (reachable[t.i_state()] != 0) && + (out[t.o_state()] != 0); + if (support) + supported[n_supported++] = v.val(); + } + if (n_supported == 0) + return ES_FAILED; + Iter::Values::Array v(supported,n_supported); + GECODE_ME_CHECK(x.narrow_v(home,v,false)); + + for (int s=0; s value(x); value(); ++value) + for (DFA::Transitions t(dfa,value.val()); t(); ++t) + if ((reachable[t.i_state()] != 0) && + (out[t.o_state()] != 0)) + next[t.o_state()] = 1; + unsigned char* swap = reachable; + reachable = next; + next = swap; + } + + // Pruning an item that aliases length can reduce the suffix budget. + // Recompute support before consuming the prefix. + if (length.max()-size < remaining) + return ES_NOFIX; + + int consumed = 0; + while ((consumed < n) && list[offset+consumed].assigned()) { + const unsigned char* live = + backward + static_cast(consumed+1)*k; + for (int s=0; s list, DFA dfa) { + for (int i=0; i + ::post(home,list,dfa))); + } + + void + extensional(Home home, OpenBoolVarList list, DFA dfa, + IntPropLevel) { + GECODE_POST; + GECODE_ES_FAIL((Int::Extensional::OpenRegular + ::post(home,list,dfa))); + } + +} + +// STATISTICS: int-prop diff --git a/gecode/int/open-var-list.cpp b/gecode/int/open-var-list.cpp new file mode 100644 index 0000000000..70a47db021 --- /dev/null +++ b/gecode/int/open-var-list.cpp @@ -0,0 +1,953 @@ +/* -*- 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 + +namespace Gecode { + + OpenIntVarList::OpenIntVarList(void) {} + + OpenIntVarList::Factory + OpenIntVarList::factory(Domain d) { + if (!d) + throw InvalidFunction("OpenIntVarList"); + return [d](Space& home, int i) { return IntVar(home,d(i)); }; + } + + OpenIntVarList::OpenIntVarList(Space& home, int max) + : OpenIntVarList(home, + IntSet(Int::Limits::min,Int::Limits::max),max) {} + + OpenIntVarList::OpenIntVarList(Space& home, const IntSet& d, + int max) + : OpenIntVarList(home,[d](int) { return d; },max) {} + + OpenIntVarList::OpenIntVarList(Space& home, const IntSet& d, + Transition t, int max) + : OpenIntVarList(home,[d](int) { return d; },t,max) {} + + OpenIntVarList::OpenIntVarList(Space& home, Domain d, int max) + : OpenVarList(home,factory(d),max) {} + + OpenIntVarList::OpenIntVarList(Space& home, Domain d, + Transition t, int max) + : OpenVarList(home,factory(d),t,max) {} + + OpenBoolVarList::OpenBoolVarList(void) {} + + OpenBoolVarList::OpenBoolVarList(Space& home, int max) + : OpenVarList(home, + [](Space& home, int) { return BoolVar(home,0,1); },max) {} + + OpenBoolVarList::OpenBoolVarList(Space& home, Transition t, + int max) + : OpenVarList(home, + [](Space& home, int) { return BoolVar(home,0,1); },t,max) {} + +} + +namespace Gecode { namespace Int { + + /// Follow list growth and remember which positions have been incorporated + class OpenListPropagator : public Propagator { + protected: + OpenIntVarList list; + IntView length; + int posted; + + OpenListPropagator(Home home, OpenIntVarList list0) + : Propagator(home), list(list0), + length(list0.length()), + posted(0) { + length.subscribe(home,*this,PC_INT_BND); + list.subscribe(home,*this); + } + + OpenListPropagator(Space& home, OpenListPropagator& p) + : Propagator(home,p), posted(p.posted) { + list.update(home,p.list); + length.update(home,p.length); + } + + bool + closed(void) const { + return length.max() == list.known_length(); + } + + void + dispose_base(Space& home) { + length.cancel(home,*this,PC_INT_BND); + list.cancel(home,*this); + list.~OpenIntVarList(); + (void) Propagator::dispose(home); + } + + public: + virtual PropCost + cost(const Space&, const ModEventDelta&) const { + return PropCost::linear(PropCost::LO,list.known_length()-posted); + } + + virtual void + reschedule(Space& home) { + length.reschedule(home,*this,PC_INT_BND); + list.reschedule(home,*this); + } + }; + + + class OpenDistinct : public OpenListPropagator { + protected: + OpenDistinct(Home home, OpenIntVarList list) + : OpenListPropagator(home,list) {} + + OpenDistinct(Space& home, OpenDistinct& p) + : OpenListPropagator(home,p) {} + + public: + virtual PropCost + cost(const Space&, const ModEventDelta&) const { + return PropCost::linear(PropCost::LO,list.known_length()); + } + + virtual Actor* + copy(Space& home) { + return new (home) OpenDistinct(home,*this); + } + + virtual ExecStatus + propagate(Space& home, const ModEventDelta&) { + const int size = list.known_length(); + for (int i=posted; i(size); + int n = 0; + for (int i=0; i 1) { + std::sort(values,values+n); + for (int i=1; i + class OpenMinMax : public OpenListPropagator { + protected: + IntView result; + IntPropLevel ipl; + + OpenMinMax(Home home, OpenIntVarList list, + IntView result0, IntPropLevel ipl0) + : OpenListPropagator(home,list), + result(result0), ipl(ipl0) {} + + OpenMinMax(Space& home, OpenMinMax& p) + : OpenListPropagator(home,p), ipl(p.ipl) { + result.update(home,p.result); + } + + public: + virtual Actor* + copy(Space& home) { + return new (home) OpenMinMax(home,*this); + } + + virtual ExecStatus + propagate(Space& home, const ModEventDelta&) { + const int size = list.known_length(); + for (int i=posted; i 0) + seen.update(home,p.seen); + } + + public: + virtual Actor* + copy(Space& home) { + return new (home) OpenPrecede(home,*this); + } + + virtual ExecStatus + propagate(Space& home, const ModEventDelta&) { + if ((posted > 0) && seen.one()) + return home.ES_SUBSUMED(*this); + const int size = list.known_length(); + for (int i=posted; i + class OpenListBrancher : public Brancher { + protected: + typedef typename View::VarType Var; + OpenVarList list; + ViewSel* vs[n]; + ValSelCommitBase* vsc; + BrancherFilter filter; + SharedData> printer; + IntValBranch::Select values; + bool select_first; + mutable int start; + + OpenListBrancher(Home home, OpenVarList list0, + ViewSel* vs0[n], + ValSelCommitBase* vsc0, + IntValBranch::Select values0, + bool select_first0, + BranchFilter bf, VarValPrint vvp) + : Brancher(home), list(list0), vsc(vsc0), + filter(bf ? bf : [](const Space&, Var, int) { return true; }), + printer(vvp), values(values0), + select_first(select_first0), start(0) { + for (int i=0; icopy(home); + for (; copiedcopy(home); + } catch (...) { + for (int i=0; idispose(home); + if (vsc) + vsc->dispose(home); + throw; + } + } + + /// Select from a temporary array, keeping positions stable across growth + int + position(Space& home) { + if (select_first) + return start; + Region r; + ViewArray x(r,list.known_length()); + for (int i=0; iselect(home,x,start,filter); + int* ties = r.alloc(x.size()-start); + int n_ties; + vs[0]->ties(home,x,start,ties,n_ties,filter); + for (int i=1; (i1); i++) + vs[i]->brk(home,x,ties,n_ties); + return (n_ties > 1) ? vs[n-1]->select(home,x,ties,n_ties) : ties[0]; + } + + static const Choice* + value_choice(const Brancher& b, int i, IntView x) { + return new Branch::PosValuesChoice(b,Pos(i),x); + } + + static const Choice* + value_choice(const Brancher&, int, BoolView) { + GECODE_NEVER; + return nullptr; + } + + int + value(const Choice& c, unsigned int a) const { + const Branch::PosValuesChoice& pvc = + static_cast(c); + return pvc.val(values == IntValBranch::SEL_VALUES_MIN + ? a : pvc.alternatives()-1-a); + } + + public: + virtual bool + status(const Space& home) const { + for (int i=start; i list.known_length(); + } + + virtual const Choice* + choice(Space& home) { + for (int i=start; i(*this,2,Pos(p),vsc->val(home,x,p)) + : value_choice(*this,p,x); + } + // Position -1 marks a length choice: close here or require another item. + return new PosValChoice(*this,2,Pos(-1),list.known_length()); + } + + virtual const Choice* + choice(const Space&, Archive& e) { + int p; + e >> p; + if ((p < 0) || vsc) { + int v; + e >> v; + return new PosValChoice(*this,2,Pos(p),v); + } + unsigned int a; + e >> a; + return new Branch::PosValuesChoice(*this,a,Pos(p),e); + } + + virtual ExecStatus + commit(Space& home, const Choice& c, unsigned int a) { + const int p = static_cast(c).pos().pos; + if (p >= 0) { + // Recomputation can replay choices before propagation grows the prefix. + if (p >= list.known_length()) { + list.materialize(home,p+1); + if (home.failed()) + return ES_FAILED; + } + View x(list[p]); + const ModEvent me = vsc + ? vsc->commit(home,a,x,p,static_cast&>(c).val()) + : x.eq(home,value(c,a)); + return me_failed(me) ? ES_FAILED : ES_OK; + } + const int size = static_cast&>(c).val(); + if (a == 0) + return me_failed(IntView(list.length()).eq(home,size)) + ? ES_FAILED : ES_OK; + list.materialize(home,size+1); + return home.failed() ? ES_FAILED : ES_OK; + } + + virtual NGL* + ngl(Space& home, const Choice& c, unsigned int a) const { + const int p = static_cast(c).pos().pos; + if (p < 0) + return (a == 0) ? new (home) Branch::EqNGL + (home,IntView(list.length()), + static_cast&>(c).val()) : nullptr; + // No-good extraction can run in an ancestor with a shorter prefix. + if (p >= list.known_length()) + return nullptr; + View x(list[p]); + return vsc + ? vsc->ngl(home,a,x,static_cast&>(c).val()) + : new (home) Branch::EqNGL(home,x,value(c,a)); + } + + virtual void + print(const Space& home, const Choice& c, unsigned int a, + std::ostream& o) const { + const int p = static_cast(c).pos().pos; + if (p < 0) { + o << "list length " << ((a == 0) ? "=" : ">") << " " + << static_cast&>(c).val(); + } else { + const int v = vsc ? static_cast&>(c).val() + : value(c,a); + // Tracing can print replayed choices before commit grows the prefix. + if (p >= list.known_length()) { + o << "list[" << p << "] alternative " << a << " (value " << v << ")"; + return; + } + View x(list[p]); + if (printer()) + printer()(home,*this,a,list[p],p,v,o); + else if (vsc) + vsc->print(home,a,x,p,v,o); + else + o << "list[" << p << "] = " << v; + } + } + + virtual Actor* + copy(Space& home) { + return new (home) OpenListBrancher(home,*this); + } + + virtual size_t + dispose(Space& home) { + home.ignore(*this,AP_DISPOSE,true); + for (int i=0; idispose(home); + if (vsc) + vsc->dispose(home); + filter.dispose(home); + printer.~SharedData>(); + list.~OpenVarList(); + (void) Brancher::dispose(home); + return sizeof(*this); + } + + template + static void + post(Home home, OpenVarList list, VarBranch vars[n], + ValSelCommitBase* vsc, + IntValBranch::Select values, + BranchFilter bf, VarValPrint vvp) { + ViewSel* vs[n]; + for (int i=0; i* + open_valselcommit(Home home, IntValBranch vals) { + return ((vals.select() == IntValBranch::SEL_VALUES_MIN) || + (vals.select() == IntValBranch::SEL_VALUES_MAX)) + ? nullptr : Branch::valselcommit(home,vals); + } + + ValSelCommitBase* + open_valselcommit(Home home, BoolValBranch vals) { + return Branch::valselcommit(home,vals); + } + + template + void + post_open_brancher(Home home, OpenVarList list, + TieBreak vars, ValBranch vals, + BranchFilter bf, + VarValPrint vvp) { + VarBranch selectors[4] = {vars.a,vars.b,vars.c,vars.d}; + int n = 1; + while ((n < 4) && (selectors[n-1].select() != VarBranch::SEL_NONE) && + (selectors[n-1].select() != VarBranch::SEL_RND) && + (selectors[n].select() != VarBranch::SEL_NONE)) + n++; + typedef typename View::VarType Var; + typename ArrayTraits>::ArgsType x(list.known_length()); + for (int i=0; i list.known_length()); + selectors[i].expand(home,x); + } + const IntValBranch::Select values = + static_cast(vals.select()); + ValSelCommitBase* vsc = open_valselcommit(home,vals); + switch (n) { + case 1: + OpenListBrancher::post + (home,list,selectors,vsc,values,bf,vvp); + break; + case 2: + OpenListBrancher::post + (home,list,selectors,vsc,values,bf,vvp); + break; + case 3: + OpenListBrancher::post + (home,list,selectors,vsc,values,bf,vvp); + break; + case 4: + OpenListBrancher::post + (home,list,selectors,vsc,values,bf,vvp); + break; + } + } + +}} + +namespace Gecode { + + void + distinct(Home home, OpenIntVarList list, IntPropLevel) { + GECODE_POST; + Int::OpenDistinct::post(home,list); + } + + void + rel(Home home, OpenIntVarList list, + IntRelType irt, IntPropLevel ipl) { + switch (irt) { + case IRT_EQ: + case IRT_NQ: + case IRT_LQ: + case IRT_LE: + case IRT_GQ: + case IRT_GR: + break; + default: + throw Int::UnknownRelation("Int::rel"); + } + GECODE_POST; + Int::OpenRel::post(home,list,irt,ipl); + } + + void + sequence(Home home, OpenIntVarList x, const IntSet& values, + int width, int lower, int upper, IntPropLevel ipl) { + Int::Limits::check(values.min(),"Int::sequence"); + Int::Limits::check(values.max(),"Int::sequence"); + Int::Limits::check(width,"Int::sequence"); + Int::Limits::check(lower,"Int::sequence"); + Int::Limits::check(upper,"Int::sequence"); + if (width < 1) + throw Int::OutOfLimits("Int::sequence"); + GECODE_POST; + lower = std::max(0,lower); + upper = std::min(width,upper); + if (upper < lower) { + rel(home,x.length(),IRT_LE,width); + return; + } + if ((lower == 0) && (upper == width)) + return; + Int::OpenSliding::post(home,x,values,width,lower,upper,ipl); + } + + void + slidingsum(Home home, OpenIntVarList list, + int width, int lower, int upper, IntPropLevel ipl) { + Int::Limits::check(width,"Int::slidingsum"); + Int::Limits::check(lower,"Int::slidingsum"); + Int::Limits::check(upper,"Int::slidingsum"); + if (width < 1) + throw Int::OutOfLimits("Int::slidingsum"); + GECODE_POST; + if (upper < lower) { + rel(home,list.length(),IRT_LE,width); + return; + } + Int::OpenSlidingSum::post(home,list,width,lower,upper,ipl); + } + + void + min(Home home, OpenIntVarList list, + IntVar result, IntPropLevel ipl) { + GECODE_POST; + Int::OpenMinMax::post(home,list,Int::IntView(result),ipl); + } + + void + max(Home home, OpenIntVarList list, + IntVar result, IntPropLevel ipl) { + GECODE_POST; + Int::OpenMinMax::post(home,list,Int::IntView(result),ipl); + } + + void + precede(Home home, OpenIntVarList list, + int before, int after, IntPropLevel) { + Int::Limits::check(before,"Int::precede"); + Int::Limits::check(after,"Int::precede"); + GECODE_POST; + Int::OpenPrecede::post(home,list,before,after); + } + + void + precede(Home home, OpenIntVarList list, + const IntArgs& values, IntPropLevel ipl) { + if (values.size() < 2) + return; + for (int i=values.size(); i--; ) + Int::Limits::check(values[i],"Int::precede"); + GECODE_POST; + for (int i=values.size()-1; i--; ) + Int::OpenPrecede::post(home,list,values[i],values[i+1]); + (void) ipl; + } + + void + branch(Home home, OpenIntVarList list) { + branch(home,list,INT_VAR_NONE(),INT_VAL_MIN()); + } + + void + branch(Home home, OpenBoolVarList list) { + branch(home,list,BOOL_VAR_NONE(),BOOL_VAL_MIN()); + } + + void + branch(Home home, OpenIntVarList list, + IntVarBranch vars, IntValBranch vals, + IntBranchFilter bf, IntVarValPrint vvp) { + branch(home,list,TieBreak(vars),vals,bf,vvp); + } + + void + branch(Home home, OpenIntVarList list, + TieBreak vars, IntValBranch vals, + IntBranchFilter bf, IntVarValPrint vvp) { + GECODE_POST; + Int::post_open_brancher(home,list,vars,vals,bf,vvp); + } + + void + branch(Home home, OpenBoolVarList list, + BoolVarBranch vars, BoolValBranch vals, + BoolBranchFilter bf, BoolVarValPrint vvp) { + branch(home,list,TieBreak(vars),vals,bf,vvp); + } + + void + branch(Home home, OpenBoolVarList list, + TieBreak vars, BoolValBranch vals, + BoolBranchFilter bf, BoolVarValPrint vvp) { + GECODE_POST; + Int::post_open_brancher(home,list,vars,vals,bf,vvp); + } + +} + +// STATISTICS: int-prop diff --git a/gecode/int/open-var-list.hpp b/gecode/int/open-var-list.hpp new file mode 100644 index 0000000000..369d04b5d1 --- /dev/null +++ b/gecode/int/open-var-list.hpp @@ -0,0 +1,368 @@ +/* -*- 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. + * + */ + +namespace Gecode { + + template + class OpenVarList::Object : public LocalObject { + public: + class Functions { + public: + Factory create; + Transition transition; + + Functions(Factory create0, Transition transition0) + : create(create0), transition(transition0) {} + }; + + IntVar length; + // A separate event source reports appended positions even when length.min() + // already requires them and therefore does not change during construction. + IntVar materialized; + Var* x; + int n; + int capacity; + SharedData functions; + + static int + maximum(int max) { + Int::Limits::check(max,"OpenVarList"); + if (max < 0) + throw Int::VariableEmptyDomain("OpenVarList"); + return max; + } + + static Factory + valid(Factory create) { + if (!create) + throw InvalidFunction("OpenVarList"); + return create; + } + + static Transition + valid(Transition t) { + if (!t) + throw InvalidFunction("OpenVarList"); + return t; + } + + Object(Home home, Factory create, int max) + : LocalObject(home), + length(home,0,maximum(max)), + materialized(home,0,max), + x(nullptr), n(0), capacity(0), + functions(Functions(valid(create),Transition())) { + home.notice(*this,AP_DISPOSE); + } + + Object(Home home, Factory create, Transition t, int max) + : LocalObject(home), + length(home,0,maximum(max)), + materialized(home,0,max), + x(nullptr), n(0), capacity(0), + functions(Functions(valid(create),valid(t))) { + home.notice(*this,AP_DISPOSE); + } + + Object(Space& home, Object& s) + : LocalObject(home,s), x(nullptr), n(s.n), capacity(s.n), + functions(s.functions) { + length.update(home,s.length); + materialized.update(home,s.materialized); + if (capacity > 0) { + x = heap.alloc(capacity); + try { + for (int i=0; i(x,capacity); + throw; + } + } + } + + virtual Actor* + copy(Space& home) { + return new (home) Object(home,*this); + } + + virtual size_t + dispose(Space& home) { + home.ignore(*this,AP_DISPOSE); + if (capacity > 0) + heap.free(x,capacity); + functions.~SharedData(); + return sizeof(*this); + } + + void + reserve(void) { + if (n == capacity) { + int next = (capacity < 4) ? 4 : + capacity + std::min(capacity / 2,Int::Limits::max-capacity); + x = heap.realloc(x,capacity,next); + capacity = next; + } + } + + void + append(Var y) { + x[n++] = y; + } + }; + +} + +namespace Gecode { namespace Int { + + /// Materialize positions required by the minimum eventual length + template + class OpenMaterialize : public Propagator { + protected: + OpenVarList list; + IntView length; + + OpenMaterialize(Home home, OpenVarList list0) + : Propagator(home), list(list0), length(list0.length()) { + length.subscribe(home,*this,PC_INT_BND); + } + + OpenMaterialize(Space& home, OpenMaterialize& p) + : Propagator(home,p) { + list.update(home,p.list); + length.update(home,p.length); + } + + public: + virtual Actor* + copy(Space& home) { + return new (home) OpenMaterialize(home,*this); + } + + virtual PropCost + cost(const Space&, const ModEventDelta&) const { + const int n = length.min()-list.known_length(); + return (n <= 1) ? PropCost::unary(PropCost::LO) : + PropCost::linear(PropCost::LO,n); + } + + virtual void + reschedule(Space& home) { + length.reschedule(home,*this,PC_INT_BND); + } + + virtual ExecStatus + propagate(Space& home, const ModEventDelta&) { + list.materialize(home,length.min()); + if (home.failed()) + return ES_FAILED; + if (length.assigned()) + return home.ES_SUBSUMED(*this); + return ES_FIX; + } + + virtual size_t + dispose(Space& home) { + length.cancel(home,*this,PC_INT_BND); + list.~OpenVarList(); + (void) Propagator::dispose(home); + return sizeof(*this); + } + + static void + post(Home home, OpenVarList list) { + if (!list.length().assigned()) + (void) new (home) OpenMaterialize(home,list); + } + }; + +}} + +namespace Gecode { + + template + OpenVarList::OpenVarList(void) {} + + template + OpenVarList::OpenVarList(Space& home, Factory create, int max) + : LocalHandle(new (home) Object(home,create,max)) { + Int::OpenMaterialize::post(home,*this); + } + + template + OpenVarList::OpenVarList(Space& home, Factory create, + Transition t, int max) + : LocalHandle(new (home) Object(home,create,t,max)) { + Int::OpenMaterialize::post(home,*this); + } + + forceinline + OpenIntVarList::OpenIntVarList(const OpenVarList& x) + : OpenVarList(x) {} + + forceinline + OpenBoolVarList::OpenBoolVarList(const OpenVarList& x) + : OpenVarList(x) {} + + template + void + OpenVarList::update(Space& home, OpenVarList& s) { + LocalHandle::update(home,s); + } + + template + int + OpenVarList::known_length(void) const { + return static_cast(object())->n; + } + + template + Var + OpenVarList::operator [](int i) const { + Object* s = static_cast(object()); + assert((i >= 0) && (i < s->n)); + return s->x[i]; + } + + template + IntVar + OpenVarList::length(void) const { + return static_cast(object())->length; + } + + template + void + OpenVarList::append(Space& home, Var y) { + if (home.failed()) + return; + Object* s = static_cast(object()); + if (s->n == Int::Limits::max) { + home.fail(); + return; + } + for (int i=0; in; i++) + if (!y.assigned() && !s->x[i].assigned() && + (y.varimp() == s->x[i].varimp())) + throw Int::ArgumentSame("OpenVarList::append"); + s->reserve(); + const int next = s->n + 1; + Int::IntView l(s->length); + Int::IntView m(s->materialized); + if (me_failed(l.gq(home,next)) || me_failed(m.gq(home,next))) { + home.fail(); + return; + } + s->append(y); + const Transition& transition = s->functions().transition; + if (transition) { + GECODE_VALID_FUNCTION(transition); + Space::NoGPI no_gpi(home); + transition(home,*this,next-1); + } + } + + template + void + OpenVarList::materialize(Space& home, int n) { + Space::NoGPI no_gpi(home); + if (home.failed()) + return; + Int::Limits::nonnegative(n,"OpenVarList::materialize"); + Object* s = static_cast(object()); + Int::IntView l(s->length); + if (me_failed(l.gq(home,n))) { + home.fail(); + return; + } + while (s->n < l.min()) { + const int i = s->n; + GECODE_VALID_FUNCTION(s->functions().create); + Var y = s->functions().create(home,i); + append(home,y); + if (home.failed()) + return; + } + } + + template + Var + OpenVarList::get(Space& home, int i) { + Int::Limits::nonnegative(i,"OpenVarList::get"); + if (home.failed()) + return Var(); + Object* s = static_cast(object()); + if (i >= s->length.max()) + throw Int::OutOfLimits("OpenVarList::get"); + materialize(home,i+1); + if (home.failed()) + return Var(); + return static_cast(object())->x[i]; + } + + template + void + OpenVarList::close(Space& home) { + if (home.failed()) + return; + materialize(home,length().min()); + if (home.failed()) + return; + Object* s = static_cast(object()); + if (me_failed(Int::IntView(s->length).eq(home,s->n))) + home.fail(); + } + + template + void + OpenVarList::subscribe(Space& home, Propagator& p) { + Object* s = static_cast(object()); + Int::IntView(s->materialized).subscribe(home,p,Int::PC_INT_BND); + } + + template + void + OpenVarList::cancel(Space& home, Propagator& p) { + Object* s = static_cast(object()); + Int::IntView(s->materialized).cancel(home,p,Int::PC_INT_BND); + } + + template + void + OpenVarList::reschedule(Space& home, Propagator& p) { + Object* s = static_cast(object()); + Int::IntView(s->materialized).reschedule(home,p,Int::PC_INT_BND); + } + +} + +// STATISTICS: int-var diff --git a/test/int/extensional.cpp b/test/int/extensional.cpp index 7422bf4896..b7c45d2582 100755 --- a/test/int/extensional.cpp +++ b/test/int/extensional.cpp @@ -855,6 +855,355 @@ namespace Test { namespace Int { } }; + /// Space used by open regular tests + class OpenRegularSpace : public Gecode::Space { + public: + Gecode::IntVarArray x; + Gecode::OpenIntVarList list; + + OpenRegularSpace(Gecode::DFA d, int max=8) + : x(*this,3,0,2), list(*this,max) { + Gecode::extensional(*this,list,d); + } + + OpenRegularSpace(OpenRegularSpace& s) + : Gecode::Space(s) { + x.update(*this,s.x); + list.update(*this,s.list); + } + + virtual Gecode::Space* + copy(void) { + return new OpenRegularSpace(*this); + } + }; + + /// Space used by open list materialization tests + class OpenListSpace : public Gecode::Space { + public: + Gecode::OpenIntVarList list; + + OpenListSpace(void) + : list(*this, + [](int i) { + return Gecode::IntSet(i,i); + },6) {} + + OpenListSpace(OpenListSpace& s) + : Gecode::Space(s) { + list.update(*this,s.list); + } + + virtual Gecode::Space* + copy(void) { + return new OpenListSpace(*this); + } + }; + + /// Test automatic and explicit list materialization + class OpenListMaterialize : public ::Test::Base { + public: + OpenListMaterialize(void) + : ::Test::Base("Int::OpenList::Materialize",TestTags::check()) {} + + virtual bool + run(void) { + using namespace Gecode; + OpenListSpace source; + rel(source,source.list.length(),IRT_GQ,3); + if ((source.status() == SS_FAILED) || + (source.list.known_length() != 3)) + return false; + for (int i=0; i<3; i++) + if (!source.list[i].assigned() || + (source.list[i].val() != i)) + return false; + + OpenListSpace* clone = + static_cast(source.clone()); + rel(*clone,clone->list.length(),IRT_GQ,5); + const bool clone_ok = + (clone->status() != SS_FAILED) && + (clone->list.known_length() == 5) && + clone->list[4].assigned() && + (clone->list[4].val() == 4); + delete clone; + if (!clone_ok || (source.list.known_length() != 3)) + return false; + + OpenListSpace direct; + IntVar x = direct.list.get(direct,3); + return (direct.list.known_length() == 4) && + (direct.list.length().min() == 4) && + x.assigned() && (x.val() == 3) && + (direct.status() != SS_FAILED); + } + }; + + /// Test appending and closing an open regular list + class OpenRegularIncremental : public ::Test::Base { + public: + OpenRegularIncremental(void) + : ::Test::Base("Int::Extensional::OpenReg::Incremental", + TestTags::check()) {} + + virtual bool + run(void) { + using namespace Gecode; + DFA d(0,{{0,0,1},{1,1,2}},{2},false); + OpenRegularSpace s(d); + + if (s.status() == SS_FAILED) + return false; + s.list.append(s,s.x[0]); + if ((s.status() == SS_FAILED) || + !s.x[0].assigned() || (s.x[0].val() != 0)) + return false; + s.list.append(s,s.x[1]); + if ((s.status() == SS_FAILED) || + !s.x[1].assigned() || (s.x[1].val() != 1)) + return false; + s.list.close(s); + if ((s.status() == SS_FAILED) || + !s.list.length().assigned() || + (s.list.length().val() != 2)) + return false; + + // Pruning the first item to 1 also closes its aliased length. + DFA alias_dfa(0,{{0,1,1},{1,2,2}},{2},false); + OpenRegularSpace alias(alias_dfa,2); + alias.list.append(alias,alias.list.length()); + return alias.status() == SS_FAILED; + } + }; + + /// Required positions wake regular filtering when materialization catches up + class OpenRegularLengthWake : public ::Test::Base { + public: + OpenRegularLengthWake(void) + : ::Test::Base("Int::Extensional::OpenReg::LengthWake") {} + + virtual bool + run(void) { + using namespace Gecode; + DFA d(0,{{0,0,1},{1,1,2}},{2},false); + OpenRegularSpace s(d,3); + rel(s,s.list.length(),IRT_GQ,2); + if (s.status() == SS_FAILED) + return false; + return (s.list.known_length() == 2) && + s.list[0].assigned() && + (s.list[0].val() == 0) && + s.list[1].assigned() && + (s.list[1].val() == 1); + } + }; + + /// Filter prefixes whose accepting continuations exceed the length bound + class OpenRegularMaxLength : public ::Test::Base { + public: + OpenRegularMaxLength(void) + : ::Test::Base("Int::Extensional::OpenReg::MaxLength", + TestTags::check()) {} + + virtual bool + run(void) { + using namespace Gecode; + // Accept 00 or 1000: only the first word fits a maximum of three. + DFA d(0,{{0,0,1},{1,0,5},{0,1,2},{2,0,3},{3,0,4},{4,0,5}}, + {5},false); + OpenRegularSpace short_list(d,1); + if (short_list.status() != SS_FAILED) + return false; + OpenRegularSpace initial(d,3); + initial.list.append(initial,initial.x[0]); + if ((initial.status() == SS_FAILED) || + !initial.x[0].assigned() || (initial.x[0].val() != 0)) + return false; + + OpenRegularSpace source(d,4); + source.list.append(source,source.x[0]); + if ((source.status() == SS_FAILED) || (source.x[0].size() != 2)) + return false; + OpenRegularSpace* clone = + static_cast(source.clone()); + rel(*clone,clone->list.length(),IRT_LQ,3); + const bool clone_ok = (clone->status() != SS_FAILED) && + clone->x[0].assigned() && (clone->x[0].val() == 0); + delete clone; + if (!clone_ok || (source.x[0].size() != 2)) + return false; + + // A bound reduction also checks an already consumed prefix. + rel(source,source.x[0],IRT_EQ,1); + if (source.status() == SS_FAILED) + return false; + rel(source,source.list.length(),IRT_LQ,3); + if (source.status() != SS_FAILED) + return false; + + // Keep the shorter nondeterministic path, even with a cycle. + DFA nfa = DFA::nfa(0, + {{0,0,1},{0,0,2},{1,0,3},{3,0,4},{4,0,5},{2,1,2},{2,0,5}}, + {5}); + OpenRegularSpace nondeterministic(nfa,2); + nondeterministic.list.append(nondeterministic,nondeterministic.x[0]); + if (nondeterministic.status() == SS_FAILED) + return false; + nondeterministic.list.append(nondeterministic,nondeterministic.x[1]); + if ((nondeterministic.status() == SS_FAILED) || + !nondeterministic.x[1].assigned() || + (nondeterministic.x[1].val() != 0)) + return false; + + // Pruning an item aliased with length lowers the maximum while open. + DFA alias_dfa(0, + {{0,2,1},{1,0,2},{2,0,3},{3,0,4},{0,3,5},{5,0,6},{6,0,4}}, + {4},false); + OpenRegularSpace alias(alias_dfa,5); + alias.list.append(alias,alias.list.length()); + alias.list.append(alias,alias.x[0]); + return (alias.status() != SS_FAILED) && + alias.list.length().assigned() && + (alias.list.length().val() == 3); + } + }; + + /// Test backward pruning after appending a restricted variable + class OpenRegularBackward : public ::Test::Base { + public: + OpenRegularBackward(void) + : ::Test::Base("Int::Extensional::OpenReg::Backward") {} + + virtual bool + run(void) { + using namespace Gecode; + DFA d(0, + { + {0,0,1}, {0,1,2}, + {1,0,3}, {2,1,4}, + {3,2,5}, {4,2,5} + }, + {5},false); + OpenRegularSpace s(d); + rel(s,s.x[1],IRT_EQ,0); + s.list.append(s,s.x[0]); + if ((s.status() == SS_FAILED) || + (s.x[0].min() != 0) || (s.x[0].max() != 1)) + return false; + s.list.append(s,s.x[1]); + return (s.status() != SS_FAILED) && + s.x[0].assigned() && (s.x[0].val() == 0); + } + }; + + /// Test domain notification on an appended variable + class OpenRegularDomainWake : public ::Test::Base { + public: + OpenRegularDomainWake(void) + : ::Test::Base("Int::Extensional::OpenReg::DomainWake") {} + + virtual bool + run(void) { + using namespace Gecode; + DFA d(0, + { + {0,0,1}, {0,1,2}, + {1,0,3}, {2,1,4}, + {3,2,5}, {4,2,5} + }, + {5},false); + OpenRegularSpace s(d); + s.list.append(s,s.x[0]); + s.list.append(s,s.x[1]); + if ((s.status() == SS_FAILED) || s.x[0].assigned()) + return false; + rel(s,s.x[1],IRT_EQ,0); + return (s.status() != SS_FAILED) && + s.x[0].assigned() && (s.x[0].val() == 0); + } + }; + + /// Test independent extension after cloning + class OpenRegularClone : public ::Test::Base { + public: + OpenRegularClone(void) + : ::Test::Base("Int::Extensional::OpenReg::Clone",TestTags::check()) {} + + virtual bool + run(void) { + using namespace Gecode; + DFA d(0,{{0,0,1},{1,1,2}},{2},false); + OpenRegularSpace source(d); + source.list.append(source,source.x[0]); + if (source.status() == SS_FAILED) + return false; + + OpenRegularSpace* clone = + static_cast(source.clone()); + clone->list.append(*clone,clone->x[1]); + clone->list.close(*clone); + const bool clone_ok = + (clone->status() != SS_FAILED) && + (clone->list.known_length() == 2); + delete clone; + + source.list.close(source); + const bool source_failed = source.status() == SS_FAILED; + return clone_ok && source_failed && + (source.list.known_length() == 1); + } + }; + + /// Test empty words and automata without accepting states + class OpenRegularEmpty : public Test { + protected: + int kind; + public: + OpenRegularEmpty(int k) + : Test("Extensional::OpenReg::Empty::"+str(k),1,0,0), kind(k) {} + virtual bool solution(const Assignment&) const { return kind == 0; } + virtual void post(Gecode::Space& home, Gecode::IntVarArray&) { + using namespace Gecode; + OpenIntVarList list(home,8); + if (kind == 0) { + extensional(home,list,DFA()); + } else if (kind == 1) { + extensional(home,list,DFA(0,{{0,0,1}},{1},false)); + } else { + extensional(home,list, + DFA(0,{{0,0,0}},std::initializer_list{},false)); + } + if (kind != 2) + list.close(home); + } + }; + + /// Test closed open regular with the standard domain-consistency checks + class OpenRegularClosed : public Test { + public: + OpenRegularClosed(void) + : Test("Extensional::OpenReg::Closed",3,0,2,false,Gecode::IPL_DOM) {} + virtual bool solution(const Assignment& x) const { + int parity = 0; + 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 "test/int.hh" +#include "examples/common-pattern.hh" + +#include + +#ifdef GECODE_HAS_SET_VARS +#include +#endif +#ifdef GECODE_HAS_FLOAT_VARS +#include +#endif + +#include +#include + +namespace Test { namespace Int { + + namespace OpenList { + + class BranchOrderSpace : public Gecode::Space { + public: + Gecode::OpenIntVarList list; + + enum Order { INCREMENTAL, LENGTH_MIN, LENGTH_MAX }; + + BranchOrderSpace(Order order) + : list(*this,Gecode::IntSet(0,1),3) { + using namespace Gecode; + DFA accepts_one(0,{ + {0,0,0}, {0,1,1}, + {1,0,1}, {1,1,1} + },{1}); + extensional(*this,list,accepts_one); + if (order == LENGTH_MIN) + branch(*this,list.length(),INT_VAL_MIN()); + else if (order == LENGTH_MAX) + branch(*this,list.length(),INT_VAL_MAX()); + branch(*this,list); + } + + BranchOrderSpace(BranchOrderSpace& s) + : Gecode::Space(s) { + list.update(*this,s.list); + } + + virtual Gecode::Space* + copy(void) { + return new BranchOrderSpace(*this); + } + }; + + class BranchOrder : public ::Test::Base { + protected: + static bool + check(BranchOrderSpace::Order order, int first_length) { + using namespace Gecode; + Search::Options options; + options.threads = 1; + options.c_d = 1000; + options.a_d = 1000; + BranchOrderSpace* root = new BranchOrderSpace(order); + DFS engine(root,options); + delete root; + + int solutions = 0; + int first = -1; + while (BranchOrderSpace* solution = engine.next()) { + if (solutions == 0) + first = solution->list.known_length(); + solutions++; + delete solution; + } + return (first == first_length) && (solutions == 11); + } + + public: + BranchOrder(void) + : ::Test::Base("Int::OpenList::BranchOrder") {} + + virtual bool + run(void) { + using namespace Gecode; + return check(BranchOrderSpace::INCREMENTAL,3) && + check(BranchOrderSpace::LENGTH_MIN,1) && + check(BranchOrderSpace::LENGTH_MAX,3); + } + }; + + class SelectorSpace : public Gecode::Space { + public: + Gecode::OpenIntVarList list; + + SelectorSpace(Gecode::IntValBranch vals, bool length_first=false) + : list(*this,[](int i) { return Gecode::IntSet(i,i+1); },3) { + using namespace Gecode; + if (length_first) + branch(*this,list.length(),INT_VAL_MIN()); + branch(*this,list,tiebreak(INT_VAR_SIZE_MIN(),INT_VAR_MAX_MAX()),vals); + } + + SelectorSpace(SelectorSpace& s) : Gecode::Space(s) { + list.update(*this,s.list); + } + + virtual Gecode::Space* + copy(void) { + return new SelectorSpace(*this); + } + }; + + class Selectors : public ::Test::Base { + public: + Selectors(void) : ::Test::Base("Int::OpenList::Selectors") {} + + virtual bool + run(void) { + using namespace Gecode; + const IntValBranch values[] = { + INT_VAL_MIN(), INT_VAL_MAX(), INT_VAL_MED(), INT_VAL_RND(Rnd(1)), + INT_VAL_SPLIT_MIN(), INT_VAL_SPLIT_MAX(), + INT_VAL_RANGE_MIN(), INT_VAL_RANGE_MAX(), + INT_VALUES_MIN(), INT_VALUES_MAX(), + INT_VAL([](const Space&, IntVar x, int) { return x.max(); }) + }; + for (const IntValBranch& vals : values) + for (bool length_first : {false,true}) { + SelectorSpace root(vals,length_first); + Search::Options options; + options.threads = 1; + options.c_d = 1000; + options.a_d = 1000; + DFS engine(&root,options); + int solutions = 0; + while (SelectorSpace* solution = engine.next()) { + bool valid = solution->list.length().assigned() && + (solution->list.length().val() == solution->list.known_length()); + for (int i=0; ilist.known_length(); i++) + valid &= solution->list[i].assigned() && + (solution->list[i].val() >= i) && + (solution->list[i].val() <= i+1); + delete solution; + if (!valid) + return false; + solutions++; + } + if (solutions != 15) + return false; + } + return true; + } + }; + + class NoGoods : public ::Test::Base { + public: + NoGoods(void) : ::Test::Base("Int::OpenList::NoGoods") {} + + virtual bool + run(void) { + using namespace Gecode; + SelectorSpace root(INT_VAL_MIN()); + root.list.materialize(root,1); + Search::NodeStop stop(6); + Search::Options options; + options.stop = &stop; + options.threads = 1; + options.nogoods_limit = 100; + std::unique_ptr engine + (Search::dfsengine(&root,options)); + bool found[16] = {}; + const auto record = [&found](SelectorSpace* solution) { + int code = 1; + for (int i=0; ilist.known_length(); i++) + code = 2*code + solution->list[i].val()-i; + found[code] = true; + delete solution; + }; + while (Space* solution = engine->next()) + record(static_cast(solution)); + if (!engine->stopped()) + return false; + // Deeper choices refer to positions absent from the restart root. + engine->nogoods().post(root); + DFS restarted(&root); + while (SelectorSpace* solution = restarted.next()) + record(solution); + // Extraction may omit literals, but must retain every unvisited word. + for (int code=2; code<16; code++) + if (!found[code]) + return false; + return true; + } + }; + + class SelectorChoiceSpace : public Gecode::Space { + public: + Gecode::OpenIntVarList list; + + SelectorChoiceSpace(Gecode::IntValBranch vals) + : list(*this,Gecode::IntSet({0,2,5}),3) { + using namespace Gecode; + list.materialize(*this,3); + branch(*this,list, + tiebreak(INT_VAR_SIZE_MIN(), + INT_VAR_MERIT_MAX([](const Space&, IntVar, int i) { + return i; + })),vals, + [](const Space&, IntVar, int i) { return i != 1; }, + [](const Space&, const Brancher&, unsigned int, IntVar, + int i, const int& value, std::ostream& out) { + out << i << ":" << value; + }); + } + + SelectorChoiceSpace(SelectorChoiceSpace& s) : Gecode::Space(s) { + list.update(*this,s.list); + } + + virtual Gecode::Space* + copy(void) { + return new SelectorChoiceSpace(*this); + } + }; + + class SelectorChoice : public ::Test::Base { + public: + SelectorChoice(void) : ::Test::Base("Int::OpenList::SelectorChoice") {} + + virtual bool + run(void) { + using namespace Gecode; + for (int multi=0; multi<2; multi++) { + SelectorChoiceSpace source(multi ? INT_VALUES_MAX() : INT_VAL_MAX()); + if (source.status() != SS_BRANCH) + return false; + const Choice* original = source.choice(); + Archive archive; + original->archive(archive); + const Choice* restored = source.choice(archive); + bool valid = restored->alternatives() == (multi ? 3U : 2U); + for (unsigned int a=0; aalternatives(); a++) { + SelectorChoiceSpace* clone = + static_cast(source.clone()); + clone->commit(*restored,a); + const int expected[] = {5,2,0}; + valid &= !clone->failed() && !clone->list[0].assigned() && + !clone->list[1].assigned(); + if (multi || (a == 0)) + valid &= clone->list[2].assigned() && + (clone->list[2].val() == expected[a]); + else + valid &= !clone->list[2].in(5); + delete clone; + } + SelectorChoiceSpace* excluded = + static_cast(source.clone()); + NGL* literal = excluded->ngl(*restored,0); + valid &= (literal != nullptr) && (literal->status(*excluded) == NGL::NONE); + if (literal) + valid &= (literal->prune(*excluded) != ES_FAILED) && + !excluded->list[2].in(5); + delete excluded; + std::ostringstream printed; + source.print(*restored,0,printed); + valid &= printed.str() == "2:5"; + delete restored; + delete original; + if (!valid) + return false; + } + return true; + } + }; + + class BoolSpace : public Gecode::Space { + public: + Gecode::OpenBoolVarList list; + + BoolSpace(bool length_first) + : list(*this,3) { + using namespace Gecode; + DFA accepts_one(0,{{0,0,0},{0,1,1},{1,0,1},{1,1,1}},{1}); + extensional(*this,list,accepts_one); + if (length_first) + branch(*this,list.length(),INT_VAL_MIN()); + branch(*this,list, + BOOL_VAR_MERIT_MAX([](const Space&, BoolVar, int i) { return i; }), + BOOL_VAL_MAX()); + } + + BoolSpace(BoolSpace& s) : Gecode::Space(s) { + list.update(*this,s.list); + } + + virtual Gecode::Space* + copy(void) { + return new BoolSpace(*this); + } + }; + + class BoolList : public ::Test::Base { + public: + BoolList(void) : ::Test::Base("Int::OpenList::Bool",TestTags::check()) {} + + virtual bool + run(void) { + using namespace Gecode; + for (bool length_first : {false,true}) + for (int recompute=0; recompute<2; recompute++) { + BoolSpace root(length_first); + Search::Options options; + options.threads = 1; + options.c_d = recompute ? 1000 : 1; + options.a_d = options.c_d; + DFS engine(&root,options); + int solutions = 0; + while (BoolSpace* solution = engine.next()) { + bool valid = solution->list.length().assigned() && + (solution->list.length().val() == solution->list.known_length()); + int ones = 0; + for (int i=0; ilist.known_length(); i++) { + valid &= solution->list[i].assigned(); + ones += solution->list[i].val(); + } + delete solution; + if (!valid || (ones == 0)) + return false; + solutions++; + } + if (solutions != 11) + return false; + } + return true; + } + }; + + class FactorySpace : public Gecode::Space { + public: +#ifdef GECODE_HAS_SET_VARS + Gecode::OpenVarList sets; +#endif +#ifdef GECODE_HAS_FLOAT_VARS + Gecode::OpenVarList floats; +#endif + + FactorySpace(void) { + using namespace Gecode; +#ifdef GECODE_HAS_SET_VARS + sets = OpenVarList(*this, + [](Space& home, int i) { + return SetVar(home,IntSet(i,i),IntSet(i,i)); + },3); + sets.materialize(*this,1); +#endif +#ifdef GECODE_HAS_FLOAT_VARS + floats = OpenVarList(*this, + [](Space& home, int i) { return FloatVar(home,i,i); },3); + floats.materialize(*this,1); +#endif + } + + FactorySpace(FactorySpace& s) : Gecode::Space(s) { +#ifdef GECODE_HAS_SET_VARS + sets.update(*this,s.sets); +#endif +#ifdef GECODE_HAS_FLOAT_VARS + floats.update(*this,s.floats); +#endif + } + + virtual Gecode::Space* + copy(void) { + return new FactorySpace(*this); + } + }; + + class Factory : public ::Test::Base { + public: + Factory(void) : ::Test::Base("Int::OpenList::Factory",TestTags::check()) {} + + virtual bool + run(void) { + using namespace Gecode; + FactorySpace source; + if (source.status() == SS_FAILED) + return false; + FactorySpace* clone = static_cast(source.clone()); +#ifdef GECODE_HAS_SET_VARS + clone->sets.materialize(*clone,3); + clone->sets.close(*clone); + source.sets.close(source); +#endif +#ifdef GECODE_HAS_FLOAT_VARS + clone->floats.materialize(*clone,3); + clone->floats.close(*clone); + source.floats.close(source); +#endif + bool valid = (clone->status() != SS_FAILED) && + (source.status() != SS_FAILED); +#ifdef GECODE_HAS_SET_VARS + valid &= (source.sets.known_length() == 1) && + (clone->sets.known_length() == 3) && + (clone->sets.length().val() == 3) && + clone->sets[2].assigned() && (clone->sets[2].glbMin() == 2); +#endif +#ifdef GECODE_HAS_FLOAT_VARS + valid &= (source.floats.known_length() == 1) && + (clone->floats.known_length() == 3) && + (clone->floats.length().val() == 3) && + clone->floats[2].assigned() && (clone->floats[2].min() == 2); +#endif + delete clone; + return valid; + } + }; + + class FixedStatistics : public ::Test::Base { + public: + FixedStatistics(void) : ::Test::Base("Int::OpenList::FixedStatistics") {} + + virtual bool + run(void) { + using namespace Gecode; + for (int chb=0; chb<2; chb++) { + SelectorSpace open(INT_VAL_MIN()); + bool rejected = false; + try { + branch(open,open.list, + chb ? INT_VAR_CHB_MIN() : INT_VAR_ACTION_MIN(1.0), + INT_VAL_MIN()); + } catch (const Gecode::Int::UnknownBranching&) { + rejected = true; + } + if (!rejected) + return false; + SelectorChoiceSpace fixed(INT_VAL_MIN()); + branch(fixed,fixed.list, + chb ? INT_VAR_CHB_MIN() : INT_VAR_ACTION_MIN(1.0), + INT_VAL_MIN()); + DFS engine(&fixed); + int solutions = 0; + while (SelectorChoiceSpace* solution = engine.next()) { + solutions++; + delete solution; + } + if (solutions != 27) + return false; + } + return true; + } + }; + + Factory factory; + FixedStatistics fixed_statistics; + Selectors selectors; + NoGoods no_goods; + SelectorChoice selector_choice; + BoolList bool_list; + + class TransitionSpace : public Gecode::Space { + public: + Gecode::OpenIntVarList list; + + TransitionSpace(void) + : list(*this,Gecode::IntSet(0,4), + [](Gecode::Space& home, Gecode::OpenIntVarList x, int i) { + using namespace Gecode; + if (i == 0) { + rel(home,x[i],IRT_EQ,0); + } else { + linear(home,IntArgs({-1,1}), + IntVarArgs({x[i-1],x[i]}),IRT_EQ,1); + } + },4) { + Gecode::branch(*this,list); + } + + TransitionSpace(TransitionSpace& s) + : Gecode::Space(s) { + list.update(*this,s.list); + } + + virtual Gecode::Space* + copy(void) { + return new TransitionSpace(*this); + } + }; + + class Transition : public ::Test::Base { + public: + Transition(void) + : ::Test::Base("Int::OpenList::Transition") {} + + virtual bool + run(void) { + using namespace Gecode; + Search::Options options; + options.threads = 1; + options.c_d = 1000; + options.a_d = 1000; + TransitionSpace* root = new TransitionSpace; + DFS engine(root,options); + delete root; + + int solutions = 0; + while (TransitionSpace* solution = engine.next()) { + bool valid = + solution->list.length().assigned() && + (solution->list.length().val() == + solution->list.known_length()); + for (int i=0; ilist.known_length(); i++) + valid &= solution->list[i].assigned() && + (solution->list[i].val() == i); + delete solution; + if (!valid) + return false; + solutions++; + } + return solutions == 5; + } + }; + + + /// Test the relations on a closed open list with the integer harness + class Constraint : public Test { + public: + enum Kind { DISTINCT, REL, SLIDING, SLIDING_SUM, MIN_MAX, PRECEDE }; + protected: + Kind kind; + public: + Constraint(const std::string& name, Kind k, int size) + : Test("OpenList::"+name,size,0,2), kind(k) {} + + virtual bool + solution(const Assignment& x) const { + switch (kind) { + case DISTINCT: + for (int i=0; i= x[i]) + return false; + return true; + case SLIDING: + for (int i=1; i(source.clone()); + clone->list.materialize(*clone,2); + clone->list.close(*clone); + const bool valid = + (clone->status() != SS_FAILED) && + clone->list[1].assigned() && + (clone->list[1].val() == 2) && + clone->minimum.assigned() && (clone->minimum.val() == 0) && + clone->maximum.assigned() && (clone->maximum.val() == 2); + delete clone; + return valid && (source.list.known_length() == 1); + } + }; + + class CallbackSpace : public Gecode::Space { + public: + Gecode::OpenIntVarList list; + + CallbackSpace(Gecode::OpenIntVarList::Domain domain, + Gecode::OpenIntVarList::Transition transition) + : list(*this,domain,transition,8) {} + + CallbackSpace(CallbackSpace& s) : Gecode::Space(s) { + list.update(*this,s.list); + } + + virtual Gecode::Space* + copy(void) { + return new CallbackSpace(*this); + } + }; + + class GroupSpace : public Gecode::Space { + public: + enum Kind { REL, SUM, PRECEDE }; + Gecode::OpenIntVarList list; + Gecode::PropagatorGroup group; + + GroupSpace(Kind kind, bool close) + : list(*this,Gecode::IntSet(0,2),3) { + using namespace Gecode; + switch (kind) { + case REL: + rel((*this)(group),list,IRT_NQ); + break; + case SUM: + slidingsum((*this)(group),list,2,2,2); + break; + case PRECEDE: + precede((*this)(group),list,0,1); + break; + } + list.materialize(*this,2); + if (close) + list.close(*this); + } + + GroupSpace(GroupSpace& s) : Gecode::Space(s), group(s.group) { + list.update(*this,s.list); + } + + virtual Gecode::Space* + copy(void) { + return new GroupSpace(*this); + } + }; + + /// Test group control after an open constraint has posted its children + class Group : public ::Test::Base { + protected: + GroupSpace::Kind kind; + bool kill; + public: + Group(const std::string& name, GroupSpace::Kind kind0, bool kill0) + : ::Test::Base("Int::OpenList::Group::"+name+ + (kill0 ? "::Kill" : "::Disable")), + kind(kind0), kill(kill0) {} + + virtual bool + run(void) { + using namespace Gecode; + for (int close=0; close<2; close++) { + GroupSpace source(kind,close != 0); + if (source.status() == SS_FAILED) + return false; + if (kill) + source.group.kill(source); + else + source.group.disable(source); + // These values remain in the domains but violate each constraint. + rel(source,source.list[0],IRT_EQ, + kind == GroupSpace::PRECEDE ? 2 : 0); + rel(source,source.list[1],IRT_EQ, + kind == GroupSpace::PRECEDE ? 1 : 0); + if (source.status() == SS_FAILED) + return false; + if (!kill) { + source.group.enable(source); + if (source.status() != SS_FAILED) + return false; + } + } + return true; + } + }; + + Group group_rel_kill("Rel",GroupSpace::REL,true); + Group group_rel_disable("Rel",GroupSpace::REL,false); + Group group_sum_kill("Sum",GroupSpace::SUM,true); + Group group_sum_disable("Sum",GroupSpace::SUM,false); + Group group_precede_kill("Precede",GroupSpace::PRECEDE,true); + Group group_precede_disable("Precede",GroupSpace::PRECEDE,false); + + class Dispose : public ::Test::Base { + public: + Dispose(void) + : ::Test::Base("Int::OpenList::Dispose",TestTags::check()) {} + + virtual bool + run(void) { + using namespace Gecode; + std::shared_ptr domain(new int(0)); + std::shared_ptr transition(new int(0)); + CallbackSpace* source = new CallbackSpace( + [domain](int) { return IntSet(0,1); }, + [transition](Space&, OpenIntVarList, int) {}); + source->list.materialize(*source,2); + DFA dfa(0,{{0,0,0},{0,1,0}},{0}); + extensional(*source,source->list,dfa); + if (source->status() == SS_FAILED) { + delete source; + return false; + } + CallbackSpace* clone = static_cast(source->clone()); + delete source; + const bool retained = (domain.use_count() > 1) && + (transition.use_count() > 1); + delete clone; + return retained && (domain.use_count() == 1) && + (transition.use_count() == 1); + } + }; + + class CallbackLength : public ::Test::Base { + public: + CallbackLength(void) + : ::Test::Base("Int::OpenList::CallbackLength",TestTags::check()) {} + + virtual bool + run(void) { + using namespace Gecode; + for (int assign=0; assign<2; assign++) + for (int close=0; close<2; close++) { + CallbackSpace source([](int) { return IntSet(0,1); }, + [assign](Space& home, OpenIntVarList list, int i) { + if (i == 0) + rel(home,list.length(),assign ? IRT_EQ : IRT_GQ,3); + }); + rel(source,source.list.length(),IRT_GQ,1); + if (close) + source.list.close(source); + if ((source.status() == SS_FAILED) || + (source.list.known_length() != 3)) + return false; + } + return true; + } + }; + + /// Callbacks suppress AFC unless opted in; nested growth restores each scope + class NoGPI : public ::Test::Base { + protected: + static void + post(Gecode::Space& home, Gecode::PropagatorGroup group) { + Gecode::IntVar x(home,0,10), y(home,0,10); + Gecode::rel(home(group),x,Gecode::IRT_LE,y); + } + + static int + count(const Gecode::Space& home, Gecode::PropagatorGroup group, + double afc) { + int n = 0; + for (Gecode::Propagators ps(home,group); ps(); ++ps) { + if (ps.propagator().afc() != afc) + return -1; + n++; + } + return n; + } + + public: + NoGPI(void) + : ::Test::Base("Int::OpenList::NoGPI",TestTags::check()) {} + + virtual bool + run(void) { + using namespace Gecode; + CallbackSpace home([](int) { return IntSet(0,1); }, + [](Space&, OpenIntVarList, int) {}); + PropagatorGroup factory, transition, selected, ordinary, suppressed; + OpenVarList list(home, + [factory,selected](Space& home, int) { + post(home,factory); + { + Space::UseGPI use_gpi(home); + post(home,selected); + } + return IntVar(home,0,10); + }, + [transition,selected](Space& home, OpenVarList list, int i) { + post(home,transition); + if (i == 3) + throw 1; + { + Space::UseGPI use_gpi(home); + post(home,selected); + if (i == 0) + list.materialize(home,2); + post(home,selected); + } + post(home,transition); + },4); + + list.materialize(home,1); + post(home,ordinary); + if ((list.known_length() != 2) || + (count(home,factory,0.0) != 2) || + (count(home,transition,0.0) != 4) || + (count(home,selected,1.0) != 6)) + return false; + { + Space::NoGPI no_gpi(home); + { + Space::UseGPI use_gpi(home); + list.append(home,IntVar(home,0,10)); + post(home,ordinary); + } + post(home,suppressed); + } + bool threw = false; + try { + list.append(home,IntVar(home,0,10)); + } catch (int) { + threw = true; + } + post(home,ordinary); + return threw && (count(home,factory,0.0) == 2) && + (count(home,transition,0.0) == 7) && + (count(home,selected,1.0) == 8) && + (count(home,ordinary,1.0) == 3) && + (count(home,suppressed,0.0) == 1) && + (home.status() != SS_FAILED); + } + }; + + class FailedGet : public ::Test::Base { + public: + FailedGet(void) + : ::Test::Base("Int::OpenList::FailedGet") {} + + virtual bool + run(void) { + using namespace Gecode; + CallbackSpace source([](int) { return IntSet(0,1); }, + [](Space& home, OpenIntVarList, int) { home.fail(); }); + IntVar x = source.list.get(source,4); + return source.failed() && (x.varimp() == nullptr); + } + }; + + /// A value commit must not construct positions required by pending propagation + class ValueCommit : public ::Test::Base { + public: + ValueCommit(void) + : ::Test::Base("Int::OpenList::ValueCommit") {} + + virtual bool + run(void) { + using namespace Gecode; + CallbackSpace source([](int) { return IntSet(0,1); }, + [](Space& home, OpenIntVarList, int i) { + if (i == 1) + home.fail(); + }); + source.list.materialize(source,1); + branch(source,source.list); + if (source.status() != SS_BRANCH) + return false; + const Choice* choice = source.choice(); + bool valid = true; + for (unsigned int alternative=0; alternative<2; alternative++) { + CallbackSpace* clone = static_cast(source.clone()); + rel(*clone,clone->list.length(),IRT_GQ,2); + clone->commit(*choice,alternative); + valid &= !clone->failed() && (clone->list.known_length() == 1) && + clone->list[0].assigned() && + (clone->list[0].val() == static_cast(alternative)); + valid &= clone->status() == SS_FAILED; + delete clone; + } + delete choice; + return valid; + } + }; + + /// Replay value choices after propagation has extended the original list + class ValueReplay : public ::Test::Base { + public: + ValueReplay(void) + : ::Test::Base("Int::OpenList::ValueReplay",TestTags::check()) {} + + virtual bool + run(void) { + using namespace Gecode; + CallbackSpace source([](int) { return IntSet(0,1); }, + [](Space& home, OpenIntVarList list, int i) { + if (i == 0) { + BoolVar zero(home,0,1); + rel(home,list[0],IRT_EQ,0,Reify(zero)); + rel(home,list.length(),IRT_GQ,2,Reify(zero,RM_IMP)); + } + }); + source.list.materialize(source,1); + branch(source,source.list); + if (source.status() != SS_BRANCH) + return false; + std::unique_ptr first(source.choice()); + std::unique_ptr replay + (static_cast(source.clone())); + source.commit(*first,0); + if ((source.status() != SS_BRANCH) || + (source.list.known_length() != 2)) + return false; + std::unique_ptr second(source.choice()); + // Recomputation commits the path without propagating between choices. + replay->commit(*first,0); + for (unsigned int a=0; aalternatives(); a++) { + std::ostringstream printed; + replay->print(*second,a,printed); + if (printed.str().empty() || (replay->list.known_length() != 1)) + return false; + } + replay->commit(*second,0); + return (replay->status() != SS_FAILED) && + (replay->list.known_length() == 2) && + replay->list[0].assigned() && (replay->list[0].val() == 0) && + replay->list[1].assigned() && (replay->list[1].val() == 0); + } + }; + + /// Impossible window bounds still permit lists too short to contain a window + class ImpossibleWindow : public ::Test::Base { + public: + ImpossibleWindow(void) + : ::Test::Base("Int::OpenList::ImpossibleWindow") {} + + virtual bool + run(void) { + using namespace Gecode; + for (int sum=0; sum<2; sum++) + for (int size=0; size<3; size++) { + CallbackSpace source([](int) { return IntSet(0,1); }, + [](Space&, OpenIntVarList, int) {}); + if (sum) + slidingsum(source,source.list,2,2,1); + else + Gecode::sequence(source,source.list,IntSet(0,1),2,3,3); + rel(source,source.list.length(),IRT_EQ,size); + const bool failed = source.status() == SS_FAILED; + if (failed != (size == 2)) + return false; + if (!failed && (source.list.known_length() != size)) + return false; + } + return true; + } + }; + + /// Search model for the private common-pattern projection + class PatternSpace : public Gecode::Space { + public: + Gecode::OpenIntVarList list; + Gecode::IntVar frequency; + + PatternSpace(const PatternMining::Database& data, int support) + : list(*this,data.domain(),data.max_length()) { + Gecode::rel(*this,list.length(),Gecode::IRT_GQ,1); + frequency = PatternMining::frequent(*this,list,data,support); + Gecode::branch(*this,list); + } + + PatternSpace(PatternSpace& s) : Gecode::Space(s) { + list.update(*this,s.list); + frequency.update(*this,s.frequency); + } + + virtual Gecode::Space* copy(void) { + return new PatternSpace(*this); + } + }; + + /// Compare complete solution sets with independently enumerated subsequences + class PatternEnumeration : public ::Test::Base { + public: + PatternEnumeration(void) + : ::Test::Base("Int::OpenList::Pattern::Enumeration", + TestTags::check()) {} + + virtual bool run(void) { + using namespace Gecode; + const PatternMining::Sequences inputs[] = { + {{17,1000000,17},{17,1000000,17},{1000000,17,42},{17,42}}, + {{1,1},{1,2},{2,1}} + }; + for (const PatternMining::Sequences& input : inputs) { + std::set> candidates; + for (const std::vector& row : input) + for (unsigned int mask=1; mask<(1U< pattern; + for (unsigned int i=0; i(input.size())}; + for (int threshold : thresholds) { + std::set> expected, found; + for (const std::vector& pattern : candidates) { + int support = 0; + for (const std::vector& row : input) { + auto next = row.begin(); + bool matches = true; + for (int item : pattern) { + next = std::find(next,row.end(),item); + if (next == row.end()) { + matches = false; + break; + } + ++next; + } + support += matches; + } + if (support >= threshold) + expected.insert(pattern); + } + PatternSpace root(data,threshold); + Search::Options options; + options.threads = 1; + options.c_d = 1000; + options.a_d = 1000; + DFS search(&root,options); + while (PatternSpace* solution = search.next()) { + std::unique_ptr owned(solution); + if (!solution->list.length().assigned() || + (solution->list.length().val() != + solution->list.known_length())) + return false; + std::vector pattern; + for (int i=0; ilist.known_length(); i++) { + if (!solution->list[i].assigned()) + return false; + pattern.push_back(solution->list[i].val()); + } + if (!solution->frequency.assigned() || + (solution->frequency.val() != data.support(pattern)) || + !found.insert(pattern).second) + return false; + } + if (found != expected) + return false; + } + } + return true; + } + }; + + /// Clones keep separate projections; later assignments wait for their prefix + class PatternGrowth : public ::Test::Base { + public: + PatternGrowth(void) + : ::Test::Base("Int::OpenList::Pattern::Growth",TestTags::check()) {} + + virtual bool run(void) { + using namespace Gecode; + const PatternMining::Database data({{1,2,1},{1,2,2},{1,1,2}}); + PatternSpace source(data,2); + source.list.materialize(source,1); + rel(source,source.list[0],IRT_EQ,1); + if (source.status() == SS_FAILED) + return false; + std::unique_ptr clone + (static_cast(source.clone())); + clone->list.materialize(*clone,2); + rel(*clone,clone->list[1],IRT_EQ,1); + if ((clone->status() == SS_FAILED) || + !clone->list.length().assigned() || + (clone->list.length().val() != 2) || + (source.list.known_length() != 1) || + (source.list.length().min() != 1)) + return false; + source.list.materialize(source,2); + rel(source,source.list[1],IRT_EQ,2); + if ((source.status() == SS_FAILED) || + !source.list.length().assigned() || + (source.list.length().val() != 2) || + (clone->list[1].val() != 1)) + return false; + + // Assigning a later position must not advance the projected prefix. + PatternSpace later(data,2); + later.list.materialize(later,2); + rel(later,later.list[1],IRT_EQ,2); + if ((later.status() == SS_FAILED) || later.list[0].assigned()) + return false; + std::unique_ptr invalid + (static_cast(later.clone())); + rel(*invalid,invalid->list[0],IRT_EQ,2); + if (invalid->status() != SS_FAILED) + return false; + rel(later,later.list[0],IRT_EQ,1); + if (later.status() == SS_FAILED) + return false; + + // Both singleton prefixes are frequent, but no extension is frequent. + const PatternMining::Database terminal({{1,1},{1,2},{2,1}}); + for (int item=1; item<=2; item++) { + PatternSpace leaf(terminal,2); + leaf.list.materialize(leaf,1); + rel(leaf,leaf.list[0],IRT_EQ,item); + if ((leaf.status() == SS_FAILED) || + !leaf.list.length().assigned() || + (leaf.list.length().val() != 1)) + return false; + } + return true; + } + }; + + /// Input validation and exact rounding of percentage support + class PatternInput : public ::Test::Base { + public: + PatternInput(void) + : ::Test::Base("Int::OpenList::Pattern::Input",TestTags::check()) {} + + virtual bool run(void) { + using namespace PatternMining; + std::istringstream rows("# comment\n17 -1 1000000 -1 -2\r\n" + "17 -1 1000000 -1 -2\n"); + if (read_database(rows,SPMF) != Sequences({{17,1000000},{17,1000000}}) || + read_sequence("ABA",LETTERS) != std::vector({1,2,1}) || + read_sequence("17 1000000",INTEGERS) != + std::vector({17,1000000}) || + (minimum_support("33.333333",3) != 1) || + (minimum_support("33.333334",3) != 2) || + (minimum_support("100",2000000000) != 2000000000) || + (minimum_support("0.000001",2000000000) != 20)) + return false; + for (const char* text : {"1 2 -1 -2", "1 -2", "1 -1", + "1 -1 -2 2", "2147483648 -1 -2"}) { + bool rejected = false; + try { + (void) read_sequence(text,SPMF); + } catch (const std::invalid_argument&) { + rejected = true; + } + if (!rejected) + return false; + } + for (const char* text : {"0", "101", "0.0000001", + "184467440737095516160"}) { + bool rejected = false; + try { + (void) minimum_support(text,3); + } catch (const std::invalid_argument&) { + rejected = true; + } + if (!rejected) + return false; + } + return true; + } + }; + + PatternEnumeration pattern_enumeration; + PatternGrowth pattern_growth; + PatternInput pattern_input; + + ValueCommit value_commit; + ValueReplay value_replay; + ImpossibleWindow impossible_window; + + Dispose disposal; + CallbackLength callback_length; + NoGPI no_gpi; + FailedGet failed_get; + + BranchOrder branch_order; + Transition transition; + Constraint distinct("Distinct",Constraint::DISTINCT,3); + Constraint relation("Rel",Constraint::REL,3); + Constraint sliding("Sliding",Constraint::SLIDING,4); + Constraint sliding_sum("SlidingSum",Constraint::SLIDING_SUM,4); + Constraint min_max("MinMax",Constraint::MIN_MAX,3); + Constraint precedence("Precede",Constraint::PRECEDE,3); + EmptyMinimum empty_minimum; + IncrementalConstraints incremental_constraints; + + } + +}} + +// STATISTICS: test-int