From bde590380852c2aa0b16f8a6df92afad00d19d48 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Thu, 10 Sep 2026 10:04:25 +0200 Subject: [PATCH 01/16] random: add splittable engines and full-state test replay --- AGENTS.md | 2 +- CMakeLists.txt | 7 + Makefile.in | 2 +- cmake/GecodeSources.cmake | 1 + gecode/kernel/data/rnd.cpp | 8 +- gecode/kernel/data/rnd.hpp | 34 ++-- gecode/support/random.hpp | 196 ++++++++++++++++++- plans/random.md | 369 +++++++++++++++++++++++++++++++++++ test/public-runner-smoke.cpp | 16 +- test/random-replay.cmake | 23 +++ test/random-replay.cpp | 23 +++ test/random.cpp | 118 +++++++++++ test/test.cpp | 75 ++++--- test/test.hh | 6 +- test/test.hpp | 2 +- 15 files changed, 820 insertions(+), 62 deletions(-) create mode 100644 plans/random.md create mode 100644 test/random-replay.cmake create mode 100644 test/random-replay.cpp create mode 100644 test/random.cpp diff --git a/AGENTS.md b/AGENTS.md index 220a97805f..cc70e7661f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -108,7 +108,7 @@ need none. Run a focused selection first with `-test `, then a suite in proportion to the change: `check` (the small `TestTags::check()` set), `check-standard` (all but sweeps; new tests are `standard` by default), or -`check-sweep` (exhaustive). A failure prints `-replay -test `; +`check-sweep` (exhaustive). A failure prints `-replay -test ''`; rerun with those options to reproduce it. For deeper checks, configure with `GECODE_SANITIZER`, and for changes to copying, disposal, or ownership, with `GECODE_ENABLE_FAULT_INJECTION` and run `check-fault`. diff --git a/CMakeLists.txt b/CMakeLists.txt index d293267aec..301b81d94b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1541,6 +1541,13 @@ if(BUILD_TESTING) list(APPEND GECODE_TEST_LINK_LIBS gecodeflatzinc) endif() target_link_libraries(gecode-test PRIVATE ${GECODE_TEST_LINK_LIBS}) + add_executable(gecode-random-replay EXCLUDE_FROM_ALL + ${GECODE_TEST_MAIN_SOURCE} test/random-replay.cpp) + target_link_libraries(gecode-random-replay PRIVATE gecodetest) + add_dependencies(gecode-test gecode-random-replay) + add_test(NAME random-state-replay + COMMAND ${CMAKE_COMMAND} -DREPLAY=$ + -P ${CMAKE_CURRENT_SOURCE_DIR}/test/random-replay.cmake) if(GECODE_ENABLE_FAULT_INJECTION) add_executable(gecode-fault-test EXCLUDE_FROM_ALL ${GECODE_FAULT_TEST_SOURCES}) diff --git a/Makefile.in b/Makefile.in index bfa8c75286..db24977ce3 100755 --- a/Makefile.in +++ b/Makefile.in @@ -1378,7 +1378,7 @@ TESTAUXOBJ = $(TESTMAINOBJ) $(TESTPUBLICRUNNERSMOKEOBJ) \ $(TESTPUBLICINTSMOKEOBJ) $(TESTPUBLICSETSMOKEOBJ) \ $(TESTPUBLICFLOATSMOKEOBJ) -TESTSRC0 = $(TESTCORESRC) test/afc.cpp test/ldsb.cpp test/region.cpp \ +TESTSRC0 = $(TESTCORESRC) test/afc.cpp test/ldsb.cpp test/region.cpp test/random.cpp \ test/groups.cpp # FailPoint is CMake-only; keep the Autoconf test executable fault-free. diff --git a/cmake/GecodeSources.cmake b/cmake/GecodeSources.cmake index efde7a61c2..b125fc6bef 100644 --- a/cmake/GecodeSources.cmake +++ b/cmake/GecodeSources.cmake @@ -460,6 +460,7 @@ set(GECODE_TEST_SOURCES test/ldsb.cpp test/nogoods.cpp test/region.cpp + test/random.cpp test/search.cpp test/set.cpp test/set/channel.cpp diff --git a/gecode/kernel/data/rnd.cpp b/gecode/kernel/data/rnd.cpp index f7dc782889..395e050452 100644 --- a/gecode/kernel/data/rnd.cpp +++ b/gecode/kernel/data/rnd.cpp @@ -40,13 +40,13 @@ namespace Gecode { Support::Mutex Rnd::IMP::m; forceinline - Rnd::IMP::IMP(unsigned int s) + Rnd::IMP::IMP(uint64_t s) : rg(s) {} Rnd::IMP::~IMP(void) {} forceinline void - Rnd::_seed(unsigned int s) { + Rnd::_seed(uint64_t s) { if (object() == nullptr) { object(new IMP(s)); } else { @@ -55,7 +55,7 @@ namespace Gecode { } Rnd::Rnd(void) {} - Rnd::Rnd(unsigned int s) { + Rnd::Rnd(uint64_t s) { object(new IMP(s)); } Rnd::Rnd(const Rnd& r) @@ -68,7 +68,7 @@ namespace Gecode { Rnd::~Rnd(void) {} void - Rnd::seed(unsigned int s) { + Rnd::seed(uint64_t s) { _seed(s); } void diff --git a/gecode/kernel/data/rnd.hpp b/gecode/kernel/data/rnd.hpp index 9cdd4afd2b..7de75e136f 100755 --- a/gecode/kernel/data/rnd.hpp +++ b/gecode/kernel/data/rnd.hpp @@ -50,11 +50,11 @@ namespace Gecode { Support::RandomGenerator rg; public: /// Initialize generator with seed \a s - IMP(unsigned int s); - /// Return seed - unsigned int seed(void) const; + IMP(uint64_t s); + /// Return complete state + Support::RandomGenerator::State state(void) const; /// Set seed to \a s - void seed(unsigned int s); + void seed(uint64_t s); /// Returns a random integer from the interval \f$[0\ldots n)\f$ unsigned int operator ()(unsigned int n); /// Returns a random integer from the interval \f$[0\ldots n)\f$ @@ -67,7 +67,7 @@ namespace Gecode { virtual ~IMP(void); }; /// Set the current seed to \a s (initializes if needed) - void _seed(unsigned int s); + void _seed(uint64_t s); public: /// Default constructor that does not initialize the generator GECODE_KERNEL_EXPORT @@ -83,18 +83,18 @@ namespace Gecode { ~Rnd(void); /// Initialize with seed \a s GECODE_KERNEL_EXPORT - Rnd(unsigned int s); + Rnd(uint64_t s); /// Set the current seed to \a s (initializes if needed) GECODE_KERNEL_EXPORT - void seed(unsigned int s); + void seed(uint64_t s); /// Set current seed based on time (initializes if needed) GECODE_KERNEL_EXPORT void time(void); /// Set current seed to hardware-based random number (initializes if needed) GECODE_KERNEL_EXPORT void hw(void); - /// Return current seed - unsigned int seed(void) const; + /// Return complete current state + Support::RandomGenerator::State state(void) const; /// Returns a random integer from the interval \f$[0\ldots n)\f$ unsigned int operator ()(unsigned int n); /// Returns a random integer from the interval \f$[0\ldots n)\f$ @@ -105,16 +105,16 @@ namespace Gecode { long long int operator ()(long long int n); }; - forceinline unsigned int - Rnd::IMP::seed(void) const { - unsigned int s; + forceinline Support::RandomGenerator::State + Rnd::IMP::state(void) const { + Support::RandomGenerator::State s; const_cast(*this).m.acquire(); - s = rg.seed(); + s = rg.state(); const_cast(*this).m.release(); return s; } forceinline void - Rnd::IMP::seed(unsigned int s) { + Rnd::IMP::seed(uint64_t s) { m.acquire(); rg.seed(s); m.release(); @@ -152,10 +152,10 @@ namespace Gecode { return r; } - forceinline unsigned int - Rnd::seed(void) const { + forceinline Support::RandomGenerator::State + Rnd::state(void) const { const IMP* i = static_cast(object()); - return i->seed(); + return i->state(); } forceinline unsigned int Rnd::operator ()(unsigned int n) { diff --git a/gecode/support/random.hpp b/gecode/support/random.hpp index 760176ee64..1d087a4ae3 100755 --- a/gecode/support/random.hpp +++ b/gecode/support/random.hpp @@ -34,6 +34,13 @@ */ #include +#include +#include +#include +#include +#include +#include +#include namespace Gecode { namespace Support { @@ -180,7 +187,194 @@ namespace Gecode { namespace Support { * \ingroup FuncSupport */ typedef LinearCongruentialGenerator<2147483647, 48271, 44488, 3399> - RandomGenerator; + LegacyRandomGenerator; + + /// Parse a decimal or hexadecimal 64-bit seed without truncation. + inline uint64_t + random_seed(const std::string& text) { + const char* first = text.data(); + const char* last = first + text.size(); + int base = 10; + if ((text.size() > 2) && (text[0] == '0') && + ((text[1] == 'x') || (text[1] == 'X'))) { + first += 2; + base = 16; + } + uint64_t value; + auto r = std::from_chars(first,last,value,base); + if ((r.ec != std::errc()) || (r.ptr != last)) + throw std::invalid_argument("Invalid 64-bit random seed"); + return value; + } + + /** \brief Splittable SplitMix with two 64-bit state words + * + * Implements the SplitMix design of Steele, Lea, and Flood (OOPSLA 2014), + * using Stafford's Mix13 output permutation and the MurmurHash3 finalizer + * for gamma selection. Indexed splitting returns the child of the (a+1)th + * successive split without changing the parent. For a 32-bit alternative + * index, the inputs s + (2*a+1)*gamma are distinct: gamma is odd and the + * offsets span less than 2^64. Mix13 is bijective, so child states differ. + * + * \ingroup FuncSupport + */ + class SplitMix { + public: + using State = std::array; + private: + State s; + static uint64_t mix(uint64_t z) { + z = (z ^ (z >> 30)) * UINT64_C(0xbf58476d1ce4e5b9); + z = (z ^ (z >> 27)) * UINT64_C(0x94d049bb133111eb); + return z ^ (z >> 31); + } + static uint64_t gamma(uint64_t z) { + z = (z ^ (z >> 33)) * UINT64_C(0xff51afd7ed558ccd); + z = (z ^ (z >> 33)) * UINT64_C(0xc4ceb9fe1a85ec53); + z = (z ^ (z >> 33)) | 1; + unsigned int n = 0; + for (uint64_t bits = z ^ (z >> 1); bits; bits &= bits-1) + ++n; + return (n < 24) ? z ^ UINT64_C(0xaaaaaaaaaaaaaaaa) : z; + } + public: + explicit SplitMix(uint64_t value=1) { seed(value); } + static const char* name(void) { return "splitmix-v1"; } + static constexpr uint64_t min(void) { return 0; } + static constexpr uint64_t max(void) { return UINT64_MAX; } + void seed(uint64_t value) { + s = {{value, UINT64_C(0x9e3779b97f4a7c15)}}; + } + State state(void) const { return s; } + void state(const State& value) { + if (!(value[1] & 1)) + throw std::invalid_argument("SplitMix increment must be odd"); + s = value; + } + uint64_t next(void) { return mix(s[0] += s[1]); } + SplitMix split(uint32_t alternative) const { + uint64_t first = s[0] + (2*uint64_t(alternative)+1)*s[1]; + SplitMix child; + child.s = {{mix(first),gamma(first+s[1])}}; + return child; + } + }; + + /** \brief One-word xorshift64* engine for standalone use + * + * Uses shifts 12, 25, 27 and Vigna's multiplier. Zero seeds map to one; + * restoring zero state is an error. This engine does not provide splitting. + * \ingroup FuncSupport + */ + class Xorshift64Star { + public: + using State = std::array; + private: + uint64_t s; + public: + explicit Xorshift64Star(uint64_t value=1) { seed(value); } + static const char* name(void) { return "xorshift64star-v1"; } + static constexpr uint64_t min(void) { return 1; } + static constexpr uint64_t max(void) { return UINT64_MAX; } + void seed(uint64_t value) { s = value ? value : 1; } + State state(void) const { return {{s}}; } + void state(const State& value) { + if (!value[0]) + throw std::invalid_argument("Xorshift64* state must be nonzero"); + s = value[0]; + } + uint64_t next(void) { + s ^= s >> 12; + s ^= s << 25; + s ^= s >> 27; + return s * UINT64_C(2685821657736338717); + } + }; + + /** \brief Value-type generator with reproducible bounded draws and state + * + * Engine supplies a State array of 64-bit words, name(), seed(), state() + * getter/setter, and next() over [0,UINT64_MAX] or [1,UINT64_MAX]. Search + * engines additionally supply split(uint32_t) const. No state-size limit + * is imposed. Copying a generator preserves its exact state. + * \ingroup FuncSupport + */ + template + class Random { + private: + Engine e; + public: + using State = typename Engine::State; + using result_type = uint64_t; + static_assert(Engine::max() == UINT64_MAX && Engine::min() <= 1, + "Random engine must generate full or nonzero 64-bit words"); + explicit Random(uint64_t seed=1) : e(seed) {} + explicit Random(const Engine& engine) : e(engine) {} + static constexpr result_type min(void) { return Engine::min(); } + static constexpr result_type max(void) { return Engine::max(); } + static const char* name(void) { return Engine::name(); } + void seed(uint64_t value) { e.seed(value); } + State state(void) const { return e.state(); } + void state(const State& value) { e.state(value); } + result_type next(void) { return e.next(); } + result_type operator ()(void) { return next(); } + size_t size(void) const { return sizeof(*this); } + Random split(uint32_t alternative) const { + return Random(e.split(alternative)); + } + /// Bounds <= 1 return zero without consuming a draw. + template + Type operator ()(Type bound) { + static_assert(std::is_integral::value && sizeof(Type) <= 8, + "Random bound must be an integer of at most 64 bits"); + if (bound <= 1) + return 0; + uint64_t n = static_cast(bound); + uint64_t value; + if constexpr (Engine::min() == 0) { + // Accept an exact multiple of n values from the 2^64-value source. + uint64_t threshold = (uint64_t(0)-n) % n; + do { value = next(); } while (value < threshold); + } else { + // Nonzero engines have 2^64-1 values, not 2^64. + uint64_t limit = UINT64_MAX - (UINT64_MAX % n); + do { value = next()-1; } while (value >= limit); + } + return static_cast(value % n); + } + /// Canonical identifier followed by fixed-width hexadecimal state words. + std::string state_string(void) const { + std::string text(name()); + constexpr char digits[] = "0123456789abcdef"; + for (uint64_t word : state()) { + text += ':'; + for (int shift=60; shift>=0; shift-=4) + text += digits[(word >> shift) & 15]; + } + return text; + } + /// Restore full state, rejecting incompatible identifiers or invalid words. + void state(const std::string& text) { + const std::string prefix = std::string(name()) + ':'; + State words{}; + if ((text.compare(0,prefix.size(),prefix) != 0) || + (text.size() != prefix.size()+17*words.size()-1)) + throw std::invalid_argument("Invalid or incompatible random state"); + size_t pos = prefix.size(); + for (size_t i=0; i; }} diff --git a/plans/random.md b/plans/random.md new file mode 100644 index 0000000000..4cd68b8ca3 --- /dev/null +++ b/plans/random.md @@ -0,0 +1,369 @@ +# Plan: Compact, splittable random generators for Gecode 7 + +> Source: the feature/random design discussion, 2026-09-10. +> Status: implementation authorized; Phase 1 complete, Phase 2 next. +> Workflow: review, update this plan, and commit after each phase. + +## Goal + +Replace the old default random generator, provide user-extensible alternatives, +and make random branching streams stable under recomputation. Gecode 7 permits +breaking source, binary, and seeded-sequence compatibility for this change. + +Xorshift64* was the original proposed replacement. The subsequent requirement +for alternative-specific splitting must also be satisfied before selecting the +default. The design must stay compact in branching descriptions, branchers, +choices, and search paths. + +## User requirements + +1. A model author can use a better default generator and supply a custom engine + through Gecode's supported extension interface. +2. A test failure can be replayed from its complete random state, including state + required for subsequent splitting, without reconstructing preceding draws. +3. Every alternative of a branching decision gets a distinct successor state. + Replaying the same recorded decision and alternative gets the same state. +4. Cloning and recomputation do not introduce additional splits or make streams + depend on exploration order. +5. A command-line executable can use one configured engine. Large internal state + does not force users to supply equally large numeric seeds. +6. Memory and execution costs remain appropriate for Gecode search. + +## Architectural decisions + +### Branching and replay contract + +Generating a choice may consume random values for variable and value selection. +After those selections, the choice captures the complete state needed to derive +the streams for its alternatives. Committing an alternative derives and installs +its successor state from that immutable choice data and the alternative index. +It must not derive it from whatever mutable generator state happens to be in the +destination space. + +The same operation applies during exploration and recomputation. Reconstructing +a choice from an archive restores its splitting data without drawing or splitting. +Cloning preserves generator state without advancing either source or destination. +Sibling spaces must not consume each other's mutable streams. + +The sibling-state distinction must hold for all valid alternative indices, +including large multiway choices. A hash with merely a low collision probability +does not establish this requirement. Distinct states do not imply globally +non-overlapping sequences or uniqueness across an unbounded search tree. + +Store one common splitting payload per choice for the common single-stream case, +not an array of successor states. Deriving a late alternative must not require +generating all preceding alternatives. If multiple streams need separate state, +their cost must be explicit; do not silently assume one snapshot covers them. + +This is a guarantee about random state for a recorded path. It does not promise +identical global scheduling, solution order, adaptive heuristic state, or search +trees under parallel search, restarts, or weakly monotonic propagation. + +### Generator operations and extensibility + +Specify initialization, raw generation, bounded generation, exact copying, +alternative-indexed splitting, and full-state save/restore separately. +The names and C++ representation remain to be chosen during Phase 1. + +Custom engines must be usable by supported branching APIs, not only by callers +of the low-level support library. Define the output range, valid states, state +size, and splitting obligations of an engine. Engines without the required +splitting behavior may be usable as standalone generators but cannot silently +qualify as search generators. + +Keep the command-line engine selection independent of the extension mechanism: +one configured engine per executable is sufficient. No runtime plugin registry +or mandatory catalogue of engines is required. If configuration changes public +types or layouts, export the configuration consistently to downstream builds. + +Gecode specifies bounded integer conversion and draw consumption, including zero, +one, negative signed bounds, and maximum supported bounds. Use integer conversion +with rejection where required; account for the engine's actual output range. +In particular, a nonzero-state xorshift engine must not be treated as emitting +every 64-bit value with equal frequency over its period. Avoid distribution +caches unless their state is part of snapshots. + +### Seeds and complete state + +Ordinary initialization accepts a checked 64-bit seed, in decimal or hexadecimal. +Specify seed expansion and the treatment of zero for every supported engine. +Restoring state bypasses seed expansion and never silently repairs invalid state. + +Use a canonical text representation containing an algorithm/format identifier +and all state words in a specified order. Stream increments, counters, and other +mutable or per-instance parameters belong in it. An executable rejects an +incompatible state identifier rather than switching engines implicitly. + +The test runner records a snapshot immediately before an iteration and prints a +command that restores that snapshot directly for the named test. That command +bypasses normal suite-level seed derivation. Ordinary failure and exception paths +must both report the relevant iteration state, along with existing test options. + +The test runner needs full-state input in Phase 1. Public drivers receive the +same seed/state conventions in Phase 4. Use separate seed and state options; +reject conflicting input. A wider seed interface is unnecessary for this scope. + +### Compactness + +Measure the complete representation, not just the engine's state words: public +descriptions, selectors, local storage, choice objects, archive words, and retained +search paths. Include padding, dispatch data, and allocations. + +An 8-byte engine state is preferred; 16 bytes is a candidate budget for a truly +splittable engine, not an already approved limit. Keep runtime dispatch metadata +and textual identifiers out of repeated state payloads where they are implied +by the configured type. Avoid adding random-state storage to models that never +use random branching where practical, and measure any unavoidable common cost. + +## Decisions to close during implementation + +### Engine and indexed splitting + +Compare the original xorshift64* candidate with a published splittable design. +Distinguish fixed-increment SplitMix64 (one state word) from splittable SplitMix +(state plus a per-stream increment). Neither the name "SplitMix" nor the +existence of a sequential split operation establishes our indexed sibling-state +contract. Document the indexed adaptation, its cost, and why sibling states are +distinct before accepting it. + +Select a small initial set of alternatives with useful differences. At least one +additional search-capable engine should exercise the extension interface before +release. Preserve xorshift64* as a comparison candidate; do not invent an untested +splitting construction just to keep it as the default. Record the default decision +after correctness, published quality evidence, and Gecode measurements agree. + +### Ownership across selectors and branchers + +This decision must be closed before the Phase 2 integration is considered done. +Random variable selection and value selection may currently share a handle or +use separate handles; later posted branchers may also retain those handles. +Splitting only the active selector leaves later consumers unchanged. + +Start by evaluating a space-local random context shared by cooperating branchers, +using Gecode's existing local-object cloning mechanism if appropriate. Compare it +with compact state held directly in branchers. Choose the smallest design that +meets the contract; neither representation is mandated by this plan. + +Specify what reusing a generator in multiple descriptions means, how distinct +generators coexist, when external initialization is bound to a space, and how +later branchers inherit the alternative-specific state. Cover deterministic +choices before a randomized brancher: the later random stream must reflect the +selected alternative even when the earlier choice used no random values. +Also cover one-alternative assignments, dynamically posted branchers, and custom +callbacks. Explain where splitting state is captured and installed so custom +branchers have a clear participation contract. + +Use a small worked example with two sequential branchers, random variable and +value selection, and both shared and separate initial generators to settle this. +Do not add a general stream registry or per-node map without demonstrating why +a simpler ownership model cannot meet these cases. + +## Phase 1: Extensible generation with exact test replay + +**User requirements:** 1, 2, 5, 6. + +### What to build + +Introduce the engine contract and a compact candidate implementation, connected +to the existing test runner through complete-state input and failure reporting. +Specify raw output, bounded output, initialization, indexed splitting, and state +encoding together. Demonstrate another engine supplied outside the implementation +without editing the engine-selection logic. Resolve the ownership design needed +for the next phase using the worked example above. + +### Acceptance criteria + +- [x] Saving after mixed bounded draws and splits, restoring, and continuing + reproduces both future draws and future split states exactly. +- [x] A deliberately failing test iteration can be replayed directly from the + reported command, including when the failure is an exception. +- [x] Published vectors validate the selected engine where available; focused + checks cover invalid state, seed expansion, bounds, and state round trips. +- [x] The indexed splitting rule has an argument for sibling-state distinction + and handles the full valid alternative-index range without linear replay. +- [x] Custom engine state size is not artificially fixed to the default's size. +- [x] The ownership decision and initial memory measurements are recorded. + +### Phase 1 review + +Implemented `Support::Random` with full-state encoding and integer +rejection sampling, a 16-byte splittable SplitMix candidate, and the 8-byte +xorshift64* standalone alternative. The old congruential engine remains named +for comparison, but is no longer the default. Final default selection remains +subject to the Phase 3 measurements. + +For SplitMix parent `(s,g)`, alternative `a` gets state +`(Mix13(s+(2a+1)g), mixGamma(s+(2a+2)g))`, using unsigned 64-bit arithmetic. +This is the child of the `(a+1)`th sequential SplitMix split, computed directly. +Since `g` is odd and `a` is 32 bits, the first inputs are distinct modulo 2^64; +Mix13 is a permutation, so the complete sibling states are distinct. No new +generator recurrence or probabilistic collision assumption is introduced. + +The chosen ownership direction for Phase 2 is a lazily allocated space-local +collection of bound streams. Reusing the same initialization handle binds the +same local stream; separate handles retain separate streams. Space cloning +duplicates their mutable state. Choices snapshot all bound streams, including +ones belonging to later branchers, and commit derives each from the recorded +alternative. This also handles deterministic choices preceding random ones. +A compact linear collection is sufficient for the normally small number of +streams; there is no general registry or per-node lookup map. Raw engine words +are packed into one optional choice payload, with layout implied by the bound +engines. Phase 2 must measure and review the actual overhead of this design. + +Worked example: descriptions for branchers A and B reuse handle R, and A's +variable and value selectors both use R. All three bind one stream in the space. +After A's selections, its choice records R's state. Committing alternative 1 +installs R's child 1, which B subsequently uses. If A's value selector instead +uses a separate handle V, the choice stores R and V once each and commits both +child states. Replaying from an earlier clone uses those snapshots even though +A's selection draws were not re-executed. + +Validation: `Random::Contract` passed, including known raw-output vectors, +mixed draw/split replay, invalid state/seed input, bounds, and a user engine with +three state words. The dedicated `random-state-replay` CTest passes both ordinary +failure and exception replay by executing the printed command and comparing the +failing draw and state. The exception fixture first completes two iterations, +so this checks an advanced state rather than just initial seeding. Filtered +random tie selection also passes with the maximum 64-bit seed. +The existing `check` target passes, including its fault-injection checks and +selected integer, set, float, FlatZinc, branching, and search regressions. + +Measured on arm64 macOS: engine 4 -> 16 bytes; Rnd 8, IntVarBranch 112, +IntValBranch 80, Choice 16, integer PosValChoice 24, and Space 288 bytes remain +unchanged in this phase. Search still has its old shared-handle behavior until +Phase 2; this intermediate limitation is intentional and not a completed search +reproducibility claim. + +## Phase 2: Alternative-specific streams through one search path + +**User requirements:** 1, 3, 4, 6. + +### What to build + +Carry the new random state through a representative integer branching path, +choice archiving, commit, and sequential recomputation. Include both random +variable and value selection and the transition to a second brancher. Exercise +the same path with a user-supplied engine to prove that extension reaches search. + +### Acceptance criteria + +- [ ] All alternatives of a recorded choice have distinct successor states. +- [ ] Committing an alternative directly, after intervening sibling exploration, + or after restoring an archived choice produces identical successor state + and subsequent draws on equivalent spaces. +- [ ] The same recorded path yields the same state with frequent cloning and + substantial recomputation, including last-alternative optimization. +- [ ] A deterministic choice followed by random branching, and a transition + between randomized branchers, both retain the alternative-specific stream. +- [ ] Shared and separate variable/value generators follow the documented + ownership policy; cloning does not mutate the source's generators. +- [ ] Choice payload growth is independent of the number of alternatives. +- [ ] Measure description, brancher, choice, and archive sizes against baseline. + +## Phase 3: Complete branching and search integration + +**User requirements:** 1, 3, 4, 6. + +### What to build + +Apply the verified contract across integer, Boolean, set, and float random +branching, tie breaking, multiway branching, assignment, and supported custom +branchers. Check the common commit boundary and both sequential and parallel +search replay paths. Account explicitly for restarts, relaxation, and model +callbacks that use randomness outside ordinary choice selection. + +### Acceptance criteria + +- [ ] Focused integration cases cover binary, multiway, and one-alternative + branching, plus handover to later branchers and custom-engine use. +- [ ] Alternative identity follows the public choice index, including when a + brancher reverses the mapping from alternative index to selected value. +- [ ] Clone, archive, disposal, failure, traced commit, and conditional commit + paths preserve the contract without shared mutable state between spaces. +- [ ] Sequential and parallel replay of the same recorded path agree on random + state; checks do not require identical parallel solution order. +- [ ] Restart and portfolio initialization policies are documented and do not + accidentally introduce worker-scheduling-dependent shared streams. +- [ ] Existing relevant branching and search correctness tests pass. Add tests + for the new semantic guarantees rather than duplicating each API wrapper. +- [ ] Random generation, bounded draws, splitting, clone cost, and retained-path + memory are measured in representative Gecode workloads. Use controlled + paths to separate overhead from changes in the randomized search tree. + +## Phase 4: Configured command lines and Gecode 7 migration + +**User requirements:** 1, 2, 5, 6. + +### What to build + +Complete seed/state handling in the example driver and FlatZinc, propagate the +configured engine through their random consumers, and finalize the default and +initial alternatives using the preceding evidence. Document the extension API, +ownership rules, replay contract, and compatibility changes for Gecode 7. + +### Acceptance criteria + +- [ ] Drivers accept full 64-bit seeds without signed narrowing or truncation + and use the same initialization/state conventions as the test runner. +- [ ] Complete state is accepted and reproduced for the configured engine; + malformed, incompatible, and conflicting options are rejected clearly. +- [ ] Time/hardware initialization can report the concrete initialized state + needed for a later replay. +- [ ] A runnable custom-engine example works through built-in branching; custom + brancher documentation explains choice snapshots and replay obligations. +- [ ] FlatZinc restart sampling no longer relies on the old generator's restricted + range or sequence-preservation workaround where the new contract replaces it. +- [ ] Release notes describe changed seeded sequences, state replay, and copying + versus sharing semantics. No legacy sequence mode is required. +- [ ] Supported build configurations and relevant regression suites pass; the + default choice and measured memory/performance tradeoffs are documented. + +## Validation boundaries + +Keep a small set of high-value tests: reference vectors, save/restore including +splits, direct failure replay, and equivalent-path integration tests. Assertions +on state are stronger than expecting different first outputs from siblings: +distinct states can legitimately produce equal individual outputs. + +Do not put probabilistic distribution tests into ordinary CI. Use established +statistical tools during engine evaluation when needed, especially for any +indexed-splitting adaptation. Passing a statistical battery alone is not evidence +that sibling states are always distinct or replay is correct. + +Implementation proceeds in the phase order above. Each phase review records +its evidence and any adjustment before the phase is committed. + +## Initial code observations supporting the plan + +- `gecode/support/random.hpp`: current state is one unsigned integer, normally + 32 bits. Bounded output combines low-bit chunks and scaling/modulo operations. +- `gecode/kernel/data/rnd.hpp` and `.cpp`: public random handles share mutable + implementation state and use a static mutex across implementations. +- `gecode/kernel/branch/var.hpp`, `val.hpp`, and `view-sel.hpp`: branching + descriptions and selectors retain these handles, including during cloning. +- `gecode/kernel/branch/view-val.hpp` and `gecode/int/branch/view-values.hpp`: + choices currently store selected positions/values, without random snapshots. +- `gecode/kernel/core.cpp`: exploration and replay reach brancher commit through + common space operations; archive reconstruction dispatches by brancher identity. +- `gecode/kernel/core.hpp`: space-local objects already support cloning shared + objects within a space. Their suitability must be weighed against overhead. +- `gecode/kernel/archive.hpp`: archive storage uses unsigned integer words and + has no existing 64-bit integer overload; explicitly preserve every state bit. +- `gecode/search/seq/path.hpp` and `gecode/search/par/path.hpp`: retained choices + and alternative indices drive recomputation and make payload size significant. +- `test/test.cpp`: iteration replay currently treats the current generator state + as an unsigned seed; exceptions report the suite seed instead. +- `gecode/driver.hh` and `gecode/flatzinc.hh`: seed option types differ, with a + signed seed path in FlatZinc. +- `gecode/flatzinc/restart-random.hpp`: special handling compensates for the + current bounded generator while preserving established sequences. + +## Algorithm references + +- [Steele, Lea, and Flood: Fast Splittable Pseudorandom Number Generators](https://gee.cs.oswego.edu/dl/papers/oopsla14.pdf) + describes the splittable SplitMix design. +- [Vigna: older scrambled linear generators](https://prng.di.unimi.it/xorshift.php) + discusses limitations of the xorshift family and its low bits. +- [Blackman and Vigna's generator overview](https://prng.di.unimi.it/) + provides modern alternatives, reference implementations, and seed-expansion + guidance. Fixed-increment SplitMix64 is distinct from full splittable SplitMix. diff --git a/test/public-runner-smoke.cpp b/test/public-runner-smoke.cpp index 3ac13abdc6..c199eded2b 100644 --- a/test/public-runner-smoke.cpp +++ b/test/public-runner-smoke.cpp @@ -67,19 +67,17 @@ namespace { } } failing_smoke_test; - unsigned int observed_seed = 0; + std::string observed_state; int replay_runs = 0; class ReplaySmokeTest : public Test::Base { public: ReplaySmokeTest(void) : Test::Base("Smoke::Replay", Test::TestTags()) {} bool run(void) override { if (++replay_runs == 1) { - // Leave a state above the seed setter's modulus for the next iteration. - _rand.seed(44488); (void) _rand.next(); return true; } - observed_seed = _rand.seed(); + observed_state = _rand.state_string(); (void) _rand(10); return false; } @@ -200,7 +198,7 @@ main(void) { "filtered failing run did not preserve seed diagnostics")) { return EXIT_FAILURE; } - if (!require(fail_output.find("-test Smoke::B-Fail") != std::string::npos, + if (!require(fail_output.find("-test 'Smoke::B-Fail'") != std::string::npos, "filtered failing run did not preserve test diagnostics")) { return EXIT_FAILURE; } @@ -215,13 +213,13 @@ main(void) { replay_output) == EXIT_FAILURE && replay_runs == 2, "seed-sensitive test should fail on its second iteration")) return EXIT_FAILURE; - const std::string seed = std::to_string(observed_seed); - if (!require(replay_output.find("Options: -replay " + seed) != std::string::npos, + const std::string state = observed_state; + if (!require(replay_output.find("Options: -replay " + state) != std::string::npos, "failure should report the iteration's initial random state")) return EXIT_FAILURE; if (!require(run_and_capture({"public-runner-smoke", "-test", "Smoke::Replay", - "-replay", seed.c_str()}, replay_output) == EXIT_FAILURE && - std::to_string(observed_seed) == seed, + "-replay", state.c_str()}, replay_output) == EXIT_FAILURE && + observed_state == state, "replay should restore the reported random state")) return EXIT_FAILURE; diff --git a/test/random-replay.cmake b/test/random-replay.cmake new file mode 100644 index 0000000000..4e974f4e8e --- /dev/null +++ b/test/random-replay.cmake @@ -0,0 +1,23 @@ +# Copyright (c) 2026 Mikael Zayenz Lagerkvist. MIT license; see LICENSE. +foreach(kind Failure Exception) + execute_process(COMMAND "${REPLAY}" -seed 1 -iter 100 -test "Random::Replay::${kind}" + RESULT_VARIABLE first_result OUTPUT_VARIABLE first ERROR_VARIABLE first_error) + if(NOT first_result EQUAL 1) + message(FATAL_ERROR "Fixture did not fail: ${first} ${first_error}") + endif() + string(REGEX MATCH "Options: ([^\n]+)" command "${first}") + set(arguments "${CMAKE_MATCH_1}") + string(REGEX MATCH "Replay draw: [0-9]+" draw "${first}") + if(NOT command OR NOT draw) + message(FATAL_ERROR "Missing replay report: ${first}") + endif() + separate_arguments(arguments UNIX_COMMAND "${arguments}") + execute_process(COMMAND "${REPLAY}" ${arguments} + RESULT_VARIABLE replay_result OUTPUT_VARIABLE replay ERROR_VARIABLE replay_error) + string(REGEX MATCH "Replay draw: [0-9]+" replay_draw "${replay}") + string(REGEX MATCH "Options: [^\n]+" replay_command "${replay}") + if(NOT replay_result EQUAL 1 OR NOT draw STREQUAL replay_draw OR NOT command STREQUAL replay_command) + message(FATAL_ERROR "Replay differs:\n${first}\n${replay}\n${replay_error}") + endif() +endforeach() +message(STATUS "Failure and exception state replay agree") diff --git a/test/random-replay.cpp b/test/random-replay.cpp new file mode 100644 index 0000000000..cf7bbd4e9f --- /dev/null +++ b/test/random-replay.cpp @@ -0,0 +1,23 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +// Copyright (c) 2026 Mikael Zayenz Lagerkvist. MIT license; see LICENSE. +// Deliberately failing fixtures for the test runner's state replay protocol. +#include "test/test.hh" + +namespace { + class Replay : public Test::Base { + bool exception; + public: + explicit Replay(bool e) + : Base(e ? "Random::Replay::Exception" : "Random::Replay::Failure"), + exception(e) {} + bool run() override { + if (_rand(4) != 0) + return true; + auto child = _rand.split(7); + std::cout << "Replay draw: " << child.next() << '\n'; + if (exception) + throw Gecode::Exception("Random::Replay", "deliberate exception"); + return false; + } + } failure(false), exception(true); +} diff --git a/test/random.cpp b/test/random.cpp new file mode 100644 index 0000000000..850988c6f9 --- /dev/null +++ b/test/random.cpp @@ -0,0 +1,118 @@ +/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +// Copyright (c) 2026 Mikael Zayenz Lagerkvist. MIT license; see LICENSE. + +#include "test/test.hh" + +namespace Test { + namespace Random { + using namespace Gecode::Support; + + // A user engine with extra state exercises the public engine contract. + // The counter deliberately affects output, so omitting it breaks replay. + class CountedSplitMix { + SplitMix source; + uint64_t count = 0; + public: + using State = std::array; + explicit CountedSplitMix(uint64_t s=1) : source(s) {} + static const char* name() { return "counted-splitmix-test-v1"; } + static constexpr uint64_t min() { return 0; } + static constexpr uint64_t max() { return UINT64_MAX; } + void seed(uint64_t s) { source.seed(s); count=0; } + uint64_t next() { return source.next() ^ count++; } + State state() const { + auto s = source.state(); + return {{s[0],s[1],count}}; + } + void state(const State& s) { + source.state({{s[0],s[1]}}); + count=s[2]; + } + CountedSplitMix split(uint32_t a) const { + auto child = *this; + child.source = source.split(a); + return child; + } + }; + + template + bool replay() { + Gecode::Support::Random original(UINT64_MAX), restored; + for (unsigned int i=0; i<17; ++i) + (void) original(13); + original = original.split(37); + (void) original(UINT64_MAX); + restored.state(original.state_string()); + for (uint32_t a : {0U,1U,17U,UINT32_MAX}) { + if (original.split(a).state() != restored.split(a).state()) + return false; + if (original.next() != restored.next()) + return false; + } + auto before = restored.state(); + if (restored(0) || restored(1) || restored(-1) || + restored.state() != before) + return false; + return true; + } + + class Contract : public Base { + public: + Contract() : Base("Random::Contract", TestTags::check()) {} + bool run() override { + // SplitMix64 reference sequence, seed zero and golden-ratio increment. + RandomGenerator r(0); + for (uint64_t expected : {UINT64_C(0xe220a8397b1dcdaf), + UINT64_C(0x6e789e6aa1b965f4), + UINT64_C(0x06c45d188009454f)}) + if (r.next() != expected) + return false; + Xorshift64Star xs(1); + if (xs.next() != UINT64_C(0x47e4ce4b896cdd1d)) + return false; + if (!replay() || !replay()) + return false; + auto parent = r.state(); + // Indexing skips pairs of parent words, exactly as sequential splits. + RandomGenerator sequential = r; + for (uint32_t a=0; a<100; ++a) { + auto child = r.split(a); + if (child.state()[0] != sequential.next()) + return false; + (void) sequential.next(); + if ((a>0) && (child.state() == r.split(a-1).state())) + return false; + } + if (r.state() != parent || r.split(0).state()==r.split(UINT32_MAX).state()) + return false; + if (random_seed("18446744073709551615") != UINT64_MAX || + random_seed("0xffffffffffffffff") != UINT64_MAX) + return false; + for (const char* invalid : {"", "-1", "+1", "1x", "0x", "18446744073709551616"}) { + try { (void) random_seed(invalid); return false; } + catch (const std::invalid_argument&) {} + } + const auto saved = r.state(); + for (const char* invalid : { + "splitmix-v1:0000000000000000:0000000000000000", + "splitmix-v1:0000000000000000:0000000000000002", + "splitmix-v1:000000000000000g:9e3779b97f4a7c15", + "splitmix-v1:0:9e3779b97f4a7c15", + "other-v1:0000000000000000:9e3779b97f4a7c15"}) { + try { r.state(std::string(invalid)); return false; } + catch (const std::invalid_argument&) {} + if (r.state() != saved) + return false; + } + Gecode::Support::Random x(0); + try { x.state(Xorshift64Star::State{{0}}); return false; } + catch (const std::invalid_argument&) {} + for (uint64_t bound : {UINT64_C(2),UINT64_C(3),UINT64_C(0x8000000000000001),UINT64_MAX}) + for (unsigned int i=0; i<100; ++i) + if (r(bound)>=bound || x(bound)>=bound) + return false; + return true; + } + } contract; + } +} diff --git a/test/test.cpp b/test/test.cpp index 2153d39b5d..8d423a762a 100644 --- a/test/test.cpp +++ b/test/test.cpp @@ -164,11 +164,18 @@ namespace Test { const int Options::defiter; const unsigned int Options::deffixprob; - void report_error(const std::string& name, unsigned int seed, const Options& options, std::ostream& ostream) { - ostream << "Options: -replay " << seed; + void report_error(const std::string& name, const std::string& state, + const Options& options, std::ostream& ostream) { + ostream << "Options: -replay " << state; if (options.fixprob != Test::Options::deffixprob) ostream << " -fixprob " << options.fixprob; - ostream << " -test " << name << std::endl; + ostream << " -test '"; + for (char c : name) + if (c == '\'') + ostream << "'\\''"; + else + ostream << c; + ostream << "'" << std::endl; if (options.log) ostream << olog.str(); } @@ -183,13 +190,13 @@ namespace Test { << "\t\tnumber of threads to use. If 0, as many threads as there are cores are used.\n" << "\t\tThreaded execution and logging can not be used at the same time." << std::endl - << "\t-seed (unsigned int or \"time\") default: " + << "\t-seed (64-bit unsigned integer or \"time\") default: " << seed << std::endl - << "\t\tseed for random number generator (unsigned int)," + << "\t\tseed for random number generator (decimal or hexadecimal)," << std::endl << "\t\tor \"time\" for a random seed based on " << "current time" << std::endl - << "\t-replay (unsigned int)\n" + << "\t-replay (complete random state)\n" << "\t\treplay the exact -test name once, on one thread\n" << "\t-fixprob (unsigned int) default: " << fixprob << std::endl @@ -236,13 +243,27 @@ namespace Test { } else { threads = argument; } - } else if (!strcmp(argv[i],"-seed") || !strcmp(argv[i],"-replay")) { - replay = !strcmp(argv[i],"-replay"); + } else if (!strcmp(argv[i],"-seed")) { if (++i == argc) goto missing; if (!strcmp(argv[i],"time")) { - seed = static_cast(time(nullptr)); + seed = static_cast(time(nullptr)); } else { - seed = static_cast(atoi(argv[i])); + try { + seed = Gecode::Support::random_seed(argv[i]); + } catch (const std::invalid_argument& e) { + std::cerr << e.what() << std::endl; + exit(EXIT_FAILURE); + } + } + } else if (!strcmp(argv[i],"-replay")) { + if (++i == argc) goto missing; + replay = argv[i]; + try { + Gecode::Support::RandomGenerator check; + check.state(replay); + } catch (const std::invalid_argument& e) { + std::cerr << e.what() << std::endl; + exit(EXIT_FAILURE); } } else if (!strcmp(argv[i],"-iter")) { if (++i == argc) goto missing; @@ -289,7 +310,7 @@ namespace Test { i++; } - if (replay) { + if (!replay.empty()) { if (testpat.size() != 1 || testpat[0].first != MT_ANY || start_from != nullptr) { std::cerr << "-replay requires one exact -test name." << std::endl; @@ -312,7 +333,7 @@ namespace Test { } bool Options::is_test_name_matching(const std::string& test_name) const { - if (replay) + if (!replay.empty()) return testpat.size() == 1 && test_name == testpat[0].second; if (!testpat.empty()) { bool positive_patterns = false; @@ -358,22 +379,22 @@ namespace Test { } /// Run a single test, returning true iff the test succeeded - bool run_test(Base* test, unsigned int test_seed, const Options& options, std::ostream& ostream) { - unsigned int seed = options.replay ? options.seed : test_seed; + bool run_test(Base* test, uint64_t test_seed, const Options& options, std::ostream& ostream) { + test->_rand.seed(test_seed); + if (!options.replay.empty()) + test->_rand.state(options.replay); + std::string iteration_state = test->_rand.state_string(); try { ostream << test->name() << " "; ostream.flush(); - test->_rand.seed(seed); for (unsigned int i = options.iter; i--;) { - // Normalize evolved states so the reported seed restores this iteration. - test->_rand.seed(test->_rand.seed()); - seed = test->_rand.seed(); + iteration_state = test->_rand.state_string(); if (test->run()) { ostream << '+'; ostream.flush(); } else { ostream << "-" << std::endl; - report_error(test->name(), seed, options, ostream); + report_error(test->name(), iteration_state, options, ostream); return false; } } @@ -383,7 +404,11 @@ namespace Test { ostream << "Exception in \"Gecode::" << e.what() << "." << std::endl << "Stopping..." << std::endl; - report_error(test->name(), seed, options, ostream); + report_error(test->name(), iteration_state, options, ostream); + return false; + } catch (const std::exception& e) { + ostream << "Exception: " << e.what() << std::endl; + report_error(test->name(), iteration_state, options, ostream); return false; } } @@ -403,7 +428,7 @@ namespace Test { size_t passed = 0; size_t failed = 0; for (auto test : tests) { - unsigned int test_seed = seed_sequence.next(); + uint64_t test_seed = seed_sequence.next(); if (run_test(test, test_seed, options, std::cout)) { ++passed; } else { @@ -525,10 +550,10 @@ namespace Test { /// The common controller for running tests TestExecutionControl& tec; /// The initial seed to start with - const int initial_seed; + const uint64_t initial_seed; public: - TestExecutor(TestExecutionControl& tec, const int initialSeed) + TestExecutor(TestExecutionControl& tec, uint64_t initialSeed) : tec(tec), initial_seed(initialSeed) {} void run(void) override { @@ -551,7 +576,7 @@ namespace Test { break; } auto test = tec.tests[i]; - unsigned int test_seed = seed_sequence.next(); + uint64_t test_seed = seed_sequence.next(); std::ostringstream test_output; tec.record_result(run_test(test, test_seed, tec.options, test_output)); tec.write_output(test_output.str()); @@ -622,7 +647,7 @@ namespace Test { } const std::vector tests = select_tests(opt); - if (opt.replay && tests.size() != 1) { + if (!opt.replay.empty() && tests.size() != 1) { std::cerr << "-replay requires exactly one registered test." << std::endl; return EXIT_FAILURE; } diff --git a/test/test.hh b/test/test.hh index 32d1a67149..98d6efbf22 100755 --- a/test/test.hh +++ b/test/test.hh @@ -135,9 +135,9 @@ namespace Test { /// Number of threads to use unsigned int threads; /// The random seed to be used - unsigned int seed; - /// Replay one named test from its reported random state - bool replay; + uint64_t seed; + /// Complete random state for replaying one named test (empty if none) + std::string replay; /// Number of iterations for each test unsigned int iter; /// Default number of iterations diff --git a/test/test.hpp b/test/test.hpp index 7681468118..2693c3fd3d 100755 --- a/test/test.hpp +++ b/test/test.hpp @@ -78,7 +78,7 @@ namespace Test { */ inline Options::Options(void) - : threads(1), seed(0), replay(false), iter(defiter), fixprob(deffixprob), + : threads(1), seed(0), iter(defiter), fixprob(deffixprob), stop(true), log(false), testpat(), testtags(), start_from(nullptr), list(false), list_tags(false), list_with_tags(false) {} From 24303e101068e8410c87333786b7f9428286b645 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Thu, 10 Sep 2026 10:14:34 +0200 Subject: [PATCH 02/16] random: derive alternative streams from recorded choices --- gecode/flatzinc/branch.hpp | 3 +- gecode/float/branch/val-sel.hpp | 5 +- gecode/int/branch/val-sel.hpp | 5 +- gecode/kernel/branch/view-sel.hpp | 4 +- gecode/kernel/core.cpp | 44 ++++- gecode/kernel/core.hpp | 14 +- gecode/kernel/data/rnd.cpp | 64 ++++--- gecode/kernel/data/rnd.hpp | 287 ++++++++++++++++-------------- gecode/set/branch/val-sel.hpp | 5 +- plans/random.md | 57 +++++- test/random.cpp | 124 +++++++++++++ 11 files changed, 421 insertions(+), 191 deletions(-) diff --git a/gecode/flatzinc/branch.hpp b/gecode/flatzinc/branch.hpp index cb287f1b62..70517cdd17 100644 --- a/gecode/flatzinc/branch.hpp +++ b/gecode/flatzinc/branch.hpp @@ -35,7 +35,7 @@ namespace Gecode { namespace FlatZinc { forceinline IntBoolVarBranch::IntBoolVarBranch(Select s0, double d) - : VarBranch(d), s(s0) {} + : VarBranch(d,nullptr), s(s0) {} forceinline IntBoolVarBranch::IntBoolVarBranch(Select s0, IntAFC i, BoolAFC b) @@ -442,4 +442,3 @@ namespace Gecode { namespace FlatZinc { }} // STATISTICS: flatzinc-branch - diff --git a/gecode/float/branch/val-sel.hpp b/gecode/float/branch/val-sel.hpp index 811663f4ab..c41a18ec19 100644 --- a/gecode/float/branch/val-sel.hpp +++ b/gecode/float/branch/val-sel.hpp @@ -77,10 +77,10 @@ namespace Gecode { namespace Float { namespace Branch { forceinline ValSelRnd::ValSelRnd(Space& home, const ValBranch& vb) - : ValSel(home,vb), r(vb.rnd()) {} + : ValSel(home,vb), r(home,vb.rnd()) {} forceinline ValSelRnd::ValSelRnd(Space& home, ValSelRnd& vs) - : ValSel(home,vs), r(vs.r) { + : ValSel(home,vs), r(home,vs.r) { } forceinline FloatNumBranch ValSelRnd::val(const Space&, FloatView x, int) { @@ -100,4 +100,3 @@ namespace Gecode { namespace Float { namespace Branch { }}} // STATISTICS: float-branch - diff --git a/gecode/int/branch/val-sel.hpp b/gecode/int/branch/val-sel.hpp index 7d91368c3e..0e2e172796 100755 --- a/gecode/int/branch/val-sel.hpp +++ b/gecode/int/branch/val-sel.hpp @@ -97,11 +97,11 @@ namespace Gecode { namespace Int { namespace Branch { forceinline ValSelRnd::ValSelRnd (Space& home, const ValBranch::Var>& vb) - : ValSel(home,vb), r(vb.rnd()) {} + : ValSel(home,vb), r(home,vb.rnd()) {} template forceinline ValSelRnd::ValSelRnd(Space& home, ValSelRnd& vs) - : ValSel(home,vs), r(vs.r) { + : ValSel(home,vs), r(home,vs.r) { } template forceinline int @@ -166,4 +166,3 @@ namespace Gecode { namespace Int { namespace Branch { }}} // STATISTICS: int-branch - diff --git a/gecode/kernel/branch/view-sel.hpp b/gecode/kernel/branch/view-sel.hpp index c224133937..f389b0f66a 100644 --- a/gecode/kernel/branch/view-sel.hpp +++ b/gecode/kernel/branch/view-sel.hpp @@ -482,11 +482,11 @@ namespace Gecode { template forceinline ViewSelRnd::ViewSelRnd(Space& home, const VarBranch& vb) - : ViewSel(home,vb), r(vb.rnd()) {} + : ViewSel(home,vb), r(home,vb.rnd()) {} template forceinline ViewSelRnd::ViewSelRnd(Space& home, ViewSelRnd& vs) - : ViewSel(home,vs), r(vs.r) {} + : ViewSel(home,vs), r(home,vs.r) {} template int ViewSelRnd::select(Space&, ViewArray& x, int s) { diff --git a/gecode/kernel/core.cpp b/gecode/kernel/core.cpp index 54ab989695..002fc4fb34 100644 --- a/gecode/kernel/core.cpp +++ b/gecode/kernel/core.cpp @@ -235,6 +235,7 @@ namespace Gecode { if (_vars_d[i] != nullptr) vd[i]->dispose(*this, _vars_d[i]); #endif + delete randoms; // Release memory from memory manager mm.release(ssd.data().sm); } @@ -594,17 +595,36 @@ namespace Gecode { } // Make sure that b_commit does not point to a deleted brancher! b_commit = b_status; - return Brancher::cast(b_status)->choice(*this); + std::unique_ptr c(Brancher::cast(b_status)->choice(*this)); + if (randoms) + const_cast(c.get())->random_state = randoms->snapshot(); + return c.release(); } const Choice* Space::choice(Archive& e) const { unsigned int id; e >> id; + unsigned int n; e >> n; + std::unique_ptr data; + if (n) { + if (n > static_cast((e.size()-2)/2)) + throw std::invalid_argument("Invalid random choice archive size"); + data = std::make_unique(size_t(n)+1); + data[0]=n; + for (unsigned int i=0; i> lo >> hi; + data[i+1] = uint64_t(lo) | (uint64_t(hi)<<32); + } + } ActorLink* b_cur = bl.next(); while (b_cur != &bl) { Brancher* b = Brancher::cast(b_cur); - if (id == b->id()) - return b->choice(*this,e); + if (id == b->id()) { + const Choice* c = b->choice(*this,e); + const_cast(c)->random_state = data.release(); + return c; + } b_cur = b_cur->next(); } throw SpaceNoBrancher("Space::choice"); @@ -617,6 +637,11 @@ namespace Gecode { if (failed()) return; if (Brancher* b = brancher(c.bid)) { + if (c.random_state) { + if (!randoms) + throw std::invalid_argument("Random choice requires space streams"); + randoms->commit(c.random_state,a); + } // There is a matching brancher if (pc.p.bid_sc & sc_trace) { TraceRecorder* tr = findtracerecorder(); @@ -648,6 +673,11 @@ namespace Gecode { if (failed()) return; if (Brancher* b = brancher(c.bid)) { + if (c.random_state) { + if (!randoms) + throw std::invalid_argument("Random choice requires space streams"); + randoms->commit(c.random_state,a); + } // There is a matching brancher if (pc.p.bid_sc & sc_trace) { TraceRecorder* tr = findtracerecorder(); @@ -741,6 +771,8 @@ namespace Gecode { pl.init(); bl.init(); b_status = b_commit = &bl; + if (s.randoms) + randoms = new RandomContext(*s.randoms); // Copy all propagators { ActorLink* p = &pl; @@ -785,6 +817,7 @@ namespace Gecode { } catch (...) { recover(s); pc.c.source = nullptr; + delete randoms; mm.release(ssd.data().sm); throw; } @@ -957,6 +990,11 @@ namespace Gecode { void Choice::archive(Archive& e) const { e << id(); + unsigned int n = random_state ? static_cast(random_state[0]) : 0; + e << n; + for (unsigned int i=0; i(random_state[i+1]) + << static_cast(random_state[i+1]>>32); } bool diff --git a/gecode/kernel/core.hpp b/gecode/kernel/core.hpp index 08423430ee..8fbdc95403 100755 --- a/gecode/kernel/core.hpp +++ b/gecode/kernel/core.hpp @@ -144,6 +144,8 @@ namespace Gecode { class Advisor; class AFC; class Choice; + class Rnd; + class RandomContext; class Brancher; class Group; class PropagatorGroup; @@ -1432,6 +1434,10 @@ namespace Gecode { private: unsigned int bid; ///< Identity to match creating brancher unsigned int alt; ///< Number of alternatives + /// Optional packed random-state snapshot (independent of alternative count) + uint64_t* random_state; + Choice(const Choice&) = delete; + Choice& operator =(const Choice&) = delete; /// Return id of the creating brancher unsigned int id(void) const; @@ -1804,6 +1810,8 @@ namespace Gecode { Kernel::SharedSpaceData ssd; /// Performs memory management for space Kernel::MemoryManager mm; + /// Lazily allocated random streams, owned by this space + RandomContext* randoms = nullptr; #ifdef GECODE_HAS_CBS /// Global counter for variable ids unsigned int var_id_counter; @@ -2048,6 +2056,8 @@ namespace Gecode { GECODE_KERNEL_EXPORT void ap_ignore_dispose(Actor* a, bool d); public: + /// Bind a random handle to this space, mapping local handles during cloning + GECODE_KERNEL_EXPORT Rnd random(const Rnd& source); /** * \brief Suppress global propagator information during scoped posting * \ingroup TaskModelScript @@ -3983,7 +3993,7 @@ namespace Gecode { */ forceinline Choice::Choice(const Brancher& b, const unsigned int a) - : bid(b.id()), alt(a) {} + : bid(b.id()), alt(a), random_state(nullptr) {} forceinline unsigned int Choice::alternatives(void) const { @@ -3996,7 +4006,7 @@ namespace Gecode { } forceinline - Choice::~Choice(void) {} + Choice::~Choice(void) { delete[] random_state; } diff --git a/gecode/kernel/data/rnd.cpp b/gecode/kernel/data/rnd.cpp index 395e050452..3be7cc82b2 100644 --- a/gecode/kernel/data/rnd.cpp +++ b/gecode/kernel/data/rnd.cpp @@ -36,48 +36,46 @@ #include namespace Gecode { + Rnd::Rnd(Space& home, const Rnd& source) + : SharedHandle(home.random(source)) {} - Support::Mutex Rnd::IMP::m; - - forceinline - Rnd::IMP::IMP(uint64_t s) - : rg(s) {} - - Rnd::IMP::~IMP(void) {} + void Rnd::seed(uint64_t value) { + if (!object()) + *this = Rnd(value); + else + imp().seed(value); + } - forceinline void - Rnd::_seed(uint64_t s) { - if (object() == nullptr) { - object(new IMP(s)); + void Rnd::state(const std::string& text) { + if (!object()) { + Rnd candidate(1); + candidate.state(text); + *this = candidate; } else { - static_cast(object())->seed(s); + imp().state(text); } } - Rnd::Rnd(void) {} - Rnd::Rnd(uint64_t s) { - object(new IMP(s)); - } - Rnd::Rnd(const Rnd& r) - : SharedHandle(r) {} - Rnd& - Rnd::operator =(const Rnd& r) { - (void) SharedHandle::operator =(r); - return *this; + void Rnd::time(void) { + seed(static_cast(::time(nullptr))); } - Rnd::~Rnd(void) {} - void - Rnd::seed(uint64_t s) { - _seed(s); + void Rnd::hw(void) { + seed((uint64_t(Support::hwrnd()) << 32) | Support::hwrnd()); } - void - Rnd::time(void) { - _seed(static_cast(::time(nullptr))); - } - void - Rnd::hw(void) { - _seed(Support::hwrnd()); + + Rnd Space::random(const Rnd& source) { + if (!source) + throw UninitializedRnd("Space::random"); + // During actor/model copying, map source-local handles by their position. + if (is_partial_clone() && pc.c.source && pc.c.source->randoms) { + size_t i = pc.c.source->randoms->find(source); + if (i < pc.c.source->randoms->size()) + return randoms->at(i); + } + if (!randoms) + randoms = new RandomContext; + return randoms->bind(source); } } diff --git a/gecode/kernel/data/rnd.hpp b/gecode/kernel/data/rnd.hpp index 7de75e136f..f8f3b5c71b 100755 --- a/gecode/kernel/data/rnd.hpp +++ b/gecode/kernel/data/rnd.hpp @@ -32,152 +32,175 @@ */ #include +#include +#include namespace Gecode { + class RandomContext; + /** - * \brief Random number generator + * \brief Handle to a random stream + * + * Standalone handles share a stream. Binding to a space creates a local + * stream, shared by handles bound from the same source. Space clones have + * independent stream state. Use copy() for an independent standalone copy. + * A stream must not be drawn from concurrently. * \ingroup TaskModel */ class Rnd : public SharedHandle { + friend class RandomContext; private: - /// Implementation of generator class IMP : public SharedHandle::Object { - protected: - /// Mutex for locking - GECODE_KERNEL_EXPORT static Support::Mutex m; - /// The actual generator - Support::RandomGenerator rg; public: - /// Initialize generator with seed \a s - IMP(uint64_t s); - /// Return complete state - Support::RandomGenerator::State state(void) const; - /// Set seed to \a s - void seed(uint64_t s); - /// Returns a random integer from the interval \f$[0\ldots n)\f$ - unsigned int operator ()(unsigned int n); - /// Returns a random integer from the interval \f$[0\ldots n)\f$ - int operator ()(int n); - /// Returns a random integer from the interval \f$[0\ldots n)\f$ - unsigned long long int operator ()(unsigned long long int n); - /// Returns a random integer from the interval \f$[0\ldots n)\f$ - long long int operator ()(long long int n); - /// Delete implemenentation - virtual ~IMP(void); + virtual IMP* copy(void) const = 0; + virtual IMP* split(uint32_t a) const = 0; + virtual void seed(uint64_t value) = 0; + virtual uint64_t draw(uint64_t bound) = 0; + virtual size_t words(void) const = 0; + virtual void save(uint64_t* out) const = 0; + virtual void commit(const uint64_t* in, uint32_t a) = 0; + virtual std::string state(void) const = 0; + virtual void state(const std::string& text) = 0; + virtual const char* name(void) const = 0; + }; + template + class Implementation : public IMP { + Support::Random r; + public: + explicit Implementation(const Support::Random& value) : r(value) {} + IMP* copy(void) const override { return new Implementation(r); } + IMP* split(uint32_t a) const override { + return new Implementation(r.split(a)); + } + void seed(uint64_t value) override { r.seed(value); } + uint64_t draw(uint64_t bound) override { return r(bound); } + size_t words(void) const override { + return std::tuple_size::value; + } + void save(uint64_t* out) const override { + auto s = r.state(); + std::copy(s.begin(),s.end(),out); + } + void commit(const uint64_t* in, uint32_t a) override { + typename Engine::State s; + std::copy(in,in+s.size(),s.begin()); + auto parent = r; + parent.state(s); + r = parent.split(a); + } + std::string state(void) const override { return r.state_string(); } + void state(const std::string& text) override { r.state(text); } + const char* name(void) const override { return Engine::name(); } }; - /// Set the current seed to \a s (initializes if needed) - void _seed(uint64_t s); + IMP& imp(void) const { + if (!object()) + throw UninitializedRnd("Rnd"); + return *static_cast(object()); + } + Rnd(IMP* value, bool) : SharedHandle(value) {} public: - /// Default constructor that does not initialize the generator - GECODE_KERNEL_EXPORT - Rnd(void); - /// Initialize from generator \a r - GECODE_KERNEL_EXPORT - Rnd(const Rnd& r); - /// Assignment operator - GECODE_KERNEL_EXPORT - Rnd& operator =(const Rnd& r); - /// Destructor - GECODE_KERNEL_EXPORT - ~Rnd(void); - /// Initialize with seed \a s - GECODE_KERNEL_EXPORT - Rnd(uint64_t s); - /// Set the current seed to \a s (initializes if needed) - GECODE_KERNEL_EXPORT - void seed(uint64_t s); - /// Set current seed based on time (initializes if needed) - GECODE_KERNEL_EXPORT - void time(void); - /// Set current seed to hardware-based random number (initializes if needed) - GECODE_KERNEL_EXPORT - void hw(void); - /// Return complete current state - Support::RandomGenerator::State state(void) const; - /// Returns a random integer from the interval \f$[0\ldots n)\f$ - unsigned int operator ()(unsigned int n); - /// Returns a random integer from the interval \f$[0\ldots n)\f$ - int operator ()(int n); - /// Returns a random integer from the interval \f$[0\ldots n)\f$ - unsigned long long int operator ()(unsigned long long int n); - /// Returns a random integer from the interval \f$[0\ldots n)\f$ - long long int operator ()(long long int n); + /// Uninitialized handle + Rnd(void) = default; + /// Share an existing handle + Rnd(const Rnd&) = default; + Rnd& operator =(const Rnd&) = default; + ~Rnd(void) = default; + /// Create a standalone default stream + explicit Rnd(uint64_t seed) + : Rnd(Support::RandomGenerator(seed)) {} + /// Create a standalone user-defined splittable stream + template + explicit Rnd(const Support::Random& r) + : SharedHandle(new Implementation(r)) {} + /// Bind a source stream to a space, or update its handle during cloning + GECODE_KERNEL_EXPORT Rnd(Space& home, const Rnd& source); + /// Create and bind a default stream to a space + Rnd(Space& home, uint64_t seed) : Rnd(home,Rnd(seed)) {} + /// Make an independent exact copy; does not split or advance the source + Rnd copy(void) const { return Rnd(imp().copy(),true); } + /// Derive an alternative stream without modifying this stream + Rnd split(uint32_t a) const { return Rnd(imp().split(a),true); } + /// Seed this stream (initializes a default engine if uninitialized) + GECODE_KERNEL_EXPORT void seed(uint64_t value); + /// Initialize using time or hardware entropy + GECODE_KERNEL_EXPORT void time(void); + GECODE_KERNEL_EXPORT void hw(void); + /// Complete state, with algorithm identifier + std::string state(void) const { return imp().state(); } + /// Restore a state for this engine (default engine if uninitialized) + GECODE_KERNEL_EXPORT void state(const std::string& text); + const char* name(void) const { return imp().name(); } + /// Number of 64-bit words in the engine state + size_t words(void) const { return imp().words(); } + /// Draw an integer in [0,bound); bounds <= 1 consume no values + template + Type operator ()(Type bound) { + static_assert(std::is_integral::value && sizeof(Type)<=8, + "Random bound must be an integer of at most 64 bits"); + return bound<=1 ? 0 : + static_cast(imp().draw(static_cast(bound))); + } }; - forceinline Support::RandomGenerator::State - Rnd::IMP::state(void) const { - Support::RandomGenerator::State s; - const_cast(*this).m.acquire(); - s = rg.state(); - const_cast(*this).m.release(); - return s; - } - forceinline void - Rnd::IMP::seed(uint64_t s) { - m.acquire(); - rg.seed(s); - m.release(); - } - forceinline unsigned int - Rnd::IMP::operator ()(unsigned int n) { - unsigned int r; - m.acquire(); - r=rg(n); - m.release(); - return r; - } - forceinline int - Rnd::IMP::operator ()(int n) { - int r; - m.acquire(); - r=rg(n); - m.release(); - return r; - } - forceinline unsigned long long int - Rnd::IMP::operator ()(unsigned long long int n) { - unsigned long long int r; - m.acquire(); - r=rg(n); - m.release(); - return r; - } - forceinline long long int - Rnd::IMP::operator ()(long long int n) { - long long int r; - m.acquire(); - r=rg(n); - m.release(); - return r; - } - - forceinline Support::RandomGenerator::State - Rnd::state(void) const { - const IMP* i = static_cast(object()); - return i->state(); - } - forceinline unsigned int - Rnd::operator ()(unsigned int n) { - IMP* i = static_cast(object()); - return (*i)(n); - } - forceinline int - Rnd::operator ()(int n) { - IMP* i = static_cast(object()); - return (*i)(n); - } - forceinline unsigned long long int - Rnd::operator ()(unsigned long long int n) { - IMP* i = static_cast(object()); - return (*i)(n); - } - forceinline long long int - Rnd::operator ()(long long int n) { - IMP* i = static_cast(object()); - return (*i)(n); - } - + /// Internal space-local stream storage. Entries keep initialization handles alive. + class RandomContext { + struct Entry { + Rnd source; + Rnd local; + }; + std::vector streams; + public: + RandomContext(void) = default; + RandomContext(const RandomContext& other) { + streams.reserve(other.streams.size()); + for (const auto& entry : other.streams) + streams.push_back({entry.source,entry.local.copy()}); + } + size_t find(const Rnd& source) const { + for (size_t i=0; i(n+1); + data[0]=n; + size_t pos=1; + for (const auto& entry : streams) { + entry.local.imp().save(data.get()+pos); + pos += entry.local.words(); + } + return data.release(); + } + /// Restore and split the recorded streams; layout follows registration order. + void commit(const uint64_t* data, uint32_t a) { + size_t n=0; + for (const auto& entry : streams) + n += entry.local.words(); + if (data[0] != n) + throw std::invalid_argument("Random choice does not match space streams"); + size_t pos=1; + for (auto& entry : streams) { + entry.local.imp().commit(data+pos,a); + pos += entry.local.words(); + } + } + }; } // STATISTICS: kernel-other diff --git a/gecode/set/branch/val-sel.hpp b/gecode/set/branch/val-sel.hpp index 113c6314e4..51fbf7db00 100644 --- a/gecode/set/branch/val-sel.hpp +++ b/gecode/set/branch/val-sel.hpp @@ -91,10 +91,10 @@ namespace Gecode { namespace Set { namespace Branch { forceinline ValSelRnd::ValSelRnd(Space& home, const ValBranch& vb) - : ValSel(home,vb), r(vb.rnd()) {} + : ValSel(home,vb), r(home,vb.rnd()) {} forceinline ValSelRnd::ValSelRnd(Space& home, ValSelRnd& vs) - : ValSel(home,vs), r(vs.r) { + : ValSel(home,vs), r(home,vs.r) { } forceinline int ValSelRnd::val(const Space&, SetView x, int) { @@ -120,4 +120,3 @@ namespace Gecode { namespace Set { namespace Branch { }}} // STATISTICS: set-branch - diff --git a/plans/random.md b/plans/random.md index 4cd68b8ca3..752e19979b 100644 --- a/plans/random.md +++ b/plans/random.md @@ -1,7 +1,7 @@ # Plan: Compact, splittable random generators for Gecode 7 > Source: the feature/random design discussion, 2026-09-10. -> Status: implementation authorized; Phase 1 complete, Phase 2 next. +> Status: Phases 1 and 2 complete; Phase 3 next. > Workflow: review, update this plan, and commit after each phase. ## Goal @@ -247,18 +247,59 @@ the same path with a user-supplied engine to prove that extension reaches search ### Acceptance criteria -- [ ] All alternatives of a recorded choice have distinct successor states. -- [ ] Committing an alternative directly, after intervening sibling exploration, +- [x] All alternatives of a recorded choice have distinct successor states. +- [x] Committing an alternative directly, after intervening sibling exploration, or after restoring an archived choice produces identical successor state and subsequent draws on equivalent spaces. -- [ ] The same recorded path yields the same state with frequent cloning and +- [x] The same recorded path yields the same state with frequent cloning and substantial recomputation, including last-alternative optimization. -- [ ] A deterministic choice followed by random branching, and a transition +- [x] A deterministic choice followed by random branching, and a transition between randomized branchers, both retain the alternative-specific stream. -- [ ] Shared and separate variable/value generators follow the documented +- [x] Shared and separate variable/value generators follow the documented ownership policy; cloning does not mutate the source's generators. -- [ ] Choice payload growth is independent of the number of alternatives. -- [ ] Measure description, brancher, choice, and archive sizes against baseline. +- [x] Choice payload growth is independent of the number of alternatives. +- [x] Measure description, brancher, choice, and archive sizes against baseline. + +### Phase 2 review + +Implemented the space-local collection and common choice/commit integration. +`Rnd` accepts a user engine through `Support::Random` and keeps its +8-byte handle representation. Each space binds initialization handles to local +streams and clones those streams independently. Selector and model handles are +mapped back to the corresponding local stream during copying. Standalone handles +retain handle-style sharing; `copy()` makes an independent exact copy. + +All bound state words are captured after choice selection and restored/split +before the brancher's commit. Archives include the complete packed payload. +This works for custom choices that use the standard space choice/commit and base +choice archive interfaces, without per-brancher snapshot code. Binary and multiway +choices have the same random payload for the same registered streams. The small +selector binding change was applied to set and float selectors in this phase too, +because the ownership contract is common; their wider verification is Phase 3. + +The global draw mutex is removed: mutable streams are space-local, and standalone +handles are documented as requiring caller synchronization if shared by threads. +Making numeric construction explicit also exposed a FlatZinc decay constructor +that accidentally converted a double to a random seed. It now selects the actual +decay constructor explicitly. + +Validation: `Random::BranchReplay` checks reverse-order alternative exploration, +archived choices replayed on pre-selection clones, intentionally perturbed +destination state, shared/separate streams, deterministic-to-random handover, +and a user engine with three state words. For each configuration, all 81 solution +values and final states agree between commit distances 1 and 100. Both random +tests and filtered tie selection pass twice, state-report replay still passes, +and the full existing `check` target (including fault tests) passes. + +Arm64 sizes: Rnd 8, IntVarBranch 112, IntValBranch 80, and selectors remain +unchanged. Space grows 288 -> 296 bytes; Choice 16 -> 24 and integer PosValChoice +24 -> 32 bytes. One default stream adds a separately allocated 24-byte snapshot +(8-byte length + 16-byte state), making the logical integer choice footprint +56 bytes before allocator overhead. Its archive grows from 3 to 8 unsigned words. +Two default streams use a 40-byte snapshot; the custom engine uses 8 additional +bytes per stream. Nonrandom choices have no snapshot allocation but pay the +8-byte optional pointer and one archive count word. These costs are explicit +inputs to Phase 3 performance review, not yet a performance acceptance claim. ## Phase 3: Complete branching and search integration diff --git a/test/random.cpp b/test/random.cpp index 850988c6f9..cd304f6ae7 100644 --- a/test/random.cpp +++ b/test/random.cpp @@ -114,5 +114,129 @@ namespace Test { return true; } } contract; + + class ReplaySpace : public Gecode::Space { + public: + Gecode::IntVarArray x; + Gecode::Rnd variable, value; + ReplaySpace(const Gecode::Rnd& source, bool separate, bool multi=false) + : x(*this,4,0,2), variable(*this,source), + value(*this,separate ? source.copy() : source) { + using namespace Gecode; + // The first branch is deterministic, but later streams must split. + branch(*this,x[0],multi ? INT_VALUES_MIN() : INT_VAL_MIN()); + IntVarArgs first(2); + first[0]=x[1]; first[1]=x[2]; + branch(*this,first,INT_VAR_RND(variable),INT_VAL_RND(value)); + branch(*this,x[3],INT_VAL_RND(value)); + } + ReplaySpace(ReplaySpace& s) + : Space(s), variable(*this,s.variable), value(*this,s.value) { + x.update(*this,s.x); + } + Space* copy() override { return new ReplaySpace(*this); } + }; + + bool same_archive(const Gecode::Choice& a, const Gecode::Choice& b) { + Gecode::Archive x,y; + a.archive(x); b.archive(y); + if (x.size()!=y.size()) + return false; + for (int i=0; i root(new ReplaySpace(source,separate,multi)); + while (root->status()==SS_BRANCH) { + std::unique_ptr before(static_cast(root->clone())); + const auto source_state = source.state(); + std::unique_ptr choice(root->choice()); + Archive packed; + choice->archive(packed); + if (packed[1] != source.words()*(separate ? 2 : 1)) + return false; + const auto variable = root->variable.copy(); + const auto value = root->value.copy(); + std::vector siblings; + // Explore backwards, exercising late alternatives without earlier draws. + for (unsigned int a=choice->alternatives(); a--;) { + std::unique_ptr direct(static_cast(root->clone())); + std::unique_ptr replay(static_cast(before->clone())); + Archive archive; + choice->archive(archive); + std::unique_ptr restored(replay->choice(archive)); + direct->commit(*choice,a); + // State on the recomputed space is intentionally different before commit. + (void) replay->variable(13); + replay->commit(*restored,a); + if (direct->variable.state()!=variable.split(a).state() || + direct->value.state()!=value.split(a).state() || + replay->variable.state()!=direct->variable.state() || + replay->value.state()!=direct->value.state()) + return false; + for (const auto& previous : siblings) + if (previous==direct->variable.state()) + return false; + siblings.push_back(direct->variable.state()); + auto status = direct->status(); + if (status!=replay->status()) + return false; + if (status==SS_BRANCH) { + std::unique_ptr next(direct->choice()); + std::unique_ptr next_replay(replay->choice()); + if (!same_archive(*next,*next_replay)) + return false; + } + } + if (source.state()!=source_state || root->variable.state()!=variable.state()) + return false; + root->commit(*choice,0); + } + return true; + } + + std::vector solutions(const Gecode::Rnd& source, + unsigned int distance, bool separate, + bool multi) { + using namespace Gecode; + ReplaySpace root(source,separate,multi); + Search::Options options; + options.c_d=distance; + options.a_d=distance; + DFS search(&root,options); + std::vector result; + while (std::unique_ptr s{search.next()}) { + std::ostringstream item; + item << s->x << ':' << s->variable.state() << ':' << s->value.state(); + result.push_back(item.str()); + } + return result; + } + + class BranchReplay : public Base { + public: + BranchReplay() : Base("Random::BranchReplay", TestTags::check()) {} + bool run() override { + Gecode::Rnd engines[] = { + Gecode::Rnd(42), + Gecode::Rnd(Gecode::Support::Random(42)) + }; + for (const auto& engine : engines) + for (bool separate : {false,true}) + for (bool multi : {false,true}) { + if (!choice_replay(engine,separate,multi)) + return false; + auto cloned = solutions(engine,1,separate,multi); + auto recomputed = solutions(engine,100,separate,multi); + if (cloned.size()!=81 || cloned!=recomputed) + return false; + } + return true; + } + } branch_replay; } } From 618cfee10c7e9c6a627564e2a32899abd727ad69 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Thu, 10 Sep 2026 10:36:23 +0200 Subject: [PATCH 03/16] random: complete stream integration and compare splitting costs --- docs/random.md | 200 +++++++++++++++++++++++++++++++++++ examples/job-shop.cpp | 5 +- examples/photo.cpp | 6 +- gecode/flatzinc/flatzinc.cpp | 10 +- gecode/kernel/core.hpp | 4 + gecode/kernel/data/rnd.cpp | 24 +++-- gecode/kernel/data/rnd.hpp | 50 +++++---- gecode/search/relax.hh | 1 + gecode/support/random.hpp | 65 ++++++++++-- plans/random.md | 70 ++++++++++-- test/fault.cpp | 66 ++++++++++++ test/random.cpp | 101 +++++++++++++++--- tools/random-benchmark.cpp | 139 ++++++++++++++++++++++++ tools/random-benchmark.py | 72 +++++++++++++ 14 files changed, 752 insertions(+), 61 deletions(-) create mode 100644 docs/random.md create mode 100644 tools/random-benchmark.cpp create mode 100644 tools/random-benchmark.py diff --git a/docs/random.md b/docs/random.md new file mode 100644 index 0000000000..0f69567c07 --- /dev/null +++ b/docs/random.md @@ -0,0 +1,200 @@ +# Random generators in Gecode 7 + +Random branching uses a stream local to each space. Each recorded choice contains +the state needed to derive a different stream for each alternative. Recomputing +that choice installs the same stream as the original commit. Cloning copies state; +it does not split or consume random values. + +## Engines and full state + +`Support::Random` is a value type. Copying it makes an independent, exact +copy. It supplies bounded integer generation and state text on top of the engine. +An engine provides: + +- `State`, a nonempty `std::array` containing all mutable state and + per-stream parameters; +- construction and `seed(uint64_t)`, with a documented rule for every seed; +- `next()`, `min()`, and `max()`, with raw output in `[0,UINT64_MAX]` or + `[1,UINT64_MAX]`; +- `state()` and `state(const State&)` for exact capture and validated restoration; +- `name()`, a stable identifier for its state format and algorithm; +- `split(uint32_t) const`, returning the selected child without changing its + parent, for use in search. + +All valid alternative indices of a parent must produce distinct child states. +An engine must document that property; different first output values are neither +required nor sufficient. Copying and restoring state must reproduce subsequent +draws and splits. A state setter must reject invalid input before mutating state. +Keep engine-owned resources exception-safe; copying an engine may occur while +cloning a space. Raw pointers to external mutable state are unsuitable snapshots. + +The built-in engines are: + +| Engine | State | Splitting | +| --- | ---: | --- | +| `Support::SplitMix` | 16 bytes | Indexed sequential SplitMix splits, constant time | +| `Support::Xorshift64Star` | 8 bytes | Indexed jumps in the native recurrence | + +SplitMix uses the published two-word splittable design: a state and an odd +increment. It is the preferred default. For parent `(s,g)`, child `a` is +`(Mix13(s+(2a+1)g), mixGamma(s+(2a+2)g))` modulo 2^64. The first word is distinct +for every 32-bit index because `g` is odd and Mix13 is a permutation. This is +exactly the child obtained by sequentially splitting `a+1` times, without doing +the intervening work. + +Xorshift64* uses shifts 12, 25, and 27, with multiplier 2685821657736338717. +Child `a` starts `(a+1)*2^32` recurrence steps ahead. Since 2^32 is coprime to +the period 2^64-1, sibling states are distinct. Binary powers of the transition +matrix compute jumps in logarithmic time, with a shared 16 KiB table initialized +once. That table is not part of individual states or choices. For the maximum +32-bit index, 2^64 steps reduce to one step modulo the period. These jumps keep +the original recurrence; they are not a new seed-hashing scheme. + +Neither engine guarantees globally disjoint streams throughout an unbounded +search tree. Xorshift64* also has known statistical weaknesses in its low bits; +its smaller state and different splitting cost should be considered together. +See [Vigna's discussion](https://prng.di.unimi.it/xorshift.php) and the +[SplitMix paper](https://gee.cs.oswego.edu/dl/papers/oopsla14.pdf). + +Bounded draws use integer rejection sampling. Bounds zero, one, or negative +signed bounds return zero without consuming output. Full-range engines reject +the incomplete bucket at the bottom of the raw range; nonzero engines first +subtract one and reject the incomplete bucket at the top. Thus xorshift's missing +zero is accounted for. No distribution state is cached. The conversion and its +draw consumption are part of Gecode's reproducible sequence. + +Full state text is an identifier followed by fixed-width hexadecimal words, for +example `splitmix-v1:000000000000002a:9e3779b97f4a7c15`. Word order is explicit +and independent of host byte order. Restoration does not run seed expansion or +repair invalid states. A normal 64-bit seed is a convenience for initialization, +not a substitute for a complete snapshot. + +## Handles, binding, and callbacks + +`Rnd(seed)` creates a standalone default stream. `Rnd(Support::Random(seed))` +creates a user-defined one. Copying a standalone `Rnd` shares its stream; +`r.copy()` creates an independent exact copy, and `r.split(a)` creates a child. +Concurrent draws through shared standalone handles require caller synchronization. + +`Rnd(home, source)` binds a stream to a space. Binding the same stream again +returns the same local stream. Different initialization handles remain different +streams even when their initial states are equal. Binding retains stream identity +across clones, so callbacks can resolve an ancestor's handle in their own space. +Reseeding an external initialization handle after binding does not reseed the +space-local copy; reseed the bound handle explicitly if that is intended. + +Built-in random selectors bind during posting and remap during cloning. A model +that retains a bound handle should likewise use `rnd(*this, s.rnd)` in its copy +constructor. User callbacks that capture a handle must resolve it through the +space passed to the callback rather than draw directly from the captured handle: + +```cpp +Rnd source(*this, 42); +branch(*this, x, INT_VAR_NONE(), + INT_VAL([source](const Space& home, IntVar v, int) { + Rnd local(home, source); + unsigned int offset = local(v.size()); + IntVarValues values(v); + while (offset--) ++values; + return values.val(); + })); +``` + +The const-space overload only looks up an already bound stream. Bind streams +before taking choices or clones that will replay their use. In particular, a +choice-selection callback must not first register a new stream that is absent +from an earlier clone. Dynamically posted branchers can reuse existing streams; +new streams introduced during commit must be introduced consistently on replay. +An explicitly new stream starts at its specified initialization point; it does +not retrospectively consume earlier alternatives. + +Each choice snapshots all bound streams, including streams belonging to later +branchers. Therefore a deterministic branch before a random branch still gives +the later branch different states for its alternatives. Multiple selectors sharing +one stream incur only one snapshot. Custom branchers participate through +`Space::choice()`, `Space::commit()`/`trycommit()`, and the base `Choice::archive()`; +their choice payload still contains their usual position/value data. Choices own +their snapshots and cannot be copied by C++ copy construction; use archiving when +an independent choice representation is needed. + +## Restarts, portfolios, and reproducibility limits + +Ordinary space cloning preserves stream states exactly. Generic meta-engines do +not add implicit RNG splits when creating clones. Models can explicitly call +`random_split(index)` in their `slave()` callback to derive all bound streams +from a logical restart or asset index. The callback must follow a fixed policy, +independent of which worker happens to execute it. + +FlatZinc uses a fixed three-step policy: split by 0 for restart or 1 for portfolio, +then by the high and low 32-bit words of the logical index. The Photo example +uses the high and low restart-index words before relaxation. The common relaxation +helper binds its input stream to its destination space, avoiding shared draws +between sibling spaces. + +The guarantee is the RNG state for a recorded path, not identical scheduling or +solution order in parallel search. Adaptive heuristics, restart constraints, and +weakly monotonic propagation can still change the search tree. Reproducing a +whole run also requires the same model, relevant options, and compatible Gecode +code and random algorithm. Standard-library distributions are outside Gecode's +bounded-sequence contract. + +## Test failure replay + +The test runner records complete state immediately before each iteration. On a +failure or exception it prints arguments using `-state`, `-iter 1`, and +`-test-exact`. Run the same test executable with those arguments. This restores +the iteration directly, bypassing suite seed derivation. A state replay requires +one thread and rejects an accompanying `-seed` or an incompatible state format. + +## Measurements + +Build the benchmark against the candidate using the same release compiler flags +as Gecode (adjust library search paths for your platform): + +```sh +clang++ -O3 -DNDEBUG -std=c++17 -DRANDOM_NEW -Ibuild/random -I. \ + tools/random-benchmark.cpp -Lbuild/random -lgecodeint -lgecodesearch \ + -lgecodekernel -lgecodesupport -Wl,-rpath,build/random \ + -o build/random/random-benchmark +python3 tools/random-benchmark.py build/random/random-benchmark \ + --baseline /path/to/baseline/random-benchmark --repeat 5 --output results.json +``` + +For the baseline, compile the same benchmark source against baseline headers and +libraries without `-DRANDOM_NEW`. The script alternates execution order, discards +one warmup, and reports medians while retaining individual measurements in JSON. +Use a new output filename for another run. Avoid concurrent compilation or other +heavy work while collecting timings. + +The controlled search enumerates a complete binary tree of 32767 nodes and 16384 +solutions for every engine and seed, so timings compare overhead without changes +in tree size. The queens case also reports node count: changed random choices can +change its search tree, and wall time alone is not an overhead comparison. Raw +baseline draws emit 32-bit words while the new engines emit 64-bit words. Raw +throughput can also benefit from compiler optimizations that do not apply inside +branchers; the search measurements matter more for the default decision. + +On arm64 macOS 26.6.2 with Apple Clang 21, baseline 6b7de57b04, and seed 42, +five measured repetitions after one warmup gave these medians: + +| Measurement | Baseline | SplitMix with recorded streams | +| --- | ---: | ---: | +| Bounded draw through Rnd | 14.85 ns | 2.96 ns | +| Clone a random space | 178.70 ns | 345.95 ns | +| Binary-tree node, frequent cloning | 153.07 ns | 254.32 ns | +| Binary-tree node, recomputation | 226.76 ns | 368.53 ns | +| Enumerate 10-queens | 20.04 ms | 20.96 ms | + +The controlled random tree is about 1.6x slower: local copying and recorded state +have a cost that faster drawing does not eliminate. Nonrandom tree overhead was +1–3%. Seeds 1 and 1337 gave similar results; queens time increased about 4–5%, +with node counts within 0.1% of baseline. These measurements do not establish a +general speedup or statistical confidence beyond this machine and these cases. + +A binary SplitMix split followed by a draw measured about 15.7 ns; xorshift's +native jump followed by a draw measured about 237 ns. This favors SplitMix as the +default while retaining xorshift for its 8-byte state. The default integer choice +occupies 32 bytes plus a 24-byte snapshot allocation, compared with 24 bytes on +baseline. Archives occupy 32 rather than 12 bytes. No snapshot is allocated for +a nonrandom choice, although its optional pointer costs 8 bytes. Allocator +overhead is additional to these logical sizes. diff --git a/examples/job-shop.cpp b/examples/job-shop.cpp index fcfe95f182..833fba8e06 100755 --- a/examples/job-shop.cpp +++ b/examples/job-shop.cpp @@ -536,7 +536,7 @@ class JobShopSolve : public JobShopBase { JobShopSolve(const JobShopOptions& o) : JobShopBase(o), sorder(*this, spec.machines()*spec.jobs()*(spec.jobs()-1)/2, 0, 1), - rnd(o.seed()) { + rnd(*this,o.seed()) { if (opt.propagation() == PROP_UNARY) nooverload(); @@ -636,7 +636,7 @@ class JobShopSolve : public JobShopBase { JobShopSolve(JobShopSolve& s) : JobShopBase(s), sorder(s.sorder), fst(s.fst), snd(s.snd), iafc(s.iafc), iaction(s.iaction), baction(s.baction), - ichb(s.ichb), bchb(s.bchb), rnd(s.rnd) {} + ichb(s.ichb), bchb(s.bchb), rnd(*this,s.rnd) {} /// Copy during cloning virtual Space* copy(void) { @@ -904,4 +904,3 @@ main(int argc, char* argv[]) { #include "examples/job-shop-instances.hpp" // STATISTICS: example-any - diff --git a/examples/photo.cpp b/examples/photo.cpp index ed264158c5..7d92baa9be 100644 --- a/examples/photo.cpp +++ b/examples/photo.cpp @@ -99,7 +99,7 @@ class Photo : public IntMinimizeScript { spec(opt.size()), pos(*this,spec.people(), 0, spec.people()-1), violations(*this,0,spec.preferences()), - rnd(opt.seed()), p(opt.relax()) + rnd(*this,opt.seed()), p(opt.relax()) { // Map preferences to violation BoolVarArgs viol(spec.preferences()); @@ -132,6 +132,8 @@ class Photo : public IntMinimizeScript { bool slave(const MetaInfo& mi) { if ((mi.type() == MetaInfo::RESTART) && (mi.restart() > 0) && (p > 0.0)) { + random_split(static_cast(uint64_t(mi.restart())>>32)); + random_split(static_cast(mi.restart())); const Photo& l = static_cast(*mi.last()); relax(*this, pos, l.pos, rnd, p); return false; @@ -141,7 +143,7 @@ class Photo : public IntMinimizeScript { } /// Constructor for cloning \a s Photo(Photo& s) : - IntMinimizeScript(s), spec(s.spec), rnd(s.rnd), p(s.p) { + IntMinimizeScript(s), spec(s.spec), rnd(*this,s.rnd), p(s.p) { pos.update(*this, s.pos); violations.update(*this, s.violations); } diff --git a/gecode/flatzinc/flatzinc.cpp b/gecode/flatzinc/flatzinc.cpp index 0d525dd1f0..92b0e0a230 100644 --- a/gecode/flatzinc/flatzinc.cpp +++ b/gecode/flatzinc/flatzinc.cpp @@ -780,7 +780,7 @@ namespace Gecode { namespace FlatZinc { FlatZincSpace::FlatZincSpace(FlatZincSpace& f) : Space(f), - _initData(nullptr), _random(f._random), + _initData(nullptr), _random(*this,f._random), _solveAnnotations(nullptr), restart_data(f.restart_data), iv_boolalias(nullptr), @@ -864,7 +864,7 @@ namespace Gecode { namespace FlatZinc { : _initData(new FlatZincSpaceInitData), intVarCount(-1), boolVarCount(-1), floatVarCount(-1), setVarCount(-1), _optVar(-1), _optVarIsInt(true), _lns(0), _lnsInitialSolution(0), - _random(random), + _random(*this,random), _solveAnnotations(nullptr), needAuxVars(true) { branchInfo.init(); } @@ -2080,6 +2080,12 @@ namespace Gecode { namespace FlatZinc { bool FlatZincSpace::slave(const MetaInfo& mi) { + // Meta-engine clones start from the master's state. Derive their streams + // from logical restart/asset indices, never from worker scheduling. + uint64_t index = mi.type()==MetaInfo::RESTART ? mi.restart() : mi.asset(); + random_split(mi.type()==MetaInfo::RESTART ? 0 : 1); + random_split(static_cast(index>>32)); + random_split(static_cast(index)); if (mi.type() == MetaInfo::RESTART) { if (restart_data.initialized() && restart_data().mark_complete) { // Fail the space diff --git a/gecode/kernel/core.hpp b/gecode/kernel/core.hpp index 8fbdc95403..b422af5e10 100755 --- a/gecode/kernel/core.hpp +++ b/gecode/kernel/core.hpp @@ -2058,6 +2058,10 @@ namespace Gecode { public: /// Bind a random handle to this space, mapping local handles during cloning GECODE_KERNEL_EXPORT Rnd random(const Rnd& source); + /// Access an already bound stream, for callbacks receiving a const space + GECODE_KERNEL_EXPORT Rnd random(const Rnd& source) const; + /// Explicitly split all bound streams, for restart/portfolio initialization + GECODE_KERNEL_EXPORT void random_split(uint32_t alternative); /** * \brief Suppress global propagator information during scoped posting * \ingroup TaskModelScript diff --git a/gecode/kernel/data/rnd.cpp b/gecode/kernel/data/rnd.cpp index 3be7cc82b2..ef26d92a92 100644 --- a/gecode/kernel/data/rnd.cpp +++ b/gecode/kernel/data/rnd.cpp @@ -38,6 +38,8 @@ namespace Gecode { Rnd::Rnd(Space& home, const Rnd& source) : SharedHandle(home.random(source)) {} + Rnd::Rnd(const Space& home, const Rnd& source) + : SharedHandle(home.random(source)) {} void Rnd::seed(uint64_t value) { if (!object()) @@ -67,16 +69,26 @@ namespace Gecode { Rnd Space::random(const Rnd& source) { if (!source) throw UninitializedRnd("Space::random"); - // During actor/model copying, map source-local handles by their position. - if (is_partial_clone() && pc.c.source && pc.c.source->randoms) { - size_t i = pc.c.source->randoms->find(source); - if (i < pc.c.source->randoms->size()) - return randoms->at(i); - } if (!randoms) randoms = new RandomContext; return randoms->bind(source); } + + Rnd Space::random(const Rnd& source) const { + if (randoms) { + size_t i = randoms->find(source); + if (i < randoms->size()) + return randoms->at(i); + } + throw UninitializedRnd("Space::random: stream is not bound"); + } + + void Space::random_split(uint32_t alternative) { + if (randoms) { + std::unique_ptr snapshot(randoms->snapshot()); + randoms->commit(snapshot.get(),alternative); + } + } } // STATISTICS: kernel-other diff --git a/gecode/kernel/data/rnd.hpp b/gecode/kernel/data/rnd.hpp index f8f3b5c71b..1b17d11aa3 100755 --- a/gecode/kernel/data/rnd.hpp +++ b/gecode/kernel/data/rnd.hpp @@ -51,8 +51,18 @@ namespace Gecode { class Rnd : public SharedHandle { friend class RandomContext; private: + class Origin : public SharedHandle { + public: + Origin(void) = default; + explicit Origin(Object* value) : SharedHandle(value) {} + const Object* get(void) const { return object(); } + }; class IMP : public SharedHandle::Object { public: + Origin origin; + const Object* identity(void) const { + return origin ? origin.get() : this; + } virtual IMP* copy(void) const = 0; virtual IMP* split(uint32_t a) const = 0; virtual void seed(uint64_t value) = 0; @@ -99,6 +109,11 @@ namespace Gecode { return *static_cast(object()); } Rnd(IMP* value, bool) : SharedHandle(value) {} + Rnd local_copy(void) const { + Rnd result(imp().copy(),true); + result.imp().origin = imp().origin ? imp().origin : Origin(&imp()); + return result; + } public: /// Uninitialized handle Rnd(void) = default; @@ -115,6 +130,8 @@ namespace Gecode { : SharedHandle(new Implementation(r)) {} /// Bind a source stream to a space, or update its handle during cloning GECODE_KERNEL_EXPORT Rnd(Space& home, const Rnd& source); + /// Access an already bound stream through a const space + GECODE_KERNEL_EXPORT Rnd(const Space& home, const Rnd& source); /// Create and bind a default stream to a space Rnd(Space& home, uint64_t seed) : Rnd(home,Rnd(seed)) {} /// Make an independent exact copy; does not split or advance the source @@ -143,47 +160,42 @@ namespace Gecode { } }; - /// Internal space-local stream storage. Entries keep initialization handles alive. + /// Internal space-local stream storage. Local handles retain their stream origin. class RandomContext { - struct Entry { - Rnd source; - Rnd local; - }; - std::vector streams; + std::vector streams; public: RandomContext(void) = default; RandomContext(const RandomContext& other) { streams.reserve(other.streams.size()); for (const auto& entry : other.streams) - streams.push_back({entry.source,entry.local.copy()}); + streams.push_back(entry.local_copy()); } size_t find(const Rnd& source) const { for (size_t i=0; i(n+1); data[0]=n; size_t pos=1; for (const auto& entry : streams) { - entry.local.imp().save(data.get()+pos); - pos += entry.local.words(); + entry.imp().save(data.get()+pos); + pos += entry.words(); } return data.release(); } @@ -191,13 +203,13 @@ namespace Gecode { void commit(const uint64_t* data, uint32_t a) { size_t n=0; for (const auto& entry : streams) - n += entry.local.words(); + n += entry.words(); if (data[0] != n) throw std::invalid_argument("Random choice does not match space streams"); size_t pos=1; for (auto& entry : streams) { - entry.local.imp().commit(data+pos,a); - pos += entry.local.words(); + entry.imp().commit(data+pos,a); + pos += entry.words(); } } }; diff --git a/gecode/search/relax.hh b/gecode/search/relax.hh index 5e8b1aee60..dcd5bb9f36 100755 --- a/gecode/search/relax.hh +++ b/gecode/search/relax.hh @@ -52,6 +52,7 @@ namespace Gecode { namespace Search { double p, Post& post) { if (home.failed()) return; + r = Rnd(static_cast(home),r); Region reg; // Which variables to assign Support::BitSet ax(reg, static_cast(x.size())); diff --git a/gecode/support/random.hpp b/gecode/support/random.hpp index 1d087a4ae3..65129ad23f 100755 --- a/gecode/support/random.hpp +++ b/gecode/support/random.hpp @@ -232,9 +232,17 @@ namespace Gecode { namespace Support { z = (z ^ (z >> 33)) * UINT64_C(0xff51afd7ed558ccd); z = (z ^ (z >> 33)) * UINT64_C(0xc4ceb9fe1a85ec53); z = (z ^ (z >> 33)) | 1; - unsigned int n = 0; - for (uint64_t bits = z ^ (z >> 1); bits; bits &= bits-1) - ++n; + uint64_t bits = z ^ (z >> 1); +#ifdef GECODE_HAS_BUILTIN_POPCOUNTLL + unsigned int n = __builtin_popcountll(bits); +#else + bits -= (bits >> 1) & UINT64_C(0x5555555555555555); + bits = (bits & UINT64_C(0x3333333333333333)) + + ((bits >> 2) & UINT64_C(0x3333333333333333)); + bits = (bits + (bits >> 4)) & UINT64_C(0x0f0f0f0f0f0f0f0f); + unsigned int n = static_cast + ((bits * UINT64_C(0x0101010101010101)) >> 56); +#endif return (n < 24) ? z ^ UINT64_C(0xaaaaaaaaaaaaaaaa) : z; } public: @@ -260,10 +268,12 @@ namespace Gecode { namespace Support { } }; - /** \brief One-word xorshift64* engine for standalone use + /** \brief One-word xorshift64* engine with indexed jump splitting * * Uses shifts 12, 25, 27 and Vigna's multiplier. Zero seeds map to one; - * restoring zero state is an error. This engine does not provide splitting. + * restoring zero state is an error. Alternative a jumps (a+1)*2^32 steps + * along the native recurrence. Sibling states are distinct since 2^32 is + * coprime to the period 2^64-1. Jump matrices are shared, not per-stream state. * \ingroup FuncSupport */ class Xorshift64Star { @@ -271,6 +281,24 @@ namespace Gecode { namespace Support { using State = std::array; private: uint64_t s; + using Matrix = std::array; + static uint64_t transition(uint64_t x) { + x ^= x >> 12; + x ^= x << 25; + return x ^ (x >> 27); + } + static uint64_t apply(const Matrix& m, uint64_t x) { + uint64_t result=0; + for (unsigned int i=0; x; ++i,x>>=1) + if (x & 1) result ^= m[i]; + return result; + } + static Matrix square(const Matrix& m) { + Matrix result; + for (unsigned int i=0; i<64; ++i) + result[i]=apply(m,m[i]); + return result; + } public: explicit Xorshift64Star(uint64_t value=1) { seed(value); } static const char* name(void) { return "xorshift64star-v1"; } @@ -284,11 +312,32 @@ namespace Gecode { namespace Support { s = value[0]; } uint64_t next(void) { - s ^= s >> 12; - s ^= s << 25; - s ^= s >> 27; + s = transition(s); return s * UINT64_C(2685821657736338717); } + Xorshift64Star split(uint32_t alternative) const { + // Binary powers of the linear transition, starting at T^(2^32). + static const std::array powers = [] { + Matrix m; + for (unsigned int i=0; i<64; ++i) + m[i]=transition(uint64_t(1)< p; + p[0]=m; + for (unsigned int i=1; i<32; ++i) p[i]=square(p[i-1]); + return p; + }(); + Xorshift64Star child=*this; + if (alternative==UINT32_MAX) { + // 2^64 steps equal one step modulo the period 2^64-1. + child.s=transition(s); + } else { + uint32_t steps=alternative+1; + for (unsigned int i=0; steps; ++i,steps>>=1) + if (steps & 1) child.s=apply(powers[i],child.s); + } + return child; + } }; /** \brief Value-type generator with reproducible bounded draws and state diff --git a/plans/random.md b/plans/random.md index 752e19979b..4b8a25bbcb 100644 --- a/plans/random.md +++ b/plans/random.md @@ -1,7 +1,7 @@ # Plan: Compact, splittable random generators for Gecode 7 > Source: the feature/random design discussion, 2026-09-10. -> Status: Phases 1 and 2 complete; Phase 3 next. +> Status: Phases 1–3 complete; Phase 4 next. > Workflow: review, update this plan, and commit after each phase. ## Goal @@ -315,22 +315,76 @@ callbacks that use randomness outside ordinary choice selection. ### Acceptance criteria -- [ ] Focused integration cases cover binary, multiway, and one-alternative +- [x] Focused integration cases cover binary, multiway, and one-alternative branching, plus handover to later branchers and custom-engine use. -- [ ] Alternative identity follows the public choice index, including when a +- [x] Alternative identity follows the public choice index, including when a brancher reverses the mapping from alternative index to selected value. -- [ ] Clone, archive, disposal, failure, traced commit, and conditional commit +- [x] Clone, archive, disposal, failure, traced commit, and conditional commit paths preserve the contract without shared mutable state between spaces. -- [ ] Sequential and parallel replay of the same recorded path agree on random +- [x] Sequential and parallel replay of the same recorded path agree on random state; checks do not require identical parallel solution order. -- [ ] Restart and portfolio initialization policies are documented and do not +- [x] Restart and portfolio initialization policies are documented and do not accidentally introduce worker-scheduling-dependent shared streams. -- [ ] Existing relevant branching and search correctness tests pass. Add tests +- [x] Existing relevant branching and search correctness tests pass. Add tests for the new semantic guarantees rather than duplicating each API wrapper. -- [ ] Random generation, bounded draws, splitting, clone cost, and retained-path +- [x] Random generation, bounded draws, splitting, clone cost, and retained-path memory are measured in representative Gecode workloads. Use controlled paths to separate overhead from changes in the randomized search tree. +### Phase 3 review + +Added const-space stream lookup for callbacks and retained stream identity across +ancestor handles. Review showed that mapping only the immediate source space +was insufficient for a callback capturing an already bound ancestor handle. +The collection now holds one handle per stream; each local implementation keeps +its initialization origin alive. Mutable states remain independent across spaces. +The net per-stream collection/implementation storage is unchanged from Phase 2; +the default standalone implementation gains an 8-byte origin handle. + +FlatZinc binds its restart/relaxation handle to the space and derives meta-engine +streams from fixed restart/portfolio indices. The shared relaxation helper and +the Photo and JobShop examples now use local handles. Generic cloning still does +not split; models have an explicit `random_split()` operation for their own +meta-engine policy. Callback, registration, and replay boundaries are documented +in `docs/random.md`. + +Xorshift64* is now also a search-capable alternative: indexed splitting jumps +`(a+1)*2^32` native recurrence steps using shared transition matrices. This keeps +8-byte engine state, costs a shared 16 KiB table, and preserves the original +generator rather than inventing a new seeding construction. Composition and +period-wrap checks supplement its raw vector and full-state replay checks. +Its sibling-state distinction follows from coprimality with the native period. + +Validation passes for three engines (SplitMix, xorshift64*, and an external +three-word engine), shared/separate streams, binary/multiway choices, dynamic +posting through a one-alternative custom brancher, const callbacks, and sequential +versus two-worker search. Complete solution/state sets agree in the parallel +fixture without assuming solution order. Traced and conditional commits, failed +spaces, and illegal alternatives are checked explicitly. A dedicated fault test +counts live engine instances across allocation and brancher-copy failures and +checks that source states survive. Existing Boolean, set, float, FlatZinc restart, +and the full `check` selections also pass. + +Release measurements use Apple Clang 21, arm64 macOS 26.6.2, baseline commit +6b7de57b04, one warmup, five repetitions for seed 42, and three repetitions each +for seeds 1 and 1337. Raw records are in `build/random/phase3-benchmark*.json`; +the reusable harness and summary are documented in `docs/random.md`. +Representative seed-42 medians: bounded Rnd draws 14.85 -> 2.96 ns; random-space +clones 178.70 -> 345.95 ns; controlled random binary-tree nodes 153.07 -> 254.32 ns +with frequent cloning and 226.76 -> 368.53 ns with recomputation. Nonrandom tree +overhead was 1–3%. Queens wall time rose about 4–5% across the three seeds, with +similar but not identical node counts. These are local measurements, not a +cross-platform performance guarantee. + +The cost of independent streams and recorded state is real: the smallest random +tree is about 1.6x slower, despite much faster individual draws. We retain the +simple optional packed snapshot representation rather than add a second compact +choice hierarchy to hide that cost. SplitMix is the preferred default: binary +split-plus-draw measured about 15.7 ns, versus 237 ns for xorshift's jump-plus-draw. +The latter remains the 8-byte-state alternative. The popcount implementation was +improved after measurement without changing its bit sequence. Phase 4 will make +the executable's default configurable and verify both configurations. + ## Phase 4: Configured command lines and Gecode 7 migration **User requirements:** 1, 2, 5, 6. diff --git a/test/fault.cpp b/test/fault.cpp index fb4a4847a6..4760de5dc8 100644 --- a/test/fault.cpp +++ b/test/fault.cpp @@ -1114,6 +1114,72 @@ namespace Test { namespace Fault { } }; + // Count engine instances so failed clones cannot hide leaked stream handles. + class LiveRandom : public Support::SplitMix { + public: + static int live; + explicit LiveRandom(uint64_t seed=1) : SplitMix(seed) { ++live; } + LiveRandom(const LiveRandom& r) : SplitMix(r) { ++live; } + explicit LiveRandom(const SplitMix& r) : SplitMix(r) { ++live; } + ~LiveRandom() { --live; } + LiveRandom split(uint32_t a) const { return LiveRandom(SplitMix::split(a)); } + }; + int LiveRandom::live=0; + + class RandomSpace : public Space { + public: + IntVarArray x; + Rnd r; + RandomSpace() : x(*this,3,0,2), + r(*this,Rnd(Support::Random(7))) { + branch(*this,x,INT_VAR_RND(r),INT_VAL_RND(r)); + ThrowingBrancher::post(*this); + } + RandomSpace(RandomSpace& s) : Space(s), r(*this,s.r) { + x.update(*this,s.x); + } + Space* copy() override { return new RandomSpace(*this); } + }; + + class RandomCloneFailures : public Base { + public: + RandomCloneFailures() : Base("Fault::Random::CloneFailures") {} + bool run() override { + FaultScope scope; + { + RandomSpace root; + if (root.status()!=SS_BRANCH) + return false; + const auto state=root.r.state(); + const int live=LiveRandom::live; + bool succeeded=false; + for (unsigned int n=0; n<128 && !succeeded; ++n) { + Support::FailPoint::fail_after(Phase::Heap,n); + try { + std::unique_ptr copy(root.clone()); + succeeded=true; + } catch (const MemoryExhausted&) {} + Support::FailPoint::reset(); + if (LiveRandom::live!=live || root.r.state()!=state) + return false; + } + if (!succeeded) + return false; + // Fail after a random brancher and its stream handles have been copied. + Support::FailPoint::fail_after(Phase::BrancherCopy,0); + try { + std::unique_ptr copy(root.clone()); + return false; + } catch (const MemoryExhausted&) {} + Support::FailPoint::reset(); + if (LiveRandom::live!=live || root.r.state()!=state) + return false; + std::unique_ptr copy(root.clone()); + } + return LiveRandom::live==0; + } + } random_clone_failures; + BranchActionHeapFailures branch_action_heap_failures; BranchChbHeapFailures branch_chb_heap_failures; CloneDisposalArray clone_disposal_array; diff --git a/test/random.cpp b/test/random.cpp index cd304f6ae7..60ca029965 100644 --- a/test/random.cpp +++ b/test/random.cpp @@ -70,7 +70,14 @@ namespace Test { Xorshift64Star xs(1); if (xs.next() != UINT64_C(0x47e4ce4b896cdd1d)) return false; - if (!replay() || !replay()) + if (!replay() || !replay() || + !replay()) + return false; + if (xs.split(0).split(0).state()!=xs.split(1).state()) + return false; + auto one_step=xs; + (void) one_step.next(); + if (xs.split(UINT32_MAX).state()!=one_step.state()) return false; auto parent = r.state(); // Indexing skips pairs of parent words, exactly as sequential splits. @@ -119,7 +126,8 @@ namespace Test { public: Gecode::IntVarArray x; Gecode::Rnd variable, value; - ReplaySpace(const Gecode::Rnd& source, bool separate, bool multi=false) + ReplaySpace(const Gecode::Rnd& source, bool separate, bool multi=false, + bool callback=false) : x(*this,4,0,2), variable(*this,source), value(*this,separate ? source.copy() : source) { using namespace Gecode; @@ -128,7 +136,22 @@ namespace Test { IntVarArgs first(2); first[0]=x[1]; first[1]=x[2]; branch(*this,first,INT_VAR_RND(variable),INT_VAL_RND(value)); - branch(*this,x[3],INT_VAL_RND(value)); + if (callback) { + // A custom one-alternative branch posts a later random brancher. + // The callback resolves the stream through its own space each time. + branch(*this,[source=variable](Space& home) { + auto& self = static_cast(home); + branch(home,self.x[3],INT_VAL([source](const Space& h, IntVar v, int) { + Rnd local(h,source); + unsigned int p=local(v.size()); + IntVarValues values(v); + while (p--) ++values; + return values.val(); + })); + }); + } else { + branch(*this,x[3],INT_VAL_RND(value)); + } } ReplaySpace(ReplaySpace& s) : Space(s), variable(*this,s.variable), value(*this,s.value) { @@ -148,9 +171,10 @@ namespace Test { return true; } - bool choice_replay(const Gecode::Rnd& source, bool separate, bool multi) { + bool choice_replay(const Gecode::Rnd& source, bool separate, bool multi, + bool callback) { using namespace Gecode; - std::unique_ptr root(new ReplaySpace(source,separate,multi)); + std::unique_ptr root(new ReplaySpace(source,separate,multi,callback)); while (root->status()==SS_BRANCH) { std::unique_ptr before(static_cast(root->clone())); const auto source_state = source.state(); @@ -201,12 +225,14 @@ namespace Test { std::vector solutions(const Gecode::Rnd& source, unsigned int distance, bool separate, - bool multi) { + bool multi, bool callback, + unsigned int threads=1) { using namespace Gecode; - ReplaySpace root(source,separate,multi); + ReplaySpace root(source,separate,multi,callback); Search::Options options; options.c_d=distance; options.a_d=distance; + options.threads=threads; DFS search(&root,options); std::vector result; while (std::unique_ptr s{search.next()}) { @@ -223,20 +249,69 @@ namespace Test { bool run() override { Gecode::Rnd engines[] = { Gecode::Rnd(42), + Gecode::Rnd(Gecode::Support::Random(42)), Gecode::Rnd(Gecode::Support::Random(42)) }; for (const auto& engine : engines) for (bool separate : {false,true}) for (bool multi : {false,true}) { - if (!choice_replay(engine,separate,multi)) - return false; - auto cloned = solutions(engine,1,separate,multi); - auto recomputed = solutions(engine,100,separate,multi); - if (cloned.size()!=81 || cloned!=recomputed) - return false; + for (bool callback : {false,true}) { + if (!choice_replay(engine,separate,multi,callback)) + return false; + auto cloned = solutions(engine,1,separate,multi,callback); + auto recomputed = solutions(engine,100,separate,multi,callback); + if (cloned.size()!=81 || cloned!=recomputed) + return false; + auto parallel = solutions(engine,100,separate,multi,callback,2); + std::sort(cloned.begin(),cloned.end()); + std::sort(parallel.begin(),parallel.end()); + if (cloned!=parallel) + return false; + } } return true; } } branch_replay; + + class StateTracer : public Gecode::Tracer { + public: + std::string observed; + void propagate(const Gecode::Space&, const Gecode::PropagateTraceInfo&) override {} + void post(const Gecode::Space&, const Gecode::PostTraceInfo&) override {} + void commit(const Gecode::Space& home, const Gecode::CommitTraceInfo&) override { + observed=static_cast(home).variable.state(); + } + }; + + class CommitBoundary : public Base { + public: + CommitBoundary() : Base("Random::CommitBoundary", TestTags::check()) {} + bool run() override { + using namespace Gecode; + StateTracer tracer; + ReplaySpace root(Rnd(7),false); + trace(root,TE_COMMIT,tracer); + root.status(); + std::unique_ptr choice(root.choice()); + auto expected=root.variable.split(1).state(); + std::unique_ptr copy(static_cast(root.clone())); + copy->trycommit(*choice,1); + if (copy->variable.state()!=expected || tracer.observed!=expected) + return false; + std::unique_ptr skipped(static_cast(root.clone())); + BrancherGroup::all.kill(*skipped); + auto before=skipped->variable.state(); + skipped->trycommit(*choice,1); + if (skipped->variable.state()!=before) + return false; + skipped->fail(); + skipped->commit(*choice,1); + if (skipped->variable.state()!=before) + return false; + try { root.commit(*choice,choice->alternatives()); return false; } + catch (const SpaceIllegalAlternative&) {} + return root.variable.state()==before; + } + } commit_boundary; } } diff --git a/tools/random-benchmark.cpp b/tools/random-benchmark.cpp new file mode 100644 index 0000000000..aefeefeb38 --- /dev/null +++ b/tools/random-benchmark.cpp @@ -0,0 +1,139 @@ +// Copyright (c) 2026 Mikael Zayenz Lagerkvist. MIT license; see LICENSE. +// Compile against main without RANDOM_NEW, or against feature/random with it. +#include +#include +#include +#include +#include + +using namespace Gecode; +using Clock = std::chrono::steady_clock; + +template +void measure(const char* name, uint64_t operations, F work) { + auto start=Clock::now(); + uint64_t checksum=work(); + double ns=std::chrono::duration(Clock::now()-start).count(); + std::cout << name << '\t' << ns/operations << "\tns/op\t" << checksum << '\n'; +} + +class Model : public Space { +public: + IntVarArray x; + Model(unsigned int n, unsigned int seed, bool queens, bool random) + : x(*this,n,0,queens ? n-1 : 1) { + if (queens) { + distinct(*this,x,IPL_DOM); + IntArgs up(n),down(n); + for (unsigned int i=0; i1 ? std::stoull(argv[1]) : 1000000; + const unsigned int seed=argc>2 ? std::stoul(argv[2]) : 42; + if (!draws) return 1; + std::cout << "size.engine\t" << sizeof(Support::RandomGenerator) << "\tbytes\t0\n" + << "size.space\t" << sizeof(Space) << "\tbytes\t0\n" + << "size.choice\t" << sizeof(PosValChoice) << "\tbytes\t0\n"; + Support::RandomGenerator raw(seed); + measure("raw.default",draws,[&] { + uint64_t sum=0; + for (uint64_t i=0; i xs(seed); + measure("raw.xorshift64star",draws,[&] { + uint64_t sum=0; + for (uint64_t i=0; i choice(root.choice()); + Archive archive; + choice->archive(archive); + std::cout << (random ? "size.random_archive" : "size.plain_archive") + << '\t' << archive.size()*sizeof(unsigned int) << "\tbytes\t0\n"; +#ifdef RANDOM_NEW + std::cout << (random ? "size.random_snapshot" : "size.plain_snapshot") + << '\t' << (archive[1] ? (archive[1]+1)*sizeof(uint64_t) : 0) + << "\tbytes\t0\n"; +#endif + measure(random ? "clone.random" : "clone.plain",10000,[&] { + uint64_t sum=0; + for (int i=0; i<10000; ++i) { + std::unique_ptr copy(root.clone()); + sum += copy->status(); + } + return sum; + }); + for (unsigned int distance : {1U,16U}) { + Search::Options options; + options.c_d=distance; + options.a_d=distance; + const char* name=random ? (distance==1 ? "tree.random.clone" : "tree.random.recompute") + : (distance==1 ? "tree.plain.clone" : "tree.plain.recompute"); + // Complete binary tree: exactly 32767 nodes for every engine/seed. + measure(name,32767,[&] { + DFS search(&root,options); + uint64_t solutions=0; + while (std::unique_ptr s{search.next()}) ++solutions; + if (solutions!=16384 || search.statistics().node!=32767) + throw std::runtime_error("Controlled tree changed"); + return solutions; + }); + } + } + Model queens(10,seed,true,true); + Search::Options options; + uint64_t nodes=0; + measure("queens.random",1,[&] { + DFS search(&queens,options); + uint64_t solutions=0; + while (std::unique_ptr s{search.next()}) ++solutions; + nodes=search.statistics().node; + if (solutions!=724) throw std::runtime_error("Queens solutions changed"); + return solutions; + }); + std::cout << "queens.nodes\t" << nodes << "\tnodes\t0\n"; +} diff --git a/tools/random-benchmark.py b/tools/random-benchmark.py new file mode 100644 index 0000000000..96c9514b2a --- /dev/null +++ b/tools/random-benchmark.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +"""Compare compiled random-benchmark executables with bounded repeated runs. + +Example: python3 tools/random-benchmark.py build/random/random-benchmark \ + --baseline /tmp/baseline/random-benchmark --repeat 5 --output results.json +Build instructions and the controls are recorded in docs/random.md. +""" +import argparse +import json +import platform +import statistics +import subprocess +from pathlib import Path + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("candidate", type=Path) + parser.add_argument("--baseline", type=Path) + parser.add_argument("--repeat", type=int, default=5) + parser.add_argument("--draws", type=int, default=1_000_000) + parser.add_argument("--seed", type=int, default=42) + parser.add_argument("--output", type=Path) + args = parser.parse_args() + if args.repeat < 1 or args.draws < 1: + parser.error("repeat and draws must be positive") + if not 0 <= args.seed <= 0xffffffff: + parser.error("the baseline comparison requires a 32-bit unsigned seed") + binaries = {"candidate": args.candidate.resolve()} + if args.baseline: + binaries["baseline"] = args.baseline.resolve() + records = {name: [] for name in binaries} + for iteration in range(args.repeat + 1): + # Reverse order on alternating repetitions; first repetition is warmup. + names = list(binaries) + if iteration % 2: + names.reverse() + for name in names: + run = subprocess.run( + [str(binaries[name]), str(args.draws), str(args.seed)], + capture_output=True, text=True, timeout=120, + ) + if run.returncode: + parser.exit(1, f"{binaries[name]} failed:\n{run.stdout}\n{run.stderr}") + rows = {} + for line in run.stdout.splitlines(): + case, value, unit, checksum = line.split("\t") + rows[case] = {"value": float(value), "unit": unit, "checksum": checksum} + if iteration: + records[name].append(rows) + medians = { + name: {case: statistics.median(run[case]["value"] for run in runs) + for case in runs[0]} + for name, runs in records.items() + } + for case, value in medians["candidate"].items(): + unit = records["candidate"][0][case]["unit"] + previous = medians.get("baseline", {}).get(case) + comparison = f" (baseline {previous:.2f}, ratio {value / previous:.3f})" if previous else "" + print(f"{case}: {value:.2f} {unit}{comparison}") + if args.output: + # Preserve earlier runs; choose a new output name to collect another run. + with args.output.open("x") as out: + json.dump({"platform": platform.platform(), "machine": platform.machine(), + "draws": args.draws, "seed": args.seed, + "binaries": {k: str(v) for k, v in binaries.items()}, + "runs": records, "medians": medians}, out, indent=2) + out.write("\n") + + +if __name__ == "__main__": + main() From 1bd40ed1a10ef9815b0fd631ccf1a71f8b0256d1 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Thu, 10 Sep 2026 10:56:50 +0200 Subject: [PATCH 04/16] random: configure engines and complete seed/state CLI migration --- CMakeLists.txt | 19 ++++++ Makefile.in | 2 +- changelog.in | 18 ++++- configure | 25 +++++++ configure.ac | 10 +++ docs/random.md | 74 +++++++++++++++++++++ examples/CMakeLists.txt | 1 + examples/fifteen-puzzle.cpp | 4 +- examples/job-shop.cpp | 4 +- examples/photo.cpp | 2 +- examples/random-engine.cpp | 75 +++++++++++++++++++++ gecode/driver.hh | 26 +++++++- gecode/driver/options.cpp | 70 ++++++++++++++++++++ gecode/driver/options.hpp | 9 ++- gecode/flatzinc.hh | 7 +- gecode/flatzinc/flatzinc.cpp | 3 +- gecode/flatzinc/restart-random.hpp | 67 ------------------- gecode/support/config.hpp.in | 3 + gecode/support/hw-rnd.cpp | 10 ++- gecode/support/random.hpp | 6 +- plans/random.md | 81 ++++++++++++++++++++--- test/flatzinc.cpp | 3 +- test/flatzinc/on_restart_last_val_int.cpp | 67 +------------------ test/random-options.cmake | 65 ++++++++++++++++++ test/random-options.cpp | 32 +++++++++ test/random.cpp | 4 +- tools/flatzinc/fzn-gecode.cpp | 2 +- 27 files changed, 523 insertions(+), 166 deletions(-) create mode 100644 examples/random-engine.cpp delete mode 100644 gecode/flatzinc/restart-random.hpp create mode 100644 test/random-options.cmake create mode 100644 test/random-options.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 301b81d94b..59e9df0b06 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,6 +98,13 @@ option(GECODE_BUILD_SHARED "Build shared libraries" ${GECODE_BUILD_SHARED_DEFAUL option(GECODE_BUILD_STATIC "Build static libraries" ${GECODE_BUILD_STATIC_DEFAULT}) option(GECODE_ENABLE_THREAD "Enable thread support" ON) +set(GECODE_RANDOM_ENGINE "splitmix" CACHE STRING "Default random engine (splitmix or xorshift64star)") +set_property(CACHE GECODE_RANDOM_ENGINE PROPERTY STRINGS splitmix xorshift64star) +if(GECODE_RANDOM_ENGINE STREQUAL "xorshift64star") + set(GECODE_RANDOM_XORSHIFT64STAR 1) +elseif(NOT GECODE_RANDOM_ENGINE STREQUAL "splitmix") + message(FATAL_ERROR "GECODE_RANDOM_ENGINE must be splitmix or xorshift64star") +endif() set(GECODE_ENABLE_QT "AUTO" CACHE STRING "Enable Qt support (AUTO, ON, or OFF)") set_property(CACHE GECODE_ENABLE_QT PROPERTY STRINGS AUTO ON OFF) set(GECODE_ENABLE_GIST_DEFAULT AUTO) @@ -1548,6 +1555,18 @@ if(BUILD_TESTING) add_test(NAME random-state-replay COMMAND ${CMAKE_COMMAND} -DREPLAY=$ -P ${CMAKE_CURRENT_SOURCE_DIR}/test/random-replay.cmake) + if(GECODE_ENABLE_DRIVER) + add_executable(gecode-random-options EXCLUDE_FROM_ALL test/random-options.cpp) + target_link_libraries(gecode-random-options PRIVATE gecodedriver ${GECODE_TEST_LINK_LIBS}) + if(GECODE_ENABLE_FLATZINC) + target_compile_definitions(gecode-random-options PRIVATE TEST_RANDOM_FLATZINC) + endif() + add_dependencies(gecode-test gecode-random-options) + add_test(NAME random-options + COMMAND ${CMAKE_COMMAND} -DOPTIONS=$ + -DFLATZINC=${GECODE_ENABLE_FLATZINC} + -P ${CMAKE_CURRENT_SOURCE_DIR}/test/random-options.cmake) + endif() if(GECODE_ENABLE_FAULT_INJECTION) add_executable(gecode-fault-test EXCLUDE_FROM_ALL ${GECODE_FAULT_TEST_SOURCES}) diff --git a/Makefile.in b/Makefile.in index db24977ce3..954393a08c 100755 --- a/Makefile.in +++ b/Makefile.in @@ -965,7 +965,7 @@ INTEXAMPLESRC0 = \ alpha bacp bibd donald efpa eq20 golomb-ruler \ graph-color grocery ind-set magic-sequence magic-square \ magic-square-partial \ - money ortho-latin partition photo queens sudoku sudoku-advanced kakuro \ + money ortho-latin partition photo queens random-engine sudoku sudoku-advanced kakuro \ nonogram pentominoes crowded-chess black-hole \ minesweeper domino steel-mill sports-league \ all-interval langford-number warehouses radiotherapy \ diff --git a/changelog.in b/changelog.in index e757356aa0..8d6115b80c 100755 --- a/changelog.in +++ b/changelog.in @@ -73,7 +73,23 @@ Version: 7.0.0 Date: unreleased [DESCRIPTION] -This is the development changelog for the next Gecode release. +Development changes for Gecode 7; source, binary, and seeded-sequence +compatibility with Gecode 6 is not preserved. + +[ENTRY] +Module: kernel +What: new +Rank: major +[DESCRIPTION] +Random branching uses space-local streams and records complete splitting state +in choices. Every alternative derives a distinct successor state that is stable +when the same recorded path is recomputed. Splittable SplitMix is the default; +xorshift64* is a configurable smaller-state alternative. Users can supply engines +through the existing branching APIs. Standalone Rnd copies share a stream; +copy() makes an independent exact copy, while space cloning copies local state. +Drivers accept checked 64-bit seeds and complete state for replay. Test failures +report the exact iteration state. Seeded sequences and choice archives change; +no legacy sequence mode is provided. See docs/random.md for migration and costs. [RELEASE] Version: 6.5.0 diff --git a/configure b/configure index 6271f509c6..87e62cafac 100755 --- a/configure +++ b/configure @@ -825,6 +825,7 @@ SHELL' ac_subst_files='' ac_user_opts=' enable_option_checking +with_random_engine with_host_os with_compiler_vendor enable_resource @@ -1555,6 +1556,9 @@ Optional Features: Optional Packages: --with-PACKAGE[=ARG] use PACKAGE [ARG=yes] --without-PACKAGE do not use PACKAGE (same as --with-PACKAGE=no) + --with-random-engine=ENGINE + default random engine: splitmix (default) or + xorshift64star --with-host-os Override operating system test. Valid values are Linux, Darwin, FreeBSD, NetBSD, and Windows. --with-compiler-vendor Override compiler test. Valid values are gnu, intel, @@ -3289,6 +3293,27 @@ ac_config_headers="$ac_config_headers gecode/support/config.hpp" + +# Check whether --with-random-engine was given. +if test ${with_random_engine+y} +then : + withval=$with_random_engine; +else case e in #( + e) with_random_engine=splitmix ;; +esac +fi + +case $with_random_engine in #( + splitmix) : + ;; #( + xorshift64star) : + +printf "%s\n" "#define GECODE_RANDOM_XORSHIFT64STAR 1" >>confdefs.h + ;; #( + *) : + as_fn_error $? "random engine must be splitmix or xorshift64star" "$LINENO" 5 ;; +esac + ac_gecode_soversion=52 GECODE_SOVERSION=${ac_gecode_soversion} diff --git a/configure.ac b/configure.ac index a678d58c26..1c14151de8 100644 --- a/configure.ac +++ b/configure.ac @@ -41,6 +41,16 @@ AC_INIT([GECODE], GECODE_M4_VERSION, [users@gecode.dev]) AC_CONFIG_HEADERS([gecode/support/config.hpp]) AC_CONFIG_SRCDIR(gecode/kernel.hh) +AC_ARG_WITH([random-engine], + [AS_HELP_STRING([--with-random-engine=ENGINE], + [default random engine: splitmix (default) or xorshift64star])], + [], [with_random_engine=splitmix]) +AS_CASE([$with_random_engine], + [splitmix], [], + [xorshift64star], [AC_DEFINE([GECODE_RANDOM_XORSHIFT64STAR], [1], + [Use xorshift64* instead of splittable SplitMix as the default random engine.])], + [AC_MSG_ERROR([random engine must be splitmix or xorshift64star])]) + ac_gecode_soversion=GECODE_M4_SOVERSION AC_SUBST(GECODE_SOVERSION, ${ac_gecode_soversion}) diff --git a/docs/random.md b/docs/random.md index 0f69567c07..a7db1d3404 100644 --- a/docs/random.md +++ b/docs/random.md @@ -71,6 +71,23 @@ not a substitute for a complete snapshot. ## Handles, binding, and callbacks +The runnable `examples/random-engine.cpp` defines an engine outside Gecode and +uses it with built-in random variable and value selection. Its three-word state +contains SplitMix's two words and a raw-draw counter. The counter follows the +path, including across splits, and wraps modulo 2^64. This demonstrates extending +state without introducing another generator algorithm or changing Gecode's +engine-selection code. Build and run it with: + +```sh +cmake --build build/random --target random-engine +build/random/bin/random-engine +build/random/bin/random-engine counted-splitmix-v1:000000000000002a:9e3779b97f4a7c15:0000000000000000 +``` + +Both commands enumerate the same 24 permutations and print the same solution +states. The optional argument restores this example's custom engine, independently +of the library's configured default. + `Rnd(seed)` creates a standalone default stream. `Rnd(Support::Random(seed))` creates a user-defined one. Copying a standalone `Rnd` shares its stream; `r.copy()` creates an independent exact copy, and `r.split(a)` creates a child. @@ -146,6 +163,63 @@ failure or exception it prints arguments using `-state`, `-iter 1`, and the iteration directly, bypassing suite seed derivation. A state replay requires one thread and rejects an accompanying `-seed` or an incompatible state format. +## Build configuration and command lines + +The default is splittable SplitMix. Select xorshift64* with CMake +`-DGECODE_RANDOM_ENGINE=xorshift64star` or Autoconf +`--with-random-engine=xorshift64star`. Both accept `splitmix` to select the default +explicitly and reject other names. The installed configuration header carries +the choice to clients; build libraries and clients with matching headers. There +is no runtime engine registry. Both engines remain available as C++ types in +either configuration. + +The example driver accepts `-seed`; FlatZinc accepts `-r`. Values are unsigned +64-bit decimal or `0x` hexadecimal integers, or `time` and `hw`. SplitMix seeds +set its state word directly and use the fixed initial increment +`9e3779b97f4a7c15`. Xorshift seeds set its state directly, except that zero maps +to one. A 64-bit seed suffices to initialize either engine; it need not enumerate +all possible full states. + +Both drivers accept `-state` with complete state for their configured engine. +Seed and state options are mutually exclusive, in either order. Invalid words, +invalid engine states, and incompatible identifiers are errors. Double-hyphen +spellings are also accepted. For example, with a SplitMix build: + +```sh +build/random/bin/photo -seed 0xffffffffffffffff +build/random/bin/photo -state splitmix-v1:ffffffffffffffff:9e3779b97f4a7c15 +build/random/bin/fzn-gecode -r 42 model.fzn +build/random/bin/fzn-gecode -state splitmix-v1:000000000000002a:9e3779b97f4a7c15 model.fzn +``` + +Time and hardware initialization happen once during option parsing and print +`% Random state: -state ...` to standard error. Reuse those state arguments with +the same model and search options. Help also prints the configured initial state. +For complete-state test replay, use the test runner's reported command instead +of a driver command. + +## Migrating from Gecode 6 + +Seeded sequences, bounded-draw consumption, random search trees, and choice +archives change. There is no legacy sequence mode. `Rnd` numeric construction is +explicit and accepts 64 bits. `seed(value)` initializes. Replace the old +zero-argument `seed()` snapshot accessor with `state()`, which returns complete +state text, and use `state(text)` to restore it. +Use `copy()` for an independent generator and ordinary handle copying for shared +standalone use. Binding and space cloning follow the ownership rules above. + +Models using driver options should replace `Rnd(opt.seed())` with `opt.rnd()`. +Each call creates an independent generator at the configured initial state; +retain and reuse that handle when selectors should share a stream. This honors +both seed and state input. The numeric `opt.seed()` accessor remains available +for numeric initialization, but throws when full state, time, or hardware input +was used. This prevents silently ignoring a requested state. + +FlatZinc's restart sampler now uses the common bounded generator directly; its +old 31-bit chunk workaround is removed. These are Gecode 7 compatibility changes. +The feature branch does not change the library's release version or ABI number; +release preparation must apply those changes before shipping. + ## Measurements Build the benchmark against the candidate using the same release compiler flags diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index f5c12bdcb9..d389b76f86 100755 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -53,6 +53,7 @@ set(GECODE_EXAMPLE_SOURCES queen-armies.cpp queens.cpp radiotherapy.cpp + random-engine.cpp sat.cpp schurs-lemma.cpp sports-league.cpp diff --git a/examples/fifteen-puzzle.cpp b/examples/fifteen-puzzle.cpp index 16ef255fb2..1282294656 100644 --- a/examples/fifteen-puzzle.cpp +++ b/examples/fifteen-puzzle.cpp @@ -111,7 +111,7 @@ neighbors(int blank) { static Board initial_board(const PuzzleOptions& opt) { Board board = goal(); - Rnd random(opt.seed()); + Rnd random = opt.rnd(); int blank = 15; int previous = -1; for (unsigned int i=0; i +#include +#include +#include + +/// A user engine that counts raw draws along each path. +/// Generation and indexed splitting retain SplitMix's algorithms and guarantees. +class CountedSplitMix { + Gecode::Support::SplitMix engine; + uint64_t draws = 0; +public: + using State = std::array; + explicit CountedSplitMix(uint64_t seed=1) : engine(seed) {} + static const char* name(void) { return "counted-splitmix-v1"; } + static constexpr uint64_t min(void) { return 0; } + static constexpr uint64_t max(void) { return UINT64_MAX; } + void seed(uint64_t value) { engine.seed(value); draws = 0; } + uint64_t next(void) { ++draws; return engine.next(); } + State state(void) const { + auto s = engine.state(); + return {{s[0],s[1],draws}}; + } + void state(const State& s) { + engine.state({{s[0],s[1]}}); // Validate before changing the counter. + draws = s[2]; + } + CountedSplitMix split(uint32_t alternative) const { + auto child = *this; + child.engine = engine.split(alternative); + return child; + } +}; + +/// Enumerate permutations using one user stream for both random selectors. +class Permutations : public Gecode::Space { + Gecode::IntVarArray x; + Gecode::Rnd random; +public: + explicit Permutations(const Gecode::Rnd& source) + : x(*this,4,0,3), random(*this,source) { + Gecode::distinct(*this,x); + Gecode::branch(*this,x,Gecode::INT_VAR_RND(random), + Gecode::INT_VAL_RND(random)); + } + Permutations(Permutations& s) + : Space(s), random(*this,s.random) { x.update(*this,s.x); } + Gecode::Space* copy(void) override { return new Permutations(*this); } + void print(void) const { + std::cout << x << " " << random.state() << '\n'; + } +}; + +int main(int argc, char* argv[]) { + try { + if (argc > 2) + throw std::invalid_argument("Usage: random-engine [complete-state]"); + Gecode::Support::Random engine(42); + if (argc == 2) + engine.state(std::string(argv[1])); + std::cout << "Initial state: " << engine.state_string() << '\n'; + Gecode::Rnd source(engine); + auto root = std::make_unique(source); + Gecode::DFS search(root.get()); + root.reset(); + while (auto solution = std::unique_ptr(search.next())) + solution->print(); + return 0; + } catch (const std::exception& e) { + std::cerr << e.what() << '\n'; + return 1; + } +} diff --git a/gecode/driver.hh b/gecode/driver.hh index 0dd99b4caf..4059b2aba4 100755 --- a/gecode/driver.hh +++ b/gecode/driver.hh @@ -146,6 +146,24 @@ namespace Gecode { static void strdel(const char* s); }; + /// Checked seed or complete state for the build-configured random engine. + class GECODE_DRIVER_EXPORT RandomOption : public BaseOption { + uint64_t cur; + std::string initial; + bool seed_given = false; + bool state_given = false; + bool state_only = false; + public: + RandomOption(const char* o, const char* e, uint64_t v); + void value(uint64_t v); + /// Return the seed; throws if initialization used complete state. + uint64_t value(void) const; + /// Construct an independent generator at the configured initial state. + Rnd rnd(void) const; + virtual int parse(int argc, char* argv[]); + virtual void help(void); + }; + /** * \brief String-valued option * @@ -433,7 +451,7 @@ namespace Gecode { Driver::IplOption _ipl; ///< Integer propagation level Driver::StringOption _branching; ///< Branching options Driver::DoubleOption _decay; ///< Decay option - Driver::UnsignedIntOption _seed; ///< Seed option + Driver::RandomOption _seed; ///< Seed or complete random state Driver::DoubleOption _step; ///< Step option //@} @@ -529,9 +547,11 @@ namespace Gecode { double decay(void) const; /// Set default seed value - void seed(unsigned int s); + void seed(uint64_t s); /// Return seed value - unsigned int seed(void) const; + uint64_t seed(void) const; + /// Independent generator initialized from the seed or full-state option + Rnd rnd(void) const; /// Set default step value void step(double s); diff --git a/gecode/driver/options.cpp b/gecode/driver/options.cpp index ec014a85cd..56926ed3db 100755 --- a/gecode/driver/options.cpp +++ b/gecode/driver/options.cpp @@ -112,6 +112,76 @@ namespace Gecode { } + RandomOption::RandomOption(const char* o, const char* e, uint64_t v) + : BaseOption(o,e) { + value(v); + } + void RandomOption::value(uint64_t v) { + cur = v; + initial = Support::RandomGenerator(v).state_string(); + seed_given = state_given = state_only = false; + } + uint64_t RandomOption::value(void) const { + if (state_only) + throw std::logic_error("Random initialization has no seed; use rnd()"); + return cur; + } + Rnd RandomOption::rnd(void) const { + Rnd r; + r.state(initial); + return r; + } + int RandomOption::parse(int argc, char* argv[]) { + bool full = argc >= 2 && + (!strcmp(argv[1],"-state") || !strcmp(argv[1],"--state")); + const char* arg; + if (full) { + if (argc < 3) { + std::cerr << "Missing argument for option -state" << std::endl; + exit(EXIT_FAILURE); + } + arg = argv[2]; + } else { + arg = argument(argc,argv); + if (!arg) + return 0; + } + try { + if ((full && seed_given) || (!full && state_given)) + throw std::invalid_argument("Seed and state options cannot be combined"); + if (full) { + Support::RandomGenerator r; + r.state(std::string(arg)); + initial = r.state_string(); + state_given = state_only = true; + } else { + if (!strcmp(arg,"time") || !strcmp(arg,"hw")) { + Rnd r; + if (!strcmp(arg,"time")) r.time(); else r.hw(); + initial = r.state(); + state_only = true; + std::cerr << "% Random state: -state " << initial << std::endl; + } else { + cur = Support::random_seed(arg); + initial = Support::RandomGenerator(cur).state_string(); + state_only = false; + } + seed_given = true; + } + } catch (const std::exception& e) { + std::cerr << "Invalid random option: " << e.what() << std::endl; + exit(EXIT_FAILURE); + } + return 2; + } + void RandomOption::help(void) { + std::cerr << "\t" << iopt << " (64-bit decimal/hex seed, time, hw)\n" + << "\t\t" << exp << "\n" + << "\t-state (complete " << Support::RandomGenerator::name() + << " state; mutually exclusive with " << iopt << ")\n" + << "\t\tCurrent initial state: " << initial << std::endl; + } + StringValueOption::StringValueOption(const char* o, const char* e, const char* v) : BaseOption(o,e), cur(strdup(v)) {} diff --git a/gecode/driver/options.hpp b/gecode/driver/options.hpp index 9e622c131f..eac934594c 100755 --- a/gecode/driver/options.hpp +++ b/gecode/driver/options.hpp @@ -273,14 +273,19 @@ namespace Gecode { } inline void - Options::seed(unsigned int s) { + Options::seed(uint64_t s) { _seed.value(s); } - inline unsigned int + inline uint64_t Options::seed(void) const { return _seed.value(); } + inline Rnd + Options::rnd(void) const { + return _seed.rnd(); + } + inline void Options::step(double s) { _step.value(s); diff --git a/gecode/flatzinc.hh b/gecode/flatzinc.hh index d4b29dc5da..a1f06a2abd 100755 --- a/gecode/flatzinc.hh +++ b/gecode/flatzinc.hh @@ -238,7 +238,7 @@ namespace Gecode { namespace FlatZinc { Gecode::Driver::UnsignedLongLongIntOption _fail; ///< Cutoff for number of failures Gecode::Driver::DoubleOption _time; ///< Cutoff for time Gecode::Driver::DoubleOption _time_limit; ///< Cutoff for time (for compatibility with flatzinc command line) - Gecode::Driver::IntOption _seed; ///< Random seed + Gecode::Driver::RandomOption _seed; ///< Random seed or state Gecode::Driver::StringOption _restart; ///< Restart method option Gecode::Driver::DoubleOption _r_base; ///< Restart base Gecode::Driver::UnsignedIntOption _r_scale; ///< Restart scale factor @@ -349,7 +349,8 @@ namespace Gecode { namespace FlatZinc { unsigned long long int node(void) const { return _node.value(); } unsigned long long int fail(void) const { return _fail.value(); } double time(void) const { return _time.value(); } - int seed(void) const { return _seed.value(); } + uint64_t seed(void) const { return _seed.value(); } + Rnd rnd(void) const { return _seed.rnd(); } double step(void) const { return _step.value(); } const char* output(void) const { return _output.value(); } @@ -655,7 +656,7 @@ namespace Gecode { namespace FlatZinc { * If \a ignoreUnknown is true, unknown solve item annotations will be * ignored, otherwise a warning is written to \a err. * - * The seed for random branchers is given by the \a seed parameter. + * Random branchers use the seed or complete state configured in \a opt. * */ void createBranchers(Printer& p, AST::Node* ann, diff --git a/gecode/flatzinc/flatzinc.cpp b/gecode/flatzinc/flatzinc.cpp index 92b0e0a230..128ca1ec0e 100644 --- a/gecode/flatzinc/flatzinc.cpp +++ b/gecode/flatzinc/flatzinc.cpp @@ -1060,9 +1060,8 @@ namespace Gecode { namespace FlatZinc { FlatZincSpace::createBranchers(Printer&p, AST::Node* ann, FlatZincOptions& opt, bool ignoreUnknown, std::ostream& err) { - int seed = opt.seed(); double decay = opt.decay(); - Rnd rnd(static_cast(seed)); + Rnd rnd = opt.rnd(); TieBreak def_int_varsel = INT_VAR_AFC_SIZE_MAX(0.99); IntBoolVarBranch def_intbool_varsel = INTBOOL_VAR_AFC_SIZE_MAX(0.99); IntValBranch def_int_valsel = INT_VAL_MIN(); diff --git a/gecode/flatzinc/restart-random.hpp b/gecode/flatzinc/restart-random.hpp deleted file mode 100644 index c272d2aaac..0000000000 --- a/gecode/flatzinc/restart-random.hpp +++ /dev/null @@ -1,67 +0,0 @@ -/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ -/* - * Main authors: - * Mikael Zayenz Lagerkvist - * - * Copyright: - * Mikael Zayenz Lagerkvist, 2026 - * - * This file is part of Gecode, the generic constraint - * development environment: - * http://www.gecode.dev - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - */ - -#ifndef GECODE_FLATZINC_RESTART_RANDOM_HPP -#define GECODE_FLATZINC_RESTART_RANDOM_HPP - -namespace Gecode { namespace FlatZinc { namespace Internal { - - /// Sample an offset for an inclusive integer restart range - template - unsigned long long int - uniform_int_offset(Random& random, unsigned long long int width) { - const unsigned long long int chunk_width = 1ULL << 31; - - // Retain the established seeded sequence for ordinary integer ranges. - if ((width <= chunk_width) || (width > (1ULL << 32))) - return random(width); - - // Draw uniformly from [0,2^62), rejecting its incomplete final bucket. - const unsigned long long int source_width = 1ULL << 62; - const unsigned long long int limit = - source_width - (source_width % width); - unsigned long long int sample; - do { - sample = - (static_cast( - random(static_cast(chunk_width))) << 31) | - random(static_cast(chunk_width)); - } while (sample >= limit); - return sample % width; - } - -}}} - -#endif - -// STATISTICS: flatzinc-other diff --git a/gecode/support/config.hpp.in b/gecode/support/config.hpp.in index f434087688..246fe09973 100644 --- a/gecode/support/config.hpp.in +++ b/gecode/support/config.hpp.in @@ -61,6 +61,9 @@ /* whether __builtin_popcountll is available */ #undef GECODE_HAS_BUILTIN_POPCOUNTLL +/* Use xorshift64* instead of splittable SplitMix as the default random engine. */ +#undef GECODE_RANDOM_XORSHIFT64STAR + /* Whether counting-based search support available */ #undef GECODE_HAS_CBS diff --git a/gecode/support/hw-rnd.cpp b/gecode/support/hw-rnd.cpp index e93012a5ae..1d672bac10 100644 --- a/gecode/support/hw-rnd.cpp +++ b/gecode/support/hw-rnd.cpp @@ -35,6 +35,7 @@ #define _CRT_RAND_S #include +#include #include @@ -42,7 +43,8 @@ namespace Gecode { namespace Support { unsigned int hwrnd(void) { unsigned int r; - (void) rand_s(&r); + if (rand_s(&r) != 0) + throw std::runtime_error("Hardware random initialization failed"); return r; } @@ -53,14 +55,17 @@ namespace Gecode { namespace Support { #include #include +#include namespace Gecode { namespace Support { unsigned int hwrnd(void) { std::fstream devrandom; - devrandom.open("/dev/random", std::fstream::in); + devrandom.open("/dev/random", std::fstream::in | std::fstream::binary); unsigned int rnd; devrandom.read(reinterpret_cast(&rnd),sizeof(unsigned int)); + if (!devrandom) + throw std::runtime_error("Cannot read hardware random source /dev/random"); devrandom.close(); return rnd; } @@ -70,4 +75,3 @@ namespace Gecode { namespace Support { #endif // STATISTICS: support-any - diff --git a/gecode/support/random.hpp b/gecode/support/random.hpp index 65129ad23f..9aeb78ddf8 100755 --- a/gecode/support/random.hpp +++ b/gecode/support/random.hpp @@ -422,8 +422,12 @@ namespace Gecode { namespace Support { } }; - /// Default generator; full state consists of two 64-bit words. + /// Build-configured default generator, also used by command-line clients. +#ifdef GECODE_RANDOM_XORSHIFT64STAR + using RandomGenerator = Random; +#else using RandomGenerator = Random; +#endif }} diff --git a/plans/random.md b/plans/random.md index 4b8a25bbcb..0bcb5ee5cb 100644 --- a/plans/random.md +++ b/plans/random.md @@ -1,7 +1,7 @@ # Plan: Compact, splittable random generators for Gecode 7 > Source: the feature/random design discussion, 2026-09-10. -> Status: Phases 1–3 complete; Phase 4 next. +> Status: All four phases complete and reviewed. > Workflow: review, update this plan, and commit after each phase. ## Goal @@ -398,21 +398,86 @@ ownership rules, replay contract, and compatibility changes for Gecode 7. ### Acceptance criteria -- [ ] Drivers accept full 64-bit seeds without signed narrowing or truncation +- [x] Drivers accept full 64-bit seeds without signed narrowing or truncation and use the same initialization/state conventions as the test runner. -- [ ] Complete state is accepted and reproduced for the configured engine; +- [x] Complete state is accepted and reproduced for the configured engine; malformed, incompatible, and conflicting options are rejected clearly. -- [ ] Time/hardware initialization can report the concrete initialized state +- [x] Time/hardware initialization can report the concrete initialized state needed for a later replay. -- [ ] A runnable custom-engine example works through built-in branching; custom +- [x] A runnable custom-engine example works through built-in branching; custom brancher documentation explains choice snapshots and replay obligations. -- [ ] FlatZinc restart sampling no longer relies on the old generator's restricted +- [x] FlatZinc restart sampling no longer relies on the old generator's restricted range or sequence-preservation workaround where the new contract replaces it. -- [ ] Release notes describe changed seeded sequences, state replay, and copying +- [x] Release notes describe changed seeded sequences, state replay, and copying versus sharing semantics. No legacy sequence mode is required. -- [ ] Supported build configurations and relevant regression suites pass; the +- [x] Supported build configurations and relevant regression suites pass; the default choice and measured memory/performance tradeoffs are documented. +### Phase 4 review + +The example driver and FlatZinc share `Driver::RandomOption`: checked 64-bit +decimal/hex seeds, full-state input, and mutually exclusive seed/state arguments. +The existing seed flag names remain `-seed` and `-r`, respectively. `time` and +`hw` initialize once and print the resulting state. Hardware-source failures now +throw rather than return uninitialized data. `opt.rnd()` produces an independent +generator at the configured initial state; all in-tree numeric seed consumers +were migrated. The numeric accessor rejects state-only initialization rather +than silently ignoring it. + +Both CMake (`GECODE_RANDOM_ENGINE`) and Autoconf (`--with-random-engine`) select +SplitMix or xorshift64*. The generated, installed configuration header carries +that selection to clients. SplitMix remains the default for the measured splitting +cost; xorshift64* remains the 8-byte alternative. Both concrete engine types and +the public custom-engine interface are available in either build. + +`examples/random-engine.cpp` uses a three-word user engine with built-in random +selectors. It delegates generation/splitting to SplitMix and adds a path-local +draw counter, demonstrating full-state extension without another algorithm. +Seeded and full-state runs enumerate the same 24 permutations and solution states. +The output also agrees across both default configurations and the no-thread and +static builds. The old unused FlatZinc restart sampler and its chunk-specific test +are removed. The range-validation and restart integration tests remain. Review +also corrected the FlatZinc test harness to pass its configured generator into +parsing, and replaced an unsupported test `--seed` flag with `-r`. + +Validation on arm64 macOS with Apple Clang 21: + +- Full CMake `check`, including fault-injection tests, passes with both defaults. + All enabled example targets also build with the default configuration. +- `random-options` and `random-state-replay` CTests pass with both defaults. + CLI checks cover maximum-width decimal/hex seeds, arbitrary full states + (including a non-default SplitMix increment), incompatible identifiers, + malformed/conflicting input, and replay of reported time/hardware state. +- Boolean, set, and float branching selections, filtered random ties, and all + FlatZinc restart tests pass with both defaults. A direct FlatZinc run enumerates + the same 27 assignments from a maximum-width seed and its complete state. +- A reduced static CMake build with threads, set, float, and FlatZinc disabled + passes the random contract/branch replay/commit tests and both replay/CLI CTests. +- Autoconf builds all libraries and the custom example with xorshift64* and + threading disabled. Out-of-tree direct example building requires the existing + `make mkcompiledirs` preparation target. Its output matches the CMake builds. + +Logs and comparison outputs are under `build/random/phase4-*`; configurations are +in `build/random`, `build/random-xorshift`, `build/random-static`, and +`build/random-autoconf`. These are local configuration checks, not a claim of +having run every platform's CI. `docs/random.md` and the Gecode 7 changelog section +describe the API, compatibility changes, and measured costs. Release version and +ABI-number changes remain release preparation, outside this feature plan. + +### Completion audit + +Requirements 1 and 5 are exercised by both configured defaults, the CLI fixture, +and the separately defined custom engine. Requirement 2 is exercised by commands +replayed from actual failure and exception reports, plus driver full-state replay. +Requirements 3 and 4 are exercised by immutable choice snapshots, reverse sibling +exploration, archived replay on pre-selection clones with perturbed state, +multiway and callback handover, and equivalent solution/state sets under cloning, +recomputation, and parallel search. Review confirms that all variable/value +selectors bind and remap local streams and that commit installs them before +callbacks. Requirement 6 is covered by the retained Phase 3 measurements and +explicit description, space, choice, snapshot, archive, and splitting costs. +The final review found no uncompleted acceptance criterion in this plan. + ## Validation boundaries Keep a small set of high-value tests: reference vectors, save/restore including diff --git a/test/flatzinc.cpp b/test/flatzinc.cpp index c1845f8522..e47dff6c13 100755 --- a/test/flatzinc.cpp +++ b/test/flatzinc.cpp @@ -160,8 +160,9 @@ namespace Test { namespace FlatZinc { _before(); } std::stringstream ss(_source); + Rnd random = fznopt.rnd(); std::unique_ptr fg( - Gecode::FlatZinc::parse(ss, p, olog)); + Gecode::FlatZinc::parse(ss, p, olog, nullptr, random)); if (fg) { fg->createBranchers(p, fg->solveAnnotations(), fznopt, diff --git a/test/flatzinc/on_restart_last_val_int.cpp b/test/flatzinc/on_restart_last_val_int.cpp index 09edac36e0..775b40b228 100644 --- a/test/flatzinc/on_restart_last_val_int.cpp +++ b/test/flatzinc/on_restart_last_val_int.cpp @@ -37,8 +37,6 @@ #include "test/flatzinc.hh" -#include "gecode/flatzinc/restart-random.hpp" - namespace Test { namespace FlatZinc { namespace { @@ -67,68 +65,6 @@ solve satisfy; } }; - /// Scripted generator for testing wide restart integer sampling - class ScriptedRandom { - private: - const unsigned int* values; - unsigned int size; - unsigned int next; - public: - /// Initialize with the values to return - ScriptedRandom(const unsigned int* values0, unsigned int size0) - : values(values0), size(size0), next(0) {} - /// Return the next scripted 31-bit chunk - unsigned int operator ()(unsigned int n) { - if ((n != (1U << 31)) || (next >= size)) - return 0; - return values[next++]; - } - /// Stub for the narrow-path overload - unsigned long long int operator ()(unsigned long long int) { - return 0; - } - /// Return whether all scripted chunks were consumed - bool done(void) const { - return next == size; - } - }; - - /// Test wide restart integer range endpoints deterministically - class WideUniformInt : public Base { - public: - /// Create and register test - WideUniformInt(void) - : Base("FlatZinc::on_restart::uniform_int_wide_endpoints") {} - /// Perform test - virtual bool run(void) { - { - const unsigned int chunks[] = { - (1U << 31) - 1U, (1U << 31) - 1U, 1U, 0U - }; - ScriptedRandom random(chunks, 4); - const unsigned long long int width = (1ULL << 31) + 1ULL; - const unsigned long long int offset = - Gecode::FlatZinc::Internal::uniform_int_offset(random,width); - if ((offset != width - 1ULL) || - (static_cast(INT_MIN) + - static_cast(offset) != 0) || !random.done()) - return false; - } - { - const unsigned int chunks[] = {1U, (1U << 31) - 1U}; - ScriptedRandom random(chunks, 2); - const unsigned long long int width = 1ULL << 32; - const unsigned long long int offset = - Gecode::FlatZinc::Internal::uniform_int_offset(random,width); - if ((offset != width - 1ULL) || - (static_cast(INT_MIN) + - static_cast(offset) != INT_MAX) || !random.done()) - return false; - } - return true; - } - }; - /// Helper class to create and register tests class Create { public: @@ -184,13 +120,12 @@ solve satisfy; )FZN", R"OUT(y = 1; ---------- -)OUT", true, {"--restart", "constant", "--restart-base", "100", "--seed", "2"}); +)OUT", true, {"--restart", "constant", "--restart-base", "100", "-r", "2"}); } }; Create c; UniformIntInvalidRange invalid_range; - WideUniformInt w; } }} diff --git a/test/random-options.cmake b/test/random-options.cmake new file mode 100644 index 0000000000..2807bc5f7d --- /dev/null +++ b/test/random-options.cmake @@ -0,0 +1,65 @@ +# Copyright (c) 2026 Mikael Zayenz Lagerkvist. MIT license; see LICENSE. +set(modes driver) +if(FLATZINC) + list(APPEND modes flatzinc) +endif() +foreach(mode IN LISTS modes) + set(prefix) + set(seed -seed) + if(mode STREQUAL flatzinc) + set(prefix flatzinc) + set(seed -r) + endif() + execute_process(COMMAND "${OPTIONS}" ${prefix} ${seed} 18446744073709551615 + RESULT_VARIABLE result OUTPUT_VARIABLE expected ERROR_VARIABLE error) + if(NOT result EQUAL 0) + message(FATAL_ERROR "64-bit seed failed: ${error}") + endif() + string(REGEX MATCH "^[^\n]+" state "${expected}") + if(NOT state MATCHES "-v1:ffffffffffffffff(:|$)") + message(FATAL_ERROR "Seed was narrowed: ${state}") + endif() + foreach(args "${seed};0xffffffffffffffff" "-state;${state}") + execute_process(COMMAND "${OPTIONS}" ${prefix} ${args} + RESULT_VARIABLE result OUTPUT_VARIABLE actual ERROR_VARIABLE error) + if(NOT result EQUAL 0 OR NOT actual STREQUAL expected) + message(FATAL_ERROR "State/hex replay differs: ${actual} ${error}") + endif() + endforeach() + if(state MATCHES "^splitmix") + set(arbitrary "splitmix-v1:fedcba9876543210:0123456789abcdef") + set(incompatible "xorshift64star-v1:0000000000000001") + else() + set(arbitrary "xorshift64star-v1:fedcba9876543210") + set(incompatible "splitmix-v1:0000000000000000:9e3779b97f4a7c15") + endif() + execute_process(COMMAND "${OPTIONS}" ${prefix} -state "${arbitrary}" + RESULT_VARIABLE result OUTPUT_VARIABLE actual ERROR_VARIABLE error) + string(REGEX MATCH "^[^\n]+" restored "${actual}") + if(NOT result EQUAL 0 OR NOT restored STREQUAL arbitrary) + message(FATAL_ERROR "Complete state was not restored: ${actual} ${error}") + endif() + foreach(args "${seed};-1" "${seed};18446744073709551616" + "${seed};3x" "-state;bad" "-state" + "-state;${incompatible}" + "${seed};1;-state;${state}" "-state;${state};${seed};1") + execute_process(COMMAND "${OPTIONS}" ${prefix} ${args} + RESULT_VARIABLE result OUTPUT_VARIABLE actual ERROR_VARIABLE error) + if(result EQUAL 0) + message(FATAL_ERROR "Invalid options accepted: ${args}") + endif() + endforeach() + foreach(source time hw) + execute_process(COMMAND "${OPTIONS}" ${prefix} ${seed} ${source} + RESULT_VARIABLE result OUTPUT_VARIABLE expected ERROR_VARIABLE report) + string(REGEX MATCH "Random state: -state ([^\n]+)" matched "${report}") + if(NOT result EQUAL 0 OR NOT matched) + message(FATAL_ERROR "${source} initialization did not report state: ${report}") + endif() + execute_process(COMMAND "${OPTIONS}" ${prefix} -state "${CMAKE_MATCH_1}" + RESULT_VARIABLE result OUTPUT_VARIABLE actual ERROR_VARIABLE error) + if(NOT result EQUAL 0 OR NOT actual STREQUAL expected) + message(FATAL_ERROR "${source} state replay differs: ${actual} ${error}") + endif() + endforeach() +endforeach() diff --git a/test/random-options.cpp b/test/random-options.cpp new file mode 100644 index 0000000000..54464a7e87 --- /dev/null +++ b/test/random-options.cpp @@ -0,0 +1,32 @@ +// Copyright (c) 2026 Mikael Zayenz Lagerkvist. MIT license; see LICENSE. +#include +#ifdef TEST_RANDOM_FLATZINC +#include +#endif +#include + +template +int check(int argc, char* argv[]) { + Options opt("random-options"); + opt.parse(argc,argv); + if (argc != 1) + return 2; + auto first = opt.rnd(); + auto second = opt.rnd(); + std::cout << first.state() << '\n'; + for (int i=0; i<8; ++i) { + const auto draw = first(UINT64_MAX); + if (draw != second(UINT64_MAX)) + return 3; + std::cout << draw << '\n'; + } + return 0; +} + +int main(int argc, char* argv[]) { +#ifdef TEST_RANDOM_FLATZINC + if (argc > 1 && std::string(argv[1]) == "flatzinc") + return check(argc-1,argv+1); +#endif + return check(argc,argv); +} diff --git a/test/random.cpp b/test/random.cpp index 60ca029965..b97a4e1a21 100644 --- a/test/random.cpp +++ b/test/random.cpp @@ -61,7 +61,7 @@ namespace Test { Contract() : Base("Random::Contract", TestTags::check()) {} bool run() override { // SplitMix64 reference sequence, seed zero and golden-ratio increment. - RandomGenerator r(0); + Gecode::Support::Random r(0); for (uint64_t expected : {UINT64_C(0xe220a8397b1dcdaf), UINT64_C(0x6e789e6aa1b965f4), UINT64_C(0x06c45d188009454f)}) @@ -81,7 +81,7 @@ namespace Test { return false; auto parent = r.state(); // Indexing skips pairs of parent words, exactly as sequential splits. - RandomGenerator sequential = r; + Gecode::Support::Random sequential = r; for (uint32_t a=0; a<100; ++a) { auto child = r.split(a); if (child.state()[0] != sequential.next()) diff --git a/tools/flatzinc/fzn-gecode.cpp b/tools/flatzinc/fzn-gecode.cpp index 3bd8fb134d..3e04defb05 100755 --- a/tools/flatzinc/fzn-gecode.cpp +++ b/tools/flatzinc/fzn-gecode.cpp @@ -56,7 +56,7 @@ int main(int argc, char** argv) { FlatZinc::Printer p; FlatZinc::FlatZincSpace* fg = nullptr; - Rnd rnd(opt.seed()); + Rnd rnd = opt.rnd(); try { if (!strcmp(filename, "-")) { fg = FlatZinc::parse(cin, p, std::cerr, nullptr, rnd); From 0b6b0c8a885cadecbe709b0b09b962f24437d2fd Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Thu, 10 Sep 2026 10:59:06 +0200 Subject: [PATCH 05/16] random: use standard Gecode file headers --- examples/random-engine.cpp | 32 +++++++++++++++++++++++++++++++- test/random-options.cmake | 32 +++++++++++++++++++++++++++++++- test/random-options.cpp | 34 +++++++++++++++++++++++++++++++++- test/random-replay.cmake | 32 +++++++++++++++++++++++++++++++- test/random-replay.cpp | 33 ++++++++++++++++++++++++++++++++- test/random.cpp | 32 +++++++++++++++++++++++++++++++- tools/random-benchmark.cpp | 34 +++++++++++++++++++++++++++++++++- tools/random-benchmark.py | 32 ++++++++++++++++++++++++++++++++ 8 files changed, 254 insertions(+), 7 deletions(-) diff --git a/examples/random-engine.cpp b/examples/random-engine.cpp index 1d15d342b6..b83fdb9e6a 100644 --- a/examples/random-engine.cpp +++ b/examples/random-engine.cpp @@ -1,5 +1,35 @@ /* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ -// Copyright (c) 2026 Mikael Zayenz Lagerkvist. MIT license; see LICENSE. +/* + * 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 diff --git a/test/random-options.cmake b/test/random-options.cmake index 2807bc5f7d..78f492ca4a 100644 --- a/test/random-options.cmake +++ b/test/random-options.cmake @@ -1,4 +1,34 @@ -# Copyright (c) 2026 Mikael Zayenz Lagerkvist. MIT license; see LICENSE. +# +# 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. +# + set(modes driver) if(FLATZINC) list(APPEND modes flatzinc) diff --git a/test/random-options.cpp b/test/random-options.cpp index 54464a7e87..bb9bc5a8f2 100644 --- a/test/random-options.cpp +++ b/test/random-options.cpp @@ -1,4 +1,36 @@ -// Copyright (c) 2026 Mikael Zayenz Lagerkvist. MIT license; see LICENSE. +/* -*- 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 #ifdef TEST_RANDOM_FLATZINC #include diff --git a/test/random-replay.cmake b/test/random-replay.cmake index 4e974f4e8e..fbec0e9e5b 100644 --- a/test/random-replay.cmake +++ b/test/random-replay.cmake @@ -1,4 +1,34 @@ -# Copyright (c) 2026 Mikael Zayenz Lagerkvist. MIT license; see LICENSE. +# +# 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. +# + foreach(kind Failure Exception) execute_process(COMMAND "${REPLAY}" -seed 1 -iter 100 -test "Random::Replay::${kind}" RESULT_VARIABLE first_result OUTPUT_VARIABLE first ERROR_VARIABLE first_error) diff --git a/test/random-replay.cpp b/test/random-replay.cpp index cf7bbd4e9f..9eaa16d8fa 100644 --- a/test/random-replay.cpp +++ b/test/random-replay.cpp @@ -1,5 +1,36 @@ /* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ -// Copyright (c) 2026 Mikael Zayenz Lagerkvist. MIT license; see LICENSE. +/* + * 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. + * + */ + // Deliberately failing fixtures for the test runner's state replay protocol. #include "test/test.hh" diff --git a/test/random.cpp b/test/random.cpp index b97a4e1a21..be311842bd 100644 --- a/test/random.cpp +++ b/test/random.cpp @@ -1,5 +1,35 @@ /* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ -// Copyright (c) 2026 Mikael Zayenz Lagerkvist. MIT license; see LICENSE. +/* + * 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 "test/test.hh" diff --git a/tools/random-benchmark.cpp b/tools/random-benchmark.cpp index aefeefeb38..8bbf408ef0 100644 --- a/tools/random-benchmark.cpp +++ b/tools/random-benchmark.cpp @@ -1,4 +1,36 @@ -// Copyright (c) 2026 Mikael Zayenz Lagerkvist. MIT license; see LICENSE. +/* -*- 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. + * + */ + // Compile against main without RANDOM_NEW, or against feature/random with it. #include #include diff --git a/tools/random-benchmark.py b/tools/random-benchmark.py index 96c9514b2a..7f0e1c429a 100644 --- a/tools/random-benchmark.py +++ b/tools/random-benchmark.py @@ -1,4 +1,36 @@ #!/usr/bin/env python3 + +# +# 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. +# + """Compare compiled random-benchmark executables with bounded repeated runs. Example: python3 tools/random-benchmark.py build/random/random-benchmark \ From 8cf2d093b2a656b1eb9fce47644ee3aaadaefa78 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Thu, 10 Sep 2026 13:54:26 +0200 Subject: [PATCH 06/16] random: keep generator state in consumer-owned values --- changelog.in | 9 +- docs/random.md | 440 ++++++++-------- examples/job-shop.cpp | 4 +- examples/photo.cpp | 8 +- examples/random-engine.cpp | 29 +- gecode/flatzinc/flatzinc.cpp | 10 +- gecode/float.hh | 2 +- gecode/float/branch.hh | 3 + gecode/float/branch/val-sel.hpp | 6 +- gecode/float/relax.cpp | 3 +- gecode/int.hh | 4 +- gecode/int/branch.hh | 9 +- gecode/int/branch/val-sel.hpp | 30 +- gecode/int/branch/view-values.cpp | 3 +- gecode/int/branch/view-values.hpp | 20 +- gecode/int/ldsb/brancher.hpp | 17 +- gecode/int/relax.cpp | 5 +- gecode/kernel/branch/val-sel-commit.hpp | 12 + gecode/kernel/branch/val-sel.hpp | 3 + gecode/kernel/branch/val.hpp | 5 +- gecode/kernel/branch/var.hpp | 5 +- gecode/kernel/branch/view-sel.hpp | 72 +-- gecode/kernel/branch/view-val.hpp | 21 +- gecode/kernel/branch/view.hpp | 52 ++ gecode/kernel/core.cpp | 44 +- gecode/kernel/core.hpp | 18 +- gecode/kernel/data/rnd.cpp | 56 -- gecode/kernel/data/rnd.hpp | 211 ++------ gecode/search/relax.hh | 5 +- gecode/set.hh | 2 +- gecode/set/branch.hh | 4 +- gecode/set/branch/val-sel.hpp | 6 +- gecode/set/relax.cpp | 3 +- gecode/support/random.hpp | 1 + plans/random.md | 656 +++++------------------- test/fault.cpp | 20 +- test/random.cpp | 299 ++++++----- tools/random-benchmark.cpp | 11 +- 38 files changed, 816 insertions(+), 1292 deletions(-) diff --git a/changelog.in b/changelog.in index 8d6115b80c..0115e1edd1 100755 --- a/changelog.in +++ b/changelog.in @@ -81,12 +81,13 @@ Module: kernel What: new Rank: major [DESCRIPTION] -Random branching uses space-local streams and records complete splitting state -in choices. Every alternative derives a distinct successor state that is stable +Random generators are compact values stored directly in their consumers. +Randomized branchers record only their own selectors' splitting state in choices. +Every alternative derives a distinct successor state that is stable when the same recorded path is recomputed. Splittable SplitMix is the default; xorshift64* is a configurable smaller-state alternative. Users can supply engines -through the existing branching APIs. Standalone Rnd copies share a stream; -copy() makes an independent exact copy, while space cloning copies local state. +through the generic branching APIs. Rnd copies are independent state copies; +there is no space-managed random context or shared mutable generator handle. Drivers accept checked 64-bit seeds and complete state for replay. Test failures report the exact iteration state. Seeded sequences and choice archives change; no legacy sequence mode is provided. See docs/random.md for migration and costs. diff --git a/docs/random.md b/docs/random.md index a7db1d3404..232a21d449 100644 --- a/docs/random.md +++ b/docs/random.md @@ -1,82 +1,92 @@ -# Random generators in Gecode 7 +# Random generators for a future Gecode release -Random branching uses a stream local to each space. Each recorded choice contains -the state needed to derive a different stream for each alternative. Recomputing -that choice installs the same stream as the original commit. Cloning copies state; -it does not split or consume random values. +This is a provisional Gecode 7 design. It changes APIs, seeded sequences, and +randomized choice archives and is intended only for a breaking-change release. -## Engines and full state +## State belongs to the consumer -`Support::Random` is a value type. Copying it makes an independent, exact -copy. It supplies bounded integer generation and state text on top of the engine. -An engine provides: +A generator is a small value. A model, selector, or custom brancher stores that +value directly in its own state. Copying it copies all its state independently. +There is no random context in Space, no registration, no shared mutable handle, +and no automatic coordination between consumers. -- `State`, a nonempty `std::array` containing all mutable state and - per-stream parameters; -- construction and `seed(uint64_t)`, with a documented rule for every seed; +```cpp +Rnd a(42); +Rnd b = a; // Independent copy at the same state. +Rnd child = a.split(1); // Does not change a. +``` + +The default `Rnd` contains exactly the configured engine's state: 16 bytes +for splittable SplitMix or 8 bytes for xorshift64*. It has no pointer, vtable, +reference count, or heap allocation. Default construction initializes seed 1. + +Passing a generator to two branching descriptions gives them two independent +copies. To start them differently, pass explicitly split generators. Posting +copies the description's state into its selector. Cloning copies selector state +without drawing or splitting. A model member is copied normally in the model's +copy constructor; it is not aliased to a selector that was initialized from it. + +## Branch selection and recomputation + +After choosing its position and value, a randomized brancher records the complete +state of its own selectors in its choice. Committing alternative `a` restores +that recorded state and derives each selector's `split(a)` state before the +value commit. The destination's current selector state is not the splitting +input. Archive reconstruction restores choice data without drawing or splitting. + +Only the active brancher's selectors participate. A deterministic brancher does +not advance a later random brancher or a model-owned generator. Nor does finishing +one random brancher change a separately posted brancher's state. Earlier branch +constraints can still affect later selection, of course. + +The rule applies to binary, multiway, and one-alternative assignment branchers. +It uses the public alternative index even when values are visited in reverse +order. Each random choice stores one parent state per selector, not one state +per alternative. Late alternatives require no replay of preceding alternatives. + +Randomized choices use `RndChoice`, with packed state words in the same +allocation as the choice. Nonrandom choices retain their original layout and +archive format. The base `Space` and `Choice` classes contain no RNG storage or +snapshot hooks. Multiple selectors are recorded independently, in tie-break order +followed by value selection; their types determine the state-word layout. + +This guarantees random state for the same recorded path. It does not guarantee +identical scheduling, solution order, adaptive heuristics, or entire search trees +under parallel search or weakly monotonic propagation. + +## Engines and extension + +`Support::Random` is the low-level value wrapper, supplying bounded +integer draws and canonical state text. `RndGenerator` is the modeling +wrapper; `Rnd` names its build-configured specialization. + +An engine supplies: + +- A nonempty `State = std::array` containing all mutable state and + per-stream parameters. +- Construction and `seed(uint64_t)`, with a documented rule for every seed. - `next()`, `min()`, and `max()`, with raw output in `[0,UINT64_MAX]` or - `[1,UINT64_MAX]`; -- `state()` and `state(const State&)` for exact capture and validated restoration; -- `name()`, a stable identifier for its state format and algorithm; -- `split(uint32_t) const`, returning the selected child without changing its - parent, for use in search. - -All valid alternative indices of a parent must produce distinct child states. -An engine must document that property; different first output values are neither -required nor sufficient. Copying and restoring state must reproduce subsequent -draws and splits. A state setter must reject invalid input before mutating state. -Keep engine-owned resources exception-safe; copying an engine may occur while -cloning a space. Raw pointers to external mutable state are unsuitable snapshots. - -The built-in engines are: - -| Engine | State | Splitting | -| --- | ---: | --- | -| `Support::SplitMix` | 16 bytes | Indexed sequential SplitMix splits, constant time | -| `Support::Xorshift64Star` | 8 bytes | Indexed jumps in the native recurrence | - -SplitMix uses the published two-word splittable design: a state and an odd -increment. It is the preferred default. For parent `(s,g)`, child `a` is -`(Mix13(s+(2a+1)g), mixGamma(s+(2a+2)g))` modulo 2^64. The first word is distinct -for every 32-bit index because `g` is odd and Mix13 is a permutation. This is -exactly the child obtained by sequentially splitting `a+1` times, without doing -the intervening work. - -Xorshift64* uses shifts 12, 25, and 27, with multiplier 2685821657736338717. -Child `a` starts `(a+1)*2^32` recurrence steps ahead. Since 2^32 is coprime to -the period 2^64-1, sibling states are distinct. Binary powers of the transition -matrix compute jumps in logarithmic time, with a shared 16 KiB table initialized -once. That table is not part of individual states or choices. For the maximum -32-bit index, 2^64 steps reduce to one step modulo the period. These jumps keep -the original recurrence; they are not a new seed-hashing scheme. - -Neither engine guarantees globally disjoint streams throughout an unbounded -search tree. Xorshift64* also has known statistical weaknesses in its low bits; -its smaller state and different splitting cost should be considered together. -See [Vigna's discussion](https://prng.di.unimi.it/xorshift.php) and the -[SplitMix paper](https://gee.cs.oswego.edu/dl/papers/oopsla14.pdf). + `[1,UINT64_MAX]`. +- `state()` and `state(const State&)` for exact capture and validated restoration. +- `name()`, identifying the algorithm and state format. +- `split(uint32_t) const`, returning a child without changing its parent. -Bounded draws use integer rejection sampling. Bounds zero, one, or negative -signed bounds return zero without consuming output. Full-range engines reject -the incomplete bucket at the bottom of the raw range; nonzero engines first -subtract one and reject the incomplete bucket at the top. Thus xorshift's missing -zero is accounted for. No distribution state is cached. The conversion and its -draw consumption are part of Gecode's reproducible sequence. +All valid sibling indices must yield distinct states, not merely different first +outputs. State restoration must reproduce subsequent draws and splits and reject +invalid input before mutation. Ordinary copying must make state independent. -Full state text is an identifier followed by fixed-width hexadecimal words, for -example `splitmix-v1:000000000000002a:9e3779b97f4a7c15`. Word order is explicit -and independent of host byte order. Restoration does not run seed expansion or -repair invalid states. A normal 64-bit seed is a convenience for initialization, -not a substitute for a complete snapshot. +The runnable `examples/random-engine.cpp` defines a three-word engine outside +Gecode. It delegates to SplitMix and adds a path-local raw-draw counter. Both +variable and value selectors store this user engine inline: -## Handles, binding, and callbacks +```cpp +using Random = RndGenerator; +using VariableSelector = ViewSelRnd; +using ValueSelector = Int::Branch::ValSelRnd; +``` -The runnable `examples/random-engine.cpp` defines an engine outside Gecode and -uses it with built-in random variable and value selection. Its three-word state -contains SplitMix's two words and a raw-draw counter. The counter follows the -path, including across splits, and wraps modulo 2^64. This demonstrates extending -state without introducing another generator algorithm or changing Gecode's -engine-selection code. Build and run it with: +The example posts these with the existing generic view/value brancher machinery. +Its optional argument is a complete custom-engine state: ```sh cmake --build build/random --target random-engine @@ -84,146 +94,103 @@ build/random/bin/random-engine build/random/bin/random-engine counted-splitmix-v1:000000000000002a:9e3779b97f4a7c15:0000000000000000 ``` -Both commands enumerate the same 24 permutations and print the same solution -states. The optional argument restores this example's custom engine, independently -of the library's configured default. +Both runs print the same initial state and enumerate the same 24 permutations. -`Rnd(seed)` creates a standalone default stream. `Rnd(Support::Random(seed))` -creates a user-defined one. Copying a standalone `Rnd` shares its stream; -`r.copy()` creates an independent exact copy, and `r.split(a)` creates a child. -Concurrent draws through shared standalone handles require caller synchronization. +Custom selectors participate through `random_words()`, `random_save()`, and +`random_commit()`. State size and layout must be stable across clones. +Custom branchers can instead store their generator and its choice snapshot as +ordinary typed members and implement the same capture/restore/split rule directly. -`Rnd(home, source)` binds a stream to a space. Binding the same stream again -returns the same local stream. Different initialization handles remain different -streams even when their initial states are equal. Binding retains stream identity -across clones, so callbacks can resolve an ancestor's handle in their own space. -Reseeding an external initialization handle after binding does not reseed the -space-local copy; reseed the bound handle explicitly if that is intended. +Randomness hidden inside a callback is not automatically recorded. If a callback +draws model-owned state during choice generation, its custom choice/commit +implementation must preserve that state for replay. A callback must not capture +a mutable RNG in a shared function object and assume that cloning copies it. +Callbacks that operate on model state during commit should access the destination +model and make their state transitions explicit. -Built-in random selectors bind during posting and remap during cloning. A model -that retains a bound handle should likewise use `rnd(*this, s.rnd)` in its copy -constructor. User callbacks that capture a handle must resolve it through the -space passed to the callback rather than draw directly from the captured handle: +## Built-in algorithms -```cpp -Rnd source(*this, 42); -branch(*this, x, INT_VAR_NONE(), - INT_VAL([source](const Space& home, IntVar v, int) { - Rnd local(home, source); - unsigned int offset = local(v.size()); - IntVarValues values(v); - while (offset--) ++values; - return values.val(); - })); -``` +| Engine | State | Indexed splitting | +| --- | ---: | --- | +| `Support::SplitMix` | 16 bytes | Constant-time sequential SplitMix child | +| `Support::Xorshift64Star` | 8 bytes | Jump in its native recurrence | + +For SplitMix parent `(s,g)`, child `a` is +`(Mix13(s+(2a+1)g), mixGamma(s+(2a+2)g))` modulo 2^64. This is the child +obtained by sequentially splitting `a+1` times, computed directly. Since `g` +is odd and Mix13 is a permutation, the first child word is distinct for every +32-bit alternative index. Seed expansion sets `s` to the seed and `g` to +`9e3779b97f4a7c15`. + +Xorshift64* uses shifts 12, 25, and 27 and multiplier 2685821657736338717. +Child `a` starts `(a+1)*2^32` recurrence steps ahead. Since 2^32 is coprime +to the period 2^64-1, sibling states are distinct. A shared immutable 16 KiB jump +table is initialized once; it is not per-generator state. The maximum index +jumps 2^64 steps, equivalent to one step modulo the period. Seeds set the state +directly, except seed zero maps to one; restoring zero state is an error. + +Neither construction promises globally non-overlapping streams throughout an +unbounded search tree. Xorshift64* has known low-bit weaknesses; see +[Vigna's discussion](https://prng.di.unimi.it/xorshift.php) and the +[SplitMix paper](https://gee.cs.oswego.edu/dl/papers/oopsla14.pdf). -The const-space overload only looks up an already bound stream. Bind streams -before taking choices or clones that will replay their use. In particular, a -choice-selection callback must not first register a new stream that is absent -from an earlier clone. Dynamically posted branchers can reuse existing streams; -new streams introduced during commit must be introduced consistently on replay. -An explicitly new stream starts at its specified initialization point; it does -not retrospectively consume earlier alternatives. - -Each choice snapshots all bound streams, including streams belonging to later -branchers. Therefore a deterministic branch before a random branch still gives -the later branch different states for its alternatives. Multiple selectors sharing -one stream incur only one snapshot. Custom branchers participate through -`Space::choice()`, `Space::commit()`/`trycommit()`, and the base `Choice::archive()`; -their choice payload still contains their usual position/value data. Choices own -their snapshots and cannot be copied by C++ copy construction; use archiving when -an independent choice representation is needed. - -## Restarts, portfolios, and reproducibility limits - -Ordinary space cloning preserves stream states exactly. Generic meta-engines do -not add implicit RNG splits when creating clones. Models can explicitly call -`random_split(index)` in their `slave()` callback to derive all bound streams -from a logical restart or asset index. The callback must follow a fixed policy, -independent of which worker happens to execute it. - -FlatZinc uses a fixed three-step policy: split by 0 for restart or 1 for portfolio, -then by the high and low 32-bit words of the logical index. The Photo example -uses the high and low restart-index words before relaxation. The common relaxation -helper binds its input stream to its destination space, avoiding shared draws -between sibling spaces. - -The guarantee is the RNG state for a recorded path, not identical scheduling or -solution order in parallel search. Adaptive heuristics, restart constraints, and -weakly monotonic propagation can still change the search tree. Reproducing a -whole run also requires the same model, relevant options, and compatible Gecode -code and random algorithm. Standard-library distributions are outside Gecode's -bounded-sequence contract. - -## Test failure replay - -The test runner records complete state immediately before each iteration. On a -failure or exception it prints arguments using `-state`, `-iter 1`, and -`-test-exact`. Run the same test executable with those arguments. This restores -the iteration directly, bypassing suite seed derivation. A state replay requires -one thread and rejects an accompanying `-seed` or an incompatible state format. - -## Build configuration and command lines - -The default is splittable SplitMix. Select xorshift64* with CMake -`-DGECODE_RANDOM_ENGINE=xorshift64star` or Autoconf -`--with-random-engine=xorshift64star`. Both accept `splitmix` to select the default -explicitly and reject other names. The installed configuration header carries -the choice to clients; build libraries and clients with matching headers. There -is no runtime engine registry. Both engines remain available as C++ types in -either configuration. - -The example driver accepts `-seed`; FlatZinc accepts `-r`. Values are unsigned -64-bit decimal or `0x` hexadecimal integers, or `time` and `hw`. SplitMix seeds -set its state word directly and use the fixed initial increment -`9e3779b97f4a7c15`. Xorshift seeds set its state directly, except that zero maps -to one. A 64-bit seed suffices to initialize either engine; it need not enumerate -all possible full states. - -Both drivers accept `-state` with complete state for their configured engine. -Seed and state options are mutually exclusive, in either order. Invalid words, -invalid engine states, and incompatible identifiers are errors. Double-hyphen -spellings are also accepted. For example, with a SplitMix build: +Bounded generation uses integer rejection sampling. Bounds zero, one, and negative +signed bounds return zero without drawing. Full-range engines reject an incomplete +bottom bucket; nonzero engines subtract one and reject an incomplete top bucket. +This accounts for xorshift's missing zero. There is no distribution cache. -```sh -build/random/bin/photo -seed 0xffffffffffffffff -build/random/bin/photo -state splitmix-v1:ffffffffffffffff:9e3779b97f4a7c15 -build/random/bin/fzn-gecode -r 42 model.fzn -build/random/bin/fzn-gecode -state splitmix-v1:000000000000002a:9e3779b97f4a7c15 model.fzn -``` +## Seeds, state, and command lines + +Full state text contains an algorithm/format identifier and fixed-width hexadecimal +words in a defined order, independent of host byte order. For example: +`splitmix-v1:000000000000002a:9e3779b97f4a7c15`. Restoration bypasses seed +expansion; it never repairs invalid state silently. + +The example driver accepts `-seed`; FlatZinc accepts `-r`. Values are checked +unsigned 64-bit decimal or `0x` hexadecimal integers, or `time` and `hw`. +Both accept `-state` for the configured engine and reject conflicting seed/state +arguments, invalid states, and incompatible identifiers. Double-hyphen spellings +are accepted. A 64-bit seed need not enumerate every possible full state. + +Time and hardware initialization occur once during parsing and report +`% Random state: -state ...` on standard error. Reuse those arguments with the +same executable, model, and search options. Help also prints the initial state. + +`opt.rnd()` returns an independent value at the configured initial state. Use it +instead of `Rnd(opt.seed())` to honor full-state input. The numeric accessor +throws for full-state, time, or hardware initialization. + +The test runner snapshots immediately before each iteration. Failure and exception +reports print `-state`, `-iter 1`, and `-test-exact` arguments that restore +the iteration directly, bypassing suite seed derivation. Replay requires one +thread and rejects an accompanying seed. + +CMake `-DGECODE_RANDOM_ENGINE=xorshift64star` and Autoconf +`--with-random-engine=xorshift64star` select the alternative default; both accept +`splitmix`. The installed configuration header carries that choice to clients. +Both concrete engine types remain available; no runtime registry is required. + +## Model-owned randomness and migration + +FlatZinc's restart/relaxation generator is an ordinary model member. Its explicit +meta-engine policy splits by restart/portfolio kind and the high/low words of the +logical index. This affects that member only, not the model's branchers. +Photo likewise splits its member before relaxation. The relaxation APIs take +`Rnd&` and advance the caller's value directly. + +Gecode 6 shared-handle behavior is removed: ordinary copying now copies state. +Replace the old zero-argument `seed()` snapshot accessor with `state()` and +restore with `state(text)`. Numeric construction is explicit; default construction +initializes seed 1. `copy()` remains a convenience equivalent to ordinary copying. -Time and hardware initialization happen once during option parsing and print -`% Random state: -state ...` to standard error. Reuse those state arguments with -the same model and search options. Help also prints the configured initial state. -For complete-state test replay, use the test runner's reported command instead -of a driver command. - -## Migrating from Gecode 6 - -Seeded sequences, bounded-draw consumption, random search trees, and choice -archives change. There is no legacy sequence mode. `Rnd` numeric construction is -explicit and accepts 64 bits. `seed(value)` initializes. Replace the old -zero-argument `seed()` snapshot accessor with `state()`, which returns complete -state text, and use `state(text)` to restore it. -Use `copy()` for an independent generator and ordinary handle copying for shared -standalone use. Binding and space cloning follow the ownership rules above. - -Models using driver options should replace `Rnd(opt.seed())` with `opt.rnd()`. -Each call creates an independent generator at the configured initial state; -retain and reuse that handle when selectors should share a stream. This honors -both seed and state input. The numeric `opt.seed()` accessor remains available -for numeric initialization, but throws when full state, time, or hardware input -was used. This prevents silently ignoring a requested state. - -FlatZinc's restart sampler now uses the common bounded generator directly; its -old 31-bit chunk workaround is removed. These are Gecode 7 compatibility changes. -The feature branch does not change the library's release version or ABI number; -release preparation must apply those changes before shipping. +Seeded sequences and randomized choice archives change. There is no legacy sequence +mode. The old FlatZinc 31-bit sampling workaround is removed. Release version and +ABI-number changes remain release preparation; this feature must not ship in a +compatibility-preserving release. ## Measurements -Build the benchmark against the candidate using the same release compiler flags -as Gecode (adjust library search paths for your platform): +Use the existing bounded benchmark harness after building Gecode: ```sh clang++ -O3 -DNDEBUG -std=c++17 -DRANDOM_NEW -Ibuild/random -I. \ @@ -234,41 +201,38 @@ python3 tools/random-benchmark.py build/random/random-benchmark \ --baseline /path/to/baseline/random-benchmark --repeat 5 --output results.json ``` -For the baseline, compile the same benchmark source against baseline headers and -libraries without `-DRANDOM_NEW`. The script alternates execution order, discards -one warmup, and reports medians while retaining individual measurements in JSON. -Use a new output filename for another run. Avoid concurrent compilation or other -heavy work while collecting timings. - -The controlled search enumerates a complete binary tree of 32767 nodes and 16384 -solutions for every engine and seed, so timings compare overhead without changes -in tree size. The queens case also reports node count: changed random choices can -change its search tree, and wall time alone is not an overhead comparison. Raw -baseline draws emit 32-bit words while the new engines emit 64-bit words. Raw -throughput can also benefit from compiler optimizations that do not apply inside -branchers; the search measurements matter more for the default decision. - -On arm64 macOS 26.6.2 with Apple Clang 21, baseline 6b7de57b04, and seed 42, -five measured repetitions after one warmup gave these medians: - -| Measurement | Baseline | SplitMix with recorded streams | -| --- | ---: | ---: | -| Bounded draw through Rnd | 14.85 ns | 2.96 ns | -| Clone a random space | 178.70 ns | 345.95 ns | -| Binary-tree node, frequent cloning | 153.07 ns | 254.32 ns | -| Binary-tree node, recomputation | 226.76 ns | 368.53 ns | -| Enumerate 10-queens | 20.04 ms | 20.96 ms | - -The controlled random tree is about 1.6x slower: local copying and recorded state -have a cost that faster drawing does not eliminate. Nonrandom tree overhead was -1–3%. Seeds 1 and 1337 gave similar results; queens time increased about 4–5%, -with node counts within 0.1% of baseline. These measurements do not establish a -general speedup or statistical confidence beyond this machine and these cases. - -A binary SplitMix split followed by a draw measured about 15.7 ns; xorshift's -native jump followed by a draw measured about 237 ns. This favors SplitMix as the -default while retaining xorshift for its 8-byte state. The default integer choice -occupies 32 bytes plus a 24-byte snapshot allocation, compared with 24 bytes on -baseline. Archives occupy 32 rather than 12 bytes. No snapshot is allocated for -a nonrandom choice, although its optional pointer costs 8 bytes. Allocator -overhead is additional to these logical sizes. +Compile the same source against baseline headers/libraries without `-DRANDOM_NEW`. +The harness alternates run order, discards one warmup, and reports medians. The +controlled tree always has 32767 nodes and 16384 solutions. Queens also reports +node count because changes in its tree can affect timing. Avoid concurrent +compilation while measuring. + +Measurements after the ownership correction, on arm64 macOS with Apple Clang 21 +in Release mode, use five measured repetitions, one warmup, seed 42, and main +at `6b7de57b04` as the baseline. Sizes are bytes: + +| Object | Main | SplitMix | Xorshift64* | +| --- | ---: | ---: | ---: | +| `Rnd` | 8 (handle) | 16 | 8 | +| Random variable selector | 16 | 24 | 16 | +| Random value selector | 8 | 16 | 8 | +| Integer variable description | 112 | 120 | 112 | +| Integer value description | 80 | 88 | 80 | +| `Space` | 288 | 288 | 288 | +| Nonrandom position/value choice | 24 | 24 | 24 | +| Random choice, one selector | 24 | 48 | 40 | +| Nonrandom choice archive | 12 | 12 | 12 | +| Random choice archive, one selector | 12 | 28 | 20 | + +Main's handle size excludes its separately allocated shared implementation; the +new values contain all engine state. Random choice sizes include the inline +payload. Adding another random selector adds only its state words to that payload. + +Median controlled random-tree time relative to main was 1.02x with cloning and +1.16x with recomputation for SplitMix, versus 1.63x and 2.88x for xorshift64*. +The plain tree was 1.03x and 1.05x in both builds. Indexed binary split-plus-draw +cost about 15.7 ns for SplitMix and 237 ns for xorshift64*. Queens took 20.1 ms +with SplitMix (approximately main's time) and 21.2 ms with xorshift64* (1.06x). +Its node counts were 9333 on main, 9325 with SplitMix, and 9323 with xorshift64*. +These are local measurements, not general performance guarantees. They replace +the earlier space-local results and leave the default-engine decision provisional. diff --git a/examples/job-shop.cpp b/examples/job-shop.cpp index 410912473f..e8b371b72a 100755 --- a/examples/job-shop.cpp +++ b/examples/job-shop.cpp @@ -536,7 +536,7 @@ class JobShopSolve : public JobShopBase { JobShopSolve(const JobShopOptions& o) : JobShopBase(o), sorder(*this, spec.machines()*spec.jobs()*(spec.jobs()-1)/2, 0, 1), - rnd(*this,o.rnd()) { + rnd(o.rnd()) { if (opt.propagation() == PROP_UNARY) nooverload(); @@ -636,7 +636,7 @@ class JobShopSolve : public JobShopBase { JobShopSolve(JobShopSolve& s) : JobShopBase(s), sorder(s.sorder), fst(s.fst), snd(s.snd), iafc(s.iafc), iaction(s.iaction), baction(s.baction), - ichb(s.ichb), bchb(s.bchb), rnd(*this,s.rnd) {} + ichb(s.ichb), bchb(s.bchb), rnd(s.rnd) {} /// Copy during cloning virtual Space* copy(void) { diff --git a/examples/photo.cpp b/examples/photo.cpp index db9631d373..6c9341d25f 100644 --- a/examples/photo.cpp +++ b/examples/photo.cpp @@ -99,7 +99,7 @@ class Photo : public IntMinimizeScript { spec(opt.size()), pos(*this,spec.people(), 0, spec.people()-1), violations(*this,0,spec.preferences()), - rnd(*this,opt.rnd()), p(opt.relax()) + rnd(opt.rnd()), p(opt.relax()) { // Map preferences to violation BoolVarArgs viol(spec.preferences()); @@ -132,8 +132,8 @@ class Photo : public IntMinimizeScript { bool slave(const MetaInfo& mi) { if ((mi.type() == MetaInfo::RESTART) && (mi.restart() > 0) && (p > 0.0)) { - random_split(static_cast(uint64_t(mi.restart())>>32)); - random_split(static_cast(mi.restart())); + rnd = rnd.split(static_cast(uint64_t(mi.restart())>>32)); + rnd = rnd.split(static_cast(mi.restart())); const Photo& l = static_cast(*mi.last()); relax(*this, pos, l.pos, rnd, p); return false; @@ -143,7 +143,7 @@ class Photo : public IntMinimizeScript { } /// Constructor for cloning \a s Photo(Photo& s) : - IntMinimizeScript(s), spec(s.spec), rnd(*this,s.rnd), p(s.p) { + IntMinimizeScript(s), spec(s.spec), rnd(s.rnd), p(s.p) { pos.update(*this, s.pos); violations.update(*this, s.violations); } diff --git a/examples/random-engine.cpp b/examples/random-engine.cpp index b83fdb9e6a..a2582282c1 100644 --- a/examples/random-engine.cpp +++ b/examples/random-engine.cpp @@ -32,6 +32,7 @@ */ #include +#include #include #include #include @@ -64,22 +65,30 @@ class CountedSplitMix { } }; -/// Enumerate permutations using one user stream for both random selectors. +/// Enumerate permutations with independent values of a user-defined engine. class Permutations : public Gecode::Space { Gecode::IntVarArray x; - Gecode::Rnd random; public: - explicit Permutations(const Gecode::Rnd& source) - : x(*this,4,0,3), random(*this,source) { - Gecode::distinct(*this,x); - Gecode::branch(*this,x,Gecode::INT_VAR_RND(random), - Gecode::INT_VAL_RND(random)); + using Random = Gecode::RndGenerator; + explicit Permutations(const Random& source) : x(*this,4,0,3) { + using namespace Gecode; + using namespace Gecode::Int; + distinct(*this,x); + IntVarArgs variables(x); + ViewArray views(*this,variables); + ViewSel* selectors[] = { + new (*this) ViewSelRnd(*this,source) + }; + using Values = ValSelCommit, + Branch::ValCommitEq>; + auto* values = new (*this) Values(*this,INT_VAL_MIN(),source.split(0)); + postviewvalbrancher(*this,views,selectors,values,nullptr,nullptr); } Permutations(Permutations& s) - : Space(s), random(*this,s.random) { x.update(*this,s.x); } + : Space(s) { x.update(*this,s.x); } Gecode::Space* copy(void) override { return new Permutations(*this); } void print(void) const { - std::cout << x << " " << random.state() << '\n'; + std::cout << x << '\n'; } }; @@ -91,7 +100,7 @@ int main(int argc, char* argv[]) { if (argc == 2) engine.state(std::string(argv[1])); std::cout << "Initial state: " << engine.state_string() << '\n'; - Gecode::Rnd source(engine); + Permutations::Random source(engine); auto root = std::make_unique(source); Gecode::DFS search(root.get()); root.reset(); diff --git a/gecode/flatzinc/flatzinc.cpp b/gecode/flatzinc/flatzinc.cpp index 128ca1ec0e..e351a47e22 100644 --- a/gecode/flatzinc/flatzinc.cpp +++ b/gecode/flatzinc/flatzinc.cpp @@ -780,7 +780,7 @@ namespace Gecode { namespace FlatZinc { FlatZincSpace::FlatZincSpace(FlatZincSpace& f) : Space(f), - _initData(nullptr), _random(*this,f._random), + _initData(nullptr), _random(f._random), _solveAnnotations(nullptr), restart_data(f.restart_data), iv_boolalias(nullptr), @@ -864,7 +864,7 @@ namespace Gecode { namespace FlatZinc { : _initData(new FlatZincSpaceInitData), intVarCount(-1), boolVarCount(-1), floatVarCount(-1), setVarCount(-1), _optVar(-1), _optVarIsInt(true), _lns(0), _lnsInitialSolution(0), - _random(*this,random), + _random(random), _solveAnnotations(nullptr), needAuxVars(true) { branchInfo.init(); } @@ -2082,9 +2082,9 @@ namespace Gecode { namespace FlatZinc { // Meta-engine clones start from the master's state. Derive their streams // from logical restart/asset indices, never from worker scheduling. uint64_t index = mi.type()==MetaInfo::RESTART ? mi.restart() : mi.asset(); - random_split(mi.type()==MetaInfo::RESTART ? 0 : 1); - random_split(static_cast(index>>32)); - random_split(static_cast(index)); + _random = _random.split(mi.type()==MetaInfo::RESTART ? 0 : 1); + _random = _random.split(static_cast(index>>32)); + _random = _random.split(static_cast(index)); if (mi.type() == MetaInfo::RESTART) { if (restart_data.initialized() && restart_data().mark_complete) { // Fail the space diff --git a/gecode/float.hh b/gecode/float.hh index e2f0620deb..02f35c2724 100755 --- a/gecode/float.hh +++ b/gecode/float.hh @@ -2045,7 +2045,7 @@ namespace Gecode { */ GECODE_FLOAT_EXPORT void relax(Home home, const FloatVarArgs& x, const FloatVarArgs& sx, - Rnd r, double p); + Rnd& r, double p); } diff --git a/gecode/float/branch.hh b/gecode/float/branch.hh index 75dcddcdb8..619fe273ed 100644 --- a/gecode/float/branch.hh +++ b/gecode/float/branch.hh @@ -259,6 +259,9 @@ namespace Gecode { namespace Float { namespace Branch { /// The used random number generator Rnd r; public: + unsigned int random_words(void) const { return r.words(); } + uint64_t* random_save(uint64_t* out) const { return r.save(out); } + const uint64_t* random_commit(const uint64_t* in, unsigned int a) { return r.restore_split(in,a); } /// Constructor for initialization ValSelRnd(Space& home, const ValBranch& vb); /// Constructor for cloning diff --git a/gecode/float/branch/val-sel.hpp b/gecode/float/branch/val-sel.hpp index c41a18ec19..f506b99a90 100644 --- a/gecode/float/branch/val-sel.hpp +++ b/gecode/float/branch/val-sel.hpp @@ -77,10 +77,10 @@ namespace Gecode { namespace Float { namespace Branch { forceinline ValSelRnd::ValSelRnd(Space& home, const ValBranch& vb) - : ValSel(home,vb), r(home,vb.rnd()) {} + : ValSel(home,vb), r(vb.rnd()) {} forceinline ValSelRnd::ValSelRnd(Space& home, ValSelRnd& vs) - : ValSel(home,vs), r(home,vs.r) { + : ValSel(home,vs), r(vs.r) { } forceinline FloatNumBranch ValSelRnd::val(const Space&, FloatView x, int) { @@ -90,7 +90,7 @@ namespace Gecode { namespace Float { namespace Branch { } forceinline bool ValSelRnd::notice(void) const { - return true; + return false; } forceinline void ValSelRnd::dispose(Space&) { diff --git a/gecode/float/relax.cpp b/gecode/float/relax.cpp index 0f778084f1..94249926b7 100644 --- a/gecode/float/relax.cpp +++ b/gecode/float/relax.cpp @@ -55,7 +55,7 @@ namespace Gecode { void relax(Home home, const FloatVarArgs& x, const FloatVarArgs& sx, - Rnd r, double p) { + Rnd& r, double p) { if (x.size() != sx.size()) throw Float::ArgumentSizeMismatch("Float::relax"); if ((p < 0.0) || (p > 1.0)) @@ -67,4 +67,3 @@ namespace Gecode { } // STATISTICS: float-other - diff --git a/gecode/int.hh b/gecode/int.hh index b30217ccbb..e8d392e593 100755 --- a/gecode/int.hh +++ b/gecode/int.hh @@ -6660,7 +6660,7 @@ namespace Gecode { */ GECODE_INT_EXPORT void relax(Home home, const IntVarArgs& x, const IntVarArgs& sx, - Rnd r, double p); + Rnd& r, double p); /* * \brief Relaxed assignment of variables in \a x from values in \a sx @@ -6685,7 +6685,7 @@ namespace Gecode { */ GECODE_INT_EXPORT void relax(Home home, const BoolVarArgs& x, const BoolVarArgs& sx, - Rnd r, double p); + Rnd& r, double p); } diff --git a/gecode/int/branch.hh b/gecode/int/branch.hh index 3faab43c37..32b0eed85c 100755 --- a/gecode/int/branch.hh +++ b/gecode/int/branch.hh @@ -349,13 +349,18 @@ namespace Gecode { namespace Int { namespace Branch { * Requires \code #include \endcode * \ingroup FuncIntValSel */ - template + template class ValSelRnd : public ValSel { using typename ValSel::Var; protected: /// The used random number generator - Rnd r; + Random r; public: + ValSelRnd(Space& home, const Random& random) + : ValSel(home,ValBranch()), r(random) {} + unsigned int random_words(void) const { return r.words(); } + uint64_t* random_save(uint64_t* out) const { return r.save(out); } + const uint64_t* random_commit(const uint64_t* in, unsigned int a) { return r.restore_split(in,a); } /// Constructor for initialization ValSelRnd(Space& home, const ValBranch& vb); /// Constructor for cloning diff --git a/gecode/int/branch/val-sel.hpp b/gecode/int/branch/val-sel.hpp index 0e2e172796..7d5af94453 100755 --- a/gecode/int/branch/val-sel.hpp +++ b/gecode/int/branch/val-sel.hpp @@ -93,19 +93,19 @@ namespace Gecode { namespace Int { namespace Branch { return (x.width() == 2U) ? x.min() : ((x.min()+x.max()) / 2); } - template + template forceinline - ValSelRnd::ValSelRnd - (Space& home, const ValBranch::Var>& vb) - : ValSel(home,vb), r(home,vb.rnd()) {} - template + ValSelRnd::ValSelRnd + (Space& home, const ValBranch::Var>& vb) + : ValSel(home,vb), r(vb.rnd()) {} + template forceinline - ValSelRnd::ValSelRnd(Space& home, ValSelRnd& vs) - : ValSel(home,vs), r(home,vs.r) { + ValSelRnd::ValSelRnd(Space& home, ValSelRnd& vs) + : ValSel(home,vs), r(vs.r) { } - template + template forceinline int - ValSelRnd::val(const Space&, View x, int) { + ValSelRnd::val(const Space&, View x, int) { unsigned int p = r(x.size()); for (ViewRanges i(x); i(); ++i) { if (i.width() > p) @@ -115,15 +115,15 @@ namespace Gecode { namespace Int { namespace Branch { GECODE_NEVER; return 0; } - template + template forceinline bool - ValSelRnd::notice(void) const { - return true; + ValSelRnd::notice(void) const { + return !std::is_trivially_destructible::value; } - template + template forceinline void - ValSelRnd::dispose(Space&) { - r.~Rnd(); + ValSelRnd::dispose(Space&) { + r.~Random(); } forceinline diff --git a/gecode/int/branch/view-values.cpp b/gecode/int/branch/view-values.cpp index 7d9686753d..0d0d122e5f 100644 --- a/gecode/int/branch/view-values.cpp +++ b/gecode/int/branch/view-values.cpp @@ -48,6 +48,7 @@ namespace Gecode { namespace Int { namespace Branch { w += r.width(); i++; } pm[i].pos = w; + pm[i].min = 0; } PosValuesChoice::PosValuesChoice(const Brancher& b, unsigned int a, Pos p, @@ -65,7 +66,7 @@ namespace Gecode { namespace Int { namespace Branch { heap.free(pm,n+1); } - forceinline void + void PosValuesChoice::archive(Archive& e) const { PosChoice::archive(e); e << this->alternatives() << n; diff --git a/gecode/int/branch/view-values.hpp b/gecode/int/branch/view-values.hpp index fa4dd44c37..f173f5094e 100644 --- a/gecode/int/branch/view-values.hpp +++ b/gecode/int/branch/view-values.hpp @@ -123,8 +123,14 @@ namespace Gecode { namespace Int { namespace Branch { const Choice* ViewValuesBrancher::choice(Space& home) { Pos p = ViewBrancher::pos(home); - return new PosValuesChoice(*this,p, - ViewBrancher::view(p)); + unsigned int words = this->random_words(); + auto view = ViewBrancher::view(p); + if (!words) + return new PosValuesChoice(*this,p,view); + std::unique_ptr> c( + new (words) RndChoice(words,*this,p,view)); + this->random_save(c->data()); + return c.release(); } template @@ -135,7 +141,13 @@ namespace Gecode { namespace Int { namespace Branch { int p; unsigned int a; e >> p >> a; - return new PosValuesChoice(*this,a,p,e); + unsigned int words = this->random_words(); + if (!words) + return new PosValuesChoice(*this,a,p,e); + std::unique_ptr> c( + new (words) RndChoice(words,*this,a,p,e)); + c->read(e); + return c.release(); } template @@ -144,6 +156,8 @@ namespace Gecode { namespace Int { namespace Branch { unsigned int a) { const PosValuesChoice& pvc = static_cast(c); + if (this->random_words()) + this->random_commit(pvc.random_data(),a); IntView x(ViewBrancher::view(pvc.pos())); unsigned int b = min ? a : (pvc.alternatives() - 1 - a); return me_failed(x.eq(home,pvc.val(b))) ? ES_FAILED : ES_OK; diff --git a/gecode/int/ldsb/brancher.hpp b/gecode/int/ldsb/brancher.hpp index e9569c4799..d7de70ddd7 100755 --- a/gecode/int/ldsb/brancher.hpp +++ b/gecode/int/ldsb/brancher.hpp @@ -189,7 +189,14 @@ namespace Gecode { namespace Int { namespace LDSB { ++it; } - return new LDSBChoice(*this,a,choicePos,choiceVal, literals, nliterals); + unsigned int words = this->random_words()+this->vsc->random_words(); + if (!words) + return new LDSBChoice(*this,a,choicePos,choiceVal,literals,nliterals); + std::unique_ptr>> result( + new (words) RndChoice> + (words,*this,a,choicePos,choiceVal,literals,nliterals)); + this->vsc->random_save(this->random_save(result->data())); + return result.release(); } @@ -207,7 +214,13 @@ namespace Gecode { namespace Int { namespace LDSB { e >> literals[i]._variable; e >> literals[i]._value; } - return new LDSBChoice(*this,a,p,v, literals, nliterals); + unsigned int words = this->random_words()+this->vsc->random_words(); + if (!words) + return new LDSBChoice(*this,a,p,v,literals,nliterals); + std::unique_ptr>> result( + new (words) RndChoice>(words,*this,a,p,v,literals,nliterals)); + result->read(e); + return result.release(); } template <> diff --git a/gecode/int/relax.cpp b/gecode/int/relax.cpp index cb06c91d83..cbc7f31cfc 100644 --- a/gecode/int/relax.cpp +++ b/gecode/int/relax.cpp @@ -63,7 +63,7 @@ namespace Gecode { void relax(Home home, const IntVarArgs& x, const IntVarArgs& sx, - Rnd r, double p) { + Rnd& r, double p) { if (x.size() != sx.size()) throw Int::ArgumentSizeMismatch("Int::relax"); if ((p < 0.0) || (p > 1.0)) @@ -74,7 +74,7 @@ namespace Gecode { void relax(Home home, const BoolVarArgs& x, const BoolVarArgs& sx, - Rnd r, double p) { + Rnd& r, double p) { if (x.size() != sx.size()) throw Int::ArgumentSizeMismatch("Int::relax"); if ((p < 0.0) || (p > 1.0)) @@ -86,4 +86,3 @@ namespace Gecode { } // STATISTICS: int-other - diff --git a/gecode/kernel/branch/val-sel-commit.hpp b/gecode/kernel/branch/val-sel-commit.hpp index 8a21e86fdb..6671a69c0f 100644 --- a/gecode/kernel/branch/val-sel-commit.hpp +++ b/gecode/kernel/branch/val-sel-commit.hpp @@ -43,6 +43,9 @@ namespace Gecode { template class ValSelCommitBase { public: + virtual unsigned int random_words(void) const { return 0; } + virtual uint64_t* random_save(uint64_t* out) const { return out; } + virtual const uint64_t* random_commit(const uint64_t* in, unsigned int) { return in; } /// View type typedef View_ View; /// Corresponding variable type @@ -101,8 +104,17 @@ namespace Gecode { /// The commit object used ValCommit c; public: + unsigned int random_words(void) const override { return s.random_words(); } + uint64_t* random_save(uint64_t* out) const override { return s.random_save(out); } + const uint64_t* random_commit(const uint64_t* in, unsigned int a) override { + return s.random_commit(in,a); + } /// Constructor for initialization ValSelCommit(Space& home, const ValBranch& vb); + /// Construct a user-parameterized selector with an ordinary commit policy. + template + ValSelCommit(Space& home, const ValBranch& vb, const Random& random) + : ValSelCommitBase(home,vb), s(home,random), c(home,vb) {} /// Constructor for cloning ValSelCommit(Space& home, ValSelCommit& vsc); /// Return value of view \a x at position \a i diff --git a/gecode/kernel/branch/val-sel.hpp b/gecode/kernel/branch/val-sel.hpp index 6f21c02fc3..5fa821eb87 100755 --- a/gecode/kernel/branch/val-sel.hpp +++ b/gecode/kernel/branch/val-sel.hpp @@ -43,6 +43,9 @@ namespace Gecode { template class ValSel { public: + unsigned int random_words(void) const { return 0; } + uint64_t* random_save(uint64_t* out) const { return out; } + const uint64_t* random_commit(const uint64_t* in, unsigned int) { return in; } /// View type typedef View_ View; /// Corresponding variable type diff --git a/gecode/kernel/branch/val.hpp b/gecode/kernel/branch/val.hpp index fa43814a51..31f7215d41 100644 --- a/gecode/kernel/branch/val.hpp +++ b/gecode/kernel/branch/val.hpp @@ -75,10 +75,7 @@ namespace Gecode { template inline ValBranch::ValBranch(Rnd r0) - : r(r0), vf(nullptr), cf(nullptr) { - if (!r) - throw UninitializedRnd("ValBranch::ValBranch"); - } + : r(r0), vf(nullptr), cf(nullptr) {} template inline diff --git a/gecode/kernel/branch/var.hpp b/gecode/kernel/branch/var.hpp index 5107400d5d..a7e20497fa 100644 --- a/gecode/kernel/branch/var.hpp +++ b/gecode/kernel/branch/var.hpp @@ -153,10 +153,7 @@ namespace Gecode { template inline VarBranch::VarBranch(Rnd r) - : _tbl(nullptr), _rnd(r), _decay(1.0) { - if (!_rnd) - throw UninitializedRnd("VarBranch::VarBranch"); - } + : _tbl(nullptr), _rnd(r), _decay(1.0) {} template inline diff --git a/gecode/kernel/branch/view-sel.hpp b/gecode/kernel/branch/view-sel.hpp index f389b0f66a..4daf62f7c0 100644 --- a/gecode/kernel/branch/view-sel.hpp +++ b/gecode/kernel/branch/view-sel.hpp @@ -43,6 +43,10 @@ namespace Gecode { template class ViewSel { public: + /// State owned by this selector, recorded only by its brancher. + virtual unsigned int random_words(void) const { return 0; } + virtual uint64_t* random_save(uint64_t* out) const { return out; } + virtual const uint64_t* random_commit(const uint64_t* in, unsigned int) { return in; } /// Define the view type typedef View_ View; /// The corresponding variable type @@ -145,19 +149,27 @@ namespace Gecode { }; /// Select a view randomly - template + template class ViewSelRnd : public ViewSel { protected: typedef typename ViewSel::Var Var; /// The random number generator used - Rnd r; + Random r; public: + unsigned int random_words(void) const override { return r.words(); } + uint64_t* random_save(uint64_t* out) const override { return r.save(out); } + const uint64_t* random_commit(const uint64_t* in, unsigned int a) override { + return r.restore_split(in,a); + } + /// Construct a selector with a user-supplied value-type generator. + ViewSelRnd(Space& home, const Random& random) + : ViewSel(home,VarBranch()), r(random) {} /// \name Initialization //@{ /// Constructor for creation ViewSelRnd(Space& home, const VarBranch& vb); /// Constructor for copying during cloning - ViewSelRnd(Space& home, ViewSelRnd& vs); + ViewSelRnd(Space& home, ViewSelRnd& vs); //@} /// \name View selection and tie breaking //@{ @@ -479,17 +491,17 @@ namespace Gecode { } - template + template forceinline - ViewSelRnd::ViewSelRnd(Space& home, const VarBranch& vb) - : ViewSel(home,vb), r(home,vb.rnd()) {} - template + ViewSelRnd::ViewSelRnd(Space& home, const VarBranch& vb) + : ViewSel(home,vb), r(vb.rnd()) {} + template forceinline - ViewSelRnd::ViewSelRnd(Space& home, ViewSelRnd& vs) - : ViewSel(home,vs), r(home,vs.r) {} - template + ViewSelRnd::ViewSelRnd(Space& home, ViewSelRnd& vs) + : ViewSel(home,vs), r(vs.r) {} + template int - ViewSelRnd::select(Space&, ViewArray& x, int s) { + ViewSelRnd::select(Space&, ViewArray& x, int s) { unsigned int n=1; int j=s; for (int i=s+1; i + template int - ViewSelRnd::select(Space& home, ViewArray& x, int s, + ViewSelRnd::select(Space& home, ViewArray& x, int s, BrancherFilter& f) { unsigned int n=1; int j=s; @@ -514,44 +526,44 @@ namespace Gecode { } return j; } - template + template void - ViewSelRnd::ties(Space& home, ViewArray& x, int s, + ViewSelRnd::ties(Space& home, ViewArray& x, int s, int* ties, int& n) { n=1; ties[0] = select(home,x,s); } - template + template void - ViewSelRnd::ties(Space& home, ViewArray& x, int s, + ViewSelRnd::ties(Space& home, ViewArray& x, int s, int* ties, int& n, BrancherFilter& f) { n=1; ties[0] = select(home,x,s,f); } - template + template void - ViewSelRnd::brk(Space&, ViewArray&, int* ties, int& n) { + ViewSelRnd::brk(Space&, ViewArray&, int* ties, int& n) { ties[0] = ties[static_cast(r(static_cast(n)))]; n=1; } - template + template int - ViewSelRnd::select(Space&, ViewArray&, int* ties, int n) { + ViewSelRnd::select(Space&, ViewArray&, int* ties, int n) { return ties[static_cast(r(static_cast(n)))]; } - template + template ViewSel* - ViewSelRnd::copy(Space& home) { - return new (home) ViewSelRnd(home,*this); + ViewSelRnd::copy(Space& home) { + return new (home) ViewSelRnd(home,*this); } - template + template forceinline bool - ViewSelRnd::notice(void) const { - return true; + ViewSelRnd::notice(void) const { + return !std::is_trivially_destructible::value; } - template + template forceinline void - ViewSelRnd::dispose(Space&) { - r.~Rnd(); + ViewSelRnd::dispose(Space&) { + r.~Random(); } diff --git a/gecode/kernel/branch/view-val.hpp b/gecode/kernel/branch/view-val.hpp index 73df69a65d..7e00ff26ab 100644 --- a/gecode/kernel/branch/view-val.hpp +++ b/gecode/kernel/branch/view-val.hpp @@ -271,7 +271,14 @@ namespace Gecode { ViewValBrancher::choice(Space& home) { Pos p = ViewBrancher::pos(home); View v = ViewBrancher::view(p); - return new PosValChoice(*this,a,p,vsc->val(home,v,p.pos)); + Val value = vsc->val(home,v,p.pos); + unsigned int words = this->random_words()+vsc->random_words(); + if (!words) + return new PosValChoice(*this,a,p,value); + std::unique_ptr>> c( + new (words) RndChoice>(words,*this,a,p,value)); + vsc->random_save(this->random_save(c->data())); + return c.release(); } template> p; Val v; e >> v; - return new PosValChoice(*this,a,p,v); + unsigned int words = this->random_words()+vsc->random_words(); + if (!words) + return new PosValChoice(*this,a,p,v); + std::unique_ptr>> c( + new (words) RndChoice>(words,*this,a,p,v)); + c->read(e); + return c.release(); } template& pvc = static_cast&>(c); + if (this->random_words()+vsc->random_words()) { + assert(pvc.random_data() != nullptr); + vsc->random_commit(this->random_commit(pvc.random_data(),b),b); + } return me_failed(vsc->commit(home,b, ViewBrancher::view(pvc.pos()), pvc.pos().pos, diff --git a/gecode/kernel/branch/view.hpp b/gecode/kernel/branch/view.hpp index 01e200dc23..8f93381650 100644 --- a/gecode/kernel/branch/view.hpp +++ b/gecode/kernel/branch/view.hpp @@ -31,6 +31,8 @@ * */ +#include + namespace Gecode { /** @@ -64,10 +66,46 @@ namespace Gecode { PosChoice(const Brancher& b, unsigned int a, const Pos& p); /// Return position in array const Pos& pos(void) const; + /// Random selector state, present only in randomized brancher choices. + virtual const uint64_t* random_data(void) const { return nullptr; } /// Archive into \a e virtual void archive(Archive& e) const; }; + /// Randomized brancher choice with state words in the same allocation. + /// Nonrandom branchers use the original choice type without this payload. + template + class alignas(uint64_t) RndChoice : public Base { + unsigned int count; + public: + template + RndChoice(unsigned int words, Args&&... args) + : Base(std::forward(args)...), count(words) { + std::uninitialized_default_construct_n(data(),count); + } + RndChoice(const RndChoice&) = delete; + static void* operator new(size_t size, unsigned int words) { + return ::operator new(size+size_t(words)*sizeof(uint64_t)); + } + static void operator delete(void* p) { ::operator delete(p); } + static void operator delete(void* p, unsigned int) { ::operator delete(p); } + uint64_t* data(void) { return reinterpret_cast(this+1); } + const uint64_t* data(void) const { return reinterpret_cast(this+1); } + const uint64_t* random_data(void) const override { return data(); } + void read(Archive& e) { + for (unsigned int i=0; i> lo >> hi; + data()[i] = uint64_t(lo) | (uint64_t(hi)<<32); + } + } + void archive(Archive& e) const override { + Base::archive(e); + for (unsigned int i=0; i(data()[i]) + << static_cast(data()[i]>>32); + } + }; + /** * \brief Generic brancher by view selection * @@ -85,6 +123,20 @@ namespace Gecode { mutable int start; /// View selection objects ViewSel* vs[n]; + /// Compact state of this brancher's variable selectors only. + unsigned int random_words(void) const { + unsigned int words=0; + for (int i=0; irandom_words(); + return words; + } + uint64_t* random_save(uint64_t* out) const { + for (int i=0; irandom_save(out); + return out; + } + const uint64_t* random_commit(const uint64_t* in, unsigned int a) { + for (int i=0; irandom_commit(in,a); + return in; + } /// Filter function Filter f; /// Return position information diff --git a/gecode/kernel/core.cpp b/gecode/kernel/core.cpp index 002fc4fb34..54ab989695 100644 --- a/gecode/kernel/core.cpp +++ b/gecode/kernel/core.cpp @@ -235,7 +235,6 @@ namespace Gecode { if (_vars_d[i] != nullptr) vd[i]->dispose(*this, _vars_d[i]); #endif - delete randoms; // Release memory from memory manager mm.release(ssd.data().sm); } @@ -595,36 +594,17 @@ namespace Gecode { } // Make sure that b_commit does not point to a deleted brancher! b_commit = b_status; - std::unique_ptr c(Brancher::cast(b_status)->choice(*this)); - if (randoms) - const_cast(c.get())->random_state = randoms->snapshot(); - return c.release(); + return Brancher::cast(b_status)->choice(*this); } const Choice* Space::choice(Archive& e) const { unsigned int id; e >> id; - unsigned int n; e >> n; - std::unique_ptr data; - if (n) { - if (n > static_cast((e.size()-2)/2)) - throw std::invalid_argument("Invalid random choice archive size"); - data = std::make_unique(size_t(n)+1); - data[0]=n; - for (unsigned int i=0; i> lo >> hi; - data[i+1] = uint64_t(lo) | (uint64_t(hi)<<32); - } - } ActorLink* b_cur = bl.next(); while (b_cur != &bl) { Brancher* b = Brancher::cast(b_cur); - if (id == b->id()) { - const Choice* c = b->choice(*this,e); - const_cast(c)->random_state = data.release(); - return c; - } + if (id == b->id()) + return b->choice(*this,e); b_cur = b_cur->next(); } throw SpaceNoBrancher("Space::choice"); @@ -637,11 +617,6 @@ namespace Gecode { if (failed()) return; if (Brancher* b = brancher(c.bid)) { - if (c.random_state) { - if (!randoms) - throw std::invalid_argument("Random choice requires space streams"); - randoms->commit(c.random_state,a); - } // There is a matching brancher if (pc.p.bid_sc & sc_trace) { TraceRecorder* tr = findtracerecorder(); @@ -673,11 +648,6 @@ namespace Gecode { if (failed()) return; if (Brancher* b = brancher(c.bid)) { - if (c.random_state) { - if (!randoms) - throw std::invalid_argument("Random choice requires space streams"); - randoms->commit(c.random_state,a); - } // There is a matching brancher if (pc.p.bid_sc & sc_trace) { TraceRecorder* tr = findtracerecorder(); @@ -771,8 +741,6 @@ namespace Gecode { pl.init(); bl.init(); b_status = b_commit = &bl; - if (s.randoms) - randoms = new RandomContext(*s.randoms); // Copy all propagators { ActorLink* p = &pl; @@ -817,7 +785,6 @@ namespace Gecode { } catch (...) { recover(s); pc.c.source = nullptr; - delete randoms; mm.release(ssd.data().sm); throw; } @@ -990,11 +957,6 @@ namespace Gecode { void Choice::archive(Archive& e) const { e << id(); - unsigned int n = random_state ? static_cast(random_state[0]) : 0; - e << n; - for (unsigned int i=0; i(random_state[i+1]) - << static_cast(random_state[i+1]>>32); } bool diff --git a/gecode/kernel/core.hpp b/gecode/kernel/core.hpp index b422af5e10..08423430ee 100755 --- a/gecode/kernel/core.hpp +++ b/gecode/kernel/core.hpp @@ -144,8 +144,6 @@ namespace Gecode { class Advisor; class AFC; class Choice; - class Rnd; - class RandomContext; class Brancher; class Group; class PropagatorGroup; @@ -1434,10 +1432,6 @@ namespace Gecode { private: unsigned int bid; ///< Identity to match creating brancher unsigned int alt; ///< Number of alternatives - /// Optional packed random-state snapshot (independent of alternative count) - uint64_t* random_state; - Choice(const Choice&) = delete; - Choice& operator =(const Choice&) = delete; /// Return id of the creating brancher unsigned int id(void) const; @@ -1810,8 +1804,6 @@ namespace Gecode { Kernel::SharedSpaceData ssd; /// Performs memory management for space Kernel::MemoryManager mm; - /// Lazily allocated random streams, owned by this space - RandomContext* randoms = nullptr; #ifdef GECODE_HAS_CBS /// Global counter for variable ids unsigned int var_id_counter; @@ -2056,12 +2048,6 @@ namespace Gecode { GECODE_KERNEL_EXPORT void ap_ignore_dispose(Actor* a, bool d); public: - /// Bind a random handle to this space, mapping local handles during cloning - GECODE_KERNEL_EXPORT Rnd random(const Rnd& source); - /// Access an already bound stream, for callbacks receiving a const space - GECODE_KERNEL_EXPORT Rnd random(const Rnd& source) const; - /// Explicitly split all bound streams, for restart/portfolio initialization - GECODE_KERNEL_EXPORT void random_split(uint32_t alternative); /** * \brief Suppress global propagator information during scoped posting * \ingroup TaskModelScript @@ -3997,7 +3983,7 @@ namespace Gecode { */ forceinline Choice::Choice(const Brancher& b, const unsigned int a) - : bid(b.id()), alt(a), random_state(nullptr) {} + : bid(b.id()), alt(a) {} forceinline unsigned int Choice::alternatives(void) const { @@ -4010,7 +3996,7 @@ namespace Gecode { } forceinline - Choice::~Choice(void) { delete[] random_state; } + Choice::~Choice(void) {} diff --git a/gecode/kernel/data/rnd.cpp b/gecode/kernel/data/rnd.cpp index ef26d92a92..98544a51b9 100644 --- a/gecode/kernel/data/rnd.cpp +++ b/gecode/kernel/data/rnd.cpp @@ -35,60 +35,4 @@ #include -namespace Gecode { - Rnd::Rnd(Space& home, const Rnd& source) - : SharedHandle(home.random(source)) {} - Rnd::Rnd(const Space& home, const Rnd& source) - : SharedHandle(home.random(source)) {} - - void Rnd::seed(uint64_t value) { - if (!object()) - *this = Rnd(value); - else - imp().seed(value); - } - - void Rnd::state(const std::string& text) { - if (!object()) { - Rnd candidate(1); - candidate.state(text); - *this = candidate; - } else { - imp().state(text); - } - } - - void Rnd::time(void) { - seed(static_cast(::time(nullptr))); - } - - void Rnd::hw(void) { - seed((uint64_t(Support::hwrnd()) << 32) | Support::hwrnd()); - } - - Rnd Space::random(const Rnd& source) { - if (!source) - throw UninitializedRnd("Space::random"); - if (!randoms) - randoms = new RandomContext; - return randoms->bind(source); - } - - Rnd Space::random(const Rnd& source) const { - if (randoms) { - size_t i = randoms->find(source); - if (i < randoms->size()) - return randoms->at(i); - } - throw UninitializedRnd("Space::random: stream is not bound"); - } - - void Space::random_split(uint32_t alternative) { - if (randoms) { - std::unique_ptr snapshot(randoms->snapshot()); - randoms->commit(snapshot.get(),alternative); - } - } -} - // STATISTICS: kernel-other diff --git a/gecode/kernel/data/rnd.hpp b/gecode/kernel/data/rnd.hpp index 1b17d11aa3..f3cfa8f1d9 100755 --- a/gecode/kernel/data/rnd.hpp +++ b/gecode/kernel/data/rnd.hpp @@ -2,9 +2,11 @@ /* * Main authors: * Christian Schulte + * Mikael Zayenz Lagerkvist * * Copyright: * Christian Schulte, 2008 + * Mikael Zayenz Lagerkvist, 2026 * * This file is part of Gecode, the generic constraint * development environment: @@ -32,187 +34,58 @@ */ #include -#include -#include namespace Gecode { - class RandomContext; - /** - * \brief Handle to a random stream + * \brief Small value-type random generator owned by its consumer * - * Standalone handles share a stream. Binding to a space creates a local - * stream, shared by handles bound from the same source. Space clones have - * independent stream state. Use copy() for an independent standalone copy. - * A stream must not be drawn from concurrently. + * Copying copies complete state. There is no shared handle, registration, + * or implicit connection to a space. Engine supplies the Support::Random + * contract, including indexed splitting. * \ingroup TaskModel */ - class Rnd : public SharedHandle { - friend class RandomContext; - private: - class Origin : public SharedHandle { - public: - Origin(void) = default; - explicit Origin(Object* value) : SharedHandle(value) {} - const Object* get(void) const { return object(); } - }; - class IMP : public SharedHandle::Object { - public: - Origin origin; - const Object* identity(void) const { - return origin ? origin.get() : this; - } - virtual IMP* copy(void) const = 0; - virtual IMP* split(uint32_t a) const = 0; - virtual void seed(uint64_t value) = 0; - virtual uint64_t draw(uint64_t bound) = 0; - virtual size_t words(void) const = 0; - virtual void save(uint64_t* out) const = 0; - virtual void commit(const uint64_t* in, uint32_t a) = 0; - virtual std::string state(void) const = 0; - virtual void state(const std::string& text) = 0; - virtual const char* name(void) const = 0; - }; - template - class Implementation : public IMP { - Support::Random r; - public: - explicit Implementation(const Support::Random& value) : r(value) {} - IMP* copy(void) const override { return new Implementation(r); } - IMP* split(uint32_t a) const override { - return new Implementation(r.split(a)); - } - void seed(uint64_t value) override { r.seed(value); } - uint64_t draw(uint64_t bound) override { return r(bound); } - size_t words(void) const override { - return std::tuple_size::value; - } - void save(uint64_t* out) const override { - auto s = r.state(); - std::copy(s.begin(),s.end(),out); - } - void commit(const uint64_t* in, uint32_t a) override { - typename Engine::State s; - std::copy(in,in+s.size(),s.begin()); - auto parent = r; - parent.state(s); - r = parent.split(a); - } - std::string state(void) const override { return r.state_string(); } - void state(const std::string& text) override { r.state(text); } - const char* name(void) const override { return Engine::name(); } - }; - IMP& imp(void) const { - if (!object()) - throw UninitializedRnd("Rnd"); - return *static_cast(object()); - } - Rnd(IMP* value, bool) : SharedHandle(value) {} - Rnd local_copy(void) const { - Rnd result(imp().copy(),true); - result.imp().origin = imp().origin ? imp().origin : Origin(&imp()); - return result; - } + template + class RndGenerator { + Support::Random r; public: - /// Uninitialized handle - Rnd(void) = default; - /// Share an existing handle - Rnd(const Rnd&) = default; - Rnd& operator =(const Rnd&) = default; - ~Rnd(void) = default; - /// Create a standalone default stream - explicit Rnd(uint64_t seed) - : Rnd(Support::RandomGenerator(seed)) {} - /// Create a standalone user-defined splittable stream - template - explicit Rnd(const Support::Random& r) - : SharedHandle(new Implementation(r)) {} - /// Bind a source stream to a space, or update its handle during cloning - GECODE_KERNEL_EXPORT Rnd(Space& home, const Rnd& source); - /// Access an already bound stream through a const space - GECODE_KERNEL_EXPORT Rnd(const Space& home, const Rnd& source); - /// Create and bind a default stream to a space - Rnd(Space& home, uint64_t seed) : Rnd(home,Rnd(seed)) {} - /// Make an independent exact copy; does not split or advance the source - Rnd copy(void) const { return Rnd(imp().copy(),true); } - /// Derive an alternative stream without modifying this stream - Rnd split(uint32_t a) const { return Rnd(imp().split(a),true); } - /// Seed this stream (initializes a default engine if uninitialized) - GECODE_KERNEL_EXPORT void seed(uint64_t value); - /// Initialize using time or hardware entropy - GECODE_KERNEL_EXPORT void time(void); - GECODE_KERNEL_EXPORT void hw(void); - /// Complete state, with algorithm identifier - std::string state(void) const { return imp().state(); } - /// Restore a state for this engine (default engine if uninitialized) - GECODE_KERNEL_EXPORT void state(const std::string& text); - const char* name(void) const { return imp().name(); } - /// Number of 64-bit words in the engine state - size_t words(void) const { return imp().words(); } - /// Draw an integer in [0,bound); bounds <= 1 consume no values - template - Type operator ()(Type bound) { - static_assert(std::is_integral::value && sizeof(Type)<=8, - "Random bound must be an integer of at most 64 bits"); - return bound<=1 ? 0 : - static_cast(imp().draw(static_cast(bound))); + using State = typename Engine::State; + explicit RndGenerator(uint64_t seed=1) : r(seed) {} + explicit RndGenerator(const Support::Random& source) : r(source) {} + RndGenerator copy(void) const { return *this; } + RndGenerator split(uint32_t a) const { return RndGenerator(r.split(a)); } + void seed(uint64_t value) { r.seed(value); } + void time(void) { seed(static_cast(::time(nullptr))); } + void hw(void) { + seed((uint64_t(Support::hwrnd()) << 32) | Support::hwrnd()); } - }; - - /// Internal space-local stream storage. Local handles retain their stream origin. - class RandomContext { - std::vector streams; - public: - RandomContext(void) = default; - RandomContext(const RandomContext& other) { - streams.reserve(other.streams.size()); - for (const auto& entry : other.streams) - streams.push_back(entry.local_copy()); - } - size_t find(const Rnd& source) const { - for (size_t i=0; i(n+1); - data[0]=n; - size_t pos=1; - for (const auto& entry : streams) { - entry.imp().save(data.get()+pos); - pos += entry.words(); - } - return data.release(); + std::string state(void) const { return r.state_string(); } + void state(const std::string& text) { r.state(text); } + State state_words(void) const { return r.state(); } + void state(const State& words) { r.state(words); } + static const char* name(void) { return Engine::name(); } + static constexpr unsigned int words(void) { return std::tuple_size::value; } + template + Type operator ()(Type bound) { return r(bound); } + /// Save compact choice data and advance the output pointer. + uint64_t* save(uint64_t* out) const { + auto s = r.state(); + return std::copy(s.begin(),s.end(),out); } - /// Restore and split the recorded streams; layout follows registration order. - void commit(const uint64_t* data, uint32_t a) { - size_t n=0; - for (const auto& entry : streams) - n += entry.words(); - if (data[0] != n) - throw std::invalid_argument("Random choice does not match space streams"); - size_t pos=1; - for (auto& entry : streams) { - entry.imp().commit(data+pos,a); - pos += entry.words(); - } + /// Derive this consumer's next state from its recorded choice data. + const uint64_t* restore_split(const uint64_t* in, uint32_t a) { + State s; + std::copy(in,in+s.size(),s.begin()); + auto parent = r; + parent.state(s); + r = parent.split(a); + return in+s.size(); } }; + + /// Build-configured default, with exactly the engine's inline state size. + using Rnd = RndGenerator; + } // STATISTICS: kernel-other diff --git a/gecode/search/relax.hh b/gecode/search/relax.hh index dcd5bb9f36..ecf2b03a18 100755 --- a/gecode/search/relax.hh +++ b/gecode/search/relax.hh @@ -42,17 +42,16 @@ namespace Gecode { namespace Search { /// Relax variables in \a x from solution \a sx with probability \a p template forceinline void - relax(Home home, const VarArgs& x, const VarArgs& sx, Rnd r, + relax(Home home, const VarArgs& x, const VarArgs& sx, Rnd& r, double p, Post& post); template forceinline void - relax(Home home, const VarArgs& x, const VarArgs& sx, Rnd r, + relax(Home home, const VarArgs& x, const VarArgs& sx, Rnd& r, double p, Post& post) { if (home.failed()) return; - r = Rnd(static_cast(home),r); Region reg; // Which variables to assign Support::BitSet ax(reg, static_cast(x.size())); diff --git a/gecode/set.hh b/gecode/set.hh index 331e8bf074..566caee175 100755 --- a/gecode/set.hh +++ b/gecode/set.hh @@ -1741,7 +1741,7 @@ namespace Gecode { */ GECODE_SET_EXPORT void relax(Home home, const SetVarArgs& x, const SetVarArgs& sx, - Rnd r, double p); + Rnd& r, double p); } diff --git a/gecode/set/branch.hh b/gecode/set/branch.hh index 4a9f678e46..5d73652e63 100644 --- a/gecode/set/branch.hh +++ b/gecode/set/branch.hh @@ -279,6 +279,9 @@ namespace Gecode { namespace Set { namespace Branch { /// The used random number generator Rnd r; public: + unsigned int random_words(void) const { return r.words(); } + uint64_t* random_save(uint64_t* out) const { return r.save(out); } + const uint64_t* random_commit(const uint64_t* in, unsigned int a) { return r.restore_split(in,a); } /// Constructor for initialization ValSelRnd(Space& home, const ValBranch& vb); /// Constructor for cloning @@ -414,4 +417,3 @@ namespace Gecode { namespace Set { namespace Branch { #endif // STATISTICS: set-branch - diff --git a/gecode/set/branch/val-sel.hpp b/gecode/set/branch/val-sel.hpp index 51fbf7db00..972fe154c8 100644 --- a/gecode/set/branch/val-sel.hpp +++ b/gecode/set/branch/val-sel.hpp @@ -91,10 +91,10 @@ namespace Gecode { namespace Set { namespace Branch { forceinline ValSelRnd::ValSelRnd(Space& home, const ValBranch& vb) - : ValSel(home,vb), r(home,vb.rnd()) {} + : ValSel(home,vb), r(vb.rnd()) {} forceinline ValSelRnd::ValSelRnd(Space& home, ValSelRnd& vs) - : ValSel(home,vs), r(home,vs.r) { + : ValSel(home,vs), r(vs.r) { } forceinline int ValSelRnd::val(const Space&, SetView x, int) { @@ -110,7 +110,7 @@ namespace Gecode { namespace Set { namespace Branch { } forceinline bool ValSelRnd::notice(void) const { - return true; + return false; } forceinline void ValSelRnd::dispose(Space&) { diff --git a/gecode/set/relax.cpp b/gecode/set/relax.cpp index 21f3a3b726..6b6594e50b 100644 --- a/gecode/set/relax.cpp +++ b/gecode/set/relax.cpp @@ -60,7 +60,7 @@ namespace Gecode { void relax(Home home, const SetVarArgs& x, const SetVarArgs& sx, - Rnd r, double p) { + Rnd& r, double p) { if (x.size() != sx.size()) throw Set::ArgumentSizeMismatch("Set::relax"); if ((p < 0.0) || (p > 1.0)) @@ -72,4 +72,3 @@ namespace Gecode { } // STATISTICS: set-other - diff --git a/gecode/support/random.hpp b/gecode/support/random.hpp index 9aeb78ddf8..27d1becb3f 100755 --- a/gecode/support/random.hpp +++ b/gecode/support/random.hpp @@ -353,6 +353,7 @@ namespace Gecode { namespace Support { private: Engine e; public: + using EngineType = Engine; using State = typename Engine::State; using result_type = uint64_t; static_assert(Engine::max() == UINT64_MAX && Engine::min() <= 1, diff --git a/plans/random.md b/plans/random.md index 0bcb5ee5cb..9d1709218f 100644 --- a/plans/random.md +++ b/plans/random.md @@ -1,529 +1,135 @@ -# Plan: Compact, splittable random generators for Gecode 7 +# Plan: Compact random values and reproducible splitting -> Source: the feature/random design discussion, 2026-09-10. -> Status: All four phases complete and reviewed. -> Workflow: review, update this plan, and commit after each phase. +> Provisional draft for Gecode 7 or another future breaking-change release only. +> Ownership corrected after review: state belongs to the consumer, not Space. +> Status: implementation corrected; final verification and measurements in progress. ## Goal -Replace the old default random generator, provide user-extensible alternatives, -and make random branching streams stable under recomputation. Gecode 7 permits -breaking source, binary, and seeded-sequence compatibility for this change. - -Xorshift64* was the original proposed replacement. The subsequent requirement -for alternative-specific splitting must also be satisfied before selecting the -default. The design must stay compact in branching descriptions, branchers, -choices, and search paths. - -## User requirements - -1. A model author can use a better default generator and supply a custom engine - through Gecode's supported extension interface. -2. A test failure can be replayed from its complete random state, including state - required for subsequent splitting, without reconstructing preceding draws. -3. Every alternative of a branching decision gets a distinct successor state. - Replaying the same recorded decision and alternative gets the same state. -4. Cloning and recomputation do not introduce additional splits or make streams - depend on exploration order. -5. A command-line executable can use one configured engine. Large internal state - does not force users to supply equally large numeric seeds. -6. Memory and execution costs remain appropriate for Gecode search. - -## Architectural decisions - -### Branching and replay contract - -Generating a choice may consume random values for variable and value selection. -After those selections, the choice captures the complete state needed to derive -the streams for its alternatives. Committing an alternative derives and installs -its successor state from that immutable choice data and the alternative index. -It must not derive it from whatever mutable generator state happens to be in the -destination space. - -The same operation applies during exploration and recomputation. Reconstructing -a choice from an archive restores its splitting data without drawing or splitting. -Cloning preserves generator state without advancing either source or destination. -Sibling spaces must not consume each other's mutable streams. - -The sibling-state distinction must hold for all valid alternative indices, -including large multiway choices. A hash with merely a low collision probability -does not establish this requirement. Distinct states do not imply globally -non-overlapping sequences or uniqueness across an unbounded search tree. - -Store one common splitting payload per choice for the common single-stream case, -not an array of successor states. Deriving a late alternative must not require -generating all preceding alternatives. If multiple streams need separate state, -their cost must be explicit; do not silently assume one snapshot covers them. - -This is a guarantee about random state for a recorded path. It does not promise -identical global scheduling, solution order, adaptive heuristic state, or search -trees under parallel search, restarts, or weakly monotonic propagation. - -### Generator operations and extensibility - -Specify initialization, raw generation, bounded generation, exact copying, -alternative-indexed splitting, and full-state save/restore separately. -The names and C++ representation remain to be chosen during Phase 1. - -Custom engines must be usable by supported branching APIs, not only by callers -of the low-level support library. Define the output range, valid states, state -size, and splitting obligations of an engine. Engines without the required -splitting behavior may be usable as standalone generators but cannot silently -qualify as search generators. - -Keep the command-line engine selection independent of the extension mechanism: -one configured engine per executable is sufficient. No runtime plugin registry -or mandatory catalogue of engines is required. If configuration changes public -types or layouts, export the configuration consistently to downstream builds. - -Gecode specifies bounded integer conversion and draw consumption, including zero, -one, negative signed bounds, and maximum supported bounds. Use integer conversion -with rejection where required; account for the engine's actual output range. -In particular, a nonzero-state xorshift engine must not be treated as emitting -every 64-bit value with equal frequency over its period. Avoid distribution -caches unless their state is part of snapshots. - -### Seeds and complete state - -Ordinary initialization accepts a checked 64-bit seed, in decimal or hexadecimal. -Specify seed expansion and the treatment of zero for every supported engine. -Restoring state bypasses seed expansion and never silently repairs invalid state. - -Use a canonical text representation containing an algorithm/format identifier -and all state words in a specified order. Stream increments, counters, and other -mutable or per-instance parameters belong in it. An executable rejects an -incompatible state identifier rather than switching engines implicitly. - -The test runner records a snapshot immediately before an iteration and prints a -command that restores that snapshot directly for the named test. That command -bypasses normal suite-level seed derivation. Ordinary failure and exception paths -must both report the relevant iteration state, along with existing test options. - -The test runner needs full-state input in Phase 1. Public drivers receive the -same seed/state conventions in Phase 4. Use separate seed and state options; -reject conflicting input. A wider seed interface is unnecessary for this scope. - -### Compactness - -Measure the complete representation, not just the engine's state words: public -descriptions, selectors, local storage, choice objects, archive words, and retained -search paths. Include padding, dispatch data, and allocations. - -An 8-byte engine state is preferred; 16 bytes is a candidate budget for a truly -splittable engine, not an already approved limit. Keep runtime dispatch metadata -and textual identifiers out of repeated state payloads where they are implied -by the configured type. Avoid adding random-state storage to models that never -use random branching where practical, and measure any unavoidable common cost. - -## Decisions to close during implementation - -### Engine and indexed splitting - -Compare the original xorshift64* candidate with a published splittable design. -Distinguish fixed-increment SplitMix64 (one state word) from splittable SplitMix -(state plus a per-stream increment). Neither the name "SplitMix" nor the -existence of a sequential split operation establishes our indexed sibling-state -contract. Document the indexed adaptation, its cost, and why sibling states are -distinct before accepting it. - -Select a small initial set of alternatives with useful differences. At least one -additional search-capable engine should exercise the extension interface before -release. Preserve xorshift64* as a comparison candidate; do not invent an untested -splitting construction just to keep it as the default. Record the default decision -after correctness, published quality evidence, and Gecode measurements agree. - -### Ownership across selectors and branchers - -This decision must be closed before the Phase 2 integration is considered done. -Random variable selection and value selection may currently share a handle or -use separate handles; later posted branchers may also retain those handles. -Splitting only the active selector leaves later consumers unchanged. - -Start by evaluating a space-local random context shared by cooperating branchers, -using Gecode's existing local-object cloning mechanism if appropriate. Compare it -with compact state held directly in branchers. Choose the smallest design that -meets the contract; neither representation is mandated by this plan. - -Specify what reusing a generator in multiple descriptions means, how distinct -generators coexist, when external initialization is bound to a space, and how -later branchers inherit the alternative-specific state. Cover deterministic -choices before a randomized brancher: the later random stream must reflect the -selected alternative even when the earlier choice used no random values. -Also cover one-alternative assignments, dynamically posted branchers, and custom -callbacks. Explain where splitting state is captured and installed so custom -branchers have a clear participation contract. - -Use a small worked example with two sequential branchers, random variable and -value selection, and both shared and separate initial generators to settle this. -Do not add a general stream registry or per-node map without demonstrating why -a simpler ownership model cannot meet these cases. - -## Phase 1: Extensible generation with exact test replay - -**User requirements:** 1, 2, 5, 6. - -### What to build - -Introduce the engine contract and a compact candidate implementation, connected -to the existing test runner through complete-state input and failure reporting. -Specify raw output, bounded output, initialization, indexed splitting, and state -encoding together. Demonstrate another engine supplied outside the implementation -without editing the engine-selection logic. Resolve the ownership design needed -for the next phase using the worked example above. - -### Acceptance criteria - -- [x] Saving after mixed bounded draws and splits, restoring, and continuing - reproduces both future draws and future split states exactly. -- [x] A deliberately failing test iteration can be replayed directly from the - reported command, including when the failure is an exception. -- [x] Published vectors validate the selected engine where available; focused - checks cover invalid state, seed expansion, bounds, and state round trips. -- [x] The indexed splitting rule has an argument for sibling-state distinction - and handles the full valid alternative-index range without linear replay. -- [x] Custom engine state size is not artificially fixed to the default's size. -- [x] The ownership decision and initial memory measurements are recorded. - -### Phase 1 review - -Implemented `Support::Random` with full-state encoding and integer -rejection sampling, a 16-byte splittable SplitMix candidate, and the 8-byte -xorshift64* standalone alternative. The old congruential engine remains named -for comparison, but is no longer the default. Final default selection remains -subject to the Phase 3 measurements. - -For SplitMix parent `(s,g)`, alternative `a` gets state -`(Mix13(s+(2a+1)g), mixGamma(s+(2a+2)g))`, using unsigned 64-bit arithmetic. -This is the child of the `(a+1)`th sequential SplitMix split, computed directly. -Since `g` is odd and `a` is 32 bits, the first inputs are distinct modulo 2^64; -Mix13 is a permutation, so the complete sibling states are distinct. No new -generator recurrence or probabilistic collision assumption is introduced. - -The chosen ownership direction for Phase 2 is a lazily allocated space-local -collection of bound streams. Reusing the same initialization handle binds the -same local stream; separate handles retain separate streams. Space cloning -duplicates their mutable state. Choices snapshot all bound streams, including -ones belonging to later branchers, and commit derives each from the recorded -alternative. This also handles deterministic choices preceding random ones. -A compact linear collection is sufficient for the normally small number of -streams; there is no general registry or per-node lookup map. Raw engine words -are packed into one optional choice payload, with layout implied by the bound -engines. Phase 2 must measure and review the actual overhead of this design. - -Worked example: descriptions for branchers A and B reuse handle R, and A's -variable and value selectors both use R. All three bind one stream in the space. -After A's selections, its choice records R's state. Committing alternative 1 -installs R's child 1, which B subsequently uses. If A's value selector instead -uses a separate handle V, the choice stores R and V once each and commits both -child states. Replaying from an earlier clone uses those snapshots even though -A's selection draws were not re-executed. - -Validation: `Random::Contract` passed, including known raw-output vectors, -mixed draw/split replay, invalid state/seed input, bounds, and a user engine with -three state words. The dedicated `random-state-replay` CTest passes both ordinary -failure and exception replay by executing the printed command and comparing the -failing draw and state. The exception fixture first completes two iterations, -so this checks an advanced state rather than just initial seeding. Filtered -random tie selection also passes with the maximum 64-bit seed. -The existing `check` target passes, including its fault-injection checks and -selected integer, set, float, FlatZinc, branching, and search regressions. - -Measured on arm64 macOS: engine 4 -> 16 bytes; Rnd 8, IntVarBranch 112, -IntValBranch 80, Choice 16, integer PosValChoice 24, and Space 288 bytes remain -unchanged in this phase. Search still has its old shared-handle behavior until -Phase 2; this intermediate limitation is intentional and not a completed search -reproducibility claim. - -## Phase 2: Alternative-specific streams through one search path - -**User requirements:** 1, 3, 4, 6. - -### What to build - -Carry the new random state through a representative integer branching path, -choice archiving, commit, and sequential recomputation. Include both random -variable and value selection and the transition to a second brancher. Exercise -the same path with a user-supplied engine to prove that extension reaches search. - -### Acceptance criteria - -- [x] All alternatives of a recorded choice have distinct successor states. -- [x] Committing an alternative directly, after intervening sibling exploration, - or after restoring an archived choice produces identical successor state - and subsequent draws on equivalent spaces. -- [x] The same recorded path yields the same state with frequent cloning and - substantial recomputation, including last-alternative optimization. -- [x] A deterministic choice followed by random branching, and a transition - between randomized branchers, both retain the alternative-specific stream. -- [x] Shared and separate variable/value generators follow the documented - ownership policy; cloning does not mutate the source's generators. -- [x] Choice payload growth is independent of the number of alternatives. -- [x] Measure description, brancher, choice, and archive sizes against baseline. - -### Phase 2 review - -Implemented the space-local collection and common choice/commit integration. -`Rnd` accepts a user engine through `Support::Random` and keeps its -8-byte handle representation. Each space binds initialization handles to local -streams and clones those streams independently. Selector and model handles are -mapped back to the corresponding local stream during copying. Standalone handles -retain handle-style sharing; `copy()` makes an independent exact copy. - -All bound state words are captured after choice selection and restored/split -before the brancher's commit. Archives include the complete packed payload. -This works for custom choices that use the standard space choice/commit and base -choice archive interfaces, without per-brancher snapshot code. Binary and multiway -choices have the same random payload for the same registered streams. The small -selector binding change was applied to set and float selectors in this phase too, -because the ownership contract is common; their wider verification is Phase 3. - -The global draw mutex is removed: mutable streams are space-local, and standalone -handles are documented as requiring caller synchronization if shared by threads. -Making numeric construction explicit also exposed a FlatZinc decay constructor -that accidentally converted a double to a random seed. It now selects the actual -decay constructor explicitly. - -Validation: `Random::BranchReplay` checks reverse-order alternative exploration, -archived choices replayed on pre-selection clones, intentionally perturbed -destination state, shared/separate streams, deterministic-to-random handover, -and a user engine with three state words. For each configuration, all 81 solution -values and final states agree between commit distances 1 and 100. Both random -tests and filtered tie selection pass twice, state-report replay still passes, -and the full existing `check` target (including fault tests) passes. - -Arm64 sizes: Rnd 8, IntVarBranch 112, IntValBranch 80, and selectors remain -unchanged. Space grows 288 -> 296 bytes; Choice 16 -> 24 and integer PosValChoice -24 -> 32 bytes. One default stream adds a separately allocated 24-byte snapshot -(8-byte length + 16-byte state), making the logical integer choice footprint -56 bytes before allocator overhead. Its archive grows from 3 to 8 unsigned words. -Two default streams use a 40-byte snapshot; the custom engine uses 8 additional -bytes per stream. Nonrandom choices have no snapshot allocation but pay the -8-byte optional pointer and one archive count word. These costs are explicit -inputs to Phase 3 performance review, not yet a performance acceptance claim. - -## Phase 3: Complete branching and search integration - -**User requirements:** 1, 3, 4, 6. - -### What to build - -Apply the verified contract across integer, Boolean, set, and float random -branching, tie breaking, multiway branching, assignment, and supported custom -branchers. Check the common commit boundary and both sequential and parallel -search replay paths. Account explicitly for restarts, relaxation, and model -callbacks that use randomness outside ordinary choice selection. - -### Acceptance criteria - -- [x] Focused integration cases cover binary, multiway, and one-alternative - branching, plus handover to later branchers and custom-engine use. -- [x] Alternative identity follows the public choice index, including when a - brancher reverses the mapping from alternative index to selected value. -- [x] Clone, archive, disposal, failure, traced commit, and conditional commit - paths preserve the contract without shared mutable state between spaces. -- [x] Sequential and parallel replay of the same recorded path agree on random - state; checks do not require identical parallel solution order. -- [x] Restart and portfolio initialization policies are documented and do not - accidentally introduce worker-scheduling-dependent shared streams. -- [x] Existing relevant branching and search correctness tests pass. Add tests - for the new semantic guarantees rather than duplicating each API wrapper. -- [x] Random generation, bounded draws, splitting, clone cost, and retained-path - memory are measured in representative Gecode workloads. Use controlled - paths to separate overhead from changes in the randomized search tree. - -### Phase 3 review - -Added const-space stream lookup for callbacks and retained stream identity across -ancestor handles. Review showed that mapping only the immediate source space -was insufficient for a callback capturing an already bound ancestor handle. -The collection now holds one handle per stream; each local implementation keeps -its initialization origin alive. Mutable states remain independent across spaces. -The net per-stream collection/implementation storage is unchanged from Phase 2; -the default standalone implementation gains an 8-byte origin handle. - -FlatZinc binds its restart/relaxation handle to the space and derives meta-engine -streams from fixed restart/portfolio indices. The shared relaxation helper and -the Photo and JobShop examples now use local handles. Generic cloning still does -not split; models have an explicit `random_split()` operation for their own -meta-engine policy. Callback, registration, and replay boundaries are documented -in `docs/random.md`. - -Xorshift64* is now also a search-capable alternative: indexed splitting jumps -`(a+1)*2^32` native recurrence steps using shared transition matrices. This keeps -8-byte engine state, costs a shared 16 KiB table, and preserves the original -generator rather than inventing a new seeding construction. Composition and -period-wrap checks supplement its raw vector and full-state replay checks. -Its sibling-state distinction follows from coprimality with the native period. - -Validation passes for three engines (SplitMix, xorshift64*, and an external -three-word engine), shared/separate streams, binary/multiway choices, dynamic -posting through a one-alternative custom brancher, const callbacks, and sequential -versus two-worker search. Complete solution/state sets agree in the parallel -fixture without assuming solution order. Traced and conditional commits, failed -spaces, and illegal alternatives are checked explicitly. A dedicated fault test -counts live engine instances across allocation and brancher-copy failures and -checks that source states survive. Existing Boolean, set, float, FlatZinc restart, -and the full `check` selections also pass. - -Release measurements use Apple Clang 21, arm64 macOS 26.6.2, baseline commit -6b7de57b04, one warmup, five repetitions for seed 42, and three repetitions each -for seeds 1 and 1337. Raw records are in `build/random/phase3-benchmark*.json`; -the reusable harness and summary are documented in `docs/random.md`. -Representative seed-42 medians: bounded Rnd draws 14.85 -> 2.96 ns; random-space -clones 178.70 -> 345.95 ns; controlled random binary-tree nodes 153.07 -> 254.32 ns -with frequent cloning and 226.76 -> 368.53 ns with recomputation. Nonrandom tree -overhead was 1–3%. Queens wall time rose about 4–5% across the three seeds, with -similar but not identical node counts. These are local measurements, not a -cross-platform performance guarantee. - -The cost of independent streams and recorded state is real: the smallest random -tree is about 1.6x slower, despite much faster individual draws. We retain the -simple optional packed snapshot representation rather than add a second compact -choice hierarchy to hide that cost. SplitMix is the preferred default: binary -split-plus-draw measured about 15.7 ns, versus 237 ns for xorshift's jump-plus-draw. -The latter remains the 8-byte-state alternative. The popcount implementation was -improved after measurement without changing its bit sequence. Phase 4 will make -the executable's default configurable and verify both configurations. - -## Phase 4: Configured command lines and Gecode 7 migration - -**User requirements:** 1, 2, 5, 6. - -### What to build - -Complete seed/state handling in the example driver and FlatZinc, propagate the -configured engine through their random consumers, and finalize the default and -initial alternatives using the preceding evidence. Document the extension API, -ownership rules, replay contract, and compatibility changes for Gecode 7. - -### Acceptance criteria - -- [x] Drivers accept full 64-bit seeds without signed narrowing or truncation - and use the same initialization/state conventions as the test runner. -- [x] Complete state is accepted and reproduced for the configured engine; - malformed, incompatible, and conflicting options are rejected clearly. -- [x] Time/hardware initialization can report the concrete initialized state - needed for a later replay. -- [x] A runnable custom-engine example works through built-in branching; custom - brancher documentation explains choice snapshots and replay obligations. -- [x] FlatZinc restart sampling no longer relies on the old generator's restricted - range or sequence-preservation workaround where the new contract replaces it. -- [x] Release notes describe changed seeded sequences, state replay, and copying - versus sharing semantics. No legacy sequence mode is required. -- [x] Supported build configurations and relevant regression suites pass; the - default choice and measured memory/performance tradeoffs are documented. - -### Phase 4 review - -The example driver and FlatZinc share `Driver::RandomOption`: checked 64-bit -decimal/hex seeds, full-state input, and mutually exclusive seed/state arguments. -The existing seed flag names remain `-seed` and `-r`, respectively. `time` and -`hw` initialize once and print the resulting state. Hardware-source failures now -throw rather than return uninitialized data. `opt.rnd()` produces an independent -generator at the configured initial state; all in-tree numeric seed consumers -were migrated. The numeric accessor rejects state-only initialization rather -than silently ignoring it. - -Both CMake (`GECODE_RANDOM_ENGINE`) and Autoconf (`--with-random-engine`) select -SplitMix or xorshift64*. The generated, installed configuration header carries -that selection to clients. SplitMix remains the default for the measured splitting -cost; xorshift64* remains the 8-byte alternative. Both concrete engine types and -the public custom-engine interface are available in either build. - -`examples/random-engine.cpp` uses a three-word user engine with built-in random -selectors. It delegates generation/splitting to SplitMix and adds a path-local -draw counter, demonstrating full-state extension without another algorithm. -Seeded and full-state runs enumerate the same 24 permutations and solution states. -The output also agrees across both default configurations and the no-thread and -static builds. The old unused FlatZinc restart sampler and its chunk-specific test -are removed. The range-validation and restart integration tests remain. Review -also corrected the FlatZinc test harness to pass its configured generator into -parsing, and replaced an unsupported test `--seed` flag with `-r`. - -Validation on arm64 macOS with Apple Clang 21: - -- Full CMake `check`, including fault-injection tests, passes with both defaults. - All enabled example targets also build with the default configuration. -- `random-options` and `random-state-replay` CTests pass with both defaults. - CLI checks cover maximum-width decimal/hex seeds, arbitrary full states - (including a non-default SplitMix increment), incompatible identifiers, - malformed/conflicting input, and replay of reported time/hardware state. -- Boolean, set, and float branching selections, filtered random ties, and all - FlatZinc restart tests pass with both defaults. A direct FlatZinc run enumerates - the same 27 assignments from a maximum-width seed and its complete state. -- A reduced static CMake build with threads, set, float, and FlatZinc disabled - passes the random contract/branch replay/commit tests and both replay/CLI CTests. -- Autoconf builds all libraries and the custom example with xorshift64* and - threading disabled. Out-of-tree direct example building requires the existing - `make mkcompiledirs` preparation target. Its output matches the CMake builds. - -Logs and comparison outputs are under `build/random/phase4-*`; configurations are -in `build/random`, `build/random-xorshift`, `build/random-static`, and -`build/random-autoconf`. These are local configuration checks, not a claim of -having run every platform's CI. `docs/random.md` and the Gecode 7 changelog section -describe the API, compatibility changes, and measured costs. Release version and -ABI-number changes remain release preparation, outside this feature plan. - -### Completion audit - -Requirements 1 and 5 are exercised by both configured defaults, the CLI fixture, -and the separately defined custom engine. Requirement 2 is exercised by commands -replayed from actual failure and exception reports, plus driver full-state replay. -Requirements 3 and 4 are exercised by immutable choice snapshots, reverse sibling -exploration, archived replay on pre-selection clones with perturbed state, -multiway and callback handover, and equivalent solution/state sets under cloning, -recomputation, and parallel search. Review confirms that all variable/value -selectors bind and remap local streams and that commit installs them before -callbacks. Requirement 6 is covered by the retained Phase 3 measurements and -explicit description, space, choice, snapshot, archive, and splitting costs. -The final review found no uncompleted acceptance criterion in this plan. - -## Validation boundaries - -Keep a small set of high-value tests: reference vectors, save/restore including -splits, direct failure replay, and equivalent-path integration tests. Assertions -on state are stronger than expecting different first outputs from siblings: -distinct states can legitimately produce equal individual outputs. - -Do not put probabilistic distribution tests into ordinary CI. Use established -statistical tools during engine evaluation when needed, especially for any -indexed-splitting adaptation. Passing a statistical battery alone is not evidence -that sibling states are always distinct or replay is correct. - -Implementation proceeds in the phase order above. Each phase review records -its evidence and any adjustment before the phase is committed. - -## Initial code observations supporting the plan - -- `gecode/support/random.hpp`: current state is one unsigned integer, normally - 32 bits. Bounded output combines low-bit chunks and scaling/modulo operations. -- `gecode/kernel/data/rnd.hpp` and `.cpp`: public random handles share mutable - implementation state and use a static mutex across implementations. -- `gecode/kernel/branch/var.hpp`, `val.hpp`, and `view-sel.hpp`: branching - descriptions and selectors retain these handles, including during cloning. -- `gecode/kernel/branch/view-val.hpp` and `gecode/int/branch/view-values.hpp`: - choices currently store selected positions/values, without random snapshots. -- `gecode/kernel/core.cpp`: exploration and replay reach brancher commit through - common space operations; archive reconstruction dispatches by brancher identity. -- `gecode/kernel/core.hpp`: space-local objects already support cloning shared - objects within a space. Their suitability must be weighed against overhead. -- `gecode/kernel/archive.hpp`: archive storage uses unsigned integer words and - has no existing 64-bit integer overload; explicitly preserve every state bit. -- `gecode/search/seq/path.hpp` and `gecode/search/par/path.hpp`: retained choices - and alternative indices drive recomputation and make payload size significant. -- `test/test.cpp`: iteration replay currently treats the current generator state - as an unsigned seed; exceptions report the suite seed instead. -- `gecode/driver.hh` and `gecode/flatzinc.hh`: seed option types differ, with a - signed seed path in FlatZinc. -- `gecode/flatzinc/restart-random.hpp`: special handling compensates for the - current bounded generator while preserving established sequences. - -## Algorithm references - -- [Steele, Lea, and Flood: Fast Splittable Pseudorandom Number Generators](https://gee.cs.oswego.edu/dl/papers/oopsla14.pdf) - describes the splittable SplitMix design. -- [Vigna: older scrambled linear generators](https://prng.di.unimi.it/xorshift.php) - discusses limitations of the xorshift family and its low bits. -- [Blackman and Vigna's generator overview](https://prng.di.unimi.it/) - provides modern alternatives, reference implementations, and seed-expansion - guidance. Fixed-increment SplitMix64 is distinct from full splittable SplitMix. +Replace the old default generator, provide user-extensible alternatives and exact +state replay, and split randomized branching states by alternative. Keep states +small enough to store directly in the model, selector, or brancher that uses them. + +The original implementation introduced a space-managed context and shared stream +identity. That was an incorrect expansion of the requirement. The earlier phase +reviews and performance claims are superseded where they depend on that ownership +model. Their history remains in commits 626fb2d307, b4ec5dfe99, 2130ef3850, +3665a1bed2, and a1ff8e3385. + +## Required behavior + +1. A generator is a value containing its engine state inline. Copying it produces + independent state, with no allocation, registry, or aliasing for built-in engines. +2. Model members and selectors own their own copies. Posting or cloning copies + state without drawing. Passing the same value to two consumers does not share + their future state. +3. After variable/value selection, the active randomized brancher captures its + own selectors' states in its choice. Committing alternative a derives each + next state from that snapshot and a, not from destination mutable state. +4. Each sibling index produces a distinct successor state. Direct exploration + and archived replay of the same choice/alternative reproduce state exactly. + Cloning does not split. Late alternatives do not replay preceding siblings. +5. Deterministic branchers, unrelated branchers, and model-owned RNGs are not + implicitly advanced. Custom owners explicitly record/split their own state + when required; hidden callback randomness is not automatically enrolled. +6. Nonrandom Space and Choice representations have no additional RNG fields or + archive words. Randomized brancher choices store only the states they need, + in the same allocation as the choice. +7. Users can define engines with different state sizes and use generic branching + machinery. No runtime engine registry or fixed custom-state-size cap is needed. +8. Checked 64-bit seeds and canonical full-state text remain distinct interfaces. + Drivers use one configured engine. Test failures and exceptions report exact + iteration-state replay commands. +9. This remains a provisional breaking-change design, not a compatibility release. + +## Implementation + +### Value ownership + +- [x] Replace shared-handle Rnd with RndGenerator and a configured Rnd alias. +- [x] Remove RandomContext, origin identities, binding, Space::random(), and + Space::random_split(). +- [x] Restore kernel/core.hpp and core.cpp to their pre-feature state. +- [x] Copy selector and model generators normally; remove random-handle disposal + overhead for trivially destructible built-in engines. +- [x] Make relaxation take a generator reference, explicitly advancing its owner. + +### Brancher-local replay + +- [x] Add selector state save/restore/split hooks with no-op defaults. +- [x] Capture only the active brancher's selectors, in tie-break then value order. +- [x] Store words inline in a randomized choice subtype, without per-engine + identifiers, a state array per alternative, or a separate payload allocation. +- [x] Cover binary, multiway, assignment, reversed alternatives, and LDSB choices. +- [x] Support user-parameterized variable and integer/Boolean value selectors + through the existing generic brancher machinery. +- [x] Keep model/custom-brancher state transitions explicit. + +### Tests and documentation + +- [x] Replace tests of global stream coordination with tests of independent copies, + untouched model/later-selector state, and consumer-local splitting. +- [x] Check direct/archived choices, perturbed destination selector state, + sibling exploration, cloning/recomputation, and parallel solution agreement. +- [x] Exercise a three-word external engine in selectors and a runnable example. +- [x] Retain engine vectors, full-state failure replay, CLI validation, and + failed-clone resource checks. +- [x] Rewrite docs and release notes to describe value ownership accurately. +- [x] Run full relevant checks for both defaults and the reduced static/no-thread build. +- [x] Remeasure compactness and representative costs after removing the context. +- [x] Review the correction and update the existing provisional draft PR. + +## Verification strategy + +Use the existing focused Random::Contract, Random::BranchReplay, and +Random::CommitBoundary tests. The latter now checks consumer ownership rather than +the removed kernel-wide commit hook. Fault::Random::CloneFailures counts inline +custom-engine instances across failed clones. Existing Boolean, set, float, +assignment, LDSB, and FlatZinc restart cases cover their integration paths. + +Run random-options and random-state-replay CTests with both configured engines. +Verify that the custom example's initial state replays its output. Keep direct +assertions on state and choice archives; different first outputs alone do not +prove sibling-state distinction. + +Measure against the same main baseline (6b7de57b04), using the existing controlled +tree and queens harness. Report actual generator/description/selector/choice sizes +and any performance costs. Do not reuse the space-local implementation's results +as evidence for the corrected design. + +## Algorithm and CLI decisions retained + +Splittable SplitMix has two state words and constant-time indexed splitting. +Xorshift64* has one state word and indexed native jumps. Both have documented +sibling-state arguments in docs/random.md. The default remains provisional +SplitMix; xorshift64* remains configurable through CMake and Autoconf. + +Support::Random defines bounded integer conversion and canonical full-state +encoding. RndGenerator provides the modeling value interface. Full state +includes every word needed for future draws and splits. Seed expansion never +substitutes for state restoration. + +Drivers retain -seed (examples), -r (FlatZinc), and -state. Time/hardware +initialization reports concrete state. Incompatible, malformed, and conflicting +input is rejected. No legacy sequence mode is required. + +## Correction review + +The ownership error was architectural, not a bug in state copying. The previous +tests verified an expanded contract that the user did not intend. This correction +removes that contract and its infrastructure instead of optimizing it. + +Both engine configurations pass CMake check and all five CTests, including +command-line state replay and fault injection. The reduced static/no-thread build +passes check and the focused Random tests. Additional Boolean, set, float, +assignment, filtered-tie, LDSB, and FlatZinc restart checks pass with both defaults. +Random::BranchReplay now also exercises randomized LDSB choice archives. The +custom-engine example's full-state replay and output agree across all three builds. + +The corrected Space and base Choice implementation matches main exactly. Rnd is +16 bytes with SplitMix and 8 with xorshift64*, without shared allocation. Fresh +five-run measurements are recorded in docs/random.md: SplitMix's controlled +random tree costs 1.02x with cloning and 1.16x with recomputation relative to main; +xorshift64* costs 1.63x and 2.88x. No result from the removed space-local design is +used to justify this correction. Draft PR #241 describes this corrected contract +and remains provisional, for a future breaking-change release only. diff --git a/test/fault.cpp b/test/fault.cpp index 4760de5dc8..98075c09a7 100644 --- a/test/fault.cpp +++ b/test/fault.cpp @@ -32,6 +32,7 @@ */ #include +#include #include #include "test/test.hh" @@ -1114,7 +1115,7 @@ namespace Test { namespace Fault { } }; - // Count engine instances so failed clones cannot hide leaked stream handles. + // Count inline engine instances across selector cloning and failed clones. class LiveRandom : public Support::SplitMix { public: static int live; @@ -1129,13 +1130,18 @@ namespace Test { namespace Fault { class RandomSpace : public Space { public: IntVarArray x; - Rnd r; - RandomSpace() : x(*this,3,0,2), - r(*this,Rnd(Support::Random(7))) { - branch(*this,x,INT_VAR_RND(r),INT_VAL_RND(r)); + RndGenerator r; + RandomSpace() : x(*this,3,0,2), r(7) { + IntVarArgs vars(x); + ViewArray views(*this,vars); + ViewSel* selectors[] = { + new (*this) ViewSelRnd>(*this,r) + }; + auto* values = Int::Branch::valselcommit(*this,INT_VAL_RND(Rnd(7))); + postviewvalbrancher(*this,views,selectors,values,nullptr,nullptr); ThrowingBrancher::post(*this); } - RandomSpace(RandomSpace& s) : Space(s), r(*this,s.r) { + RandomSpace(RandomSpace& s) : Space(s), r(s.r) { x.update(*this,s.x); } Space* copy() override { return new RandomSpace(*this); } @@ -1165,7 +1171,7 @@ namespace Test { namespace Fault { } if (!succeeded) return false; - // Fail after a random brancher and its stream handles have been copied. + // Fail after a random brancher and its inline engines have been copied. Support::FailPoint::fail_after(Phase::BrancherCopy,0); try { std::unique_ptr copy(root.clone()); diff --git a/test/random.cpp b/test/random.cpp index be311842bd..0bb9c3ad1f 100644 --- a/test/random.cpp +++ b/test/random.cpp @@ -32,6 +32,7 @@ */ #include "test/test.hh" +#include namespace Test { namespace Random { @@ -152,195 +153,233 @@ namespace Test { } } contract; + // The model's RNG is an ordinary value, independent of selector copies. + template class ReplaySpace : public Gecode::Space { public: Gecode::IntVarArray x; - Gecode::Rnd variable, value; - ReplaySpace(const Gecode::Rnd& source, bool separate, bool multi=false, - bool callback=false) - : x(*this,4,0,2), variable(*this,source), - value(*this,separate ? source.copy() : source) { + Random own; + Gecode::ViewSel* variable = nullptr; + Gecode::ValSelCommitBase* value = nullptr; + void post(const Gecode::IntVarArgs& vars, bool multi, bool different) { using namespace Gecode; - // The first branch is deterministic, but later streams must split. - branch(*this,x[0],multi ? INT_VALUES_MIN() : INT_VAL_MIN()); - IntVarArgs first(2); - first[0]=x[1]; first[1]=x[2]; - branch(*this,first,INT_VAR_RND(variable),INT_VAL_RND(value)); - if (callback) { - // A custom one-alternative branch posts a later random brancher. - // The callback resolves the stream through its own space each time. - branch(*this,[source=variable](Space& home) { - auto& self = static_cast(home); - branch(home,self.x[3],INT_VAL([source](const Space& h, IntVar v, int) { - Rnd local(h,source); - unsigned int p=local(v.size()); - IntVarValues values(v); - while (p--) ++values; - return values.val(); - })); - }); + using Int::IntView; + ViewArray views(*this,vars); + ViewSel* selectors[] = { + new (*this) ViewSelRnd(*this,own) + }; + if (multi) { + Int::Branch::postviewvaluesbrancher<1,true>(*this,views,selectors,nullptr,nullptr); } else { - branch(*this,x[3],INT_VAL_RND(value)); + using Values = ValSelCommit, + Int::Branch::ValCommitEq>; + value = new (*this) Values(*this,INT_VAL_MIN(),different ? own.split(99) : own); + postviewvalbrancher(*this,views,selectors,value,nullptr,nullptr); } + variable = selectors[0]; } - ReplaySpace(ReplaySpace& s) - : Space(s), variable(*this,s.variable), value(*this,s.value) { - x.update(*this,s.x); + ReplaySpace(const Random& source, bool different, bool multi=false, + bool callback=false) : x(*this,4,0,2), own(source) { + using namespace Gecode; + branch(*this,x[0],INT_VAL_MIN()); + IntVarArgs first(2); first[0]=x[1]; first[1]=x[2]; + post(first,multi,different); + if (callback) + branch(*this,[](Space& home) { + auto& model=static_cast(home); + // Explicit model-owned state transition in a one-alternative commit. + model.own=model.own.split(0); + }); + branch(*this,x[3],INT_VAL_MIN()); } + ReplaySpace(ReplaySpace& s) : Space(s), own(s.own) { x.update(*this,s.x); } Space* copy() override { return new ReplaySpace(*this); } }; bool same_archive(const Gecode::Choice& a, const Gecode::Choice& b) { Gecode::Archive x,y; a.archive(x); b.archive(y); - if (x.size()!=y.size()) - return false; + if (x.size()!=y.size()) return false; for (int i=0; i + bool choice_replay(const Random& source, bool different, bool multi, bool callback) { using namespace Gecode; - std::unique_ptr root(new ReplaySpace(source,separate,multi,callback)); + using Model=ReplaySpace; + std::unique_ptr root(new Model(source,different,multi,callback)); while (root->status()==SS_BRANCH) { - std::unique_ptr before(static_cast(root->clone())); - const auto source_state = source.state(); + std::unique_ptr before(static_cast(root->clone())); + const auto owner_state=root->own.state(); std::unique_ptr choice(root->choice()); - Archive packed; - choice->archive(packed); - if (packed[1] != source.words()*(separate ? 2 : 1)) - return false; - const auto variable = root->variable.copy(); - const auto value = root->value.copy(); - std::vector siblings; - // Explore backwards, exercising late alternatives without earlier draws. + if (root->own.state()!=owner_state) return false; for (unsigned int a=choice->alternatives(); a--;) { - std::unique_ptr direct(static_cast(root->clone())); - std::unique_ptr replay(static_cast(before->clone())); - Archive archive; - choice->archive(archive); + std::unique_ptr direct(static_cast(root->clone())); + std::unique_ptr replay(static_cast(before->clone())); + Archive archive; choice->archive(archive); std::unique_ptr restored(replay->choice(archive)); + if (!same_archive(*choice,*restored)) return false; direct->commit(*choice,a); - // State on the recomputed space is intentionally different before commit. - (void) replay->variable(13); replay->commit(*restored,a); - if (direct->variable.state()!=variable.split(a).state() || - direct->value.state()!=value.split(a).state() || - replay->variable.state()!=direct->variable.state() || - replay->value.state()!=direct->value.state()) - return false; - for (const auto& previous : siblings) - if (previous==direct->variable.state()) - return false; - siblings.push_back(direct->variable.state()); - auto status = direct->status(); - if (status!=replay->status()) + auto status=direct->status(); + if (status!=replay->status() || direct->own.state()!=replay->own.state()) return false; if (status==SS_BRANCH) { std::unique_ptr next(direct->choice()); std::unique_ptr next_replay(replay->choice()); - if (!same_archive(*next,*next_replay)) - return false; + if (!same_archive(*next,*next_replay)) return false; } } - if (source.state()!=source_state || root->variable.state()!=variable.state()) - return false; root->commit(*choice,0); } return true; } - std::vector solutions(const Gecode::Rnd& source, - unsigned int distance, bool separate, - bool multi, bool callback, + template + std::vector solutions(const Random& source, unsigned int distance, + bool different, bool multi, bool callback, unsigned int threads=1) { using namespace Gecode; - ReplaySpace root(source,separate,multi,callback); + using Model=ReplaySpace; + Model root(source,different,multi,callback); Search::Options options; - options.c_d=distance; - options.a_d=distance; - options.threads=threads; - DFS search(&root,options); + options.c_d=distance; options.a_d=distance; options.threads=threads; + DFS search(&root,options); std::vector result; - while (std::unique_ptr s{search.next()}) { + while (std::unique_ptr s{search.next()}) { std::ostringstream item; - item << s->x << ':' << s->variable.state() << ':' << s->value.state(); + item << s->x << ':' << s->own.state(); result.push_back(item.str()); } return result; } + template + bool branch_replay(const Random& source) { + for (bool different : {false,true}) + for (bool multi : {false,true}) + for (bool callback : {false,true}) { + if (!choice_replay(source,different,multi,callback)) return false; + auto cloned=solutions(source,1,different,multi,callback); + auto recomputed=solutions(source,100,different,multi,callback); + if (cloned.size()!=81 || cloned!=recomputed) return false; + auto parallel=solutions(source,100,different,multi,callback,2); + std::sort(cloned.begin(),cloned.end()); + std::sort(parallel.begin(),parallel.end()); + if (cloned!=parallel) return false; + } + return true; + } + + class LDSBSpace : public Gecode::Space { + public: + Gecode::IntVarArray x; + LDSBSpace() : x(*this,4,0,3) { + using namespace Gecode; + Symmetries syms; + syms << VariableSymmetry(x); + distinct(*this,x); + branch(*this,x,INT_VAR_RND(Rnd(42)),INT_VAL_RND(Rnd(7)),syms); + } + LDSBSpace(LDSBSpace& s) : Space(s) { x.update(*this,s.x); } + Space* copy() override { return new LDSBSpace(*this); } + }; + + bool ldsb_replay() { + using namespace Gecode; + LDSBSpace root; + while (root.status()==SS_BRANCH) { + std::unique_ptr before(root.clone()); + std::unique_ptr choice(root.choice()); + for (unsigned int a=choice->alternatives(); a--;) { + std::unique_ptr direct(root.clone()), replay(before->clone()); + Archive archive; choice->archive(archive); + std::unique_ptr restored(replay->choice(archive)); + if (!same_archive(*choice,*restored)) return false; + direct->commit(*choice,a); replay->commit(*restored,a); + auto status=direct->status(); + if (status!=replay->status()) return false; + if (status==SS_BRANCH) { + std::unique_ptr next(direct->choice()); + std::unique_ptr next_replay(replay->choice()); + if (!same_archive(*next,*next_replay)) return false; + } + } + root.commit(*choice,0); + } + return true; + } + class BranchReplay : public Base { public: BranchReplay() : Base("Random::BranchReplay", TestTags::check()) {} bool run() override { - Gecode::Rnd engines[] = { - Gecode::Rnd(42), - Gecode::Rnd(Gecode::Support::Random(42)), - Gecode::Rnd(Gecode::Support::Random(42)) - }; - for (const auto& engine : engines) - for (bool separate : {false,true}) - for (bool multi : {false,true}) { - for (bool callback : {false,true}) { - if (!choice_replay(engine,separate,multi,callback)) - return false; - auto cloned = solutions(engine,1,separate,multi,callback); - auto recomputed = solutions(engine,100,separate,multi,callback); - if (cloned.size()!=81 || cloned!=recomputed) - return false; - auto parallel = solutions(engine,100,separate,multi,callback,2); - std::sort(cloned.begin(),cloned.end()); - std::sort(parallel.begin(),parallel.end()); - if (cloned!=parallel) - return false; - } - } - return true; + return ldsb_replay() && branch_replay(Gecode::Rnd(42)) && + branch_replay(Gecode::RndGenerator(42)) && + branch_replay(Gecode::RndGenerator(42)); } - } branch_replay; + } branch_replay_test; - class StateTracer : public Gecode::Tracer { - public: - std::string observed; - void propagate(const Gecode::Space&, const Gecode::PropagateTraceInfo&) override {} - void post(const Gecode::Space&, const Gecode::PostTraceInfo&) override {} - void commit(const Gecode::Space& home, const Gecode::CommitTraceInfo&) override { - observed=static_cast(home).variable.state(); + template + bool consumer_states(const Random& source, bool multi) { + using namespace Gecode; + // Inspect original-space selectors only; clones own separate copies. + for (unsigned int a=0; a<(multi ? 3U : 2U); ++a) { + ReplaySpace root(source,false,multi); + if (root.status()!=SS_BRANCH) return false; + auto original=source.state_words(); + std::vector observed(Random::words()); + root.variable->random_save(observed.data()); + if (!std::equal(original.begin(),original.end(),observed.begin())) return false; + std::unique_ptr first(root.choice()); + const auto& deterministic=static_cast(*first); + if (deterministic.random_data()!=nullptr) return false; + Archive plain; first->archive(plain); + if (plain.size()!=3) return false; // No global RNG pointer/count/archive data. + root.commit(*first,0); + root.variable->random_save(observed.data()); + if (!std::equal(original.begin(),original.end(),observed.begin())) return false; + if (root.status()!=SS_BRANCH) return false; + std::unique_ptr choice(root.choice()); + const uint64_t* recorded=static_cast(*choice).random_data(); + if (!recorded) return false; + Random expected=source; + expected.restore_split(recorded,a); + // Perturb the selector after taking the choice. Commit must restore it. + root.variable->random_commit(recorded,123); + root.commit(*choice,a); + root.variable->random_save(observed.data()); + auto expected_words=expected.state_words(); + if (!std::equal(expected_words.begin(),expected_words.end(),observed.begin())) + return false; + if (!multi) { + expected.restore_split(recorded+Random::words(),a); + root.value->random_save(observed.data()); + expected_words=expected.state_words(); + if (!std::equal(expected_words.begin(),expected_words.end(),observed.begin())) + return false; + } + if (root.own.state()!=source.state()) return false; } - }; + return true; + } class CommitBoundary : public Base { public: CommitBoundary() : Base("Random::CommitBoundary", TestTags::check()) {} bool run() override { using namespace Gecode; - StateTracer tracer; - ReplaySpace root(Rnd(7),false); - trace(root,TE_COMMIT,tracer); - root.status(); - std::unique_ptr choice(root.choice()); - auto expected=root.variable.split(1).state(); - std::unique_ptr copy(static_cast(root.clone())); - copy->trycommit(*choice,1); - if (copy->variable.state()!=expected || tracer.observed!=expected) - return false; - std::unique_ptr skipped(static_cast(root.clone())); - BrancherGroup::all.kill(*skipped); - auto before=skipped->variable.state(); - skipped->trycommit(*choice,1); - if (skipped->variable.state()!=before) - return false; - skipped->fail(); - skipped->commit(*choice,1); - if (skipped->variable.state()!=before) - return false; - try { root.commit(*choice,choice->alternatives()); return false; } - catch (const SpaceIllegalAlternative&) {} - return root.variable.state()==before; + static_assert(sizeof(Rnd)==sizeof(Support::RandomGenerator), + "Rnd must contain only inline engine state"); + Rnd r(7), copy=r; + auto state=r.state(); + (void) copy(UINT64_MAX); + if (r.state()!=state || copy.state()==state) return false; + return consumer_states(r,false) && consumer_states(r,true) && + consumer_states(RndGenerator(7),false) && + consumer_states(RndGenerator(7),true); } } commit_boundary; } diff --git a/tools/random-benchmark.cpp b/tools/random-benchmark.cpp index 8bbf408ef0..6e3699bc5d 100644 --- a/tools/random-benchmark.cpp +++ b/tools/random-benchmark.cpp @@ -33,6 +33,7 @@ // Compile against main without RANDOM_NEW, or against feature/random with it. #include +#include #include #include #include @@ -72,6 +73,11 @@ int main(int argc, char** argv) { const unsigned int seed=argc>2 ? std::stoul(argv[2]) : 42; if (!draws) return 1; std::cout << "size.engine\t" << sizeof(Support::RandomGenerator) << "\tbytes\t0\n" + << "size.rnd\t" << sizeof(Rnd) << "\tbytes\t0\n" + << "size.var_selector\t" << sizeof(ViewSelRnd) << "\tbytes\t0\n" + << "size.val_selector\t" << sizeof(Int::Branch::ValSelRnd) << "\tbytes\t0\n" + << "size.var_description\t" << sizeof(IntVarBranch) << "\tbytes\t0\n" + << "size.val_description\t" << sizeof(IntValBranch) << "\tbytes\t0\n" << "size.space\t" << sizeof(Space) << "\tbytes\t0\n" << "size.choice\t" << sizeof(PosValChoice) << "\tbytes\t0\n"; Support::RandomGenerator raw(seed); @@ -128,8 +134,11 @@ int main(int argc, char** argv) { << '\t' << archive.size()*sizeof(unsigned int) << "\tbytes\t0\n"; #ifdef RANDOM_NEW std::cout << (random ? "size.random_snapshot" : "size.plain_snapshot") - << '\t' << (archive[1] ? (archive[1]+1)*sizeof(uint64_t) : 0) + << '\t' << (random ? sizeof(Support::RandomGenerator) : 0) << "\tbytes\t0\n"; + std::cout << (random ? "size.random_choice" : "size.plain_choice") + << '\t' << (random ? sizeof(RndChoice>)+sizeof(Support::RandomGenerator) + : sizeof(PosValChoice)) << "\tbytes\t0\n"; #endif measure(random ? "clone.random" : "clone.plain",10000,[&] { uint64_t sum=0; From 90db8f2a1b7172ceff0af69624b797abfa270d19 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Thu, 10 Sep 2026 14:54:20 +0200 Subject: [PATCH 07/16] random: clarify feature overview and clean up integration --- CMakeLists.txt | 4 +++ Makefile.in | 1 - changelog.in | 25 +++++++++++-------- cmake/GecodeSources.cmake | 1 - docs/random.md | 20 +++++++++++---- gecode/flatzinc.hh | 2 +- gecode/int/ldsb/brancher.hpp | 13 +++------- gecode/kernel/data/rnd.cpp | 38 ----------------------------- plans/random.md | 47 ++++++++++++------------------------ 9 files changed, 54 insertions(+), 97 deletions(-) delete mode 100644 gecode/kernel/data/rnd.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 59e9df0b06..66302a694f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1555,6 +1555,8 @@ if(BUILD_TESTING) add_test(NAME random-state-replay COMMAND ${CMAKE_COMMAND} -DREPLAY=$ -P ${CMAKE_CURRENT_SOURCE_DIR}/test/random-replay.cmake) + set_tests_properties(random-state-replay PROPERTIES + FIXTURES_REQUIRED gecode-test-built) if(GECODE_ENABLE_DRIVER) add_executable(gecode-random-options EXCLUDE_FROM_ALL test/random-options.cpp) target_link_libraries(gecode-random-options PRIVATE gecodedriver ${GECODE_TEST_LINK_LIBS}) @@ -1566,6 +1568,8 @@ if(BUILD_TESTING) COMMAND ${CMAKE_COMMAND} -DOPTIONS=$ -DFLATZINC=${GECODE_ENABLE_FLATZINC} -P ${CMAKE_CURRENT_SOURCE_DIR}/test/random-options.cmake) + set_tests_properties(random-options PROPERTIES + FIXTURES_REQUIRED gecode-test-built) endif() if(GECODE_ENABLE_FAULT_INJECTION) add_executable(gecode-fault-test EXCLUDE_FROM_ALL diff --git a/Makefile.in b/Makefile.in index 954393a08c..31c325930f 100755 --- a/Makefile.in +++ b/Makefile.in @@ -201,7 +201,6 @@ VARIMP = $(VARIMPHDR) KERNELSRC0 = \ archive core exception gpi \ - data/rnd \ branch/action branch/afc branch/chb branch/function \ memory/manager memory/region \ trace/recorder trace/filter trace/tracer trace/general \ diff --git a/changelog.in b/changelog.in index 0115e1edd1..7f78b9336c 100755 --- a/changelog.in +++ b/changelog.in @@ -81,16 +81,21 @@ Module: kernel What: new Rank: major [DESCRIPTION] -Random generators are compact values stored directly in their consumers. -Randomized branchers record only their own selectors' splitting state in choices. -Every alternative derives a distinct successor state that is stable -when the same recorded path is recomputed. Splittable SplitMix is the default; -xorshift64* is a configurable smaller-state alternative. Users can supply engines -through the generic branching APIs. Rnd copies are independent state copies; -there is no space-managed random context or shared mutable generator handle. -Drivers accept checked 64-bit seeds and complete state for replay. Test failures -report the exact iteration state. Seeded sequences and choice archives change; -no legacy sequence mode is provided. See docs/random.md for migration and costs. +Added modern, user-extensible random number generators with reproducible +stream splitting. Splittable SplitMix replaces the old default generator; +xorshift64* is available as a smaller-state alternative. The default is +configurable at build time, and users can supply their own engines. +Randomized branching splits the recorded generator state by alternative, +giving distinct successor states that are reproduced during recomputation. +Complete-state save/restore and command-line state input support exact replay, +including test failures. +[MORE] +Generators store their compact state directly in their consumers, and copying +a generator produces independent state. Drivers accept checked 64-bit seeds +as well as complete state. APIs, seeded sequences, and randomized choice +archives change; no legacy sequence mode is provided. This design remains +provisional for a future breaking-change release. See docs/random.md for +engine contracts, migration, and measured costs. [RELEASE] Version: 6.5.0 diff --git a/cmake/GecodeSources.cmake b/cmake/GecodeSources.cmake index b125fc6bef..4224c040ea 100644 --- a/cmake/GecodeSources.cmake +++ b/cmake/GecodeSources.cmake @@ -19,7 +19,6 @@ set(GECODE_KERNEL_SOURCES gecode/kernel/branch/function.cpp gecode/kernel/core.cpp gecode/kernel/data/array.cpp - gecode/kernel/data/rnd.cpp gecode/kernel/exception.cpp gecode/kernel/gpi.cpp gecode/kernel/memory/manager.cpp diff --git a/docs/random.md b/docs/random.md index 232a21d449..6fe23fffc2 100644 --- a/docs/random.md +++ b/docs/random.md @@ -1,8 +1,18 @@ -# Random generators for a future Gecode release +# Extensible random generators and stream splitting This is a provisional Gecode 7 design. It changes APIs, seeded sequences, and randomized choice archives and is intended only for a breaking-change release. +Gecode provides splittable SplitMix and xorshift64* generators, together with an +engine interface for user-defined alternatives. The build selects one default +for the modeling API and command-line drivers; generic selectors can use other +engines. SplitMix is the provisional default because its indexed splitting is +cheaper, while xorshift64* uses half the state storage. + +Randomized branching splits streams by alternative and reproduces their states +during recomputation. Complete-state save/restore supports replay independently +of seed expansion, including engines with more than 64 bits of state. + ## State belongs to the consumer A generator is a small value. A model, selector, or custom brancher stores that @@ -207,8 +217,8 @@ controlled tree always has 32767 nodes and 16384 solutions. Queens also reports node count because changes in its tree can affect timing. Avoid concurrent compilation while measuring. -Measurements after the ownership correction, on arm64 macOS with Apple Clang 21 -in Release mode, use five measured repetitions, one warmup, seed 42, and main +Measurements on arm64 macOS with Apple Clang 21 in Release mode use five +measured repetitions, one warmup, seed 42, and main at `6b7de57b04` as the baseline. Sizes are bytes: | Object | Main | SplitMix | Xorshift64* | @@ -234,5 +244,5 @@ The plain tree was 1.03x and 1.05x in both builds. Indexed binary split-plus-dra cost about 15.7 ns for SplitMix and 237 ns for xorshift64*. Queens took 20.1 ms with SplitMix (approximately main's time) and 21.2 ms with xorshift64* (1.06x). Its node counts were 9333 on main, 9325 with SplitMix, and 9323 with xorshift64*. -These are local measurements, not general performance guarantees. They replace -the earlier space-local results and leave the default-engine decision provisional. +These are local measurements, not general performance guarantees. The +default-engine decision remains provisional. diff --git a/gecode/flatzinc.hh b/gecode/flatzinc.hh index a1f06a2abd..00a6476b04 100755 --- a/gecode/flatzinc.hh +++ b/gecode/flatzinc.hh @@ -411,7 +411,7 @@ namespace Gecode { namespace FlatZinc { BranchInformation& operator =(const BranchInformation&) = default; }; - /// Uninitialized default random number generator + /// Default random number generator, initialized with seed 0 GECODE_FLATZINC_EXPORT extern Rnd defrnd; diff --git a/gecode/int/ldsb/brancher.hpp b/gecode/int/ldsb/brancher.hpp index d7de70ddd7..8556f2df1c 100755 --- a/gecode/int/ldsb/brancher.hpp +++ b/gecode/int/ldsb/brancher.hpp @@ -146,15 +146,10 @@ namespace Gecode { namespace Int { namespace LDSB { class Filter, class Print> const Choice* LDSBBrancher::choice(Space& home) { - // Making the PVC here is not so nice, I think. - const Choice* c = ViewValBrancher::choice(home); - const PosValChoice* pvc = static_cast* >(c); - - // Compute symmetries. - - int choicePos = pvc->pos().pos; - int choiceVal = pvc->val(); - delete c; + Pos p = ViewBrancher::pos(home); + View v = ViewBrancher::view(p); + int choicePos = p.pos; + Val choiceVal = this->vsc->val(home,v,choicePos); _prevPos = choicePos; diff --git a/gecode/kernel/data/rnd.cpp b/gecode/kernel/data/rnd.cpp deleted file mode 100644 index 98544a51b9..0000000000 --- a/gecode/kernel/data/rnd.cpp +++ /dev/null @@ -1,38 +0,0 @@ -/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ -/* - * Main authors: - * Christian Schulte - * Mikael Zayenz Lagerkvist - * - * Copyright: - * Christian Schulte, 2008 - * Mikael Zayenz Lagerkvist, 2008 - * - * 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 - -// STATISTICS: kernel-other diff --git a/plans/random.md b/plans/random.md index 9d1709218f..2f818c8fc6 100644 --- a/plans/random.md +++ b/plans/random.md @@ -1,8 +1,7 @@ -# Plan: Compact random values and reproducible splitting +# Plan: Extensible random generators and reproducible splitting > Provisional draft for Gecode 7 or another future breaking-change release only. -> Ownership corrected after review: state belongs to the consumer, not Space. -> Status: implementation corrected; final verification and measurements in progress. +> Status: implemented and locally verified; design remains under review in draft PR #241. ## Goal @@ -10,12 +9,6 @@ Replace the old default generator, provide user-extensible alternatives and exac state replay, and split randomized branching states by alternative. Keep states small enough to store directly in the model, selector, or brancher that uses them. -The original implementation introduced a space-managed context and shared stream -identity. That was an incorrect expansion of the requirement. The earlier phase -reviews and performance claims are superseded where they depend on that ownership -model. Their history remains in commits 626fb2d307, b4ec5dfe99, 2130ef3850, -3665a1bed2, and a1ff8e3385. - ## Required behavior 1. A generator is a value containing its engine state inline. Copying it produces @@ -47,9 +40,6 @@ model. Their history remains in commits 626fb2d307, b4ec5dfe99, 2130ef3850, ### Value ownership - [x] Replace shared-handle Rnd with RndGenerator and a configured Rnd alias. -- [x] Remove RandomContext, origin identities, binding, Space::random(), and - Space::random_split(). -- [x] Restore kernel/core.hpp and core.cpp to their pre-feature state. - [x] Copy selector and model generators normally; remove random-handle disposal overhead for trivially destructible built-in engines. - [x] Make relaxation take a generator reference, explicitly advancing its owner. @@ -67,23 +57,22 @@ model. Their history remains in commits 626fb2d307, b4ec5dfe99, 2130ef3850, ### Tests and documentation -- [x] Replace tests of global stream coordination with tests of independent copies, - untouched model/later-selector state, and consumer-local splitting. +- [x] Test independent copies, untouched model/later-selector state, and + consumer-local splitting. - [x] Check direct/archived choices, perturbed destination selector state, sibling exploration, cloning/recomputation, and parallel solution agreement. - [x] Exercise a three-word external engine in selectors and a runnable example. - [x] Retain engine vectors, full-state failure replay, CLI validation, and failed-clone resource checks. -- [x] Rewrite docs and release notes to describe value ownership accurately. +- [x] Document engine extension, splitting, full-state replay, and migration. - [x] Run full relevant checks for both defaults and the reduced static/no-thread build. -- [x] Remeasure compactness and representative costs after removing the context. -- [x] Review the correction and update the existing provisional draft PR. +- [x] Measure compactness and representative costs against main. +- [x] Review the implementation and update the provisional draft PR. ## Verification strategy Use the existing focused Random::Contract, Random::BranchReplay, and -Random::CommitBoundary tests. The latter now checks consumer ownership rather than -the removed kernel-wide commit hook. Fault::Random::CloneFailures counts inline +Random::CommitBoundary tests. Fault::Random::CloneFailures counts inline custom-engine instances across failed clones. Existing Boolean, set, float, assignment, LDSB, and FlatZinc restart cases cover their integration paths. @@ -94,10 +83,9 @@ prove sibling-state distinction. Measure against the same main baseline (6b7de57b04), using the existing controlled tree and queens harness. Report actual generator/description/selector/choice sizes -and any performance costs. Do not reuse the space-local implementation's results -as evidence for the corrected design. +and any performance costs. -## Algorithm and CLI decisions retained +## Algorithm and CLI decisions Splittable SplitMix has two state words and constant-time indexed splitting. Xorshift64* has one state word and indexed native jumps. Both have documented @@ -113,23 +101,18 @@ Drivers retain -seed (examples), -r (FlatZinc), and -state. Time/hardware initialization reports concrete state. Incompatible, malformed, and conflicting input is rejected. No legacy sequence mode is required. -## Correction review - -The ownership error was architectural, not a bug in state copying. The previous -tests verified an expanded contract that the user did not intend. This correction -removes that contract and its infrastructure instead of optimizing it. +## Verification results Both engine configurations pass CMake check and all five CTests, including command-line state replay and fault injection. The reduced static/no-thread build passes check and the focused Random tests. Additional Boolean, set, float, assignment, filtered-tie, LDSB, and FlatZinc restart checks pass with both defaults. -Random::BranchReplay now also exercises randomized LDSB choice archives. The +Random::BranchReplay also exercises randomized LDSB choice archives. The custom-engine example's full-state replay and output agree across all three builds. -The corrected Space and base Choice implementation matches main exactly. Rnd is +The Space and base Choice implementation matches main exactly. Rnd is 16 bytes with SplitMix and 8 with xorshift64*, without shared allocation. Fresh five-run measurements are recorded in docs/random.md: SplitMix's controlled random tree costs 1.02x with cloning and 1.16x with recomputation relative to main; -xorshift64* costs 1.63x and 2.88x. No result from the removed space-local design is -used to justify this correction. Draft PR #241 describes this corrected contract -and remains provisional, for a future breaking-change release only. +xorshift64* costs 1.63x and 2.88x. Draft PR #241 remains provisional, for a future +breaking-change release only. From 91ac744646f21f86bb3406f43012de2f2f9276a1 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Sun, 4 Oct 2026 15:10:43 +0200 Subject: [PATCH 08/16] Keep random option state typed and document selector hooks --- docs/random.md | 14 +++++++++- gecode/driver.hh | 2 +- gecode/driver/options.cpp | 20 +++++-------- gecode/kernel/branch/val-sel-commit.hpp | 17 +++++++----- gecode/kernel/branch/val-sel.hpp | 3 ++ gecode/kernel/branch/view-sel.hpp | 37 ++++++++++++++++++------- 6 files changed, 61 insertions(+), 32 deletions(-) diff --git a/docs/random.md b/docs/random.md index 6fe23fffc2..8ef385bcaf 100644 --- a/docs/random.md +++ b/docs/random.md @@ -107,7 +107,19 @@ build/random/bin/random-engine counted-splitmix-v1:000000000000002a:9e3779b97f4a Both runs print the same initial state and enumerate the same 24 permutations. Custom selectors participate through `random_words()`, `random_save()`, and -`random_commit()`. State size and layout must be stable across clones. +`random_commit()`. `random_words()` reports the number of 64-bit words, with +size and layout fixed for the selector and its clones. After position and value +selection, `random_save(out)` writes that many words and returns the pointer +past them. It does not advance the generator. + +`random_commit(in,a)` reads the same number of words, restores the recorded +parent state, and replaces the selector's state with its `split(a)` state. It +returns the pointer past the words read. The input is the choice's snapshot; +the destination's current state does not affect the split. A selector with no +random state reports zero words and leaves both pointers and state unchanged. +Branchers chain these pointers in variable-selector order, then value-selector +order. + Custom branchers can instead store their generator and its choice snapshot as ordinary typed members and implement the same capture/restore/split rule directly. diff --git a/gecode/driver.hh b/gecode/driver.hh index 4059b2aba4..0d744f505a 100755 --- a/gecode/driver.hh +++ b/gecode/driver.hh @@ -149,7 +149,7 @@ namespace Gecode { /// Checked seed or complete state for the build-configured random engine. class GECODE_DRIVER_EXPORT RandomOption : public BaseOption { uint64_t cur; - std::string initial; + Rnd initial; bool seed_given = false; bool state_given = false; bool state_only = false; diff --git a/gecode/driver/options.cpp b/gecode/driver/options.cpp index 56926ed3db..f92160fc89 100755 --- a/gecode/driver/options.cpp +++ b/gecode/driver/options.cpp @@ -118,7 +118,7 @@ namespace Gecode { } void RandomOption::value(uint64_t v) { cur = v; - initial = Support::RandomGenerator(v).state_string(); + initial.seed(v); seed_given = state_given = state_only = false; } uint64_t RandomOption::value(void) const { @@ -127,9 +127,7 @@ namespace Gecode { return cur; } Rnd RandomOption::rnd(void) const { - Rnd r; - r.state(initial); - return r; + return initial; } int RandomOption::parse(int argc, char* argv[]) { bool full = argc >= 2 && @@ -150,20 +148,16 @@ namespace Gecode { if ((full && seed_given) || (!full && state_given)) throw std::invalid_argument("Seed and state options cannot be combined"); if (full) { - Support::RandomGenerator r; - r.state(std::string(arg)); - initial = r.state_string(); + initial.state(std::string(arg)); state_given = state_only = true; } else { if (!strcmp(arg,"time") || !strcmp(arg,"hw")) { - Rnd r; - if (!strcmp(arg,"time")) r.time(); else r.hw(); - initial = r.state(); + if (!strcmp(arg,"time")) initial.time(); else initial.hw(); state_only = true; - std::cerr << "% Random state: -state " << initial << std::endl; + std::cerr << "% Random state: -state " << initial.state() << std::endl; } else { cur = Support::random_seed(arg); - initial = Support::RandomGenerator(cur).state_string(); + initial.seed(cur); state_only = false; } seed_given = true; @@ -179,7 +173,7 @@ namespace Gecode { << "\t\t" << exp << "\n" << "\t-state (complete " << Support::RandomGenerator::name() << " state; mutually exclusive with " << iopt << ")\n" - << "\t\tCurrent initial state: " << initial << std::endl; + << "\t\tCurrent initial state: " << initial.state() << std::endl; } StringValueOption::StringValueOption(const char* o, const char* e, diff --git a/gecode/kernel/branch/val-sel-commit.hpp b/gecode/kernel/branch/val-sel-commit.hpp index 6671a69c0f..6f3b1e3935 100644 --- a/gecode/kernel/branch/val-sel-commit.hpp +++ b/gecode/kernel/branch/val-sel-commit.hpp @@ -43,8 +43,11 @@ namespace Gecode { template class ValSelCommitBase { public: + /// \copydoc Gecode::ViewSel::random_words virtual unsigned int random_words(void) const { return 0; } + /// \copydoc Gecode::ViewSel::random_save virtual uint64_t* random_save(uint64_t* out) const { return out; } + /// \copydoc Gecode::ViewSel::random_commit virtual const uint64_t* random_commit(const uint64_t* in, unsigned int) { return in; } /// View type typedef View_ View; @@ -118,22 +121,22 @@ namespace Gecode { /// Constructor for cloning ValSelCommit(Space& home, ValSelCommit& vsc); /// Return value of view \a x at position \a i - virtual Val val(const Space& home, View x, int i); + virtual Val val(const Space& home, View x, int i) override; /// Commit view \a x at position \a i to value \a n for alternative \a a - virtual ModEvent commit(Space& home, unsigned int a, View x, int i, Val n); + virtual ModEvent commit(Space& home, unsigned int a, View x, int i, Val n) override; /// Create no-good literal for choice \a c and alternative \a a virtual NGL* ngl(Space& home, unsigned int a, - View x, Val n) const; + View x, Val n) const override; /// Print on \a o branch for alternative \a a, view \a x at position \a i, and value \a n virtual void print(const Space& home, unsigned int a, View x, int i, const Val& n, - std::ostream& o) const; + std::ostream& o) const override; /// Perform cloning - virtual ValSelCommit* copy(Space& home); + virtual ValSelCommit* copy(Space& home) override; /// Whether dispose must always be called (that is, notice is needed) - virtual bool notice(void) const; + virtual bool notice(void) const override; /// Delete value selection - virtual void dispose(Space& home); + virtual void dispose(Space& home) override; }; //@} diff --git a/gecode/kernel/branch/val-sel.hpp b/gecode/kernel/branch/val-sel.hpp index 5fa821eb87..be21ac2e4e 100755 --- a/gecode/kernel/branch/val-sel.hpp +++ b/gecode/kernel/branch/val-sel.hpp @@ -43,8 +43,11 @@ namespace Gecode { template class ValSel { public: + /// \copydoc Gecode::ViewSel::random_words unsigned int random_words(void) const { return 0; } + /// \copydoc Gecode::ViewSel::random_save uint64_t* random_save(uint64_t* out) const { return out; } + /// \copydoc Gecode::ViewSel::random_commit const uint64_t* random_commit(const uint64_t* in, unsigned int) { return in; } /// View type typedef View_ View; diff --git a/gecode/kernel/branch/view-sel.hpp b/gecode/kernel/branch/view-sel.hpp index 4daf62f7c0..00b949b639 100644 --- a/gecode/kernel/branch/view-sel.hpp +++ b/gecode/kernel/branch/view-sel.hpp @@ -43,9 +43,26 @@ namespace Gecode { template class ViewSel { public: - /// State owned by this selector, recorded only by its brancher. + /** \brief Number of 64-bit words needed to record this selector's random state + * + * Count and layout must remain fixed for the selector and its clones. + * Selectors without random state return zero. + */ virtual unsigned int random_words(void) const { return 0; } + /** \brief Save random state after position and value selection + * + * Write exactly random_words() words to \a out without advancing the + * generator, and return the pointer past them. The caller supplies that + * many writable words. A zero-word selector returns \a out unchanged. + */ virtual uint64_t* random_save(uint64_t* out) const { return out; } + /** \brief Restore recorded random state and split for the given alternative + * + * Read exactly random_words() words from \a in and return the pointer + * past them. Replace the selector's state with the recorded parent's + * split for the public alternative index, independently of its current + * state. A zero-word selector leaves its state and \a in unchanged. + */ virtual const uint64_t* random_commit(const uint64_t* in, unsigned int) { return in; } /// Define the view type typedef View_ View; @@ -174,30 +191,30 @@ namespace Gecode { /// \name View selection and tie breaking //@{ /// Select a view from \a x starting from \a s and return its position - virtual int select(Space& home, ViewArray& x, int s); + virtual int select(Space& home, ViewArray& x, int s) override; /// Select a view from \a x starting from \a s and return its position virtual int select(Space& home, ViewArray& x, int s, - BrancherFilter& f); + BrancherFilter& f) override; /// Select ties from \a x starting from \a s virtual void ties(Space& home, ViewArray& x, int s, - int* ties, int& n); + int* ties, int& n) override; /// Select ties from \a x starting from \a s virtual void ties(Space& home, ViewArray& x, int s, int* ties, int& n, - BrancherFilter& f); + BrancherFilter& f) override; /// Break ties in \a x and update to new ties - virtual void brk(Space& home, ViewArray& x, int* ties, int& n); + virtual void brk(Space& home, ViewArray& x, int* ties, int& n) override; /// Select a view from \a x considering view with positions in \a ties - virtual int select(Space& home, ViewArray& x, int* ties, int n); + virtual int select(Space& home, ViewArray& x, int* ties, int n) override; //@} /// \name Resource management and cloning //@{ /// Create copy during cloning - virtual ViewSel* copy(Space& home); + virtual ViewSel* copy(Space& home) override; /// Whether dispose must always be called (that is, notice is needed) - virtual bool notice(void) const; + virtual bool notice(void) const override; /// Delete view selection - virtual void dispose(Space& home); + virtual void dispose(Space& home) override; //@} }; From b4f447be1f8432da9e994ee158a37e6560bfb320 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Mon, 5 Oct 2026 07:43:06 +0200 Subject: [PATCH 09/16] Fix C++17 consumer checks and diversify job-shop probes --- .github/ci/cmake-consumer-smoke.ps1 | 1 + .github/ci/custom-vis-smoke.sh | 2 +- examples/job-shop.cpp | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/ci/cmake-consumer-smoke.ps1 b/.github/ci/cmake-consumer-smoke.ps1 index c651484820..1ede4169bc 100644 --- a/.github/ci/cmake-consumer-smoke.ps1 +++ b/.github/ci/cmake-consumer-smoke.ps1 @@ -125,6 +125,7 @@ find_library(GECODE_SUPPORT_LIBRARY NO_DEFAULT_PATH REQUIRED) add_executable(manual_consumer main.cpp) +target_compile_features(manual_consumer PRIVATE cxx_std_17) target_include_directories(manual_consumer PRIVATE "${GECODE_INCLUDE_DIR}") target_link_libraries(manual_consumer PRIVATE "${GECODE_SUPPORT_LIBRARY}") '@ | Set-Content -Encoding utf8 (Join-Path $work "manual\CMakeLists.txt") diff --git a/.github/ci/custom-vis-smoke.sh b/.github/ci/custom-vis-smoke.sh index 93ecc82c21..cf6d411a60 100644 --- a/.github/ci/custom-vis-smoke.sh +++ b/.github/ci/custom-vis-smoke.sh @@ -33,7 +33,7 @@ awk ' ' gecode/int/var-imp/int.vis > custom-two.vis CFLAGS="-fPIC" \ -CXXFLAGS="-std=c++11 -fPIC -DGECODE_MEMORY_ALIGNMENT=16" \ +CXXFLAGS="-std=c++17 -fPIC -DGECODE_MEMORY_ALIGNMENT=16" \ dash ./configure \ --with-vis="$PWD/custom-one.vis,$PWD/custom-two.vis" \ --disable-cpprofiler \ diff --git a/examples/job-shop.cpp b/examples/job-shop.cpp index e8b371b72a..7331062bc9 100755 --- a/examples/job-shop.cpp +++ b/examples/job-shop.cpp @@ -723,7 +723,7 @@ solve(const JobShopOptions& opt) { for (unsigned int p=0; p(master->clone()); - jsp->branch(p,rnd); + jsp->branch(p,rnd.split(p)); DFS dfs(jsp,so); JobShopProbe* s = dfs.next(); Search::Statistics statj = dfs.statistics(); From b2f30ca3837a8a9466f41976a5ca26bef762351d Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Mon, 5 Oct 2026 17:27:09 +0200 Subject: [PATCH 10/16] Declare each variable separately --- gecode/kernel/branch/view.hpp | 4 +++- test/random.cpp | 12 ++++++++---- tools/random-benchmark.cpp | 3 ++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/gecode/kernel/branch/view.hpp b/gecode/kernel/branch/view.hpp index 8f93381650..770a486865 100644 --- a/gecode/kernel/branch/view.hpp +++ b/gecode/kernel/branch/view.hpp @@ -94,7 +94,9 @@ namespace Gecode { const uint64_t* random_data(void) const override { return data(); } void read(Archive& e) { for (unsigned int i=0; i> lo >> hi; + unsigned int lo; + unsigned int hi; + e >> lo >> hi; data()[i] = uint64_t(lo) | (uint64_t(hi)<<32); } } diff --git a/test/random.cpp b/test/random.cpp index 0bb9c3ad1f..2b37a8c3cd 100644 --- a/test/random.cpp +++ b/test/random.cpp @@ -68,7 +68,8 @@ namespace Test { template bool replay() { - Gecode::Support::Random original(UINT64_MAX), restored; + Gecode::Support::Random original(UINT64_MAX); + Gecode::Support::Random restored; for (unsigned int i=0; i<17; ++i) (void) original(13); original = original.split(37); @@ -197,7 +198,8 @@ namespace Test { }; bool same_archive(const Gecode::Choice& a, const Gecode::Choice& b) { - Gecode::Archive x,y; + Gecode::Archive x; + Gecode::Archive y; a.archive(x); b.archive(y); if (x.size()!=y.size()) return false; for (int i=0; i before(root.clone()); std::unique_ptr choice(root.choice()); for (unsigned int a=choice->alternatives(); a--;) { - std::unique_ptr direct(root.clone()), replay(before->clone()); + std::unique_ptr direct(root.clone()); + std::unique_ptr replay(before->clone()); Archive archive; choice->archive(archive); std::unique_ptr restored(replay->choice(archive)); if (!same_archive(*choice,*restored)) return false; @@ -373,7 +376,8 @@ namespace Test { using namespace Gecode; static_assert(sizeof(Rnd)==sizeof(Support::RandomGenerator), "Rnd must contain only inline engine state"); - Rnd r(7), copy=r; + Rnd r(7); + Rnd copy=r; auto state=r.state(); (void) copy(UINT64_MAX); if (r.state()!=state || copy.state()==state) return false; diff --git a/tools/random-benchmark.cpp b/tools/random-benchmark.cpp index 6e3699bc5d..0b1fd11922 100644 --- a/tools/random-benchmark.cpp +++ b/tools/random-benchmark.cpp @@ -57,7 +57,8 @@ class Model : public Space { : x(*this,n,0,queens ? n-1 : 1) { if (queens) { distinct(*this,x,IPL_DOM); - IntArgs up(n),down(n); + IntArgs up(n); + IntArgs down(n); for (unsigned int i=0; i Date: Tue, 6 Oct 2026 17:34:23 +0200 Subject: [PATCH 11/16] Keep random generator documentation beside its interfaces --- changelog.in | 6 +- docs/cmake-build.md | 1 + docs/random.md | 260 ------------------------------------- examples/random-engine.cpp | 11 +- gecode/driver.hh | 12 +- gecode/flatzinc.hh | 5 + gecode/kernel/data/rnd.hpp | 23 +++- gecode/support/random.hpp | 17 ++- plans/random.md | 6 +- test/random.cpp | 2 + tools/random-benchmark.py | 15 ++- 11 files changed, 79 insertions(+), 279 deletions(-) delete mode 100644 docs/random.md diff --git a/changelog.in b/changelog.in index 7f78b9336c..1464d1540d 100755 --- a/changelog.in +++ b/changelog.in @@ -94,8 +94,10 @@ Generators store their compact state directly in their consumers, and copying a generator produces independent state. Drivers accept checked 64-bit seeds as well as complete state. APIs, seeded sequences, and randomized choice archives change; no legacy sequence mode is provided. This design remains -provisional for a future breaking-change release. See docs/random.md for -engine contracts, migration, and measured costs. +provisional for a future breaking-change release. Replace the old seed() +snapshot accessor with state() and restore with state(text). Use opt.rnd() +instead of Rnd(opt.seed()) to honor complete-state command-line input. +Engine contracts are documented on Support::Random and RndGenerator. [RELEASE] Version: 6.5.0 diff --git a/docs/cmake-build.md b/docs/cmake-build.md index 744bc0acf1..13385a0d72 100644 --- a/docs/cmake-build.md +++ b/docs/cmake-build.md @@ -131,6 +131,7 @@ CMake exposes options aligned with the Autoconf switches: | `--enable-set-vars` | `GECODE_ENABLE_SET_VARS` | Supported directly | Default `ON` | | `--enable-float-vars` | `GECODE_ENABLE_FLOAT_VARS` | Supported directly | Default `ON` | | `--enable-minimodel` | `GECODE_ENABLE_MINIMODEL` | Supported directly | Default `ON` | +| `--with-random-engine` | `GECODE_RANDOM_ENGINE` | Supported directly | `splitmix` (default) or `xorshift64star` | | `--enable-instance` | `GECODE_ENABLE_INSTANCE` | Supported directly | Default `ON`; enabled with the driver | | `--enable-driver` | `GECODE_ENABLE_DRIVER` | Supported directly | Default `ON` | | `--enable-flatzinc` | `GECODE_ENABLE_FLATZINC` | Supported directly | Default `ON` | diff --git a/docs/random.md b/docs/random.md deleted file mode 100644 index 8ef385bcaf..0000000000 --- a/docs/random.md +++ /dev/null @@ -1,260 +0,0 @@ -# Extensible random generators and stream splitting - -This is a provisional Gecode 7 design. It changes APIs, seeded sequences, and -randomized choice archives and is intended only for a breaking-change release. - -Gecode provides splittable SplitMix and xorshift64* generators, together with an -engine interface for user-defined alternatives. The build selects one default -for the modeling API and command-line drivers; generic selectors can use other -engines. SplitMix is the provisional default because its indexed splitting is -cheaper, while xorshift64* uses half the state storage. - -Randomized branching splits streams by alternative and reproduces their states -during recomputation. Complete-state save/restore supports replay independently -of seed expansion, including engines with more than 64 bits of state. - -## State belongs to the consumer - -A generator is a small value. A model, selector, or custom brancher stores that -value directly in its own state. Copying it copies all its state independently. -There is no random context in Space, no registration, no shared mutable handle, -and no automatic coordination between consumers. - -```cpp -Rnd a(42); -Rnd b = a; // Independent copy at the same state. -Rnd child = a.split(1); // Does not change a. -``` - -The default `Rnd` contains exactly the configured engine's state: 16 bytes -for splittable SplitMix or 8 bytes for xorshift64*. It has no pointer, vtable, -reference count, or heap allocation. Default construction initializes seed 1. - -Passing a generator to two branching descriptions gives them two independent -copies. To start them differently, pass explicitly split generators. Posting -copies the description's state into its selector. Cloning copies selector state -without drawing or splitting. A model member is copied normally in the model's -copy constructor; it is not aliased to a selector that was initialized from it. - -## Branch selection and recomputation - -After choosing its position and value, a randomized brancher records the complete -state of its own selectors in its choice. Committing alternative `a` restores -that recorded state and derives each selector's `split(a)` state before the -value commit. The destination's current selector state is not the splitting -input. Archive reconstruction restores choice data without drawing or splitting. - -Only the active brancher's selectors participate. A deterministic brancher does -not advance a later random brancher or a model-owned generator. Nor does finishing -one random brancher change a separately posted brancher's state. Earlier branch -constraints can still affect later selection, of course. - -The rule applies to binary, multiway, and one-alternative assignment branchers. -It uses the public alternative index even when values are visited in reverse -order. Each random choice stores one parent state per selector, not one state -per alternative. Late alternatives require no replay of preceding alternatives. - -Randomized choices use `RndChoice`, with packed state words in the same -allocation as the choice. Nonrandom choices retain their original layout and -archive format. The base `Space` and `Choice` classes contain no RNG storage or -snapshot hooks. Multiple selectors are recorded independently, in tie-break order -followed by value selection; their types determine the state-word layout. - -This guarantees random state for the same recorded path. It does not guarantee -identical scheduling, solution order, adaptive heuristics, or entire search trees -under parallel search or weakly monotonic propagation. - -## Engines and extension - -`Support::Random` is the low-level value wrapper, supplying bounded -integer draws and canonical state text. `RndGenerator` is the modeling -wrapper; `Rnd` names its build-configured specialization. - -An engine supplies: - -- A nonempty `State = std::array` containing all mutable state and - per-stream parameters. -- Construction and `seed(uint64_t)`, with a documented rule for every seed. -- `next()`, `min()`, and `max()`, with raw output in `[0,UINT64_MAX]` or - `[1,UINT64_MAX]`. -- `state()` and `state(const State&)` for exact capture and validated restoration. -- `name()`, identifying the algorithm and state format. -- `split(uint32_t) const`, returning a child without changing its parent. - -All valid sibling indices must yield distinct states, not merely different first -outputs. State restoration must reproduce subsequent draws and splits and reject -invalid input before mutation. Ordinary copying must make state independent. - -The runnable `examples/random-engine.cpp` defines a three-word engine outside -Gecode. It delegates to SplitMix and adds a path-local raw-draw counter. Both -variable and value selectors store this user engine inline: - -```cpp -using Random = RndGenerator; -using VariableSelector = ViewSelRnd; -using ValueSelector = Int::Branch::ValSelRnd; -``` - -The example posts these with the existing generic view/value brancher machinery. -Its optional argument is a complete custom-engine state: - -```sh -cmake --build build/random --target random-engine -build/random/bin/random-engine -build/random/bin/random-engine counted-splitmix-v1:000000000000002a:9e3779b97f4a7c15:0000000000000000 -``` - -Both runs print the same initial state and enumerate the same 24 permutations. - -Custom selectors participate through `random_words()`, `random_save()`, and -`random_commit()`. `random_words()` reports the number of 64-bit words, with -size and layout fixed for the selector and its clones. After position and value -selection, `random_save(out)` writes that many words and returns the pointer -past them. It does not advance the generator. - -`random_commit(in,a)` reads the same number of words, restores the recorded -parent state, and replaces the selector's state with its `split(a)` state. It -returns the pointer past the words read. The input is the choice's snapshot; -the destination's current state does not affect the split. A selector with no -random state reports zero words and leaves both pointers and state unchanged. -Branchers chain these pointers in variable-selector order, then value-selector -order. - -Custom branchers can instead store their generator and its choice snapshot as -ordinary typed members and implement the same capture/restore/split rule directly. - -Randomness hidden inside a callback is not automatically recorded. If a callback -draws model-owned state during choice generation, its custom choice/commit -implementation must preserve that state for replay. A callback must not capture -a mutable RNG in a shared function object and assume that cloning copies it. -Callbacks that operate on model state during commit should access the destination -model and make their state transitions explicit. - -## Built-in algorithms - -| Engine | State | Indexed splitting | -| --- | ---: | --- | -| `Support::SplitMix` | 16 bytes | Constant-time sequential SplitMix child | -| `Support::Xorshift64Star` | 8 bytes | Jump in its native recurrence | - -For SplitMix parent `(s,g)`, child `a` is -`(Mix13(s+(2a+1)g), mixGamma(s+(2a+2)g))` modulo 2^64. This is the child -obtained by sequentially splitting `a+1` times, computed directly. Since `g` -is odd and Mix13 is a permutation, the first child word is distinct for every -32-bit alternative index. Seed expansion sets `s` to the seed and `g` to -`9e3779b97f4a7c15`. - -Xorshift64* uses shifts 12, 25, and 27 and multiplier 2685821657736338717. -Child `a` starts `(a+1)*2^32` recurrence steps ahead. Since 2^32 is coprime -to the period 2^64-1, sibling states are distinct. A shared immutable 16 KiB jump -table is initialized once; it is not per-generator state. The maximum index -jumps 2^64 steps, equivalent to one step modulo the period. Seeds set the state -directly, except seed zero maps to one; restoring zero state is an error. - -Neither construction promises globally non-overlapping streams throughout an -unbounded search tree. Xorshift64* has known low-bit weaknesses; see -[Vigna's discussion](https://prng.di.unimi.it/xorshift.php) and the -[SplitMix paper](https://gee.cs.oswego.edu/dl/papers/oopsla14.pdf). - -Bounded generation uses integer rejection sampling. Bounds zero, one, and negative -signed bounds return zero without drawing. Full-range engines reject an incomplete -bottom bucket; nonzero engines subtract one and reject an incomplete top bucket. -This accounts for xorshift's missing zero. There is no distribution cache. - -## Seeds, state, and command lines - -Full state text contains an algorithm/format identifier and fixed-width hexadecimal -words in a defined order, independent of host byte order. For example: -`splitmix-v1:000000000000002a:9e3779b97f4a7c15`. Restoration bypasses seed -expansion; it never repairs invalid state silently. - -The example driver accepts `-seed`; FlatZinc accepts `-r`. Values are checked -unsigned 64-bit decimal or `0x` hexadecimal integers, or `time` and `hw`. -Both accept `-state` for the configured engine and reject conflicting seed/state -arguments, invalid states, and incompatible identifiers. Double-hyphen spellings -are accepted. A 64-bit seed need not enumerate every possible full state. - -Time and hardware initialization occur once during parsing and report -`% Random state: -state ...` on standard error. Reuse those arguments with the -same executable, model, and search options. Help also prints the initial state. - -`opt.rnd()` returns an independent value at the configured initial state. Use it -instead of `Rnd(opt.seed())` to honor full-state input. The numeric accessor -throws for full-state, time, or hardware initialization. - -The test runner snapshots immediately before each iteration. Failure and exception -reports print `-state`, `-iter 1`, and `-test-exact` arguments that restore -the iteration directly, bypassing suite seed derivation. Replay requires one -thread and rejects an accompanying seed. - -CMake `-DGECODE_RANDOM_ENGINE=xorshift64star` and Autoconf -`--with-random-engine=xorshift64star` select the alternative default; both accept -`splitmix`. The installed configuration header carries that choice to clients. -Both concrete engine types remain available; no runtime registry is required. - -## Model-owned randomness and migration - -FlatZinc's restart/relaxation generator is an ordinary model member. Its explicit -meta-engine policy splits by restart/portfolio kind and the high/low words of the -logical index. This affects that member only, not the model's branchers. -Photo likewise splits its member before relaxation. The relaxation APIs take -`Rnd&` and advance the caller's value directly. - -Gecode 6 shared-handle behavior is removed: ordinary copying now copies state. -Replace the old zero-argument `seed()` snapshot accessor with `state()` and -restore with `state(text)`. Numeric construction is explicit; default construction -initializes seed 1. `copy()` remains a convenience equivalent to ordinary copying. - -Seeded sequences and randomized choice archives change. There is no legacy sequence -mode. The old FlatZinc 31-bit sampling workaround is removed. Release version and -ABI-number changes remain release preparation; this feature must not ship in a -compatibility-preserving release. - -## Measurements - -Use the existing bounded benchmark harness after building Gecode: - -```sh -clang++ -O3 -DNDEBUG -std=c++17 -DRANDOM_NEW -Ibuild/random -I. \ - tools/random-benchmark.cpp -Lbuild/random -lgecodeint -lgecodesearch \ - -lgecodekernel -lgecodesupport -Wl,-rpath,build/random \ - -o build/random/random-benchmark -python3 tools/random-benchmark.py build/random/random-benchmark \ - --baseline /path/to/baseline/random-benchmark --repeat 5 --output results.json -``` - -Compile the same source against baseline headers/libraries without `-DRANDOM_NEW`. -The harness alternates run order, discards one warmup, and reports medians. The -controlled tree always has 32767 nodes and 16384 solutions. Queens also reports -node count because changes in its tree can affect timing. Avoid concurrent -compilation while measuring. - -Measurements on arm64 macOS with Apple Clang 21 in Release mode use five -measured repetitions, one warmup, seed 42, and main -at `6b7de57b04` as the baseline. Sizes are bytes: - -| Object | Main | SplitMix | Xorshift64* | -| --- | ---: | ---: | ---: | -| `Rnd` | 8 (handle) | 16 | 8 | -| Random variable selector | 16 | 24 | 16 | -| Random value selector | 8 | 16 | 8 | -| Integer variable description | 112 | 120 | 112 | -| Integer value description | 80 | 88 | 80 | -| `Space` | 288 | 288 | 288 | -| Nonrandom position/value choice | 24 | 24 | 24 | -| Random choice, one selector | 24 | 48 | 40 | -| Nonrandom choice archive | 12 | 12 | 12 | -| Random choice archive, one selector | 12 | 28 | 20 | - -Main's handle size excludes its separately allocated shared implementation; the -new values contain all engine state. Random choice sizes include the inline -payload. Adding another random selector adds only its state words to that payload. - -Median controlled random-tree time relative to main was 1.02x with cloning and -1.16x with recomputation for SplitMix, versus 1.63x and 2.88x for xorshift64*. -The plain tree was 1.03x and 1.05x in both builds. Indexed binary split-plus-draw -cost about 15.7 ns for SplitMix and 237 ns for xorshift64*. Queens took 20.1 ms -with SplitMix (approximately main's time) and 21.2 ms with xorshift64* (1.06x). -Its node counts were 9333 on main, 9325 with SplitMix, and 9323 with xorshift64*. -These are local measurements, not general performance guarantees. The -default-engine decision remains provisional. diff --git a/examples/random-engine.cpp b/examples/random-engine.cpp index a2582282c1..c5cf164121 100644 --- a/examples/random-engine.cpp +++ b/examples/random-engine.cpp @@ -37,8 +37,13 @@ #include #include -/// A user engine that counts raw draws along each path. -/// Generation and indexed splitting retain SplitMix's algorithms and guarantees. +/** \brief A user engine that counts raw draws along each path + * + * Build the random-engine target and run it with an optional complete state, + * for example counted-splitmix-v1:000000000000002a:9e3779b97f4a7c15:0000000000000000. + * The program prints its initial state, then enumerates all 24 permutations. + * Generation and indexed splitting retain SplitMix's algorithms and guarantees. + */ class CountedSplitMix { Gecode::Support::SplitMix engine; uint64_t draws = 0; @@ -112,3 +117,5 @@ int main(int argc, char* argv[]) { return 1; } } + +// STATISTICS: example-any diff --git a/gecode/driver.hh b/gecode/driver.hh index 0d744f505a..999239869a 100755 --- a/gecode/driver.hh +++ b/gecode/driver.hh @@ -156,7 +156,11 @@ namespace Gecode { public: RandomOption(const char* o, const char* e, uint64_t v); void value(uint64_t v); - /// Return the seed; throws if initialization used complete state. + /** \brief Return the numeric seed + * + * Throws std::logic_error after complete-state, time, or hw input. + * Use rnd() to honor all initialization forms. + */ uint64_t value(void) const; /// Construct an independent generator at the configured initial state. Rnd rnd(void) const; @@ -548,7 +552,11 @@ namespace Gecode { /// Set default seed value void seed(uint64_t s); - /// Return seed value + /** \brief Return the numeric seed + * + * Throws std::logic_error after complete-state, time, or hw input. + * Use rnd() to honor all initialization forms. + */ uint64_t seed(void) const; /// Independent generator initialized from the seed or full-state option Rnd rnd(void) const; diff --git a/gecode/flatzinc.hh b/gecode/flatzinc.hh index 00a6476b04..9acae47549 100755 --- a/gecode/flatzinc.hh +++ b/gecode/flatzinc.hh @@ -349,6 +349,11 @@ namespace Gecode { namespace FlatZinc { unsigned long long int node(void) const { return _node.value(); } unsigned long long int fail(void) const { return _fail.value(); } double time(void) const { return _time.value(); } + /** \brief Return the numeric seed + * + * Throws std::logic_error after complete-state, time, or hw input. + * Use rnd() to honor all initialization forms. + */ uint64_t seed(void) const { return _seed.value(); } Rnd rnd(void) const { return _seed.rnd(); } double step(void) const { return _step.value(); } diff --git a/gecode/kernel/data/rnd.hpp b/gecode/kernel/data/rnd.hpp index f3cfa8f1d9..f0b0dc854e 100755 --- a/gecode/kernel/data/rnd.hpp +++ b/gecode/kernel/data/rnd.hpp @@ -40,9 +40,26 @@ namespace Gecode { /** * \brief Small value-type random generator owned by its consumer * - * Copying copies complete state. There is no shared handle, registration, - * or implicit connection to a space. Engine supplies the Support::Random - * contract, including indexed splitting. + * Copying copies complete state independently. Branching descriptions + * and selectors own their copies; cloning preserves selector state + * without drawing or splitting. Split explicitly to start different streams: + * \code + * Rnd parent(42); + * Rnd same = parent; + * Rnd child = parent.split(1); + * \endcode + * + * Only the active randomized brancher's selectors are recorded in a choice. + * Committing an alternative restores their recorded states and splits by + * the public alternative index. Replay reproduces random state along that + * path, but does not promise identical parallel scheduling, solution order, + * or search trees under weakly monotonic propagation. + * + * Model-owned or callback-owned generators are not recorded automatically. + * A custom brancher must preserve their state in its choices when replay + * needs it. Space cloning does not copy mutable state in shared callback + * captures. Engine supplies the Support::Random contract, including splitting. + * See examples/random-engine.cpp for a user-defined engine and selectors. * \ingroup TaskModel */ template diff --git a/gecode/support/random.hpp b/gecode/support/random.hpp index 27d1becb3f..0d03a1fc2b 100755 --- a/gecode/support/random.hpp +++ b/gecode/support/random.hpp @@ -342,10 +342,19 @@ namespace Gecode { namespace Support { /** \brief Value-type generator with reproducible bounded draws and state * - * Engine supplies a State array of 64-bit words, name(), seed(), state() - * getter/setter, and next() over [0,UINT64_MAX] or [1,UINT64_MAX]. Search - * engines additionally supply split(uint32_t) const. No state-size limit - * is imposed. Copying a generator preserves its exact state. + * Engine supplies a nonempty State = std::array containing + * all mutable state and stream parameters. It accepts a uint64_t seed in + * its constructor and seed(), with a defined rule for every seed. Its + * next(), min(), and max() describe raw output in [0,UINT64_MAX] or + * [1,UINT64_MAX]. name() identifies the algorithm and state format. + * + * state() captures complete state. state(const State&) must validate + * before mutation and restore subsequent draws and splits exactly, + * without seed expansion. Copying preserves state independently. + * Search engines additionally supply split(uint32_t) const, returning + * a child without changing the parent. Different sibling indices must + * produce distinct states, not merely different first outputs. This + * does not require non-overlapping streams across an unbounded tree. * \ingroup FuncSupport */ template diff --git a/plans/random.md b/plans/random.md index 2f818c8fc6..edc9692b59 100644 --- a/plans/random.md +++ b/plans/random.md @@ -89,8 +89,8 @@ and any performance costs. Splittable SplitMix has two state words and constant-time indexed splitting. Xorshift64* has one state word and indexed native jumps. Both have documented -sibling-state arguments in docs/random.md. The default remains provisional -SplitMix; xorshift64* remains configurable through CMake and Autoconf. +sibling-state arguments in gecode/support/random.hpp. The default remains +provisional SplitMix; xorshift64* remains configurable through CMake and Autoconf. Support::Random defines bounded integer conversion and canonical full-state encoding. RndGenerator provides the modeling value interface. Full state @@ -112,7 +112,7 @@ custom-engine example's full-state replay and output agree across all three buil The Space and base Choice implementation matches main exactly. Rnd is 16 bytes with SplitMix and 8 with xorshift64*, without shared allocation. Fresh -five-run measurements are recorded in docs/random.md: SplitMix's controlled +five-run measurements are recorded in the PR description: SplitMix's controlled random tree costs 1.02x with cloning and 1.16x with recomputation relative to main; xorshift64* costs 1.63x and 2.88x. Draft PR #241 remains provisional, for a future breaking-change release only. diff --git a/test/random.cpp b/test/random.cpp index 2b37a8c3cd..553b355d07 100644 --- a/test/random.cpp +++ b/test/random.cpp @@ -388,3 +388,5 @@ namespace Test { } commit_boundary; } } + +// STATISTICS: test-core diff --git a/tools/random-benchmark.py b/tools/random-benchmark.py index 7f0e1c429a..ec25fb6e78 100644 --- a/tools/random-benchmark.py +++ b/tools/random-benchmark.py @@ -31,11 +31,19 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # -"""Compare compiled random-benchmark executables with bounded repeated runs. +r"""Compare compiled random-benchmark executables with bounded repeated runs. Example: python3 tools/random-benchmark.py build/random/random-benchmark \ --baseline /tmp/baseline/random-benchmark --repeat 5 --output results.json -Build instructions and the controls are recorded in docs/random.md. +Compile the candidate against the configured build: + clang++ -O3 -DNDEBUG -std=c++17 -DRANDOM_NEW -Ibuild/random -I. \ + tools/random-benchmark.cpp -Lbuild/random -lgecodeint -lgecodesearch \ + -lgecodekernel -lgecodesupport -Wl,-rpath,build/random \ + -o build/random/random-benchmark +Compile the same source against baseline headers/libraries without -DRANDOM_NEW. +Runs alternate order, discard one warmup, and report medians. The controlled tree +has 32767 nodes and 16384 solutions; queens node counts can vary with the engine. +Avoid concurrent compilation while measuring. """ import argparse import json @@ -46,7 +54,8 @@ def main(): - parser = argparse.ArgumentParser(description=__doc__) + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument("candidate", type=Path) parser.add_argument("--baseline", type=Path) parser.add_argument("--repeat", type=int, default=5) From d1282e6b1123ecd2ac5a5decd9f13edbd29ce4d3 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Sun, 18 Oct 2026 10:59:27 +0200 Subject: [PATCH 12/16] Tighten random generator documentation --- changelog.in | 2 +- gecode/kernel/data/rnd.hpp | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/changelog.in b/changelog.in index 1464d1540d..7360d896ae 100755 --- a/changelog.in +++ b/changelog.in @@ -81,7 +81,7 @@ Module: kernel What: new Rank: major [DESCRIPTION] -Added modern, user-extensible random number generators with reproducible +Added user-extensible random number generators with reproducible stream splitting. Splittable SplitMix replaces the old default generator; xorshift64* is available as a smaller-state alternative. The default is configurable at build time, and users can supply their own engines. diff --git a/gecode/kernel/data/rnd.hpp b/gecode/kernel/data/rnd.hpp index f0b0dc854e..a64fef0e8a 100755 --- a/gecode/kernel/data/rnd.hpp +++ b/gecode/kernel/data/rnd.hpp @@ -40,9 +40,9 @@ namespace Gecode { /** * \brief Small value-type random generator owned by its consumer * - * Copying copies complete state independently. Branching descriptions - * and selectors own their copies; cloning preserves selector state - * without drawing or splitting. Split explicitly to start different streams: + * Copying preserves complete state in independent storage. Branching + * descriptions and selectors own their copies; cloning preserves selector + * state without drawing or splitting. Split explicitly to start different streams: * \code * Rnd parent(42); * Rnd same = parent; @@ -58,7 +58,7 @@ namespace Gecode { * Model-owned or callback-owned generators are not recorded automatically. * A custom brancher must preserve their state in its choices when replay * needs it. Space cloning does not copy mutable state in shared callback - * captures. Engine supplies the Support::Random contract, including splitting. + * captures. Engine must meet the Support::Random contract, including splitting. * See examples/random-engine.cpp for a user-defined engine and selectors. * \ingroup TaskModel */ From faaec0e16a96b71c926910ffb4d7dbc1ca681486 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Tue, 3 Nov 2026 12:58:13 +0100 Subject: [PATCH 13/16] Split random selector state on commit and per restart Selectors draw from a copy, and committing an alternative splits every random selector of the brancher by it. The state at a node is thus a function of the posted state and the path, so choices store no generator state and cloning, recomputation, and replay agree. Posting keys each selector by brancher id and role, so branchers given the same generator draw independent streams. Open-list and FlatZinc int-bool branchers follow the same scheme. Restart and portfolio search split every brancher of each slave by its restart or asset number. A restart after a solution is numbered like other restarts, so every slave's number is unique. Xorshift64* splits by hashing instead of jumping, since jumps add up along a path. The seed constructor is explicit, the driver and FlatZinc seed() accessors are removed, and Makefile.dep is regenerated. --- Makefile.dep | 478 ++++++++++++++++++------ Makefile.in | 1 + changelog.in | 43 ++- cmake/GecodeSources.cmake | 1 + gecode/driver.hh | 14 - gecode/driver/options.cpp | 15 +- gecode/driver/options.hpp | 5 - gecode/flatzinc.hh | 6 - gecode/flatzinc/branch.cpp | 7 + gecode/flatzinc/branch.hh | 2 + gecode/flatzinc/branch.hpp | 5 + gecode/float/branch.hh | 4 +- gecode/float/branch/val-sel.hpp | 3 +- gecode/int/branch.hh | 4 +- gecode/int/branch/open.hpp | 19 +- gecode/int/branch/val-sel.hpp | 3 +- gecode/int/branch/view-values.hpp | 21 +- gecode/int/ldsb/brancher.hpp | 30 +- gecode/kernel/branch/val-sel-commit.hpp | 14 +- gecode/kernel/branch/val-sel.hpp | 8 +- gecode/kernel/branch/view-sel.hpp | 43 +-- gecode/kernel/branch/view-val.hpp | 34 +- gecode/kernel/branch/view.hpp | 69 +--- gecode/kernel/core.cpp | 15 + gecode/kernel/core.hpp | 25 ++ gecode/kernel/data/rnd.hpp | 51 ++- gecode/search/pbs.hpp | 4 + gecode/search/rbs.hpp | 1 + gecode/search/seq/rbs.cpp | 5 +- gecode/set/branch.hh | 4 +- gecode/set/branch/val-sel.hpp | 3 +- gecode/support/random.hpp | 71 ++-- plans/random.md | 76 ++-- test/flatzinc/random_branching.cpp | 88 +++++ test/random.cpp | 184 ++++++--- tools/random-benchmark.cpp | 7 +- 36 files changed, 873 insertions(+), 490 deletions(-) create mode 100644 test/flatzinc/random_branching.cpp diff --git a/Makefile.dep b/Makefile.dep index 60b6e59f9c..ed5954d8da 100644 --- a/Makefile.dep +++ b/Makefile.dep @@ -161,33 +161,6 @@ gecode/kernel/gpi$(OBJSUFFIX) gecode/kernel/gpi$(SBJSUFFIX): \ ./gecode/support/ref-count.hpp ./gecode/support/run-jobs.hpp ./gecode/support/sort.hpp \ ./gecode/support/static-stack.hpp ./gecode/support/thread.hpp ./gecode/support/thread/thread.hpp \ ./gecode/support/timer.hpp -gecode/kernel/data/rnd$(OBJSUFFIX) gecode/kernel/data/rnd$(SBJSUFFIX): \ - ./gecode/kernel.hh ./gecode/kernel/archive.hpp ./gecode/kernel/branch/action.hpp \ - ./gecode/kernel/branch/afc.hpp ./gecode/kernel/branch/chb.hpp ./gecode/kernel/branch/filter.hpp \ - ./gecode/kernel/branch/merit.hpp ./gecode/kernel/branch/print.hpp ./gecode/kernel/branch/tiebreak.hpp \ - ./gecode/kernel/branch/traits.hpp ./gecode/kernel/branch/val-commit.hpp ./gecode/kernel/branch/val-sel-commit.hpp \ - ./gecode/kernel/branch/val-sel.hpp ./gecode/kernel/branch/val.hpp ./gecode/kernel/branch/var.hpp \ - ./gecode/kernel/branch/view-sel.hpp ./gecode/kernel/branch/view-val.hpp ./gecode/kernel/branch/view.hpp \ - ./gecode/kernel/core.hpp ./gecode/kernel/data/array.hpp ./gecode/kernel/data/rnd.hpp \ - ./gecode/kernel/data/shared-array.hpp ./gecode/kernel/data/shared-data.hpp ./gecode/kernel/exception.hpp \ - ./gecode/kernel/gpi.hpp ./gecode/kernel/macros.hpp ./gecode/kernel/memory/allocators.hpp \ - ./gecode/kernel/memory/config.hpp ./gecode/kernel/memory/manager.hpp ./gecode/kernel/memory/region.hpp \ - ./gecode/kernel/modevent.hpp ./gecode/kernel/propagator/advisor.hpp ./gecode/kernel/propagator/pattern.hpp \ - ./gecode/kernel/propagator/subscribed.hpp ./gecode/kernel/propagator/wait.hpp ./gecode/kernel/range-list.hpp \ - ./gecode/kernel/shared-object.hpp ./gecode/kernel/shared-space-data.hpp ./gecode/kernel/trace/filter.hpp \ - ./gecode/kernel/trace/general.hpp ./gecode/kernel/trace/print.hpp ./gecode/kernel/trace/recorder.hpp \ - ./gecode/kernel/trace/tracer.hpp ./gecode/kernel/trace/traits.hpp ./gecode/kernel/var-imp.hpp \ - ./gecode/kernel/var-type.hpp ./gecode/kernel/var.hpp ./gecode/kernel/view.hpp \ - ./gecode/support.hh ./gecode/support/allocator.hpp ./gecode/support/auto-link.hpp \ - ./gecode/support/bitset-base.hpp ./gecode/support/bitset-offset.hpp ./gecode/support/bitset.hpp \ - ./gecode/support/block-allocator.hpp ./gecode/support/cast.hpp ./gecode/support/config.hpp \ - ./gecode/support/dynamic-array.hpp ./gecode/support/dynamic-queue.hpp ./gecode/support/dynamic-stack.hpp \ - ./gecode/support/exception.hpp ./gecode/support/failpoint.hpp ./gecode/support/hash.hpp \ - ./gecode/support/heap.hpp ./gecode/support/hw-rnd.hpp ./gecode/support/int-type.hpp \ - ./gecode/support/macros.hpp ./gecode/support/marked-pointer.hpp ./gecode/support/random.hpp \ - ./gecode/support/ref-count.hpp ./gecode/support/run-jobs.hpp ./gecode/support/sort.hpp \ - ./gecode/support/static-stack.hpp ./gecode/support/thread.hpp ./gecode/support/thread/thread.hpp \ - ./gecode/support/timer.hpp gecode/kernel/branch/action$(OBJSUFFIX) gecode/kernel/branch/action$(SBJSUFFIX): \ ./gecode/kernel.hh ./gecode/kernel/archive.hpp ./gecode/kernel/branch/action.hpp \ ./gecode/kernel/branch/afc.hpp ./gecode/kernel/branch/chb.hpp ./gecode/kernel/branch/filter.hpp \ @@ -14946,6 +14919,101 @@ examples/queens$(OBJSUFFIX) examples/queens$(SBJSUFFIX): \ ./gecode/support/marked-pointer.hpp ./gecode/support/random.hpp ./gecode/support/ref-count.hpp \ ./gecode/support/run-jobs.hpp ./gecode/support/sort.hpp ./gecode/support/static-stack.hpp \ ./gecode/support/thread.hpp ./gecode/support/thread/thread.hpp ./gecode/support/timer.hpp +examples/random-engine$(OBJSUFFIX) examples/random-engine$(SBJSUFFIX): \ + ./gecode/float.hh ./gecode/float/array-traits.hpp ./gecode/float/array.hpp \ + ./gecode/float/branch.hpp ./gecode/float/branch/action.hpp ./gecode/float/branch/afc.hpp \ + ./gecode/float/branch/assign.hpp ./gecode/float/branch/chb.hpp ./gecode/float/branch/traits.hpp \ + ./gecode/float/branch/val.hpp ./gecode/float/branch/var.hpp ./gecode/float/channel.hpp \ + ./gecode/float/exception.hpp ./gecode/float/limits.hpp ./gecode/float/nextafter.hpp \ + ./gecode/float/num.hpp ./gecode/float/rounding.hpp ./gecode/float/trace.hpp \ + ./gecode/float/trace/delta.hpp ./gecode/float/trace/trace-view.hpp ./gecode/float/trace/traits.hpp \ + ./gecode/float/val.hpp ./gecode/float/var-imp.hpp ./gecode/float/var-imp/delta.hpp \ + ./gecode/float/var-imp/float.hpp ./gecode/float/var/float.hpp ./gecode/float/var/print.hpp \ + ./gecode/float/view.hpp ./gecode/float/view/float.hpp ./gecode/float/view/minus.hpp \ + ./gecode/float/view/offset.hpp ./gecode/float/view/print.hpp ./gecode/float/view/rel-test.hpp \ + ./gecode/float/view/scale.hpp ./gecode/int.hh ./gecode/int/array-traits.hpp \ + ./gecode/int/array.hpp ./gecode/int/branch.hh ./gecode/int/branch.hpp \ + ./gecode/int/branch/action.hpp ./gecode/int/branch/afc.hpp ./gecode/int/branch/assign.hpp \ + ./gecode/int/branch/cbs.hpp ./gecode/int/branch/chb.hpp ./gecode/int/branch/merit.hpp \ + ./gecode/int/branch/ngl.hpp ./gecode/int/branch/open.hpp ./gecode/int/branch/traits.hpp \ + ./gecode/int/branch/val-commit.hpp ./gecode/int/branch/val-sel.hpp ./gecode/int/branch/val.hpp \ + ./gecode/int/branch/var.hpp ./gecode/int/branch/view-values.hpp ./gecode/int/channel.hpp \ + ./gecode/int/div.hh ./gecode/int/div.hpp ./gecode/int/exception.hpp \ + ./gecode/int/extensional.hpp ./gecode/int/extensional/dfa.hpp ./gecode/int/extensional/tuple-set.hpp \ + ./gecode/int/int-set-1.hpp ./gecode/int/int-set-2.hpp ./gecode/int/ipl.hpp \ + ./gecode/int/irt.hpp ./gecode/int/limits.hpp ./gecode/int/min-distance.hpp \ + ./gecode/int/open-var-list.hpp ./gecode/int/propagator.hpp ./gecode/int/reify.hpp \ + ./gecode/int/trace.hpp ./gecode/int/trace/bool-delta.hpp ./gecode/int/trace/bool-trace-view.hpp \ + ./gecode/int/trace/int-delta.hpp ./gecode/int/trace/int-trace-view.hpp ./gecode/int/trace/traits.hpp \ + ./gecode/int/var-imp.hpp ./gecode/int/var-imp/bool.hpp ./gecode/int/var-imp/delta.hpp \ + ./gecode/int/var-imp/int.hpp ./gecode/int/var/bool.hpp ./gecode/int/var/int.hpp \ + ./gecode/int/var/print.hpp ./gecode/int/view.hpp ./gecode/int/view/bool-test.hpp \ + ./gecode/int/view/bool.hpp ./gecode/int/view/cached.hpp ./gecode/int/view/constint.hpp \ + ./gecode/int/view/int.hpp ./gecode/int/view/iter.hpp ./gecode/int/view/minus.hpp \ + ./gecode/int/view/neg-bool.hpp ./gecode/int/view/offset.hpp ./gecode/int/view/print.hpp \ + ./gecode/int/view/rel-test.hpp ./gecode/int/view/scale.hpp ./gecode/int/view/zero.hpp \ + ./gecode/iter.hh ./gecode/iter/ranges-add.hpp ./gecode/iter/ranges-append.hpp \ + ./gecode/iter/ranges-array.hpp ./gecode/iter/ranges-cache.hpp ./gecode/iter/ranges-compl.hpp \ + ./gecode/iter/ranges-diff.hpp ./gecode/iter/ranges-empty.hpp ./gecode/iter/ranges-inter.hpp \ + ./gecode/iter/ranges-list.hpp ./gecode/iter/ranges-map.hpp ./gecode/iter/ranges-minmax.hpp \ + ./gecode/iter/ranges-minus.hpp ./gecode/iter/ranges-negative.hpp ./gecode/iter/ranges-offset.hpp \ + ./gecode/iter/ranges-operations.hpp ./gecode/iter/ranges-positive.hpp ./gecode/iter/ranges-rangelist.hpp \ + ./gecode/iter/ranges-scale.hpp ./gecode/iter/ranges-singleton-append.hpp ./gecode/iter/ranges-singleton.hpp \ + ./gecode/iter/ranges-size.hpp ./gecode/iter/ranges-union.hpp ./gecode/iter/ranges-values.hpp \ + ./gecode/iter/values-array.hpp ./gecode/iter/values-bitset.hpp ./gecode/iter/values-bitsetoffset.hpp \ + ./gecode/iter/values-inter.hpp ./gecode/iter/values-list.hpp ./gecode/iter/values-map.hpp \ + ./gecode/iter/values-minus.hpp ./gecode/iter/values-negative.hpp ./gecode/iter/values-offset.hpp \ + ./gecode/iter/values-positive.hpp ./gecode/iter/values-ranges.hpp ./gecode/iter/values-singleton.hpp \ + ./gecode/iter/values-union.hpp ./gecode/iter/values-unique.hpp ./gecode/kernel.hh \ + ./gecode/kernel/archive.hpp ./gecode/kernel/branch/action.hpp ./gecode/kernel/branch/afc.hpp \ + ./gecode/kernel/branch/chb.hpp ./gecode/kernel/branch/filter.hpp ./gecode/kernel/branch/merit.hpp \ + ./gecode/kernel/branch/print.hpp ./gecode/kernel/branch/tiebreak.hpp ./gecode/kernel/branch/traits.hpp \ + ./gecode/kernel/branch/val-commit.hpp ./gecode/kernel/branch/val-sel-commit.hpp ./gecode/kernel/branch/val-sel.hpp \ + ./gecode/kernel/branch/val.hpp ./gecode/kernel/branch/var.hpp ./gecode/kernel/branch/view-sel.hpp \ + ./gecode/kernel/branch/view-val.hpp ./gecode/kernel/branch/view.hpp ./gecode/kernel/core.hpp \ + ./gecode/kernel/data/array.hpp ./gecode/kernel/data/rnd.hpp ./gecode/kernel/data/shared-array.hpp \ + ./gecode/kernel/data/shared-data.hpp ./gecode/kernel/exception.hpp ./gecode/kernel/gpi.hpp \ + ./gecode/kernel/macros.hpp ./gecode/kernel/memory/allocators.hpp ./gecode/kernel/memory/config.hpp \ + ./gecode/kernel/memory/manager.hpp ./gecode/kernel/memory/region.hpp ./gecode/kernel/modevent.hpp \ + ./gecode/kernel/propagator/advisor.hpp ./gecode/kernel/propagator/pattern.hpp ./gecode/kernel/propagator/subscribed.hpp \ + ./gecode/kernel/propagator/wait.hpp ./gecode/kernel/range-list.hpp ./gecode/kernel/shared-object.hpp \ + ./gecode/kernel/shared-space-data.hpp ./gecode/kernel/trace/filter.hpp ./gecode/kernel/trace/general.hpp \ + ./gecode/kernel/trace/print.hpp ./gecode/kernel/trace/recorder.hpp ./gecode/kernel/trace/tracer.hpp \ + ./gecode/kernel/trace/traits.hpp ./gecode/kernel/var-imp.hpp ./gecode/kernel/var-type.hpp \ + ./gecode/kernel/var.hpp ./gecode/kernel/view.hpp ./gecode/minimodel.hh \ + ./gecode/minimodel/aliases.hpp ./gecode/minimodel/bool-expr.hpp ./gecode/minimodel/channel.hpp \ + ./gecode/minimodel/exception.hpp ./gecode/minimodel/float-expr.hpp ./gecode/minimodel/float-rel.hpp \ + ./gecode/minimodel/int-expr.hpp ./gecode/minimodel/int-rel.hpp ./gecode/minimodel/ipl.hpp \ + ./gecode/minimodel/ldsb.hpp ./gecode/minimodel/matrix.hpp ./gecode/minimodel/optimize.hpp \ + ./gecode/minimodel/reg.hpp ./gecode/minimodel/set-expr.hpp ./gecode/minimodel/set-rel.hpp \ + ./gecode/search.hh ./gecode/search/astar.hpp ./gecode/search/bab.hpp \ + ./gecode/search/base.hpp ./gecode/search/build.hpp ./gecode/search/cabs.hpp \ + ./gecode/search/cutoff.hpp ./gecode/search/dfs.hpp ./gecode/search/engine.hpp \ + ./gecode/search/exception.hpp ./gecode/search/lds.hpp ./gecode/search/options.hpp \ + ./gecode/search/pbs.hpp ./gecode/search/rbs.hpp ./gecode/search/sebs.hpp \ + ./gecode/search/seq/dead.hh ./gecode/search/statistics.hpp ./gecode/search/stop.hpp \ + ./gecode/search/support.hh ./gecode/search/trace-recorder.hpp ./gecode/search/tracer.hpp \ + ./gecode/search/traits.hpp ./gecode/search/worker-control.hh ./gecode/set.hh \ + ./gecode/set/array-traits.hpp ./gecode/set/array.hpp ./gecode/set/branch.hpp \ + ./gecode/set/branch/action.hpp ./gecode/set/branch/afc.hpp ./gecode/set/branch/assign.hpp \ + ./gecode/set/branch/chb.hpp ./gecode/set/branch/traits.hpp ./gecode/set/branch/val.hpp \ + ./gecode/set/branch/var.hpp ./gecode/set/exception.hpp ./gecode/set/int.hpp \ + ./gecode/set/limits.hpp ./gecode/set/trace.hpp ./gecode/set/trace/delta.hpp \ + ./gecode/set/trace/trace-view.hpp ./gecode/set/trace/traits.hpp ./gecode/set/var-imp.hpp \ + ./gecode/set/var-imp/delta.hpp ./gecode/set/var-imp/integerset.hpp ./gecode/set/var-imp/iter.hpp \ + ./gecode/set/var-imp/set.hpp ./gecode/set/var/print.hpp ./gecode/set/var/set.hpp \ + ./gecode/set/view.hpp ./gecode/set/view/cached.hpp ./gecode/set/view/complement.hpp \ + ./gecode/set/view/const.hpp ./gecode/set/view/print.hpp ./gecode/set/view/set.hpp \ + ./gecode/set/view/singleton.hpp ./gecode/support.hh ./gecode/support/allocator.hpp \ + ./gecode/support/auto-link.hpp ./gecode/support/bitset-base.hpp ./gecode/support/bitset-offset.hpp \ + ./gecode/support/bitset.hpp ./gecode/support/block-allocator.hpp ./gecode/support/cast.hpp \ + ./gecode/support/config.hpp ./gecode/support/dynamic-array.hpp ./gecode/support/dynamic-queue.hpp \ + ./gecode/support/dynamic-stack.hpp ./gecode/support/exception.hpp ./gecode/support/failpoint.hpp \ + ./gecode/support/hash.hpp ./gecode/support/heap.hpp ./gecode/support/hw-rnd.hpp \ + ./gecode/support/int-type.hpp ./gecode/support/macros.hpp ./gecode/support/marked-pointer.hpp \ + ./gecode/support/random.hpp ./gecode/support/ref-count.hpp ./gecode/support/run-jobs.hpp \ + ./gecode/support/sort.hpp ./gecode/support/static-stack.hpp ./gecode/support/thread.hpp \ + ./gecode/support/thread/thread.hpp ./gecode/support/timer.hpp examples/sudoku$(OBJSUFFIX) examples/sudoku$(SBJSUFFIX): \ ./gecode/driver.hh ./gecode/driver/options.hpp ./gecode/driver/script.hpp \ ./gecode/float.hh ./gecode/float/array-traits.hpp ./gecode/float/array.hpp \ @@ -21578,6 +21646,74 @@ test/region$(OBJSUFFIX) test/region$(SBJSUFFIX): \ ./gecode/support/sort.hpp ./gecode/support/static-stack.hpp ./gecode/support/thread.hpp \ ./gecode/support/thread/thread.hpp ./gecode/support/timer.hpp ./test/test.hh \ ./test/test.hpp +test/random$(OBJSUFFIX) test/random$(SBJSUFFIX): \ + ./gecode/int.hh ./gecode/int/array-traits.hpp ./gecode/int/array.hpp \ + ./gecode/int/branch.hh ./gecode/int/branch.hpp ./gecode/int/branch/action.hpp \ + ./gecode/int/branch/afc.hpp ./gecode/int/branch/assign.hpp ./gecode/int/branch/cbs.hpp \ + ./gecode/int/branch/chb.hpp ./gecode/int/branch/merit.hpp ./gecode/int/branch/ngl.hpp \ + ./gecode/int/branch/open.hpp ./gecode/int/branch/traits.hpp ./gecode/int/branch/val-commit.hpp \ + ./gecode/int/branch/val-sel.hpp ./gecode/int/branch/val.hpp ./gecode/int/branch/var.hpp \ + ./gecode/int/branch/view-values.hpp ./gecode/int/channel.hpp ./gecode/int/div.hh \ + ./gecode/int/div.hpp ./gecode/int/exception.hpp ./gecode/int/extensional.hpp \ + ./gecode/int/extensional/dfa.hpp ./gecode/int/extensional/tuple-set.hpp ./gecode/int/int-set-1.hpp \ + ./gecode/int/int-set-2.hpp ./gecode/int/ipl.hpp ./gecode/int/irt.hpp \ + ./gecode/int/limits.hpp ./gecode/int/min-distance.hpp ./gecode/int/open-var-list.hpp \ + ./gecode/int/propagator.hpp ./gecode/int/reify.hpp ./gecode/int/trace.hpp \ + ./gecode/int/trace/bool-delta.hpp ./gecode/int/trace/bool-trace-view.hpp ./gecode/int/trace/int-delta.hpp \ + ./gecode/int/trace/int-trace-view.hpp ./gecode/int/trace/traits.hpp ./gecode/int/var-imp.hpp \ + ./gecode/int/var-imp/bool.hpp ./gecode/int/var-imp/delta.hpp ./gecode/int/var-imp/int.hpp \ + ./gecode/int/var/bool.hpp ./gecode/int/var/int.hpp ./gecode/int/var/print.hpp \ + ./gecode/int/view.hpp ./gecode/int/view/bool-test.hpp ./gecode/int/view/bool.hpp \ + ./gecode/int/view/cached.hpp ./gecode/int/view/constint.hpp ./gecode/int/view/int.hpp \ + ./gecode/int/view/iter.hpp ./gecode/int/view/minus.hpp ./gecode/int/view/neg-bool.hpp \ + ./gecode/int/view/offset.hpp ./gecode/int/view/print.hpp ./gecode/int/view/rel-test.hpp \ + ./gecode/int/view/scale.hpp ./gecode/int/view/zero.hpp ./gecode/iter.hh \ + ./gecode/iter/ranges-add.hpp ./gecode/iter/ranges-append.hpp ./gecode/iter/ranges-array.hpp \ + ./gecode/iter/ranges-cache.hpp ./gecode/iter/ranges-compl.hpp ./gecode/iter/ranges-diff.hpp \ + ./gecode/iter/ranges-empty.hpp ./gecode/iter/ranges-inter.hpp ./gecode/iter/ranges-list.hpp \ + ./gecode/iter/ranges-map.hpp ./gecode/iter/ranges-minmax.hpp ./gecode/iter/ranges-minus.hpp \ + ./gecode/iter/ranges-negative.hpp ./gecode/iter/ranges-offset.hpp ./gecode/iter/ranges-operations.hpp \ + ./gecode/iter/ranges-positive.hpp ./gecode/iter/ranges-rangelist.hpp ./gecode/iter/ranges-scale.hpp \ + ./gecode/iter/ranges-singleton-append.hpp ./gecode/iter/ranges-singleton.hpp ./gecode/iter/ranges-size.hpp \ + ./gecode/iter/ranges-union.hpp ./gecode/iter/ranges-values.hpp ./gecode/iter/values-array.hpp \ + ./gecode/iter/values-bitset.hpp ./gecode/iter/values-bitsetoffset.hpp ./gecode/iter/values-inter.hpp \ + ./gecode/iter/values-list.hpp ./gecode/iter/values-map.hpp ./gecode/iter/values-minus.hpp \ + ./gecode/iter/values-negative.hpp ./gecode/iter/values-offset.hpp ./gecode/iter/values-positive.hpp \ + ./gecode/iter/values-ranges.hpp ./gecode/iter/values-singleton.hpp ./gecode/iter/values-union.hpp \ + ./gecode/iter/values-unique.hpp ./gecode/kernel.hh ./gecode/kernel/archive.hpp \ + ./gecode/kernel/branch/action.hpp ./gecode/kernel/branch/afc.hpp ./gecode/kernel/branch/chb.hpp \ + ./gecode/kernel/branch/filter.hpp ./gecode/kernel/branch/merit.hpp ./gecode/kernel/branch/print.hpp \ + ./gecode/kernel/branch/tiebreak.hpp ./gecode/kernel/branch/traits.hpp ./gecode/kernel/branch/val-commit.hpp \ + ./gecode/kernel/branch/val-sel-commit.hpp ./gecode/kernel/branch/val-sel.hpp ./gecode/kernel/branch/val.hpp \ + ./gecode/kernel/branch/var.hpp ./gecode/kernel/branch/view-sel.hpp ./gecode/kernel/branch/view-val.hpp \ + ./gecode/kernel/branch/view.hpp ./gecode/kernel/core.hpp ./gecode/kernel/data/array.hpp \ + ./gecode/kernel/data/rnd.hpp ./gecode/kernel/data/shared-array.hpp ./gecode/kernel/data/shared-data.hpp \ + ./gecode/kernel/exception.hpp ./gecode/kernel/gpi.hpp ./gecode/kernel/macros.hpp \ + ./gecode/kernel/memory/allocators.hpp ./gecode/kernel/memory/config.hpp ./gecode/kernel/memory/manager.hpp \ + ./gecode/kernel/memory/region.hpp ./gecode/kernel/modevent.hpp ./gecode/kernel/propagator/advisor.hpp \ + ./gecode/kernel/propagator/pattern.hpp ./gecode/kernel/propagator/subscribed.hpp ./gecode/kernel/propagator/wait.hpp \ + ./gecode/kernel/range-list.hpp ./gecode/kernel/shared-object.hpp ./gecode/kernel/shared-space-data.hpp \ + ./gecode/kernel/trace/filter.hpp ./gecode/kernel/trace/general.hpp ./gecode/kernel/trace/print.hpp \ + ./gecode/kernel/trace/recorder.hpp ./gecode/kernel/trace/tracer.hpp ./gecode/kernel/trace/traits.hpp \ + ./gecode/kernel/var-imp.hpp ./gecode/kernel/var-type.hpp ./gecode/kernel/var.hpp \ + ./gecode/kernel/view.hpp ./gecode/search.hh ./gecode/search/astar.hpp \ + ./gecode/search/bab.hpp ./gecode/search/base.hpp ./gecode/search/build.hpp \ + ./gecode/search/cabs.hpp ./gecode/search/cutoff.hpp ./gecode/search/dfs.hpp \ + ./gecode/search/engine.hpp ./gecode/search/exception.hpp ./gecode/search/lds.hpp \ + ./gecode/search/options.hpp ./gecode/search/pbs.hpp ./gecode/search/rbs.hpp \ + ./gecode/search/sebs.hpp ./gecode/search/seq/dead.hh ./gecode/search/statistics.hpp \ + ./gecode/search/stop.hpp ./gecode/search/support.hh ./gecode/search/trace-recorder.hpp \ + ./gecode/search/tracer.hpp ./gecode/search/traits.hpp ./gecode/search/worker-control.hh \ + ./gecode/support.hh ./gecode/support/allocator.hpp ./gecode/support/auto-link.hpp \ + ./gecode/support/bitset-base.hpp ./gecode/support/bitset-offset.hpp ./gecode/support/bitset.hpp \ + ./gecode/support/block-allocator.hpp ./gecode/support/cast.hpp ./gecode/support/config.hpp \ + ./gecode/support/dynamic-array.hpp ./gecode/support/dynamic-queue.hpp ./gecode/support/dynamic-stack.hpp \ + ./gecode/support/exception.hpp ./gecode/support/failpoint.hpp ./gecode/support/hash.hpp \ + ./gecode/support/heap.hpp ./gecode/support/hw-rnd.hpp ./gecode/support/int-type.hpp \ + ./gecode/support/macros.hpp ./gecode/support/marked-pointer.hpp ./gecode/support/random.hpp \ + ./gecode/support/ref-count.hpp ./gecode/support/run-jobs.hpp ./gecode/support/sort.hpp \ + ./gecode/support/static-stack.hpp ./gecode/support/thread.hpp ./gecode/support/thread/thread.hpp \ + ./gecode/support/timer.hpp ./test/test.hh ./test/test.hpp test/groups$(OBJSUFFIX) test/groups$(SBJSUFFIX): \ ./gecode/int.hh ./gecode/int/array-traits.hpp ./gecode/int/array.hpp \ ./gecode/int/branch.hpp ./gecode/int/branch/action.hpp ./gecode/int/branch/afc.hpp \ @@ -35489,6 +35625,106 @@ test/flatzinc/radiation$(OBJSUFFIX) test/flatzinc/radiation$(SBJSUFFIX): \ ./gecode/support/sort.hpp ./gecode/support/static-stack.hpp ./gecode/support/thread.hpp \ ./gecode/support/thread/thread.hpp ./gecode/support/timer.hpp ./test/flatzinc.hh \ ./test/test.hh ./test/test.hpp +test/flatzinc/random_branching$(OBJSUFFIX) test/flatzinc/random_branching$(SBJSUFFIX): \ + ./gecode/driver.hh ./gecode/driver/options.hpp ./gecode/driver/script.hpp \ + ./gecode/flatzinc.hh ./gecode/flatzinc/ast.hh ./gecode/flatzinc/branch.hh \ + ./gecode/flatzinc/branch.hpp ./gecode/flatzinc/conexpr.hh ./gecode/flatzinc/option.hh \ + ./gecode/flatzinc/varspec.hh ./gecode/float.hh ./gecode/float/array-traits.hpp \ + ./gecode/float/array.hpp ./gecode/float/branch.hpp ./gecode/float/branch/action.hpp \ + ./gecode/float/branch/afc.hpp ./gecode/float/branch/assign.hpp ./gecode/float/branch/chb.hpp \ + ./gecode/float/branch/traits.hpp ./gecode/float/branch/val.hpp ./gecode/float/branch/var.hpp \ + ./gecode/float/channel.hpp ./gecode/float/exception.hpp ./gecode/float/limits.hpp \ + ./gecode/float/nextafter.hpp ./gecode/float/num.hpp ./gecode/float/rounding.hpp \ + ./gecode/float/trace.hpp ./gecode/float/trace/delta.hpp ./gecode/float/trace/trace-view.hpp \ + ./gecode/float/trace/traits.hpp ./gecode/float/val.hpp ./gecode/float/var-imp.hpp \ + ./gecode/float/var-imp/delta.hpp ./gecode/float/var-imp/float.hpp ./gecode/float/var/float.hpp \ + ./gecode/float/var/print.hpp ./gecode/float/view.hpp ./gecode/float/view/float.hpp \ + ./gecode/float/view/minus.hpp ./gecode/float/view/offset.hpp ./gecode/float/view/print.hpp \ + ./gecode/float/view/rel-test.hpp ./gecode/float/view/scale.hpp ./gecode/gist.hh \ + ./gecode/gist/gist.hpp ./gecode/instance.hh ./gecode/int.hh \ + ./gecode/int/array-traits.hpp ./gecode/int/array.hpp ./gecode/int/branch.hh \ + ./gecode/int/branch.hpp ./gecode/int/branch/action.hpp ./gecode/int/branch/afc.hpp \ + ./gecode/int/branch/assign.hpp ./gecode/int/branch/cbs.hpp ./gecode/int/branch/chb.hpp \ + ./gecode/int/branch/merit.hpp ./gecode/int/branch/ngl.hpp ./gecode/int/branch/open.hpp \ + ./gecode/int/branch/traits.hpp ./gecode/int/branch/val-commit.hpp ./gecode/int/branch/val-sel.hpp \ + ./gecode/int/branch/val.hpp ./gecode/int/branch/var.hpp ./gecode/int/branch/view-values.hpp \ + ./gecode/int/channel.hpp ./gecode/int/div.hh ./gecode/int/div.hpp \ + ./gecode/int/exception.hpp ./gecode/int/extensional.hpp ./gecode/int/extensional/dfa.hpp \ + ./gecode/int/extensional/tuple-set.hpp ./gecode/int/int-set-1.hpp ./gecode/int/int-set-2.hpp \ + ./gecode/int/ipl.hpp ./gecode/int/irt.hpp ./gecode/int/limits.hpp \ + ./gecode/int/min-distance.hpp ./gecode/int/open-var-list.hpp ./gecode/int/propagator.hpp \ + ./gecode/int/reify.hpp ./gecode/int/trace.hpp ./gecode/int/trace/bool-delta.hpp \ + ./gecode/int/trace/bool-trace-view.hpp ./gecode/int/trace/int-delta.hpp ./gecode/int/trace/int-trace-view.hpp \ + ./gecode/int/trace/traits.hpp ./gecode/int/var-imp.hpp ./gecode/int/var-imp/bool.hpp \ + ./gecode/int/var-imp/delta.hpp ./gecode/int/var-imp/int.hpp ./gecode/int/var/bool.hpp \ + ./gecode/int/var/int.hpp ./gecode/int/var/print.hpp ./gecode/int/view.hpp \ + ./gecode/int/view/bool-test.hpp ./gecode/int/view/bool.hpp ./gecode/int/view/cached.hpp \ + ./gecode/int/view/constint.hpp ./gecode/int/view/int.hpp ./gecode/int/view/iter.hpp \ + ./gecode/int/view/minus.hpp ./gecode/int/view/neg-bool.hpp ./gecode/int/view/offset.hpp \ + ./gecode/int/view/print.hpp ./gecode/int/view/rel-test.hpp ./gecode/int/view/scale.hpp \ + ./gecode/int/view/zero.hpp ./gecode/iter.hh ./gecode/iter/ranges-add.hpp \ + ./gecode/iter/ranges-append.hpp ./gecode/iter/ranges-array.hpp ./gecode/iter/ranges-cache.hpp \ + ./gecode/iter/ranges-compl.hpp ./gecode/iter/ranges-diff.hpp ./gecode/iter/ranges-empty.hpp \ + ./gecode/iter/ranges-inter.hpp ./gecode/iter/ranges-list.hpp ./gecode/iter/ranges-map.hpp \ + ./gecode/iter/ranges-minmax.hpp ./gecode/iter/ranges-minus.hpp ./gecode/iter/ranges-negative.hpp \ + ./gecode/iter/ranges-offset.hpp ./gecode/iter/ranges-operations.hpp ./gecode/iter/ranges-positive.hpp \ + ./gecode/iter/ranges-rangelist.hpp ./gecode/iter/ranges-scale.hpp ./gecode/iter/ranges-singleton-append.hpp \ + ./gecode/iter/ranges-singleton.hpp ./gecode/iter/ranges-size.hpp ./gecode/iter/ranges-union.hpp \ + ./gecode/iter/ranges-values.hpp ./gecode/iter/values-array.hpp ./gecode/iter/values-bitset.hpp \ + ./gecode/iter/values-bitsetoffset.hpp ./gecode/iter/values-inter.hpp ./gecode/iter/values-list.hpp \ + ./gecode/iter/values-map.hpp ./gecode/iter/values-minus.hpp ./gecode/iter/values-negative.hpp \ + ./gecode/iter/values-offset.hpp ./gecode/iter/values-positive.hpp ./gecode/iter/values-ranges.hpp \ + ./gecode/iter/values-singleton.hpp ./gecode/iter/values-union.hpp ./gecode/iter/values-unique.hpp \ + ./gecode/kernel.hh ./gecode/kernel/archive.hpp ./gecode/kernel/branch/action.hpp \ + ./gecode/kernel/branch/afc.hpp ./gecode/kernel/branch/chb.hpp ./gecode/kernel/branch/filter.hpp \ + ./gecode/kernel/branch/merit.hpp ./gecode/kernel/branch/print.hpp ./gecode/kernel/branch/tiebreak.hpp \ + ./gecode/kernel/branch/traits.hpp ./gecode/kernel/branch/val-commit.hpp ./gecode/kernel/branch/val-sel-commit.hpp \ + ./gecode/kernel/branch/val-sel.hpp ./gecode/kernel/branch/val.hpp ./gecode/kernel/branch/var.hpp \ + ./gecode/kernel/branch/view-sel.hpp ./gecode/kernel/branch/view-val.hpp ./gecode/kernel/branch/view.hpp \ + ./gecode/kernel/core.hpp ./gecode/kernel/data/array.hpp ./gecode/kernel/data/rnd.hpp \ + ./gecode/kernel/data/shared-array.hpp ./gecode/kernel/data/shared-data.hpp ./gecode/kernel/exception.hpp \ + ./gecode/kernel/gpi.hpp ./gecode/kernel/macros.hpp ./gecode/kernel/memory/allocators.hpp \ + ./gecode/kernel/memory/config.hpp ./gecode/kernel/memory/manager.hpp ./gecode/kernel/memory/region.hpp \ + ./gecode/kernel/modevent.hpp ./gecode/kernel/propagator/advisor.hpp ./gecode/kernel/propagator/pattern.hpp \ + ./gecode/kernel/propagator/subscribed.hpp ./gecode/kernel/propagator/wait.hpp ./gecode/kernel/range-list.hpp \ + ./gecode/kernel/shared-object.hpp ./gecode/kernel/shared-space-data.hpp ./gecode/kernel/trace/filter.hpp \ + ./gecode/kernel/trace/general.hpp ./gecode/kernel/trace/print.hpp ./gecode/kernel/trace/recorder.hpp \ + ./gecode/kernel/trace/tracer.hpp ./gecode/kernel/trace/traits.hpp ./gecode/kernel/var-imp.hpp \ + ./gecode/kernel/var-type.hpp ./gecode/kernel/var.hpp ./gecode/kernel/view.hpp \ + ./gecode/minimodel.hh ./gecode/minimodel/aliases.hpp ./gecode/minimodel/bool-expr.hpp \ + ./gecode/minimodel/channel.hpp ./gecode/minimodel/exception.hpp ./gecode/minimodel/float-expr.hpp \ + ./gecode/minimodel/float-rel.hpp ./gecode/minimodel/int-expr.hpp ./gecode/minimodel/int-rel.hpp \ + ./gecode/minimodel/ipl.hpp ./gecode/minimodel/ldsb.hpp ./gecode/minimodel/matrix.hpp \ + ./gecode/minimodel/optimize.hpp ./gecode/minimodel/reg.hpp ./gecode/minimodel/set-expr.hpp \ + ./gecode/minimodel/set-rel.hpp ./gecode/search.hh ./gecode/search/astar.hpp \ + ./gecode/search/bab.hpp ./gecode/search/base.hpp ./gecode/search/build.hpp \ + ./gecode/search/cabs.hpp ./gecode/search/cutoff.hpp ./gecode/search/dfs.hpp \ + ./gecode/search/engine.hpp ./gecode/search/exception.hpp ./gecode/search/lds.hpp \ + ./gecode/search/options.hpp ./gecode/search/pbs.hpp ./gecode/search/rbs.hpp \ + ./gecode/search/sebs.hpp ./gecode/search/seq/dead.hh ./gecode/search/statistics.hpp \ + ./gecode/search/stop.hpp ./gecode/search/support.hh ./gecode/search/trace-recorder.hpp \ + ./gecode/search/tracer.hpp ./gecode/search/traits.hpp ./gecode/search/worker-control.hh \ + ./gecode/set.hh ./gecode/set/array-traits.hpp ./gecode/set/array.hpp \ + ./gecode/set/branch.hpp ./gecode/set/branch/action.hpp ./gecode/set/branch/afc.hpp \ + ./gecode/set/branch/assign.hpp ./gecode/set/branch/chb.hpp ./gecode/set/branch/traits.hpp \ + ./gecode/set/branch/val.hpp ./gecode/set/branch/var.hpp ./gecode/set/exception.hpp \ + ./gecode/set/int.hpp ./gecode/set/limits.hpp ./gecode/set/trace.hpp \ + ./gecode/set/trace/delta.hpp ./gecode/set/trace/trace-view.hpp ./gecode/set/trace/traits.hpp \ + ./gecode/set/var-imp.hpp ./gecode/set/var-imp/delta.hpp ./gecode/set/var-imp/integerset.hpp \ + ./gecode/set/var-imp/iter.hpp ./gecode/set/var-imp/set.hpp ./gecode/set/var/print.hpp \ + ./gecode/set/var/set.hpp ./gecode/set/view.hpp ./gecode/set/view/cached.hpp \ + ./gecode/set/view/complement.hpp ./gecode/set/view/const.hpp ./gecode/set/view/print.hpp \ + ./gecode/set/view/set.hpp ./gecode/set/view/singleton.hpp ./gecode/support.hh \ + ./gecode/support/allocator.hpp ./gecode/support/auto-link.hpp ./gecode/support/bitset-base.hpp \ + ./gecode/support/bitset-offset.hpp ./gecode/support/bitset.hpp ./gecode/support/block-allocator.hpp \ + ./gecode/support/cast.hpp ./gecode/support/config.hpp ./gecode/support/dynamic-array.hpp \ + ./gecode/support/dynamic-queue.hpp ./gecode/support/dynamic-stack.hpp ./gecode/support/exception.hpp \ + ./gecode/support/failpoint.hpp ./gecode/support/hash.hpp ./gecode/support/heap.hpp \ + ./gecode/support/hw-rnd.hpp ./gecode/support/int-type.hpp ./gecode/support/macros.hpp \ + ./gecode/support/marked-pointer.hpp ./gecode/support/random.hpp ./gecode/support/ref-count.hpp \ + ./gecode/support/run-jobs.hpp ./gecode/support/sort.hpp ./gecode/support/static-stack.hpp \ + ./gecode/support/thread.hpp ./gecode/support/thread/thread.hpp ./gecode/support/timer.hpp \ + ./test/flatzinc.hh ./test/test.hh ./test/test.hpp test/flatzinc/shared_array_element$(OBJSUFFIX) test/flatzinc/shared_array_element$(SBJSUFFIX): \ ./gecode/driver.hh ./gecode/driver/options.hpp ./gecode/driver/script.hpp \ ./gecode/flatzinc.hh ./gecode/flatzinc/ast.hh ./gecode/flatzinc/conexpr.hh \ @@ -38014,100 +38250,100 @@ test/flatzinc/on_restart_last_val_float$(OBJSUFFIX) test/flatzinc/on_restart_las test/flatzinc/on_restart_last_val_int$(OBJSUFFIX) test/flatzinc/on_restart_last_val_int$(SBJSUFFIX): \ ./gecode/driver.hh ./gecode/driver/options.hpp ./gecode/driver/script.hpp \ ./gecode/flatzinc.hh ./gecode/flatzinc/ast.hh ./gecode/flatzinc/conexpr.hh \ - ./gecode/flatzinc/option.hh ./gecode/flatzinc/restart-random.hpp ./gecode/flatzinc/varspec.hh \ - ./gecode/float.hh ./gecode/float/array-traits.hpp ./gecode/float/array.hpp \ - ./gecode/float/branch.hpp ./gecode/float/branch/action.hpp ./gecode/float/branch/afc.hpp \ - ./gecode/float/branch/assign.hpp ./gecode/float/branch/chb.hpp ./gecode/float/branch/traits.hpp \ - ./gecode/float/branch/val.hpp ./gecode/float/branch/var.hpp ./gecode/float/channel.hpp \ - ./gecode/float/exception.hpp ./gecode/float/limits.hpp ./gecode/float/nextafter.hpp \ - ./gecode/float/num.hpp ./gecode/float/rounding.hpp ./gecode/float/trace.hpp \ - ./gecode/float/trace/delta.hpp ./gecode/float/trace/trace-view.hpp ./gecode/float/trace/traits.hpp \ - ./gecode/float/val.hpp ./gecode/float/var-imp.hpp ./gecode/float/var-imp/delta.hpp \ - ./gecode/float/var-imp/float.hpp ./gecode/float/var/float.hpp ./gecode/float/var/print.hpp \ - ./gecode/float/view.hpp ./gecode/float/view/float.hpp ./gecode/float/view/minus.hpp \ - ./gecode/float/view/offset.hpp ./gecode/float/view/print.hpp ./gecode/float/view/rel-test.hpp \ - ./gecode/float/view/scale.hpp ./gecode/gist.hh ./gecode/gist/gist.hpp \ - ./gecode/instance.hh ./gecode/int.hh ./gecode/int/array-traits.hpp \ - ./gecode/int/array.hpp ./gecode/int/branch.hpp ./gecode/int/branch/action.hpp \ - ./gecode/int/branch/afc.hpp ./gecode/int/branch/assign.hpp ./gecode/int/branch/chb.hpp \ - ./gecode/int/branch/traits.hpp ./gecode/int/branch/val.hpp ./gecode/int/branch/var.hpp \ - ./gecode/int/channel.hpp ./gecode/int/div.hh ./gecode/int/div.hpp \ - ./gecode/int/exception.hpp ./gecode/int/extensional.hpp ./gecode/int/extensional/dfa.hpp \ - ./gecode/int/extensional/tuple-set.hpp ./gecode/int/int-set-1.hpp ./gecode/int/int-set-2.hpp \ - ./gecode/int/ipl.hpp ./gecode/int/irt.hpp ./gecode/int/limits.hpp \ - ./gecode/int/min-distance.hpp ./gecode/int/open-var-list.hpp ./gecode/int/propagator.hpp \ - ./gecode/int/reify.hpp ./gecode/int/trace.hpp ./gecode/int/trace/bool-delta.hpp \ - ./gecode/int/trace/bool-trace-view.hpp ./gecode/int/trace/int-delta.hpp ./gecode/int/trace/int-trace-view.hpp \ - ./gecode/int/trace/traits.hpp ./gecode/int/var-imp.hpp ./gecode/int/var-imp/bool.hpp \ - ./gecode/int/var-imp/delta.hpp ./gecode/int/var-imp/int.hpp ./gecode/int/var/bool.hpp \ - ./gecode/int/var/int.hpp ./gecode/int/var/print.hpp ./gecode/int/view.hpp \ - ./gecode/int/view/bool-test.hpp ./gecode/int/view/bool.hpp ./gecode/int/view/cached.hpp \ - ./gecode/int/view/constint.hpp ./gecode/int/view/int.hpp ./gecode/int/view/iter.hpp \ - ./gecode/int/view/minus.hpp ./gecode/int/view/neg-bool.hpp ./gecode/int/view/offset.hpp \ - ./gecode/int/view/print.hpp ./gecode/int/view/rel-test.hpp ./gecode/int/view/scale.hpp \ - ./gecode/int/view/zero.hpp ./gecode/iter.hh ./gecode/iter/ranges-add.hpp \ - ./gecode/iter/ranges-append.hpp ./gecode/iter/ranges-array.hpp ./gecode/iter/ranges-cache.hpp \ - ./gecode/iter/ranges-compl.hpp ./gecode/iter/ranges-diff.hpp ./gecode/iter/ranges-empty.hpp \ - ./gecode/iter/ranges-inter.hpp ./gecode/iter/ranges-list.hpp ./gecode/iter/ranges-map.hpp \ - ./gecode/iter/ranges-minmax.hpp ./gecode/iter/ranges-minus.hpp ./gecode/iter/ranges-negative.hpp \ - ./gecode/iter/ranges-offset.hpp ./gecode/iter/ranges-operations.hpp ./gecode/iter/ranges-positive.hpp \ - ./gecode/iter/ranges-rangelist.hpp ./gecode/iter/ranges-scale.hpp ./gecode/iter/ranges-singleton-append.hpp \ - ./gecode/iter/ranges-singleton.hpp ./gecode/iter/ranges-size.hpp ./gecode/iter/ranges-union.hpp \ - ./gecode/iter/ranges-values.hpp ./gecode/iter/values-array.hpp ./gecode/iter/values-bitset.hpp \ - ./gecode/iter/values-bitsetoffset.hpp ./gecode/iter/values-inter.hpp ./gecode/iter/values-list.hpp \ - ./gecode/iter/values-map.hpp ./gecode/iter/values-minus.hpp ./gecode/iter/values-negative.hpp \ - ./gecode/iter/values-offset.hpp ./gecode/iter/values-positive.hpp ./gecode/iter/values-ranges.hpp \ - ./gecode/iter/values-singleton.hpp ./gecode/iter/values-union.hpp ./gecode/iter/values-unique.hpp \ - ./gecode/kernel.hh ./gecode/kernel/archive.hpp ./gecode/kernel/branch/action.hpp \ - ./gecode/kernel/branch/afc.hpp ./gecode/kernel/branch/chb.hpp ./gecode/kernel/branch/filter.hpp \ - ./gecode/kernel/branch/merit.hpp ./gecode/kernel/branch/print.hpp ./gecode/kernel/branch/tiebreak.hpp \ - ./gecode/kernel/branch/traits.hpp ./gecode/kernel/branch/val-commit.hpp ./gecode/kernel/branch/val-sel-commit.hpp \ - ./gecode/kernel/branch/val-sel.hpp ./gecode/kernel/branch/val.hpp ./gecode/kernel/branch/var.hpp \ - ./gecode/kernel/branch/view-sel.hpp ./gecode/kernel/branch/view-val.hpp ./gecode/kernel/branch/view.hpp \ - ./gecode/kernel/core.hpp ./gecode/kernel/data/array.hpp ./gecode/kernel/data/rnd.hpp \ - ./gecode/kernel/data/shared-array.hpp ./gecode/kernel/data/shared-data.hpp ./gecode/kernel/exception.hpp \ - ./gecode/kernel/gpi.hpp ./gecode/kernel/macros.hpp ./gecode/kernel/memory/allocators.hpp \ - ./gecode/kernel/memory/config.hpp ./gecode/kernel/memory/manager.hpp ./gecode/kernel/memory/region.hpp \ - ./gecode/kernel/modevent.hpp ./gecode/kernel/propagator/advisor.hpp ./gecode/kernel/propagator/pattern.hpp \ - ./gecode/kernel/propagator/subscribed.hpp ./gecode/kernel/propagator/wait.hpp ./gecode/kernel/range-list.hpp \ - ./gecode/kernel/shared-object.hpp ./gecode/kernel/shared-space-data.hpp ./gecode/kernel/trace/filter.hpp \ - ./gecode/kernel/trace/general.hpp ./gecode/kernel/trace/print.hpp ./gecode/kernel/trace/recorder.hpp \ - ./gecode/kernel/trace/tracer.hpp ./gecode/kernel/trace/traits.hpp ./gecode/kernel/var-imp.hpp \ - ./gecode/kernel/var-type.hpp ./gecode/kernel/var.hpp ./gecode/kernel/view.hpp \ - ./gecode/minimodel.hh ./gecode/minimodel/aliases.hpp ./gecode/minimodel/bool-expr.hpp \ - ./gecode/minimodel/channel.hpp ./gecode/minimodel/exception.hpp ./gecode/minimodel/float-expr.hpp \ - ./gecode/minimodel/float-rel.hpp ./gecode/minimodel/int-expr.hpp ./gecode/minimodel/int-rel.hpp \ - ./gecode/minimodel/ipl.hpp ./gecode/minimodel/ldsb.hpp ./gecode/minimodel/matrix.hpp \ - ./gecode/minimodel/optimize.hpp ./gecode/minimodel/reg.hpp ./gecode/minimodel/set-expr.hpp \ - ./gecode/minimodel/set-rel.hpp ./gecode/search.hh ./gecode/search/astar.hpp \ - ./gecode/search/bab.hpp ./gecode/search/base.hpp ./gecode/search/build.hpp \ - ./gecode/search/cabs.hpp ./gecode/search/cutoff.hpp ./gecode/search/dfs.hpp \ - ./gecode/search/engine.hpp ./gecode/search/exception.hpp ./gecode/search/lds.hpp \ - ./gecode/search/options.hpp ./gecode/search/pbs.hpp ./gecode/search/rbs.hpp \ - ./gecode/search/sebs.hpp ./gecode/search/seq/dead.hh ./gecode/search/statistics.hpp \ - ./gecode/search/stop.hpp ./gecode/search/support.hh ./gecode/search/trace-recorder.hpp \ - ./gecode/search/tracer.hpp ./gecode/search/traits.hpp ./gecode/search/worker-control.hh \ - ./gecode/set.hh ./gecode/set/array-traits.hpp ./gecode/set/array.hpp \ - ./gecode/set/branch.hpp ./gecode/set/branch/action.hpp ./gecode/set/branch/afc.hpp \ - ./gecode/set/branch/assign.hpp ./gecode/set/branch/chb.hpp ./gecode/set/branch/traits.hpp \ - ./gecode/set/branch/val.hpp ./gecode/set/branch/var.hpp ./gecode/set/exception.hpp \ - ./gecode/set/int.hpp ./gecode/set/limits.hpp ./gecode/set/trace.hpp \ - ./gecode/set/trace/delta.hpp ./gecode/set/trace/trace-view.hpp ./gecode/set/trace/traits.hpp \ - ./gecode/set/var-imp.hpp ./gecode/set/var-imp/delta.hpp ./gecode/set/var-imp/integerset.hpp \ - ./gecode/set/var-imp/iter.hpp ./gecode/set/var-imp/set.hpp ./gecode/set/var/print.hpp \ - ./gecode/set/var/set.hpp ./gecode/set/view.hpp ./gecode/set/view/cached.hpp \ - ./gecode/set/view/complement.hpp ./gecode/set/view/const.hpp ./gecode/set/view/print.hpp \ - ./gecode/set/view/set.hpp ./gecode/set/view/singleton.hpp ./gecode/support.hh \ - ./gecode/support/allocator.hpp ./gecode/support/auto-link.hpp ./gecode/support/bitset-base.hpp \ - ./gecode/support/bitset-offset.hpp ./gecode/support/bitset.hpp ./gecode/support/block-allocator.hpp \ - ./gecode/support/cast.hpp ./gecode/support/config.hpp ./gecode/support/dynamic-array.hpp \ - ./gecode/support/dynamic-queue.hpp ./gecode/support/dynamic-stack.hpp ./gecode/support/exception.hpp \ - ./gecode/support/failpoint.hpp ./gecode/support/hash.hpp ./gecode/support/heap.hpp \ - ./gecode/support/hw-rnd.hpp ./gecode/support/int-type.hpp ./gecode/support/macros.hpp \ - ./gecode/support/marked-pointer.hpp ./gecode/support/random.hpp ./gecode/support/ref-count.hpp \ - ./gecode/support/run-jobs.hpp ./gecode/support/sort.hpp ./gecode/support/static-stack.hpp \ - ./gecode/support/thread.hpp ./gecode/support/thread/thread.hpp ./gecode/support/timer.hpp \ - ./test/flatzinc.hh ./test/test.hh ./test/test.hpp + ./gecode/flatzinc/option.hh ./gecode/flatzinc/varspec.hh ./gecode/float.hh \ + ./gecode/float/array-traits.hpp ./gecode/float/array.hpp ./gecode/float/branch.hpp \ + ./gecode/float/branch/action.hpp ./gecode/float/branch/afc.hpp ./gecode/float/branch/assign.hpp \ + ./gecode/float/branch/chb.hpp ./gecode/float/branch/traits.hpp ./gecode/float/branch/val.hpp \ + ./gecode/float/branch/var.hpp ./gecode/float/channel.hpp ./gecode/float/exception.hpp \ + ./gecode/float/limits.hpp ./gecode/float/nextafter.hpp ./gecode/float/num.hpp \ + ./gecode/float/rounding.hpp ./gecode/float/trace.hpp ./gecode/float/trace/delta.hpp \ + ./gecode/float/trace/trace-view.hpp ./gecode/float/trace/traits.hpp ./gecode/float/val.hpp \ + ./gecode/float/var-imp.hpp ./gecode/float/var-imp/delta.hpp ./gecode/float/var-imp/float.hpp \ + ./gecode/float/var/float.hpp ./gecode/float/var/print.hpp ./gecode/float/view.hpp \ + ./gecode/float/view/float.hpp ./gecode/float/view/minus.hpp ./gecode/float/view/offset.hpp \ + ./gecode/float/view/print.hpp ./gecode/float/view/rel-test.hpp ./gecode/float/view/scale.hpp \ + ./gecode/gist.hh ./gecode/gist/gist.hpp ./gecode/instance.hh \ + ./gecode/int.hh ./gecode/int/array-traits.hpp ./gecode/int/array.hpp \ + ./gecode/int/branch.hpp ./gecode/int/branch/action.hpp ./gecode/int/branch/afc.hpp \ + ./gecode/int/branch/assign.hpp ./gecode/int/branch/chb.hpp ./gecode/int/branch/traits.hpp \ + ./gecode/int/branch/val.hpp ./gecode/int/branch/var.hpp ./gecode/int/channel.hpp \ + ./gecode/int/div.hh ./gecode/int/div.hpp ./gecode/int/exception.hpp \ + ./gecode/int/extensional.hpp ./gecode/int/extensional/dfa.hpp ./gecode/int/extensional/tuple-set.hpp \ + ./gecode/int/int-set-1.hpp ./gecode/int/int-set-2.hpp ./gecode/int/ipl.hpp \ + ./gecode/int/irt.hpp ./gecode/int/limits.hpp ./gecode/int/min-distance.hpp \ + ./gecode/int/open-var-list.hpp ./gecode/int/propagator.hpp ./gecode/int/reify.hpp \ + ./gecode/int/trace.hpp ./gecode/int/trace/bool-delta.hpp ./gecode/int/trace/bool-trace-view.hpp \ + ./gecode/int/trace/int-delta.hpp ./gecode/int/trace/int-trace-view.hpp ./gecode/int/trace/traits.hpp \ + ./gecode/int/var-imp.hpp ./gecode/int/var-imp/bool.hpp ./gecode/int/var-imp/delta.hpp \ + ./gecode/int/var-imp/int.hpp ./gecode/int/var/bool.hpp ./gecode/int/var/int.hpp \ + ./gecode/int/var/print.hpp ./gecode/int/view.hpp ./gecode/int/view/bool-test.hpp \ + ./gecode/int/view/bool.hpp ./gecode/int/view/cached.hpp ./gecode/int/view/constint.hpp \ + ./gecode/int/view/int.hpp ./gecode/int/view/iter.hpp ./gecode/int/view/minus.hpp \ + ./gecode/int/view/neg-bool.hpp ./gecode/int/view/offset.hpp ./gecode/int/view/print.hpp \ + ./gecode/int/view/rel-test.hpp ./gecode/int/view/scale.hpp ./gecode/int/view/zero.hpp \ + ./gecode/iter.hh ./gecode/iter/ranges-add.hpp ./gecode/iter/ranges-append.hpp \ + ./gecode/iter/ranges-array.hpp ./gecode/iter/ranges-cache.hpp ./gecode/iter/ranges-compl.hpp \ + ./gecode/iter/ranges-diff.hpp ./gecode/iter/ranges-empty.hpp ./gecode/iter/ranges-inter.hpp \ + ./gecode/iter/ranges-list.hpp ./gecode/iter/ranges-map.hpp ./gecode/iter/ranges-minmax.hpp \ + ./gecode/iter/ranges-minus.hpp ./gecode/iter/ranges-negative.hpp ./gecode/iter/ranges-offset.hpp \ + ./gecode/iter/ranges-operations.hpp ./gecode/iter/ranges-positive.hpp ./gecode/iter/ranges-rangelist.hpp \ + ./gecode/iter/ranges-scale.hpp ./gecode/iter/ranges-singleton-append.hpp ./gecode/iter/ranges-singleton.hpp \ + ./gecode/iter/ranges-size.hpp ./gecode/iter/ranges-union.hpp ./gecode/iter/ranges-values.hpp \ + ./gecode/iter/values-array.hpp ./gecode/iter/values-bitset.hpp ./gecode/iter/values-bitsetoffset.hpp \ + ./gecode/iter/values-inter.hpp ./gecode/iter/values-list.hpp ./gecode/iter/values-map.hpp \ + ./gecode/iter/values-minus.hpp ./gecode/iter/values-negative.hpp ./gecode/iter/values-offset.hpp \ + ./gecode/iter/values-positive.hpp ./gecode/iter/values-ranges.hpp ./gecode/iter/values-singleton.hpp \ + ./gecode/iter/values-union.hpp ./gecode/iter/values-unique.hpp ./gecode/kernel.hh \ + ./gecode/kernel/archive.hpp ./gecode/kernel/branch/action.hpp ./gecode/kernel/branch/afc.hpp \ + ./gecode/kernel/branch/chb.hpp ./gecode/kernel/branch/filter.hpp ./gecode/kernel/branch/merit.hpp \ + ./gecode/kernel/branch/print.hpp ./gecode/kernel/branch/tiebreak.hpp ./gecode/kernel/branch/traits.hpp \ + ./gecode/kernel/branch/val-commit.hpp ./gecode/kernel/branch/val-sel-commit.hpp ./gecode/kernel/branch/val-sel.hpp \ + ./gecode/kernel/branch/val.hpp ./gecode/kernel/branch/var.hpp ./gecode/kernel/branch/view-sel.hpp \ + ./gecode/kernel/branch/view-val.hpp ./gecode/kernel/branch/view.hpp ./gecode/kernel/core.hpp \ + ./gecode/kernel/data/array.hpp ./gecode/kernel/data/rnd.hpp ./gecode/kernel/data/shared-array.hpp \ + ./gecode/kernel/data/shared-data.hpp ./gecode/kernel/exception.hpp ./gecode/kernel/gpi.hpp \ + ./gecode/kernel/macros.hpp ./gecode/kernel/memory/allocators.hpp ./gecode/kernel/memory/config.hpp \ + ./gecode/kernel/memory/manager.hpp ./gecode/kernel/memory/region.hpp ./gecode/kernel/modevent.hpp \ + ./gecode/kernel/propagator/advisor.hpp ./gecode/kernel/propagator/pattern.hpp ./gecode/kernel/propagator/subscribed.hpp \ + ./gecode/kernel/propagator/wait.hpp ./gecode/kernel/range-list.hpp ./gecode/kernel/shared-object.hpp \ + ./gecode/kernel/shared-space-data.hpp ./gecode/kernel/trace/filter.hpp ./gecode/kernel/trace/general.hpp \ + ./gecode/kernel/trace/print.hpp ./gecode/kernel/trace/recorder.hpp ./gecode/kernel/trace/tracer.hpp \ + ./gecode/kernel/trace/traits.hpp ./gecode/kernel/var-imp.hpp ./gecode/kernel/var-type.hpp \ + ./gecode/kernel/var.hpp ./gecode/kernel/view.hpp ./gecode/minimodel.hh \ + ./gecode/minimodel/aliases.hpp ./gecode/minimodel/bool-expr.hpp ./gecode/minimodel/channel.hpp \ + ./gecode/minimodel/exception.hpp ./gecode/minimodel/float-expr.hpp ./gecode/minimodel/float-rel.hpp \ + ./gecode/minimodel/int-expr.hpp ./gecode/minimodel/int-rel.hpp ./gecode/minimodel/ipl.hpp \ + ./gecode/minimodel/ldsb.hpp ./gecode/minimodel/matrix.hpp ./gecode/minimodel/optimize.hpp \ + ./gecode/minimodel/reg.hpp ./gecode/minimodel/set-expr.hpp ./gecode/minimodel/set-rel.hpp \ + ./gecode/search.hh ./gecode/search/astar.hpp ./gecode/search/bab.hpp \ + ./gecode/search/base.hpp ./gecode/search/build.hpp ./gecode/search/cabs.hpp \ + ./gecode/search/cutoff.hpp ./gecode/search/dfs.hpp ./gecode/search/engine.hpp \ + ./gecode/search/exception.hpp ./gecode/search/lds.hpp ./gecode/search/options.hpp \ + ./gecode/search/pbs.hpp ./gecode/search/rbs.hpp ./gecode/search/sebs.hpp \ + ./gecode/search/seq/dead.hh ./gecode/search/statistics.hpp ./gecode/search/stop.hpp \ + ./gecode/search/support.hh ./gecode/search/trace-recorder.hpp ./gecode/search/tracer.hpp \ + ./gecode/search/traits.hpp ./gecode/search/worker-control.hh ./gecode/set.hh \ + ./gecode/set/array-traits.hpp ./gecode/set/array.hpp ./gecode/set/branch.hpp \ + ./gecode/set/branch/action.hpp ./gecode/set/branch/afc.hpp ./gecode/set/branch/assign.hpp \ + ./gecode/set/branch/chb.hpp ./gecode/set/branch/traits.hpp ./gecode/set/branch/val.hpp \ + ./gecode/set/branch/var.hpp ./gecode/set/exception.hpp ./gecode/set/int.hpp \ + ./gecode/set/limits.hpp ./gecode/set/trace.hpp ./gecode/set/trace/delta.hpp \ + ./gecode/set/trace/trace-view.hpp ./gecode/set/trace/traits.hpp ./gecode/set/var-imp.hpp \ + ./gecode/set/var-imp/delta.hpp ./gecode/set/var-imp/integerset.hpp ./gecode/set/var-imp/iter.hpp \ + ./gecode/set/var-imp/set.hpp ./gecode/set/var/print.hpp ./gecode/set/var/set.hpp \ + ./gecode/set/view.hpp ./gecode/set/view/cached.hpp ./gecode/set/view/complement.hpp \ + ./gecode/set/view/const.hpp ./gecode/set/view/print.hpp ./gecode/set/view/set.hpp \ + ./gecode/set/view/singleton.hpp ./gecode/support.hh ./gecode/support/allocator.hpp \ + ./gecode/support/auto-link.hpp ./gecode/support/bitset-base.hpp ./gecode/support/bitset-offset.hpp \ + ./gecode/support/bitset.hpp ./gecode/support/block-allocator.hpp ./gecode/support/cast.hpp \ + ./gecode/support/config.hpp ./gecode/support/dynamic-array.hpp ./gecode/support/dynamic-queue.hpp \ + ./gecode/support/dynamic-stack.hpp ./gecode/support/exception.hpp ./gecode/support/failpoint.hpp \ + ./gecode/support/hash.hpp ./gecode/support/heap.hpp ./gecode/support/hw-rnd.hpp \ + ./gecode/support/int-type.hpp ./gecode/support/macros.hpp ./gecode/support/marked-pointer.hpp \ + ./gecode/support/random.hpp ./gecode/support/ref-count.hpp ./gecode/support/run-jobs.hpp \ + ./gecode/support/sort.hpp ./gecode/support/static-stack.hpp ./gecode/support/thread.hpp \ + ./gecode/support/thread/thread.hpp ./gecode/support/timer.hpp ./test/flatzinc.hh \ + ./test/test.hh ./test/test.hpp test/flatzinc/on_restart_last_val_set$(OBJSUFFIX) test/flatzinc/on_restart_last_val_set$(SBJSUFFIX): \ ./gecode/driver.hh ./gecode/driver/options.hpp ./gecode/driver/script.hpp \ ./gecode/flatzinc.hh ./gecode/flatzinc/ast.hh ./gecode/flatzinc/conexpr.hh \ diff --git a/Makefile.in b/Makefile.in index 31c325930f..774d834cd0 100755 --- a/Makefile.in +++ b/Makefile.in @@ -1293,6 +1293,7 @@ FLATZINCTESTSRC0 = \ test/flatzinc/queen_cp2.cpp \ test/flatzinc/queen_ip.cpp \ test/flatzinc/radiation.cpp \ + test/flatzinc/random_branching.cpp \ test/flatzinc/shared_array_element.cpp \ test/flatzinc/simple_sat.cpp \ test/flatzinc/singHoist2.cpp \ diff --git a/changelog.in b/changelog.in index 7360d896ae..135a7c11d9 100755 --- a/changelog.in +++ b/changelog.in @@ -85,20 +85,45 @@ Added user-extensible random number generators with reproducible stream splitting. Splittable SplitMix replaces the old default generator; xorshift64* is available as a smaller-state alternative. The default is configurable at build time, and users can supply their own engines. -Randomized branching splits the recorded generator state by alternative, -giving distinct successor states that are reproduced during recomputation. -Complete-state save/restore and command-line state input support exact replay, -including test failures. +Randomized branching splits its generator state by alternative on commit, +so the state at a node depends only on the path and is reproduced during +recomputation. Restart and portfolio search split each slave's state, so +restarts explore different random paths. Complete-state save/restore and +command-line state input support exact replay, including test failures. [MORE] Generators store their compact state directly in their consumers, and copying -a generator produces independent state. Drivers accept checked 64-bit seeds -as well as complete state. APIs, seeded sequences, and randomized choice -archives change; no legacy sequence mode is provided. This design remains +a generator produces independent state. Posting splits each selector by its +brancher and role, so branchers given the same generator draw independent +streams. Choices store no generator state. Drivers accept checked 64-bit seeds +as well as complete state. APIs and seeded sequences change; no legacy +sequence mode is provided. This design remains provisional for a future breaking-change release. Replace the old seed() -snapshot accessor with state() and restore with state(text). Use opt.rnd() -instead of Rnd(opt.seed()) to honor complete-state command-line input. +snapshot accessor with state() and restore with state(text). The driver and +FlatZinc options no longer provide a seed() accessor; use opt.rnd() instead +of Rnd(opt.seed()) to honor time, hardware, and complete-state input. +The seed constructor is explicit, so an integer no longer converts to a +generator: write INT_VAL_RND(Rnd(42)) instead of INT_VAL_RND(42). Engine contracts are documented on Support::Random and RndGenerator. +[ENTRY] +Module: search +What: change +Rank: minor +[DESCRIPTION] +Restart-based search numbers a restart after a solution like any other +restart, so every slave's MetaInfo::restart() is unique. FlatZinc +large neighbourhood search therefore relaxes from the first restart after +the first solution. + +[ENTRY] +Module: test +What: change +Rank: minor +[DESCRIPTION] +The test harness option -replay takes the complete random state printed for +a failed iteration instead of a 64-bit seed, together with the failed +test's name passed to -test. + [RELEASE] Version: 6.5.0 Date: 2026-10-31 diff --git a/cmake/GecodeSources.cmake b/cmake/GecodeSources.cmake index 4224c040ea..a8deaeca33 100644 --- a/cmake/GecodeSources.cmake +++ b/cmake/GecodeSources.cmake @@ -372,6 +372,7 @@ set(GECODE_TEST_SOURCES test/flatzinc/queen_ip.cpp test/flatzinc/queens4.cpp test/flatzinc/radiation.cpp + test/flatzinc/random_branching.cpp test/flatzinc/sat_arith1.cpp test/flatzinc/sat_array_bool_and.cpp test/flatzinc/sat_array_bool_or.cpp diff --git a/gecode/driver.hh b/gecode/driver.hh index 999239869a..b78e3206ad 100755 --- a/gecode/driver.hh +++ b/gecode/driver.hh @@ -148,20 +148,12 @@ namespace Gecode { /// Checked seed or complete state for the build-configured random engine. class GECODE_DRIVER_EXPORT RandomOption : public BaseOption { - uint64_t cur; Rnd initial; bool seed_given = false; bool state_given = false; - bool state_only = false; public: RandomOption(const char* o, const char* e, uint64_t v); void value(uint64_t v); - /** \brief Return the numeric seed - * - * Throws std::logic_error after complete-state, time, or hw input. - * Use rnd() to honor all initialization forms. - */ - uint64_t value(void) const; /// Construct an independent generator at the configured initial state. Rnd rnd(void) const; virtual int parse(int argc, char* argv[]); @@ -552,12 +544,6 @@ namespace Gecode { /// Set default seed value void seed(uint64_t s); - /** \brief Return the numeric seed - * - * Throws std::logic_error after complete-state, time, or hw input. - * Use rnd() to honor all initialization forms. - */ - uint64_t seed(void) const; /// Independent generator initialized from the seed or full-state option Rnd rnd(void) const; diff --git a/gecode/driver/options.cpp b/gecode/driver/options.cpp index f92160fc89..770041ba7c 100755 --- a/gecode/driver/options.cpp +++ b/gecode/driver/options.cpp @@ -117,14 +117,8 @@ namespace Gecode { value(v); } void RandomOption::value(uint64_t v) { - cur = v; initial.seed(v); - seed_given = state_given = state_only = false; - } - uint64_t RandomOption::value(void) const { - if (state_only) - throw std::logic_error("Random initialization has no seed; use rnd()"); - return cur; + seed_given = state_given = false; } Rnd RandomOption::rnd(void) const { return initial; @@ -149,16 +143,13 @@ namespace Gecode { throw std::invalid_argument("Seed and state options cannot be combined"); if (full) { initial.state(std::string(arg)); - state_given = state_only = true; + state_given = true; } else { if (!strcmp(arg,"time") || !strcmp(arg,"hw")) { if (!strcmp(arg,"time")) initial.time(); else initial.hw(); - state_only = true; std::cerr << "% Random state: -state " << initial.state() << std::endl; } else { - cur = Support::random_seed(arg); - initial.seed(cur); - state_only = false; + initial.seed(Support::random_seed(arg)); } seed_given = true; } diff --git a/gecode/driver/options.hpp b/gecode/driver/options.hpp index eac934594c..600bc7b59c 100755 --- a/gecode/driver/options.hpp +++ b/gecode/driver/options.hpp @@ -276,11 +276,6 @@ namespace Gecode { Options::seed(uint64_t s) { _seed.value(s); } - inline uint64_t - Options::seed(void) const { - return _seed.value(); - } - inline Rnd Options::rnd(void) const { return _seed.rnd(); diff --git a/gecode/flatzinc.hh b/gecode/flatzinc.hh index 9acae47549..c6ee2dcbeb 100755 --- a/gecode/flatzinc.hh +++ b/gecode/flatzinc.hh @@ -349,12 +349,6 @@ namespace Gecode { namespace FlatZinc { unsigned long long int node(void) const { return _node.value(); } unsigned long long int fail(void) const { return _fail.value(); } double time(void) const { return _time.value(); } - /** \brief Return the numeric seed - * - * Throws std::logic_error after complete-state, time, or hw input. - * Use rnd() to honor all initialization forms. - */ - uint64_t seed(void) const { return _seed.value(); } Rnd rnd(void) const { return _seed.rnd(); } double step(void) const { return _step.value(); } const char* output(void) const { return _output.value(); } diff --git a/gecode/flatzinc/branch.cpp b/gecode/flatzinc/branch.cpp index bb0be917bf..27013e045b 100644 --- a/gecode/flatzinc/branch.cpp +++ b/gecode/flatzinc/branch.cpp @@ -67,6 +67,7 @@ namespace Gecode { namespace FlatZinc { unsigned int b) { const PosIntChoice& c = static_cast(_c); + random_split(b); int p=c.pos(); int n=c.val(); if (p < x.size()) { return me_failed(xvsc->commit(home,b,x[p],p,n)) ? @@ -78,6 +79,12 @@ namespace Gecode { namespace FlatZinc { } } + void + IntBoolBrancherBase::random_split(unsigned int b) { + xvsc->random_split(b); + yvsc->random_split(b); + } + NGL* IntBoolBrancherBase::ngl(Space& home, const Choice& _c, unsigned int b) const { diff --git a/gecode/flatzinc/branch.hh b/gecode/flatzinc/branch.hh index b780e9b5e9..792a3b8727 100644 --- a/gecode/flatzinc/branch.hh +++ b/gecode/flatzinc/branch.hh @@ -289,6 +289,8 @@ namespace Gecode { namespace FlatZinc { virtual const Choice* choice(const Space& home, Archive& e); /// Perform commit for choice \a c and alternative \a b virtual ExecStatus commit(Space& home, const Choice& c, unsigned int b); + /// Split the state of both value selectors for \a b + virtual void random_split(unsigned int b); /// Create no-good literal for choice \a c and alternative \a b virtual NGL* ngl(Space& home, const Choice& c, unsigned int b) const; /// Print branch for choice \a c and alternative \a b diff --git a/gecode/flatzinc/branch.hpp b/gecode/flatzinc/branch.hpp index 70517cdd17..8c65d509f4 100644 --- a/gecode/flatzinc/branch.hpp +++ b/gecode/flatzinc/branch.hpp @@ -308,6 +308,11 @@ namespace Gecode { namespace FlatZinc { ValSelCommitBase* xvsc0, ValSelCommitBase* yvsc0) : Brancher(home), x(x0), y(y0), start(0), xvsc(xvsc0), yvsc(yvsc0) { + // Key each selector by brancher and role, as for ViewValBrancher. + xvsc->random_split(id()); + xvsc->random_split(0U); + yvsc->random_split(id()); + yvsc->random_split(1U); home.notice(*this,AP_DISPOSE,true); } diff --git a/gecode/float/branch.hh b/gecode/float/branch.hh index 619fe273ed..e68aaec45f 100644 --- a/gecode/float/branch.hh +++ b/gecode/float/branch.hh @@ -259,9 +259,7 @@ namespace Gecode { namespace Float { namespace Branch { /// The used random number generator Rnd r; public: - unsigned int random_words(void) const { return r.words(); } - uint64_t* random_save(uint64_t* out) const { return r.save(out); } - const uint64_t* random_commit(const uint64_t* in, unsigned int a) { return r.restore_split(in,a); } + void random_split(unsigned int a) { r = r.split(a); } /// Constructor for initialization ValSelRnd(Space& home, const ValBranch& vb); /// Constructor for cloning diff --git a/gecode/float/branch/val-sel.hpp b/gecode/float/branch/val-sel.hpp index f506b99a90..b87b56b158 100644 --- a/gecode/float/branch/val-sel.hpp +++ b/gecode/float/branch/val-sel.hpp @@ -85,7 +85,8 @@ namespace Gecode { namespace Float { namespace Branch { forceinline FloatNumBranch ValSelRnd::val(const Space&, FloatView x, int) { FloatNumBranch nl; - nl.n = x.med(); nl.l = (r(2U) == 0U); + Rnd t = r; + nl.n = x.med(); nl.l = (t(2U) == 0U); return nl; } forceinline bool diff --git a/gecode/int/branch.hh b/gecode/int/branch.hh index 32b0eed85c..1a0d1ae812 100755 --- a/gecode/int/branch.hh +++ b/gecode/int/branch.hh @@ -358,9 +358,7 @@ namespace Gecode { namespace Int { namespace Branch { public: ValSelRnd(Space& home, const Random& random) : ValSel(home,ValBranch()), r(random) {} - unsigned int random_words(void) const { return r.words(); } - uint64_t* random_save(uint64_t* out) const { return r.save(out); } - const uint64_t* random_commit(const uint64_t* in, unsigned int a) { return r.restore_split(in,a); } + void random_split(unsigned int a) { r = r.split(a); } /// Constructor for initialization ValSelRnd(Space& home, const ValBranch& vb); /// Constructor for cloning diff --git a/gecode/int/branch/open.hpp b/gecode/int/branch/open.hpp index 8153a5a390..87ce05dc69 100644 --- a/gecode/int/branch/open.hpp +++ b/gecode/int/branch/open.hpp @@ -63,8 +63,16 @@ namespace Gecode { namespace Int { namespace Branch { : Brancher(home), list(list0), vsc(vsc0), filter(bf), printer(vvp), values_min(values_min0), select_first(select_first0), start(0) { - for (int i=0; irandom_split(id()); + vs[i]->random_split(1U+static_cast(i)); + } + if (vsc) { + vsc->random_split(id()); + vsc->random_split(0U); + } home.notice(*this,AP_DISPOSE,true); } @@ -168,8 +176,17 @@ namespace Gecode { namespace Int { namespace Branch { return new PosValuesChoice(*this,a,Pos(p),e); } + virtual void + random_split(unsigned int a) { + for (int i=0; irandom_split(a); + if (vsc) + vsc->random_split(a); + } + virtual ExecStatus commit(Space& home, const Choice& c, unsigned int a) { + random_split(a); const int p = static_cast(c).pos().pos; if (p >= 0) { // Recomputation can replay choices before propagation grows the prefix. diff --git a/gecode/int/branch/val-sel.hpp b/gecode/int/branch/val-sel.hpp index 7d5af94453..d7a1c61fb8 100755 --- a/gecode/int/branch/val-sel.hpp +++ b/gecode/int/branch/val-sel.hpp @@ -106,7 +106,8 @@ namespace Gecode { namespace Int { namespace Branch { template forceinline int ValSelRnd::val(const Space&, View x, int) { - unsigned int p = r(x.size()); + Random t = r; + unsigned int p = t(x.size()); for (ViewRanges i(x); i(); ++i) { if (i.width() > p) return i.min() + static_cast(p); diff --git a/gecode/int/branch/view-values.hpp b/gecode/int/branch/view-values.hpp index f173f5094e..2508a7c84c 100644 --- a/gecode/int/branch/view-values.hpp +++ b/gecode/int/branch/view-values.hpp @@ -123,14 +123,8 @@ namespace Gecode { namespace Int { namespace Branch { const Choice* ViewValuesBrancher::choice(Space& home) { Pos p = ViewBrancher::pos(home); - unsigned int words = this->random_words(); - auto view = ViewBrancher::view(p); - if (!words) - return new PosValuesChoice(*this,p,view); - std::unique_ptr> c( - new (words) RndChoice(words,*this,p,view)); - this->random_save(c->data()); - return c.release(); + return new PosValuesChoice(*this,p, + ViewBrancher::view(p)); } template @@ -141,13 +135,7 @@ namespace Gecode { namespace Int { namespace Branch { int p; unsigned int a; e >> p >> a; - unsigned int words = this->random_words(); - if (!words) - return new PosValuesChoice(*this,a,p,e); - std::unique_ptr> c( - new (words) RndChoice(words,*this,a,p,e)); - c->read(e); - return c.release(); + return new PosValuesChoice(*this,a,p,e); } template @@ -156,8 +144,7 @@ namespace Gecode { namespace Int { namespace Branch { unsigned int a) { const PosValuesChoice& pvc = static_cast(c); - if (this->random_words()) - this->random_commit(pvc.random_data(),a); + this->random_split(a); IntView x(ViewBrancher::view(pvc.pos())); unsigned int b = min ? a : (pvc.alternatives() - 1 - a); return me_failed(x.eq(home,pvc.val(b))) ? ES_FAILED : ES_OK; diff --git a/gecode/int/ldsb/brancher.hpp b/gecode/int/ldsb/brancher.hpp index 8556f2df1c..e9569c4799 100755 --- a/gecode/int/ldsb/brancher.hpp +++ b/gecode/int/ldsb/brancher.hpp @@ -146,10 +146,15 @@ namespace Gecode { namespace Int { namespace LDSB { class Filter, class Print> const Choice* LDSBBrancher::choice(Space& home) { - Pos p = ViewBrancher::pos(home); - View v = ViewBrancher::view(p); - int choicePos = p.pos; - Val choiceVal = this->vsc->val(home,v,choicePos); + // Making the PVC here is not so nice, I think. + const Choice* c = ViewValBrancher::choice(home); + const PosValChoice* pvc = static_cast* >(c); + + // Compute symmetries. + + int choicePos = pvc->pos().pos; + int choiceVal = pvc->val(); + delete c; _prevPos = choicePos; @@ -184,14 +189,7 @@ namespace Gecode { namespace Int { namespace LDSB { ++it; } - unsigned int words = this->random_words()+this->vsc->random_words(); - if (!words) - return new LDSBChoice(*this,a,choicePos,choiceVal,literals,nliterals); - std::unique_ptr>> result( - new (words) RndChoice> - (words,*this,a,choicePos,choiceVal,literals,nliterals)); - this->vsc->random_save(this->random_save(result->data())); - return result.release(); + return new LDSBChoice(*this,a,choicePos,choiceVal, literals, nliterals); } @@ -209,13 +207,7 @@ namespace Gecode { namespace Int { namespace LDSB { e >> literals[i]._variable; e >> literals[i]._value; } - unsigned int words = this->random_words()+this->vsc->random_words(); - if (!words) - return new LDSBChoice(*this,a,p,v,literals,nliterals); - std::unique_ptr>> result( - new (words) RndChoice>(words,*this,a,p,v,literals,nliterals)); - result->read(e); - return result.release(); + return new LDSBChoice(*this,a,p,v, literals, nliterals); } template <> diff --git a/gecode/kernel/branch/val-sel-commit.hpp b/gecode/kernel/branch/val-sel-commit.hpp index 6f3b1e3935..88b230c8d7 100644 --- a/gecode/kernel/branch/val-sel-commit.hpp +++ b/gecode/kernel/branch/val-sel-commit.hpp @@ -43,12 +43,8 @@ namespace Gecode { template class ValSelCommitBase { public: - /// \copydoc Gecode::ViewSel::random_words - virtual unsigned int random_words(void) const { return 0; } - /// \copydoc Gecode::ViewSel::random_save - virtual uint64_t* random_save(uint64_t* out) const { return out; } - /// \copydoc Gecode::ViewSel::random_commit - virtual const uint64_t* random_commit(const uint64_t* in, unsigned int) { return in; } + /// \copydoc Gecode::ViewSel::random_split + virtual void random_split(unsigned int a) { (void) a; } /// View type typedef View_ View; /// Corresponding variable type @@ -107,11 +103,7 @@ namespace Gecode { /// The commit object used ValCommit c; public: - unsigned int random_words(void) const override { return s.random_words(); } - uint64_t* random_save(uint64_t* out) const override { return s.random_save(out); } - const uint64_t* random_commit(const uint64_t* in, unsigned int a) override { - return s.random_commit(in,a); - } + void random_split(unsigned int a) override { s.random_split(a); } /// Constructor for initialization ValSelCommit(Space& home, const ValBranch& vb); /// Construct a user-parameterized selector with an ordinary commit policy. diff --git a/gecode/kernel/branch/val-sel.hpp b/gecode/kernel/branch/val-sel.hpp index be21ac2e4e..e518ad9475 100755 --- a/gecode/kernel/branch/val-sel.hpp +++ b/gecode/kernel/branch/val-sel.hpp @@ -43,12 +43,8 @@ namespace Gecode { template class ValSel { public: - /// \copydoc Gecode::ViewSel::random_words - unsigned int random_words(void) const { return 0; } - /// \copydoc Gecode::ViewSel::random_save - uint64_t* random_save(uint64_t* out) const { return out; } - /// \copydoc Gecode::ViewSel::random_commit - const uint64_t* random_commit(const uint64_t* in, unsigned int) { return in; } + /// \copydoc Gecode::ViewSel::random_split + void random_split(unsigned int a) { (void) a; } /// View type typedef View_ View; /// Corresponding variable type diff --git a/gecode/kernel/branch/view-sel.hpp b/gecode/kernel/branch/view-sel.hpp index 00b949b639..43f94bd4d5 100644 --- a/gecode/kernel/branch/view-sel.hpp +++ b/gecode/kernel/branch/view-sel.hpp @@ -43,27 +43,14 @@ namespace Gecode { template class ViewSel { public: - /** \brief Number of 64-bit words needed to record this selector's random state + /** \brief Replace random state by its split for \a a * - * Count and layout must remain fixed for the selector and its clones. - * Selectors without random state return zero. + * A randomized selector draws from a copy of its state when selecting, + * so selection leaves the state unchanged; only this function changes + * it. Its brancher calls it on commit and at posting, as described for + * Brancher::random_split(). Selectors without random state do nothing. */ - virtual unsigned int random_words(void) const { return 0; } - /** \brief Save random state after position and value selection - * - * Write exactly random_words() words to \a out without advancing the - * generator, and return the pointer past them. The caller supplies that - * many writable words. A zero-word selector returns \a out unchanged. - */ - virtual uint64_t* random_save(uint64_t* out) const { return out; } - /** \brief Restore recorded random state and split for the given alternative - * - * Read exactly random_words() words from \a in and return the pointer - * past them. Replace the selector's state with the recorded parent's - * split for the public alternative index, independently of its current - * state. A zero-word selector leaves its state and \a in unchanged. - */ - virtual const uint64_t* random_commit(const uint64_t* in, unsigned int) { return in; } + virtual void random_split(unsigned int a) { (void) a; } /// Define the view type typedef View_ View; /// The corresponding variable type @@ -173,11 +160,7 @@ namespace Gecode { /// The random number generator used Random r; public: - unsigned int random_words(void) const override { return r.words(); } - uint64_t* random_save(uint64_t* out) const override { return r.save(out); } - const uint64_t* random_commit(const uint64_t* in, unsigned int a) override { - return r.restore_split(in,a); - } + void random_split(unsigned int a) override { r = r.split(a); } /// Construct a selector with a user-supplied value-type generator. ViewSelRnd(Space& home, const Random& random) : ViewSel(home,VarBranch()), r(random) {} @@ -519,12 +502,13 @@ namespace Gecode { template int ViewSelRnd::select(Space&, ViewArray& x, int s) { + Random t = r; unsigned int n=1; int j=s; for (int i=s+1; i::select(Space& home, ViewArray& x, int s, BrancherFilter& f) { + Random t = r; unsigned int n=1; int j=s; for (int i=s+1; i void ViewSelRnd::brk(Space&, ViewArray&, int* ties, int& n) { - ties[0] = ties[static_cast(r(static_cast(n)))]; + Random t = r; + ties[0] = ties[static_cast(t(static_cast(n)))]; n=1; } template int ViewSelRnd::select(Space&, ViewArray&, int* ties, int n) { - return ties[static_cast(r(static_cast(n)))]; + Random t = r; + return ties[static_cast(t(static_cast(n)))]; } template ViewSel* diff --git a/gecode/kernel/branch/view-val.hpp b/gecode/kernel/branch/view-val.hpp index 7e00ff26ab..e28afc1ef1 100644 --- a/gecode/kernel/branch/view-val.hpp +++ b/gecode/kernel/branch/view-val.hpp @@ -115,6 +115,8 @@ namespace Gecode { virtual const Choice* choice(const Space& home, Archive& e); /// Perform commit for choice \a c and alternative \a b virtual ExecStatus commit(Space& home, const Choice& c, unsigned int b); + /// Split the state of every view and value selector for \a b + virtual void random_split(unsigned int b); /// Create no-good literal for choice \a c and alternative \a b virtual NGL* ngl(Space& home, const Choice& c, unsigned int b) const; /** @@ -232,6 +234,8 @@ namespace Gecode { BranchFilter bf, VarValPrint vvp) : ViewBrancher(home,x,vs,bf), vsc(vsc0), p(vvp) { + vsc->random_split(this->id()); + vsc->random_split(0U); if (vsc->notice() || f.notice() || p.notice()) home.notice(*this,AP_DISPOSE,true); } @@ -271,14 +275,7 @@ namespace Gecode { ViewValBrancher::choice(Space& home) { Pos p = ViewBrancher::pos(home); View v = ViewBrancher::view(p); - Val value = vsc->val(home,v,p.pos); - unsigned int words = this->random_words()+vsc->random_words(); - if (!words) - return new PosValChoice(*this,a,p,value); - std::unique_ptr>> c( - new (words) RndChoice>(words,*this,a,p,value)); - vsc->random_save(this->random_save(c->data())); - return c.release(); + return new PosValChoice(*this,a,p,vsc->val(home,v,p.pos)); } template> p; Val v; e >> v; - unsigned int words = this->random_words()+vsc->random_words(); - if (!words) - return new PosValChoice(*this,a,p,v); - std::unique_ptr>> c( - new (words) RndChoice>(words,*this,a,p,v)); - c->read(e); - return c.release(); + return new PosValChoice(*this,a,p,v); } template& pvc = static_cast&>(c); - if (this->random_words()+vsc->random_words()) { - assert(pvc.random_data() != nullptr); - vsc->random_commit(this->random_commit(pvc.random_data(),b),b); - } + random_split(b); return me_failed(vsc->commit(home,b, ViewBrancher::view(pvc.pos()), pvc.pos().pos, @@ -316,6 +304,14 @@ namespace Gecode { ? ES_FAILED : ES_OK; } + template + void + ViewValBrancher::random_split(unsigned int b) { + ViewBrancher::random_split(b); + vsc->random_split(b); + } + template NGL* diff --git a/gecode/kernel/branch/view.hpp b/gecode/kernel/branch/view.hpp index 770a486865..25a07a3a34 100644 --- a/gecode/kernel/branch/view.hpp +++ b/gecode/kernel/branch/view.hpp @@ -31,8 +31,6 @@ * */ -#include - namespace Gecode { /** @@ -66,48 +64,10 @@ namespace Gecode { PosChoice(const Brancher& b, unsigned int a, const Pos& p); /// Return position in array const Pos& pos(void) const; - /// Random selector state, present only in randomized brancher choices. - virtual const uint64_t* random_data(void) const { return nullptr; } /// Archive into \a e virtual void archive(Archive& e) const; }; - /// Randomized brancher choice with state words in the same allocation. - /// Nonrandom branchers use the original choice type without this payload. - template - class alignas(uint64_t) RndChoice : public Base { - unsigned int count; - public: - template - RndChoice(unsigned int words, Args&&... args) - : Base(std::forward(args)...), count(words) { - std::uninitialized_default_construct_n(data(),count); - } - RndChoice(const RndChoice&) = delete; - static void* operator new(size_t size, unsigned int words) { - return ::operator new(size+size_t(words)*sizeof(uint64_t)); - } - static void operator delete(void* p) { ::operator delete(p); } - static void operator delete(void* p, unsigned int) { ::operator delete(p); } - uint64_t* data(void) { return reinterpret_cast(this+1); } - const uint64_t* data(void) const { return reinterpret_cast(this+1); } - const uint64_t* random_data(void) const override { return data(); } - void read(Archive& e) { - for (unsigned int i=0; i> lo >> hi; - data()[i] = uint64_t(lo) | (uint64_t(hi)<<32); - } - } - void archive(Archive& e) const override { - Base::archive(e); - for (unsigned int i=0; i(data()[i]) - << static_cast(data()[i]>>32); - } - }; - /** * \brief Generic brancher by view selection * @@ -125,20 +85,6 @@ namespace Gecode { mutable int start; /// View selection objects ViewSel* vs[n]; - /// Compact state of this brancher's variable selectors only. - unsigned int random_words(void) const { - unsigned int words=0; - for (int i=0; irandom_words(); - return words; - } - uint64_t* random_save(uint64_t* out) const { - for (int i=0; irandom_save(out); - return out; - } - const uint64_t* random_commit(const uint64_t* in, unsigned int a) { - for (int i=0; irandom_commit(in,a); - return in; - } /// Filter function Filter f; /// Return position information @@ -153,6 +99,8 @@ namespace Gecode { public: /// Check status of brancher, return true if alternatives left virtual bool status(const Space& home) const; + /// Split the state of every view selector for \a a + virtual void random_split(unsigned int a); /// Delete brancher and return its size virtual size_t dispose(Space& home); }; @@ -192,8 +140,12 @@ namespace Gecode { ViewSel* vs0[n], BranchFilter bf) : Brancher(home), x(x0), start(0), f(bf) { - for (int i=0; irandom_split(id()); + vs[i]->random_split(1U+static_cast(i)); + } for (int i=0; inotice()) { home.notice(*this,AP_DISPOSE,true); @@ -222,6 +174,13 @@ namespace Gecode { return false; } + template + void + ViewBrancher::random_split(unsigned int a) { + for (int i=0; irandom_split(a); + } + template inline Pos ViewBrancher::pos(Space& home) { diff --git a/gecode/kernel/core.cpp b/gecode/kernel/core.cpp index 54ab989695..430e205a77 100644 --- a/gecode/kernel/core.cpp +++ b/gecode/kernel/core.cpp @@ -104,6 +104,9 @@ namespace Gecode { std::ostream&) const { } + void + Brancher::random_split(unsigned int) {} + /* * Space: Misc @@ -683,6 +686,18 @@ namespace Gecode { } } + void + Space::random_split(unsigned long long int i) { + if (i == 0) + return; + // Alternatives are smaller than UINT_MAX, so slave streams stay apart. + for (ActorLink* b = bl.next(); b != &bl; b = b->next()) { + Brancher::cast(b)->random_split(UINT_MAX); + Brancher::cast(b)->random_split(static_cast(i >> 32)); + Brancher::cast(b)->random_split(static_cast(i)); + } + } + void Space::print(const Choice& c, unsigned int a, std::ostream& o) const { if (a >= c.alternatives()) diff --git a/gecode/kernel/core.hpp b/gecode/kernel/core.hpp index 08423430ee..0243fa1cde 100755 --- a/gecode/kernel/core.hpp +++ b/gecode/kernel/core.hpp @@ -1530,6 +1530,18 @@ namespace Gecode { GECODE_KERNEL_EXPORT virtual void print(const Space& home, const Choice& c, unsigned int a, std::ostream& o) const; + /** + * \brief Replace the state of each random selector by its split for \a a + * + * Randomized selectors draw from copies of their state, so a choice + * leaves the state unchanged. commit() calls this for the committed + * alternative, which makes the state at a node a function of the + * posted state and the path, reproduced by cloning and recomputation. + * Posting and Space::random_split() use it to derive independent + * streams. The default does nothing. + */ + GECODE_KERNEL_EXPORT + virtual void random_split(unsigned int a); //@} /// \name Id and group support //@{ @@ -2401,6 +2413,19 @@ namespace Gecode { */ GECODE_KERNEL_EXPORT void print(const Choice& c, unsigned int a, std::ostream& o) const; + /** + * \brief Give the random selectors of all branchers a stream for slave \a i + * + * Restart and portfolio engines call this on every slave with its restart + * or asset number, so that slaves cloned from one master do not repeat + * its random decisions. Index 0 leaves the state unchanged, so the first + * slave searches as the model does without a meta engine. Other indices + * split by a separator no alternative uses, then by the high and low 32 + * bits of \a i. + * \ingroup TaskSearch + */ + GECODE_KERNEL_EXPORT + void random_split(unsigned long long int i); /** * \brief Notice actor property diff --git a/gecode/kernel/data/rnd.hpp b/gecode/kernel/data/rnd.hpp index a64fef0e8a..1d5233f8ff 100755 --- a/gecode/kernel/data/rnd.hpp +++ b/gecode/kernel/data/rnd.hpp @@ -40,26 +40,31 @@ namespace Gecode { /** * \brief Small value-type random generator owned by its consumer * - * Copying preserves complete state in independent storage. Branching - * descriptions and selectors own their copies; cloning preserves selector - * state without drawing or splitting. Split explicitly to start different streams: + * Copying preserves complete state in independent storage, and split() + * derives a different stream: * \code * Rnd parent(42); * Rnd same = parent; * Rnd child = parent.split(1); * \endcode * - * Only the active randomized brancher's selectors are recorded in a choice. - * Committing an alternative restores their recorded states and splits by - * the public alternative index. Replay reproduces random state along that - * path, but does not promise identical parallel scheduling, solution order, - * or search trees under weakly monotonic propagation. + * Branching descriptions and selectors own their copies. A brancher keys + * each selector's copy by the brancher's id and the selector's role, so + * passing one generator to several selectors or branchers gives them + * independent streams. Selectors draw from a copy of their state and + * replace the state by its split for the alternative on commit. The state + * at a node is therefore determined by the posted state and the path, and + * cloning and recomputation reproduce it. Restart and portfolio engines + * split the state for each slave; see Space::random_split(). Replay does + * not promise identical parallel scheduling, solution order, or search + * trees under weakly monotonic propagation. * - * Model-owned or callback-owned generators are not recorded automatically. - * A custom brancher must preserve their state in its choices when replay - * needs it. Space cloning does not copy mutable state in shared callback - * captures. Engine must meet the Support::Random contract, including splitting. - * See examples/random-engine.cpp for a user-defined engine and selectors. + * Model-owned or callback-owned generators change only as their owner + * changes them; a custom brancher using one must make its state a + * function of the path, for example by splitting it on commit. Space + * cloning does not copy mutable state in shared callback captures. Engine + * must meet the Support::Random contract, including splitting. See + * examples/random-engine.cpp for a user-defined engine and selectors. * \ingroup TaskModel */ template @@ -67,7 +72,10 @@ namespace Gecode { Support::Random r; public: using State = typename Engine::State; - explicit RndGenerator(uint64_t seed=1) : r(seed) {} + /// Generator seeded with 1 + RndGenerator(void) : r(1) {} + /// Seed explicitly, so an integer never converts to a generator + explicit RndGenerator(uint64_t seed) : r(seed) {} explicit RndGenerator(const Support::Random& source) : r(source) {} RndGenerator copy(void) const { return *this; } RndGenerator split(uint32_t a) const { return RndGenerator(r.split(a)); } @@ -81,23 +89,8 @@ namespace Gecode { State state_words(void) const { return r.state(); } void state(const State& words) { r.state(words); } static const char* name(void) { return Engine::name(); } - static constexpr unsigned int words(void) { return std::tuple_size::value; } template Type operator ()(Type bound) { return r(bound); } - /// Save compact choice data and advance the output pointer. - uint64_t* save(uint64_t* out) const { - auto s = r.state(); - return std::copy(s.begin(),s.end(),out); - } - /// Derive this consumer's next state from its recorded choice data. - const uint64_t* restore_split(const uint64_t* in, uint32_t a) { - State s; - std::copy(in,in+s.size(),s.begin()); - auto parent = r; - parent.state(s); - r = parent.split(a); - return in+s.size(); - } }; /// Build-configured default, with exactly the engine's inline state size. diff --git a/gecode/search/pbs.hpp b/gecode/search/pbs.hpp index f042a18f35..5c8a71021e 100644 --- a/gecode/search/pbs.hpp +++ b/gecode/search/pbs.hpp @@ -186,6 +186,7 @@ namespace Gecode { namespace Search { master : master->clone()); if (slave.get() == master) unowned_master = nullptr; + slave->random_split(static_cast(i)); (void) slave->slave(i); slaves[i] = build(slave.get(),opt); } @@ -227,6 +228,7 @@ namespace Gecode { namespace Search { master : master->clone()); if (slave.get() == master) unowned_master = nullptr; + slave->random_split(static_cast(i)); (void) slave->slave(static_cast(i)); slaves[i] = (*sebs[i])(slave.get()); } @@ -277,6 +279,7 @@ namespace Gecode { namespace Search { master : master->clone()); if (slave.get() == master) unowned_master = nullptr; + slave->random_split(static_cast(i)); (void) slave->slave(static_cast(i)); slaves[i] = build(slave.get(),opt); } @@ -321,6 +324,7 @@ namespace Gecode { namespace Search { master : master->clone()); if (slave.get() == master) unowned_master = nullptr; + slave->random_split(static_cast(i)); (void) slave->slave(static_cast(i)); slaves[i] = (*sebs[i])(slave.get()); } diff --git a/gecode/search/rbs.hpp b/gecode/search/rbs.hpp index 846d8dbb77..97e141d47d 100644 --- a/gecode/search/rbs.hpp +++ b/gecode/search/rbs.hpp @@ -103,6 +103,7 @@ namespace Gecode { std::unique_ptr master(m_opt.clone ? s->clone() : s); std::unique_ptr slave(master->clone()); MetaInfo mi(0,MetaInfo::RR_INIT,0,0,nullptr,NoGoods::eng); + slave->random_split(mi.restart()); slave->slave(mi); std::unique_ptr leaf( Search::build(slave.get(),e_opt)); diff --git a/gecode/search/seq/rbs.cpp b/gecode/search/seq/rbs.cpp index 033c525b1d..d8d88568b9 100755 --- a/gecode/search/seq/rbs.cpp +++ b/gecode/search/seq/rbs.cpp @@ -66,7 +66,8 @@ namespace Gecode { namespace Search { namespace Seq { NoGoods& ng = e->nogoods(); // Reset number of no-goods found ng.ng(0); - MetaInfo mi(stop->restarts(),MetaInfo::RR_SOL,sslr,e->statistics().fail,last,ng); + // Number this restart as limit restarts are, so every slave's is unique. + MetaInfo mi(stop->restarts()+1,MetaInfo::RR_SOL,sslr,e->statistics().fail,last,ng); bool r = master->master(mi); stop->nogood(ng.ng()); if (stop->status(master) == SS_FAILED) { @@ -79,6 +80,7 @@ namespace Gecode { namespace Search { namespace Seq { stop->update(e->statistics()); Space* slave = master; master = master->clone(); + slave->random_split(mi.restart()); complete = slave->slave(mi); e->reset(slave); sslr = 0; @@ -111,6 +113,7 @@ namespace Gecode { namespace Search { namespace Seq { return nullptr; Space* slave = master; master = master->clone(); + slave->random_split(mi.restart()); complete = slave->slave(mi); e->reset(slave); } else { diff --git a/gecode/set/branch.hh b/gecode/set/branch.hh index 5d73652e63..42bdc86952 100644 --- a/gecode/set/branch.hh +++ b/gecode/set/branch.hh @@ -279,9 +279,7 @@ namespace Gecode { namespace Set { namespace Branch { /// The used random number generator Rnd r; public: - unsigned int random_words(void) const { return r.words(); } - uint64_t* random_save(uint64_t* out) const { return r.save(out); } - const uint64_t* random_commit(const uint64_t* in, unsigned int a) { return r.restore_split(in,a); } + void random_split(unsigned int a) { r = r.split(a); } /// Constructor for initialization ValSelRnd(Space& home, const ValBranch& vb); /// Constructor for cloning diff --git a/gecode/set/branch/val-sel.hpp b/gecode/set/branch/val-sel.hpp index 972fe154c8..630a38eca0 100644 --- a/gecode/set/branch/val-sel.hpp +++ b/gecode/set/branch/val-sel.hpp @@ -99,7 +99,8 @@ namespace Gecode { namespace Set { namespace Branch { forceinline int ValSelRnd::val(const Space&, SetView x, int) { UnknownRanges u(x); - unsigned int p = r(Iter::Ranges::size(u)); + Rnd t = r; + unsigned int p = t(Iter::Ranges::size(u)); for (UnknownRanges i(x); i(); ++i) { if (i.width() > p) return i.min() + static_cast(p); diff --git a/gecode/support/random.hpp b/gecode/support/random.hpp index 0d03a1fc2b..8af97ce906 100755 --- a/gecode/support/random.hpp +++ b/gecode/support/random.hpp @@ -207,6 +207,14 @@ namespace Gecode { namespace Support { return value; } + /// Stafford's Mix13 permutation: bijective, and maps only zero to zero + inline uint64_t + random_mix(uint64_t z) { + z = (z ^ (z >> 30)) * UINT64_C(0xbf58476d1ce4e5b9); + z = (z ^ (z >> 27)) * UINT64_C(0x94d049bb133111eb); + return z ^ (z >> 31); + } + /** \brief Splittable SplitMix with two 64-bit state words * * Implements the SplitMix design of Steele, Lea, and Flood (OOPSLA 2014), @@ -223,11 +231,7 @@ namespace Gecode { namespace Support { using State = std::array; private: State s; - static uint64_t mix(uint64_t z) { - z = (z ^ (z >> 30)) * UINT64_C(0xbf58476d1ce4e5b9); - z = (z ^ (z >> 27)) * UINT64_C(0x94d049bb133111eb); - return z ^ (z >> 31); - } + static uint64_t mix(uint64_t z) { return random_mix(z); } static uint64_t gamma(uint64_t z) { z = (z ^ (z >> 33)) * UINT64_C(0xff51afd7ed558ccd); z = (z ^ (z >> 33)) * UINT64_C(0xc4ceb9fe1a85ec53); @@ -268,12 +272,17 @@ namespace Gecode { namespace Support { } }; - /** \brief One-word xorshift64* engine with indexed jump splitting + /** \brief One-word xorshift64* engine with hashed indexed splitting * * Uses shifts 12, 25, 27 and Vigna's multiplier. Zero seeds map to one; - * restoring zero state is an error. Alternative a jumps (a+1)*2^32 steps - * along the native recurrence. Sibling states are distinct since 2^32 is - * coprime to the period 2^64-1. Jump matrices are shared, not per-stream state. + * restoring zero state is an error. Alternative a hashes s + (2a+1)*g with + * Mix13, for the odd constant g. For a 32-bit index these inputs are + * distinct, as in SplitMix, and Mix13 is bijective, so sibling states + * differ. Only a zero input hashes to zero; that sibling instead hashes + * s + 2^34*g, which is nonzero and differs from every other input. + * Unlike jumps along the recurrence, which add up so that split(0) of + * split(0) equals split(1), hashed states of different nodes coincide only + * by chance. * \ingroup FuncSupport */ class Xorshift64Star { @@ -281,24 +290,11 @@ namespace Gecode { namespace Support { using State = std::array; private: uint64_t s; - using Matrix = std::array; static uint64_t transition(uint64_t x) { x ^= x >> 12; x ^= x << 25; return x ^ (x >> 27); } - static uint64_t apply(const Matrix& m, uint64_t x) { - uint64_t result=0; - for (unsigned int i=0; x; ++i,x>>=1) - if (x & 1) result ^= m[i]; - return result; - } - static Matrix square(const Matrix& m) { - Matrix result; - for (unsigned int i=0; i<64; ++i) - result[i]=apply(m,m[i]); - return result; - } public: explicit Xorshift64Star(uint64_t value=1) { seed(value); } static const char* name(void) { return "xorshift64star-v1"; } @@ -316,26 +312,10 @@ namespace Gecode { namespace Support { return s * UINT64_C(2685821657736338717); } Xorshift64Star split(uint32_t alternative) const { - // Binary powers of the linear transition, starting at T^(2^32). - static const std::array powers = [] { - Matrix m; - for (unsigned int i=0; i<64; ++i) - m[i]=transition(uint64_t(1)< p; - p[0]=m; - for (unsigned int i=1; i<32; ++i) p[i]=square(p[i-1]); - return p; - }(); - Xorshift64Star child=*this; - if (alternative==UINT32_MAX) { - // 2^64 steps equal one step modulo the period 2^64-1. - child.s=transition(s); - } else { - uint32_t steps=alternative+1; - for (unsigned int i=0; steps; ++i,steps>>=1) - if (steps & 1) child.s=apply(powers[i],child.s); - } + const uint64_t g = UINT64_C(0x9e3779b97f4a7c15); + uint64_t z = s + (2*uint64_t(alternative)+1)*g; + Xorshift64Star child; + child.s = random_mix(z ? z : s + (uint64_t(1) << 34)*g); return child; } }; @@ -353,8 +333,11 @@ namespace Gecode { namespace Support { * without seed expansion. Copying preserves state independently. * Search engines additionally supply split(uint32_t) const, returning * a child without changing the parent. Different sibling indices must - * produce distinct states, not merely different first outputs. This - * does not require non-overlapping streams across an unbounded tree. + * produce distinct states, not merely different first outputs. Branching, + * posting, and meta search derive streams by chains of splits, so chains + * along different paths should give equal states only by chance; a jump + * by an index-dependent distance does not qualify. This does not require + * non-overlapping streams across an unbounded tree. * \ingroup FuncSupport */ template diff --git a/plans/random.md b/plans/random.md index edc9692b59..0860b15242 100644 --- a/plans/random.md +++ b/plans/random.md @@ -14,26 +14,28 @@ small enough to store directly in the model, selector, or brancher that uses the 1. A generator is a value containing its engine state inline. Copying it produces independent state, with no allocation, registry, or aliasing for built-in engines. 2. Model members and selectors own their own copies. Posting or cloning copies - state without drawing. Passing the same value to two consumers does not share - their future state. -3. After variable/value selection, the active randomized brancher captures its - own selectors' states in its choice. Committing alternative a derives each - next state from that snapshot and a, not from destination mutable state. -4. Each sibling index produces a distinct successor state. Direct exploration - and archived replay of the same choice/alternative reproduce state exactly. - Cloning does not split. Late alternatives do not replay preceding siblings. + state without drawing. Posting splits each selector by brancher id and role, + so passing the same value to two consumers gives them independent streams. +3. Selection draws from a copy, so computing a choice leaves selector state + unchanged. Committing alternative a splits the state of every selector of + that brancher by a. The state at a node is thus a function of the posted + state and the path, and cloning, recomputation, and archived replay agree. +4. Each sibling index produces a distinct successor state. Cloning does not + split. Late alternatives do not replay preceding siblings. Choices store no + generator state. 5. Deterministic branchers, unrelated branchers, and model-owned RNGs are not implicitly advanced. Custom owners explicitly record/split their own state when required; hidden callback randomness is not automatically enrolled. -6. Nonrandom Space and Choice representations have no additional RNG fields or - archive words. Randomized brancher choices store only the states they need, - in the same allocation as the choice. +6. Space and Choice representations have no additional RNG fields or archive + words, for randomized and deterministic branchers alike. 7. Users can define engines with different state sizes and use generic branching machinery. No runtime engine registry or fixed custom-state-size cap is needed. 8. Checked 64-bit seeds and canonical full-state text remain distinct interfaces. Drivers use one configured engine. Test failures and exceptions report exact iteration-state replay commands. -9. This remains a provisional breaking-change design, not a compatibility release. +9. Restart and portfolio engines split every brancher of each slave by the + restart or asset number, so slaves explore different random paths. +10. This remains a provisional breaking-change design, not a compatibility release. ## Implementation @@ -46,11 +48,14 @@ small enough to store directly in the model, selector, or brancher that uses the ### Brancher-local replay -- [x] Add selector state save/restore/split hooks with no-op defaults. -- [x] Capture only the active brancher's selectors, in tie-break then value order. -- [x] Store words inline in a randomized choice subtype, without per-engine - identifiers, a state array per alternative, or a separate payload allocation. -- [x] Cover binary, multiway, assignment, reversed alternatives, and LDSB choices. +- [x] Add a selector random_split hook with a no-op default, and a virtual + Brancher::random_split that splits all selectors of a brancher. +- [x] Draw from copies in selection; split on commit only. +- [x] Key selectors at posting by brancher id and role (value 0, variable + selector i at 1+i). +- [x] Add Space::random_split, called by RBS and PBS on each slave. +- [x] Cover binary, multiway, assignment, reversed alternatives, LDSB, open-list, + and FlatZinc int-bool branchers. - [x] Support user-parameterized variable and integer/Boolean value selectors through the existing generic brancher machinery. - [x] Keep model/custom-brancher state transitions explicit. @@ -59,6 +64,9 @@ small enough to store directly in the model, selector, or brancher that uses the - [x] Test independent copies, untouched model/later-selector state, and consumer-local splitting. +- [x] Test that branchers sharing a source differ, that open-list and int-bool + results do not depend on the recomputation distance, and that restarts + find different solutions. - [x] Check direct/archived choices, perturbed destination selector state, sibling exploration, cloning/recomputation, and parallel solution agreement. - [x] Exercise a three-word external engine in selectors and a runnable example. @@ -71,8 +79,8 @@ small enough to store directly in the model, selector, or brancher that uses the ## Verification strategy -Use the existing focused Random::Contract, Random::BranchReplay, and -Random::CommitBoundary tests. Fault::Random::CloneFailures counts inline +Use the focused Random::Contract, Random::BranchReplay, Random::CommitBoundary, +Random::Streams, and FlatZinc::random_intbool_branch tests. Fault::Random::CloneFailures counts inline custom-engine instances across failed clones. Existing Boolean, set, float, assignment, LDSB, and FlatZinc restart cases cover their integration paths. @@ -88,8 +96,10 @@ and any performance costs. ## Algorithm and CLI decisions Splittable SplitMix has two state words and constant-time indexed splitting. -Xorshift64* has one state word and indexed native jumps. Both have documented -sibling-state arguments in gecode/support/random.hpp. The default remains +Xorshift64* has one state word and splits by hashing s + (2a+1)g with Mix13. +Jumps along its recurrence were rejected: they add up, so split(0) of split(0) +equals split(1), correlating subtrees. Both engines have documented sibling-state +arguments in gecode/support/random.hpp. The default remains provisional SplitMix; xorshift64* remains configurable through CMake and Autoconf. Support::Random defines bounded integer conversion and canonical full-state @@ -103,16 +113,14 @@ input is rejected. No legacy sequence mode is required. ## Verification results -Both engine configurations pass CMake check and all five CTests, including -command-line state replay and fault injection. The reduced static/no-thread build -passes check and the focused Random tests. Additional Boolean, set, float, -assignment, filtered-tie, LDSB, and FlatZinc restart checks pass with both defaults. -Random::BranchReplay also exercises randomized LDSB choice archives. The -custom-engine example's full-state replay and output agree across all three builds. - -The Space and base Choice implementation matches main exactly. Rnd is -16 bytes with SplitMix and 8 with xorshift64*, without shared allocation. Fresh -five-run measurements are recorded in the PR description: SplitMix's controlled -random tree costs 1.02x with cloning and 1.16x with recomputation relative to main; -xorshift64* costs 1.63x and 2.88x. Draft PR #241 remains provisional, for a future -breaking-change release only. +With the split-on-commit design, both engine configurations (xorshift64* built +with experimental engines) pass CMake check and all CTests. The Random and +FlatZinc random tests pass with both engines. The SplitMix build also passes +all Branch, Assign, FlatZinc, Search, and NoGoods tests. The custom-engine +example's full-state replay agrees with its output. + +The Space and base Choice implementation matches main exactly, and randomized +choices are as large as deterministic ones. Rnd is 16 bytes with SplitMix and +8 with xorshift64*, without shared allocation. The cost measurements in the PR +description predate this design and must be repeated. Draft PR #241 remains +provisional, for a future breaking-change release only. diff --git a/test/flatzinc/random_branching.cpp b/test/flatzinc/random_branching.cpp new file mode 100644 index 0000000000..15be8096ca --- /dev/null +++ b/test/flatzinc/random_branching.cpp @@ -0,0 +1,88 @@ +/* -*- 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 "test/flatzinc.hh" +#include +#include +#include +#include + +namespace Test { namespace FlatZinc { + + namespace { + /// Integer and Boolean variables share one randomized value brancher. + class IntBoolSpace : public Gecode::Space { + public: + Gecode::IntVarArray x; + Gecode::BoolVarArray b; + IntBoolSpace(void) : x(*this,5,0,3), b(*this,3,0,1) { + using namespace Gecode; + Gecode::FlatZinc::branch(*this,x,b,Gecode::FlatZinc::INTBOOL_VAR_AFC_SIZE_MAX(0.99), + INT_VAL_RND(Rnd(3))); + } + IntBoolSpace(IntBoolSpace& s) : Space(s) { + x.update(*this,s.x); + b.update(*this,s.b); + } + Space* copy(void) override { return new IntBoolSpace(*this); } + }; + + std::string solutions(unsigned int distance) { + Gecode::Search::Options options; + options.c_d = distance; options.a_d = distance; + IntBoolSpace root; + Gecode::DFS search(&root,options); + std::ostringstream result; + for (int i=0; i<40; i++) { + std::unique_ptr s(search.next()); + if (!s) break; + result << s->x << s->b << ';'; + } + return result.str(); + } + + /// Check that random value selection does not depend on recomputation + class RandomIntBoolBranch : public Base { + public: + RandomIntBoolBranch(void) : Base("FlatZinc::random_intbool_branch") {} + bool run(void) override { + const std::string cloned = solutions(1); + return (cloned == solutions(4)) && (cloned == solutions(100)); + } + } random_intbool_branch; + } + +}} + +// STATISTICS: test-flatzinc diff --git a/test/random.cpp b/test/random.cpp index 553b355d07..467b5c57f6 100644 --- a/test/random.cpp +++ b/test/random.cpp @@ -33,6 +33,8 @@ #include "test/test.hh" #include +#include +#include namespace Test { namespace Random { @@ -105,12 +107,18 @@ namespace Test { if (!replay() || !replay() || !replay()) return false; - if (xs.split(0).split(0).state()!=xs.split(1).state()) + // Hashed splits do not add up along a path, as jumps would. + if (xs.split(0).split(0).state()==xs.split(1).state()) return false; - auto one_step=xs; - (void) one_step.next(); - if (xs.split(UINT32_MAX).state()!=one_step.state()) + // Alternative 0 hashes zero here, so it must use the fallback input. + Xorshift64Star zero_input; + zero_input.state({{uint64_t(0)-UINT64_C(0x9e3779b97f4a7c15)}}); + if ((zero_input.split(0).state()[0]==0) || + (zero_input.split(0).state()==zero_input.split(1).state())) return false; + for (uint32_t a=1; a<100; ++a) + if (xs.split(a).state()==xs.split(a-1).state()) + return false; auto parent = r.state(); // Indexing skips pairs of parent words, exactly as sequential splits. Gecode::Support::Random sequential = r; @@ -328,45 +336,26 @@ namespace Test { template bool consumer_states(const Random& source, bool multi) { using namespace Gecode; - // Inspect original-space selectors only; clones own separate copies. - for (unsigned int a=0; a<(multi ? 3U : 2U); ++a) { - ReplaySpace root(source,false,multi); - if (root.status()!=SS_BRANCH) return false; - auto original=source.state_words(); - std::vector observed(Random::words()); - root.variable->random_save(observed.data()); - if (!std::equal(original.begin(),original.end(),observed.begin())) return false; - std::unique_ptr first(root.choice()); - const auto& deterministic=static_cast(*first); - if (deterministic.random_data()!=nullptr) return false; - Archive plain; first->archive(plain); - if (plain.size()!=3) return false; // No global RNG pointer/count/archive data. - root.commit(*first,0); - root.variable->random_save(observed.data()); - if (!std::equal(original.begin(),original.end(),observed.begin())) return false; - if (root.status()!=SS_BRANCH) return false; - std::unique_ptr choice(root.choice()); - const uint64_t* recorded=static_cast(*choice).random_data(); - if (!recorded) return false; - Random expected=source; - expected.restore_split(recorded,a); - // Perturb the selector after taking the choice. Commit must restore it. - root.variable->random_commit(recorded,123); - root.commit(*choice,a); - root.variable->random_save(observed.data()); - auto expected_words=expected.state_words(); - if (!std::equal(expected_words.begin(),expected_words.end(),observed.begin())) - return false; - if (!multi) { - expected.restore_split(recorded+Random::words(),a); - root.value->random_save(observed.data()); - expected_words=expected.state_words(); - if (!std::equal(expected_words.begin(),expected_words.end(),observed.begin())) - return false; - } - if (root.own.state()!=source.state()) return false; + ReplaySpace root(source,false,multi); + // Skip the deterministic brancher on x[0]. + if (root.status()!=SS_BRANCH) return false; + std::unique_ptr first(root.choice()); + root.commit(*first,0); + if (root.status()!=SS_BRANCH) return false; + std::unique_ptr choice(root.choice()); + // Choosing draws from copies, so a clone taken afterwards chooses alike. + std::unique_ptr later(root.clone()); + if (later->status()!=SS_BRANCH) return false; + std::unique_ptr again(later->choice()); + if (!same_archive(*choice,*again)) return false; + // A randomized binary choice archives no random state. + Archive plain; choice->archive(plain); + if (!multi && (plain.size()!=3)) return false; + for (unsigned int a=0; aalternatives(); ++a) { + std::unique_ptr child(root.clone()); + child->commit(*choice,a); } - return true; + return root.own.state()==source.state(); } class CommitBoundary : public Base { @@ -376,6 +365,9 @@ namespace Test { using namespace Gecode; static_assert(sizeof(Rnd)==sizeof(Support::RandomGenerator), "Rnd must contain only inline engine state"); + static_assert(!std::is_convertible_v && + !std::is_convertible_v, + "Seeds must not convert implicitly to Rnd"); Rnd r(7); Rnd copy=r; auto state=r.state(); @@ -386,6 +378,114 @@ namespace Test { consumer_states(RndGenerator(7),true); } } commit_boundary; + + /// Two branchers and all their selectors receive the same generator. + class SharedSource : public Gecode::Space { + public: + Gecode::IntVarArray x; + Gecode::IntVarArray y; + explicit SharedSource(const Gecode::Rnd& r) : x(*this,8,0,9), y(*this,8,0,9) { + using namespace Gecode; + branch(*this,x,INT_VAR_RND(r),INT_VAL_RND(r)); + branch(*this,y,INT_VAR_RND(r),INT_VAL_RND(r)); + } + SharedSource(SharedSource& s) : Space(s) { + x.update(*this,s.x); + y.update(*this,s.y); + } + Space* copy() override { return new SharedSource(*this); } + }; + + class Permutation : public Gecode::Space { + public: + Gecode::IntVarArray x; + explicit Permutation(const Gecode::Rnd& r) : x(*this,6,0,5) { + using namespace Gecode; + distinct(*this,x); + branch(*this,x,INT_VAR_RND(r),INT_VAL_RND(r)); + } + Permutation(Permutation& s) : Space(s) { x.update(*this,s.x); } + Space* copy() override { return new Permutation(*this); } + std::string text() const { + std::ostringstream o; + o << x; + return o.str(); + } + }; + + /// Open lists use their own brancher rather than ViewValBrancher. + class OpenPermutation : public Gecode::Space { + public: + Gecode::OpenIntVarList x; + explicit OpenPermutation(const Gecode::Rnd& r) : x(*this,Gecode::IntSet(0,3),6) { + using namespace Gecode; + rel(*this,x.length(),IRT_EQ,6); + branch(*this,x,INT_VAR_RND(r),INT_VAL_RND(r)); + } + OpenPermutation(OpenPermutation& s) : Space(s) { x.update(*this,s.x); } + Space* copy() override { return new OpenPermutation(*this); } + std::string text() const { + std::ostringstream o; + for (int i=0; i + std::vector first_solutions(Model& root, unsigned int distance) { + using namespace Gecode; + Search::Options options; + options.c_d=distance; options.a_d=distance; + DFS search(&root,options); + std::vector result; + for (int i=0; i<40; i++) { + std::unique_ptr s(search.next()); + if (!s) break; + result.push_back(s->text()); + } + return result; + } + + bool restarts_differ() { + using namespace Gecode; + Permutation root(Rnd(3)); + DFS dfs(&root); + std::unique_ptr plain(dfs.next()); + Search::Options options; + options.cutoff = Search::Cutoff::constant(1000); + RBS rbs(&root,options); + std::vector found; + for (int i=0; i<10; i++) { + std::unique_ptr s(rbs.next()); + if (!s) return false; + found.push_back(s->text()); + } + // The first slave keeps the posted state; every restart gets its own. + std::set distinct(found.begin(),found.end()); + return plain && (found[0]==plain->text()) && (found[1]!=found[0]) && + (distinct.size()>=8); + } + + class Streams : public Base { + public: + Streams() : Base("Random::Streams") {} + bool run() override { + using namespace Gecode; + SharedSource shared(Rnd(1)); + DFS search(&shared); + std::unique_ptr s(search.next()); + if (!s) return false; + bool same=true; + for (int i=0; ix.size(); i++) + same = same && (s->x[i].val()==s->y[i].val()); + if (same) return false; + OpenPermutation open(Rnd(3)); + auto cloned=first_solutions(open,1); + return (cloned.size()==40) && (cloned==first_solutions(open,100)) && + restarts_differ(); + } + } streams; } } diff --git a/tools/random-benchmark.cpp b/tools/random-benchmark.cpp index 0b1fd11922..3b89f2b011 100644 --- a/tools/random-benchmark.cpp +++ b/tools/random-benchmark.cpp @@ -134,12 +134,9 @@ int main(int argc, char** argv) { std::cout << (random ? "size.random_archive" : "size.plain_archive") << '\t' << archive.size()*sizeof(unsigned int) << "\tbytes\t0\n"; #ifdef RANDOM_NEW - std::cout << (random ? "size.random_snapshot" : "size.plain_snapshot") - << '\t' << (random ? sizeof(Support::RandomGenerator) : 0) - << "\tbytes\t0\n"; + // Randomized choices store no generator state. std::cout << (random ? "size.random_choice" : "size.plain_choice") - << '\t' << (random ? sizeof(RndChoice>)+sizeof(Support::RandomGenerator) - : sizeof(PosValChoice)) << "\tbytes\t0\n"; + << '\t' << sizeof(PosValChoice) << "\tbytes\t0\n"; #endif measure(random ? "clone.random" : "clone.plain",10000,[&] { uint64_t sum=0; From 403b4831288396acf7ac16fb8cc24b26cc98795c Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Tue, 3 Nov 2026 16:47:26 +0100 Subject: [PATCH 14/16] Separate meta streams and harden xorshift64* draws Space::random_split takes the MetaInfo passed to slave() and uses a separate separator for restarts and portfolio assets, so asset i's first slave no longer repeats restart i of asset 0. Bounded draws take their value from the high bits, as xorshift64*'s low bits are linear: unsplit draws modulo 2 failed PractRand at 32 MB. Xorshift64* seeds start from Mix13(seed + golden ratio), so related seeds no longer give linearly related streams, and a zero split input hashes (2a+1)g, making each split a bijection on nonzero states. --- changelog.in | 11 ++++--- gecode/kernel/core.cpp | 9 ++++-- gecode/kernel/core.hpp | 21 ++++++++------ gecode/search/pbs.hpp | 20 +++++++------ gecode/search/rbs.hpp | 2 +- gecode/search/seq/rbs.cpp | 4 +-- gecode/support/random.hpp | 60 +++++++++++++++++++++++---------------- plans/random.md | 24 ++++++++++++++-- test/random-options.cmake | 4 ++- test/random.cpp | 52 +++++++++++++++++++++++++++++---- 10 files changed, 147 insertions(+), 60 deletions(-) diff --git a/changelog.in b/changelog.in index 135a7c11d9..db22114f6a 100755 --- a/changelog.in +++ b/changelog.in @@ -87,15 +87,18 @@ xorshift64* is available as a smaller-state alternative. The default is configurable at build time, and users can supply their own engines. Randomized branching splits its generator state by alternative on commit, so the state at a node depends only on the path and is reproduced during -recomputation. Restart and portfolio search split each slave's state, so -restarts explore different random paths. Complete-state save/restore and +recomputation. Restart and portfolio search split each slave's state, with +separate streams for restarts and portfolio assets, so slaves explore +different random paths. Complete-state save/restore and command-line state input support exact replay, including test failures. [MORE] Generators store their compact state directly in their consumers, and copying a generator produces independent state. Posting splits each selector by its brancher and role, so branchers given the same generator draw independent -streams. Choices store no generator state. Drivers accept checked 64-bit seeds -as well as complete state. APIs and seeded sequences change; no legacy +streams. Choices store no generator state. Bounded draws take their value +from the high bits of the raw output. Xorshift64* hashes its seed, as its +recurrence is linear. Drivers accept checked 64-bit seeds as well as complete +state. APIs and seeded sequences change; no legacy sequence mode is provided. This design remains provisional for a future breaking-change release. Replace the old seed() snapshot accessor with state() and restore with state(text). The driver and diff --git a/gecode/kernel/core.cpp b/gecode/kernel/core.cpp index 430e205a77..ea9672a94b 100644 --- a/gecode/kernel/core.cpp +++ b/gecode/kernel/core.cpp @@ -687,12 +687,15 @@ namespace Gecode { } void - Space::random_split(unsigned long long int i) { + Space::random_split(const MetaInfo& mi) { + const bool restart = mi.type() == MetaInfo::RESTART; + const unsigned long long int i = restart ? mi.restart() : mi.asset(); if (i == 0) return; - // Alternatives are smaller than UINT_MAX, so slave streams stay apart. + // No choice has UINT_MAX-1 alternatives, so no commit uses a separator. + const unsigned int separator = restart ? UINT_MAX : UINT_MAX-1; for (ActorLink* b = bl.next(); b != &bl; b = b->next()) { - Brancher::cast(b)->random_split(UINT_MAX); + Brancher::cast(b)->random_split(separator); Brancher::cast(b)->random_split(static_cast(i >> 32)); Brancher::cast(b)->random_split(static_cast(i)); } diff --git a/gecode/kernel/core.hpp b/gecode/kernel/core.hpp index 0243fa1cde..9526749a91 100755 --- a/gecode/kernel/core.hpp +++ b/gecode/kernel/core.hpp @@ -2414,18 +2414,21 @@ namespace Gecode { GECODE_KERNEL_EXPORT void print(const Choice& c, unsigned int a, std::ostream& o) const; /** - * \brief Give the random selectors of all branchers a stream for slave \a i - * - * Restart and portfolio engines call this on every slave with its restart - * or asset number, so that slaves cloned from one master do not repeat - * its random decisions. Index 0 leaves the state unchanged, so the first - * slave searches as the model does without a meta engine. Other indices - * split by a separator no alternative uses, then by the high and low 32 - * bits of \a i. + * \brief Give the random selectors of all branchers a stream for slave \a mi + * + * Restart and portfolio engines call this on every slave with the + * information they pass to slave(), so that slaves cloned from one master + * do not repeat its random decisions. Restart or asset number 0 leaves + * the state unchanged, so the first slave searches as the model does + * without a meta engine. Other numbers split by a separator that no + * alternative uses, one for restarts and another for assets, then by the + * high and low 32 bits of the number. Restart and portfolio streams thus + * stay apart when engines nest. Branchers posted later, during search, + * draw from generators their poster owns and are not split here. * \ingroup TaskSearch */ GECODE_KERNEL_EXPORT - void random_split(unsigned long long int i); + void random_split(const MetaInfo& mi); /** * \brief Notice actor property diff --git a/gecode/search/pbs.hpp b/gecode/search/pbs.hpp index 5c8a71021e..15b40855da 100644 --- a/gecode/search/pbs.hpp +++ b/gecode/search/pbs.hpp @@ -186,8 +186,9 @@ namespace Gecode { namespace Search { master : master->clone()); if (slave.get() == master) unowned_master = nullptr; - slave->random_split(static_cast(i)); - (void) slave->slave(i); + const MetaInfo mi(static_cast(i)); + slave->random_split(mi); + (void) slave->slave(mi); slaves[i] = build(slave.get(),opt); } return Seq::pbsengine( @@ -228,8 +229,9 @@ namespace Gecode { namespace Search { master : master->clone()); if (slave.get() == master) unowned_master = nullptr; - slave->random_split(static_cast(i)); - (void) slave->slave(static_cast(i)); + const MetaInfo mi(static_cast(i)); + slave->random_split(mi); + (void) slave->slave(mi); slaves[i] = (*sebs[i])(slave.get()); } pbscleanup(sebs); @@ -279,8 +281,9 @@ namespace Gecode { namespace Search { master : master->clone()); if (slave.get() == master) unowned_master = nullptr; - slave->random_split(static_cast(i)); - (void) slave->slave(static_cast(i)); + const MetaInfo mi(static_cast(i)); + slave->random_split(mi); + (void) slave->slave(mi); slaves[i] = build(slave.get(),opt); } return Par::pbsengine(slaves,stops,n_slaves,stat,E::best); @@ -324,8 +327,9 @@ namespace Gecode { namespace Search { master : master->clone()); if (slave.get() == master) unowned_master = nullptr; - slave->random_split(static_cast(i)); - (void) slave->slave(static_cast(i)); + const MetaInfo mi(static_cast(i)); + slave->random_split(mi); + (void) slave->slave(mi); slaves[i] = (*sebs[i])(slave.get()); } pbscleanup(sebs); diff --git a/gecode/search/rbs.hpp b/gecode/search/rbs.hpp index 97e141d47d..9952f8a616 100644 --- a/gecode/search/rbs.hpp +++ b/gecode/search/rbs.hpp @@ -103,7 +103,7 @@ namespace Gecode { std::unique_ptr master(m_opt.clone ? s->clone() : s); std::unique_ptr slave(master->clone()); MetaInfo mi(0,MetaInfo::RR_INIT,0,0,nullptr,NoGoods::eng); - slave->random_split(mi.restart()); + slave->random_split(mi); slave->slave(mi); std::unique_ptr leaf( Search::build(slave.get(),e_opt)); diff --git a/gecode/search/seq/rbs.cpp b/gecode/search/seq/rbs.cpp index d8d88568b9..f1e23dd652 100755 --- a/gecode/search/seq/rbs.cpp +++ b/gecode/search/seq/rbs.cpp @@ -80,7 +80,7 @@ namespace Gecode { namespace Search { namespace Seq { stop->update(e->statistics()); Space* slave = master; master = master->clone(); - slave->random_split(mi.restart()); + slave->random_split(mi); complete = slave->slave(mi); e->reset(slave); sslr = 0; @@ -113,7 +113,7 @@ namespace Gecode { namespace Search { namespace Seq { return nullptr; Space* slave = master; master = master->clone(); - slave->random_split(mi.restart()); + slave->random_split(mi); complete = slave->slave(mi); e->reset(slave); } else { diff --git a/gecode/support/random.hpp b/gecode/support/random.hpp index 8af97ce906..8c0bb06322 100755 --- a/gecode/support/random.hpp +++ b/gecode/support/random.hpp @@ -274,15 +274,20 @@ namespace Gecode { namespace Support { /** \brief One-word xorshift64* engine with hashed indexed splitting * - * Uses shifts 12, 25, 27 and Vigna's multiplier. Zero seeds map to one; - * restoring zero state is an error. Alternative a hashes s + (2a+1)*g with - * Mix13, for the odd constant g. For a 32-bit index these inputs are - * distinct, as in SplitMix, and Mix13 is bijective, so sibling states - * differ. Only a zero input hashes to zero; that sibling instead hashes - * s + 2^34*g, which is nonzero and differs from every other input. - * Unlike jumps along the recurrence, which add up so that split(0) of - * split(0) equals split(1), hashed states of different nodes coincide only - * by chance. + * Uses shifts 12, 25, 27 and Vigna's multiplier, for the odd constant g. + * Seed v starts from Mix13(v + g), the first SplitMix64 output for v, as + * the recurrence is linear and related raw seeds would give related + * streams. The one seed hashing to zero shares the state of seed 0. + * Restoring zero state is an error. + * + * Alternative a hashes s + (2a+1)*g with Mix13. For a 32-bit index these + * inputs are distinct, as in SplitMix, and Mix13 is bijective, so sibling + * states differ. Only a zero input hashes to zero; that sibling hashes + * (2a+1)*g instead, the input of the excluded state zero. Each split is + * thus a bijection on nonzero states, so distinct nodes stay distinct + * along equal alternative sequences. Unlike jumps along the recurrence, + * which add up so that split(0) of split(0) equals split(1), hashed states + * of other nodes coincide only by chance. * \ingroup FuncSupport */ class Xorshift64Star { @@ -300,7 +305,10 @@ namespace Gecode { namespace Support { static const char* name(void) { return "xorshift64star-v1"; } static constexpr uint64_t min(void) { return 1; } static constexpr uint64_t max(void) { return UINT64_MAX; } - void seed(uint64_t value) { s = value ? value : 1; } + void seed(uint64_t value) { + uint64_t z = random_mix(value + UINT64_C(0x9e3779b97f4a7c15)); + s = z ? z : random_mix(UINT64_C(0x9e3779b97f4a7c15)); + } State state(void) const { return {{s}}; } void state(const State& value) { if (!value[0]) @@ -312,10 +320,10 @@ namespace Gecode { namespace Support { return s * UINT64_C(2685821657736338717); } Xorshift64Star split(uint32_t alternative) const { - const uint64_t g = UINT64_C(0x9e3779b97f4a7c15); - uint64_t z = s + (2*uint64_t(alternative)+1)*g; + const uint64_t c = (2*uint64_t(alternative)+1)*UINT64_C(0x9e3779b97f4a7c15); + uint64_t z = s + c; Xorshift64Star child; - child.s = random_mix(z ? z : s + (uint64_t(1) << 34)*g); + child.s = random_mix(z ? z : c); return child; } }; @@ -364,7 +372,13 @@ namespace Gecode { namespace Support { Random split(uint32_t alternative) const { return Random(e.split(alternative)); } - /// Bounds <= 1 return zero without consuming a draw. + /** \brief Uniform draw from [0,bound) + * + * Result r stands for the r-th of n equal buckets of consecutive raw + * values, so it depends on the high bits; the low bits of engines such as + * xorshift64* are linear. Raw values beyond the last full bucket are + * rejected. Bounds <= 1 return zero without consuming a draw. + */ template Type operator ()(Type bound) { static_assert(std::is_integral::value && sizeof(Type) <= 8, @@ -372,17 +386,15 @@ namespace Gecode { namespace Support { if (bound <= 1) return 0; uint64_t n = static_cast(bound); + // Bucket size: 2^64 raw values, or 2^64-1 for nonzero engines, over n. + uint64_t bucket; + if constexpr (Engine::min() == 0) + bucket = (uint64_t(0)-n)/n + 1; + else + bucket = UINT64_MAX/n; uint64_t value; - if constexpr (Engine::min() == 0) { - // Accept an exact multiple of n values from the 2^64-value source. - uint64_t threshold = (uint64_t(0)-n) % n; - do { value = next(); } while (value < threshold); - } else { - // Nonzero engines have 2^64-1 values, not 2^64. - uint64_t limit = UINT64_MAX - (UINT64_MAX % n); - do { value = next()-1; } while (value >= limit); - } - return static_cast(value % n); + do { value = (next()-Engine::min()) / bucket; } while (value >= n); + return static_cast(value); } /// Canonical identifier followed by fixed-width hexadecimal state words. std::string state_string(void) const { diff --git a/plans/random.md b/plans/random.md index 0860b15242..a202d42717 100644 --- a/plans/random.md +++ b/plans/random.md @@ -34,7 +34,8 @@ small enough to store directly in the model, selector, or brancher that uses the Drivers use one configured engine. Test failures and exceptions report exact iteration-state replay commands. 9. Restart and portfolio engines split every brancher of each slave by the - restart or asset number, so slaves explore different random paths. + restart or asset number, behind separate separators, so slaves explore + different random paths even when the engines nest. 10. This remains a provisional breaking-change design, not a compatibility release. ## Implementation @@ -98,8 +99,11 @@ and any performance costs. Splittable SplitMix has two state words and constant-time indexed splitting. Xorshift64* has one state word and splits by hashing s + (2a+1)g with Mix13. Jumps along its recurrence were rejected: they add up, so split(0) of split(0) -equals split(1), correlating subtrees. Both engines have documented sibling-state -arguments in gecode/support/random.hpp. The default remains +equals split(1), correlating subtrees. A zero input hashes (2a+1)g instead, so +each split is a bijection on nonzero states. Seeds are hashed the same way, as +related raw seeds of the linear recurrence give related streams. Bounded draws +use the high bits, since xorshift64*'s low bits are linear. Both engines have +documented sibling-state arguments in gecode/support/random.hpp. The default remains provisional SplitMix; xorshift64* remains configurable through CMake and Autoconf. Support::Random defines bounded integer conversion and canonical full-state @@ -111,6 +115,20 @@ Drivers retain -seed (examples), -r (FlatZinc), and -state. Time/hardware initialization reports concrete state. Incompatible, malformed, and conflicting input is rejected. No legacy sequence mode is required. +## Split analysis + +PractRand 0.95 (expanded tests, full folding) passes 32 GB of first draws for +both engines along leftmost, random binary, and random 10-way paths, in +depth-first order over complete trees, for all 2^32 siblings of one node, for +siblings under many parents, for parent and child draws side by side, for +posting keys, and for restart slaves; binary draws pass 16 GB. Splits without +the hash fail all of these at 32-64 MB, and the old jump split fails the +depth-first stream at 32 MB. Single-bit avalanche of every split derivation is +at the noise floor, and trees of 1.1-1.3 * 10^8 nodes repeat no state. +Unsplit xorshift64* low bits fail at 128 MB, which motivated high-bit draws. +Repeated states at about 2^32 nodes are expected for the one-word engine and +harmless: two nodes then make the same random choices. + ## Verification results With the split-on-commit design, both engine configurations (xorshift64* built diff --git a/test/random-options.cmake b/test/random-options.cmake index 78f492ca4a..3a0c51fe5a 100644 --- a/test/random-options.cmake +++ b/test/random-options.cmake @@ -46,7 +46,9 @@ foreach(mode IN LISTS modes) message(FATAL_ERROR "64-bit seed failed: ${error}") endif() string(REGEX MATCH "^[^\n]+" state "${expected}") - if(NOT state MATCHES "-v1:ffffffffffffffff(:|$)") + # SplitMix stores the seed; xorshift64* stores Mix13(seed + golden ratio). + if(NOT state MATCHES "^splitmix-v1:ffffffffffffffff:" AND + NOT state STREQUAL "xorshift64star-v1:e4d971771b652c20") message(FATAL_ERROR "Seed was narrowed: ${state}") endif() foreach(args "${seed};0xffffffffffffffff" "-state;${state}") diff --git a/test/random.cpp b/test/random.cpp index 467b5c57f6..19003f62fa 100644 --- a/test/random.cpp +++ b/test/random.cpp @@ -68,6 +68,22 @@ namespace Test { } }; + /// Repeats one raw value, to pin how bounded draws reduce it. + template + class FixedEngine { + uint64_t v; + public: + using State = std::array; + explicit FixedEngine(uint64_t s=lowest) : v(s) {} + static const char* name() { return "fixed-test-v1"; } + static constexpr uint64_t min() { return lowest; } + static constexpr uint64_t max() { return UINT64_MAX; } + void seed(uint64_t s) { v=s; } + uint64_t next() { return v; } + State state() const { return {{v}}; } + void state(const State& s) { v=s[0]; } + }; + template bool replay() { Gecode::Support::Random original(UINT64_MAX); @@ -101,20 +117,34 @@ namespace Test { UINT64_C(0x06c45d188009454f)}) if (r.next() != expected) return false; - Xorshift64Star xs(1); + Xorshift64Star xs; + xs.state({{1}}); if (xs.next() != UINT64_C(0x47e4ce4b896cdd1d)) return false; + // Seeds are hashed: seed 0 starts from the first SplitMix64 output. + if (Xorshift64Star(0).state()[0] != UINT64_C(0xe220a8397b1dcdaf)) + return false; + // Draws use the high bits, never only the linear low bits. + Gecode::Support::Random> high(UINT64_C(1) << 63); + Gecode::Support::Random> nonzero((UINT64_C(1) << 63)+1); + if (high(2)!=1 || high(3)!=1 || high(4)!=2 || nonzero(2)!=1) + return false; if (!replay() || !replay() || !replay()) return false; // Hashed splits do not add up along a path, as jumps would. if (xs.split(0).split(0).state()==xs.split(1).state()) return false; - // Alternative 0 hashes zero here, so it must use the fallback input. + // Alternative 0 hashes zero here, so it must use the fallback input, + // which no other parent reaches. + const uint64_t g = UINT64_C(0x9e3779b97f4a7c15); Xorshift64Star zero_input; - zero_input.state({{uint64_t(0)-UINT64_C(0x9e3779b97f4a7c15)}}); + zero_input.state({{uint64_t(0)-g}}); + Xorshift64Star other; + other.state({{(uint64_t(1) << 34)*g - 2*g}}); if ((zero_input.split(0).state()[0]==0) || - (zero_input.split(0).state()==zero_input.split(1).state())) + (zero_input.split(0).state()==zero_input.split(1).state()) || + (zero_input.split(0).state()==other.split(0).state())) return false; for (uint32_t a=1; a<100; ++a) if (xs.split(a).state()==xs.split(a-1).state()) @@ -467,6 +497,18 @@ namespace Test { (distinct.size()>=8); } + /// Asset 1's first slave must not repeat restart 1 of asset 0. + bool meta_streams_differ() { + using namespace Gecode; + Permutation root(Rnd(3)); + if (root.status()!=SS_BRANCH) return false; + std::unique_ptr asset(static_cast(root.clone())); + asset->random_split(MetaInfo(1U)); + std::unique_ptr restart(static_cast(root.clone())); + restart->random_split(MetaInfo(1,MetaInfo::RR_LIM,0,0,nullptr,NoGoods::eng)); + return first_solutions(*asset,1)!=first_solutions(*restart,1); + } + class Streams : public Base { public: Streams() : Base("Random::Streams") {} @@ -483,7 +525,7 @@ namespace Test { OpenPermutation open(Rnd(3)); auto cloned=first_solutions(open,1); return (cloned.size()==40) && (cloned==first_solutions(open,100)) && - restarts_differ(); + restarts_differ() && meta_streams_differ(); } } streams; } From 3c6538d5412ad93d09694567354acd0fb8df25f0 Mon Sep 17 00:00:00 2001 From: Mikael Zayenz Lagerkvist Date: Tue, 3 Nov 2026 17:58:24 +0100 Subject: [PATCH 15/16] Split nested meta streams and clean up partial brancher clones Space::random_split now splits for restart and asset number 0 too. With number 0 as a no-op, nested portfolio slaves (0,1) and (1,0) applied the same split and searched with the same random stream. A single-asset portfolio splits its slave like asset 0 of a larger one. The first slave therefore no longer repeats the search without a meta engine. ViewBrancher and ViewValBrancher dispose the selectors they already copied when a later selector copy throws. The failed clone never links the partial brancher, so it could not dispose them, leaking custom engines with non-trivial destructors. The fault test now throws inside each selector copy. Add the missing STATISTICS lines, drop a stale jump-table warmup from the benchmark, and record new cost measurements in the plan. --- changelog.in | 9 ++++---- gecode/kernel/branch/view-val.hpp | 12 +++++++++-- gecode/kernel/branch/view.hpp | 12 +++++++++-- gecode/kernel/core.cpp | 3 +-- gecode/kernel/core.hpp | 15 +++++++------ gecode/search/pbs.hpp | 4 +++- plans/random.md | 17 ++++++++++----- test/fault.cpp | 36 ++++++++++++++++++++++++++----- test/random-options.cpp | 2 ++ test/random-replay.cpp | 2 ++ test/random.cpp | 24 +++++++++++++++------ tools/random-benchmark.cpp | 4 ++-- 12 files changed, 104 insertions(+), 36 deletions(-) diff --git a/changelog.in b/changelog.in index db22114f6a..aaaa8a6e2e 100755 --- a/changelog.in +++ b/changelog.in @@ -87,10 +87,11 @@ xorshift64* is available as a smaller-state alternative. The default is configurable at build time, and users can supply their own engines. Randomized branching splits its generator state by alternative on commit, so the state at a node depends only on the path and is reproduced during -recomputation. Restart and portfolio search split each slave's state, with -separate streams for restarts and portfolio assets, so slaves explore -different random paths. Complete-state save/restore and -command-line state input support exact replay, including test failures. +recomputation. Restart and portfolio search split each slave's state, the +first slave included, with separate streams for restarts and portfolio assets, +so slaves explore different random paths, also when engines nest. +Complete-state save/restore and command-line state input support exact +replay, including test failures. [MORE] Generators store their compact state directly in their consumers, and copying a generator produces independent state. Posting splits each selector by its diff --git a/gecode/kernel/branch/view-val.hpp b/gecode/kernel/branch/view-val.hpp index e28afc1ef1..3521ab6100 100644 --- a/gecode/kernel/branch/view-val.hpp +++ b/gecode/kernel/branch/view-val.hpp @@ -258,8 +258,16 @@ namespace Gecode { ViewValBrancher:: ViewValBrancher(Space& home, ViewValBrancher& b) - : ViewBrancher(home,b), - vsc(b.vsc->copy(home)), p(b.p) {} + : ViewBrancher(home,b), vsc(nullptr), p(b.p) { + try { + vsc = b.vsc->copy(home); + } catch (...) { + // A failed clone cannot dispose a brancher that was never constructed. + for (int i=0; idispose(home); + throw; + } + } template diff --git a/gecode/kernel/branch/view.hpp b/gecode/kernel/branch/view.hpp index 25a07a3a34..8d38cc3141 100644 --- a/gecode/kernel/branch/view.hpp +++ b/gecode/kernel/branch/view.hpp @@ -159,8 +159,16 @@ namespace Gecode { ViewBrancher& vb) : Brancher(home,vb), start(vb.start), f(vb.f) { x.update(home,vb.x); - for (int i=0; icopy(home); + int copied = 0; + try { + for (; copiedcopy(home); + } catch (...) { + // A failed clone cannot dispose a brancher that was never constructed. + for (int i=0; idispose(home); + throw; + } } template diff --git a/gecode/kernel/core.cpp b/gecode/kernel/core.cpp index ea9672a94b..ea404bfd46 100644 --- a/gecode/kernel/core.cpp +++ b/gecode/kernel/core.cpp @@ -690,8 +690,7 @@ namespace Gecode { Space::random_split(const MetaInfo& mi) { const bool restart = mi.type() == MetaInfo::RESTART; const unsigned long long int i = restart ? mi.restart() : mi.asset(); - if (i == 0) - return; + // Number 0 splits too, so that nested engines keep their level apart. // No choice has UINT_MAX-1 alternatives, so no commit uses a separator. const unsigned int separator = restart ? UINT_MAX : UINT_MAX-1; for (ActorLink* b = bl.next(); b != &bl; b = b->next()) { diff --git a/gecode/kernel/core.hpp b/gecode/kernel/core.hpp index 9526749a91..09fc6a6d3d 100755 --- a/gecode/kernel/core.hpp +++ b/gecode/kernel/core.hpp @@ -2418,13 +2418,14 @@ namespace Gecode { * * Restart and portfolio engines call this on every slave with the * information they pass to slave(), so that slaves cloned from one master - * do not repeat its random decisions. Restart or asset number 0 leaves - * the state unchanged, so the first slave searches as the model does - * without a meta engine. Other numbers split by a separator that no - * alternative uses, one for restarts and another for assets, then by the - * high and low 32 bits of the number. Restart and portfolio streams thus - * stay apart when engines nest. Branchers posted later, during search, - * draw from generators their poster owns and are not split here. + * do not repeat its random decisions. It splits by a separator that no + * alternative uses, one for restarts and another for assets, and then by + * the high and low 32 bits of the restart or asset number. Number 0 + * splits as well, so each level of nested engines adds its own splits: + * restart and portfolio streams stay apart, and nested slaves numbered + * (0,1) and (1,0) differ. The first slave thus does not repeat a search + * without a meta engine. Branchers posted later, during search, draw from + * generators their poster owns and are not split here. * \ingroup TaskSearch */ GECODE_KERNEL_EXPORT diff --git a/gecode/search/pbs.hpp b/gecode/search/pbs.hpp index 15b40855da..e612d1bedb 100644 --- a/gecode/search/pbs.hpp +++ b/gecode/search/pbs.hpp @@ -380,7 +380,9 @@ namespace Gecode { // No need to create a portfolio engine but must run slave function if (o.assets == 1) { std::unique_ptr seed(master); - (void) master->slave(0); + const MetaInfo mi(0U); + master->random_split(mi); + (void) master->slave(mi); // Retain the seed if a nested builder rejects its worker control. opt.clone = true; e = Search::build(seed.get(),opt); diff --git a/plans/random.md b/plans/random.md index a202d42717..55bb9bbc34 100644 --- a/plans/random.md +++ b/plans/random.md @@ -35,7 +35,8 @@ small enough to store directly in the model, selector, or brancher that uses the iteration-state replay commands. 9. Restart and portfolio engines split every brancher of each slave by the restart or asset number, behind separate separators, so slaves explore - different random paths even when the engines nest. + different random paths even when the engines nest. Number 0 splits too, so + nested slaves (0,1) and (1,0) differ. 10. This remains a provisional breaking-change design, not a compatibility release. ## Implementation @@ -82,7 +83,8 @@ small enough to store directly in the model, selector, or brancher that uses the Use the focused Random::Contract, Random::BranchReplay, Random::CommitBoundary, Random::Streams, and FlatZinc::random_intbool_branch tests. Fault::Random::CloneFailures counts inline -custom-engine instances across failed clones. Existing Boolean, set, float, +custom-engine instances across failed clones, including a failure inside each +selector copy of a brancher. Existing Boolean, set, float, assignment, LDSB, and FlatZinc restart cases cover their integration paths. Run random-options and random-state-replay CTests with both configured engines. @@ -139,6 +141,11 @@ example's full-state replay agrees with its output. The Space and base Choice implementation matches main exactly, and randomized choices are as large as deterministic ones. Rnd is 16 bytes with SplitMix and -8 with xorshift64*, without shared allocation. The cost measurements in the PR -description predate this design and must be repeated. Draft PR #241 remains -provisional, for a future breaking-change release only. +8 with xorshift64*, without shared allocation. Against the merge base +(f1a6fa4a75), with Apple Clang 21 on ARM64 and seven alternating runs, bounded +draws take 0.11x (SplitMix) and 0.24x (xorshift64*) of the old time. +Cloning stays within run-to-run noise (0.92-1.09x). Recomputation in the +controlled tree is 12-13% (SplitMix) and 6% (xorshift64*) slower with random +branching, and 5-8% slower with deterministic branching, as every commit now +calls Brancher::random_split. Draft PR #241 remains provisional, for a future +breaking-change release only. diff --git a/test/fault.cpp b/test/fault.cpp index 98075c09a7..bf905a88f2 100644 --- a/test/fault.cpp +++ b/test/fault.cpp @@ -1119,26 +1119,41 @@ namespace Test { namespace Fault { class LiveRandom : public Support::SplitMix { public: static int live; + /// Number of copies before one throws, or negative to never throw + static int copies_before_failure; explicit LiveRandom(uint64_t seed=1) : SplitMix(seed) { ++live; } - LiveRandom(const LiveRandom& r) : SplitMix(r) { ++live; } + LiveRandom(const LiveRandom& r) : SplitMix(r) { + if (copies_before_failure == 0) { + copies_before_failure = -1; + throw MemoryExhausted(); + } + if (copies_before_failure > 0) + --copies_before_failure; + ++live; + } explicit LiveRandom(const SplitMix& r) : SplitMix(r) { ++live; } ~LiveRandom() { --live; } LiveRandom split(uint32_t a) const { return LiveRandom(SplitMix::split(a)); } }; int LiveRandom::live=0; + int LiveRandom::copies_before_failure=-1; class RandomSpace : public Space { public: + using Random = RndGenerator; IntVarArray x; - RndGenerator r; + Random r; RandomSpace() : x(*this,3,0,2), r(7) { IntVarArgs vars(x); ViewArray views(*this,vars); ViewSel* selectors[] = { - new (*this) ViewSelRnd>(*this,r) + new (*this) ViewSelRnd(*this,r), + new (*this) ViewSelRnd(*this,r) }; - auto* values = Int::Branch::valselcommit(*this,INT_VAL_RND(Rnd(7))); - postviewvalbrancher(*this,views,selectors,values,nullptr,nullptr); + using Values = ValSelCommit, + Int::Branch::ValCommitEq>; + auto* values = new (*this) Values(*this,INT_VAL_MIN(),r); + postviewvalbrancher(*this,views,selectors,values,nullptr,nullptr); ThrowingBrancher::post(*this); } RandomSpace(RandomSpace& s) : Space(s), r(s.r) { @@ -1180,6 +1195,17 @@ namespace Test { namespace Fault { Support::FailPoint::reset(); if (LiveRandom::live!=live || root.r.state()!=state) return false; + // Fail inside each selector copy of the random brancher. + for (int n=0; n<3; ++n) { + LiveRandom::copies_before_failure=n; + try { + std::unique_ptr copy(root.clone()); + return false; + } catch (const MemoryExhausted&) {} + LiveRandom::copies_before_failure=-1; + if (LiveRandom::live!=live || root.r.state()!=state) + return false; + } std::unique_ptr copy(root.clone()); } return LiveRandom::live==0; diff --git a/test/random-options.cpp b/test/random-options.cpp index bb9bc5a8f2..69b3cf5eba 100644 --- a/test/random-options.cpp +++ b/test/random-options.cpp @@ -62,3 +62,5 @@ int main(int argc, char* argv[]) { #endif return check(argc,argv); } + +// STATISTICS: test-core diff --git a/test/random-replay.cpp b/test/random-replay.cpp index 9eaa16d8fa..ac8b8d0245 100644 --- a/test/random-replay.cpp +++ b/test/random-replay.cpp @@ -52,3 +52,5 @@ namespace { } } failure(false), exception(true); } + +// STATISTICS: test-core diff --git a/test/random.cpp b/test/random.cpp index 19003f62fa..c6c31aa915 100644 --- a/test/random.cpp +++ b/test/random.cpp @@ -480,8 +480,6 @@ namespace Test { bool restarts_differ() { using namespace Gecode; Permutation root(Rnd(3)); - DFS dfs(&root); - std::unique_ptr plain(dfs.next()); Search::Options options; options.cutoff = Search::Cutoff::constant(1000); RBS rbs(&root,options); @@ -491,10 +489,9 @@ namespace Test { if (!s) return false; found.push_back(s->text()); } - // The first slave keeps the posted state; every restart gets its own. + // Every restart, including the first, gets its own stream. std::set distinct(found.begin(),found.end()); - return plain && (found[0]==plain->text()) && (found[1]!=found[0]) && - (distinct.size()>=8); + return (found[1]!=found[0]) && (distinct.size()>=8); } /// Asset 1's first slave must not repeat restart 1 of asset 0. @@ -509,6 +506,20 @@ namespace Test { return first_solutions(*asset,1)!=first_solutions(*restart,1); } + /// Nested portfolio slaves (0,1) and (1,0) must not share a stream. + bool nested_streams_differ() { + using namespace Gecode; + Permutation root(Rnd(3)); + if (root.status()!=SS_BRANCH) return false; + std::unique_ptr first(static_cast(root.clone())); + first->random_split(MetaInfo(0U)); + first->random_split(MetaInfo(1U)); + std::unique_ptr second(static_cast(root.clone())); + second->random_split(MetaInfo(1U)); + second->random_split(MetaInfo(0U)); + return first_solutions(*first,1)!=first_solutions(*second,1); + } + class Streams : public Base { public: Streams() : Base("Random::Streams") {} @@ -525,7 +536,8 @@ namespace Test { OpenPermutation open(Rnd(3)); auto cloned=first_solutions(open,1); return (cloned.size()==40) && (cloned==first_solutions(open,100)) && - restarts_differ() && meta_streams_differ(); + restarts_differ() && meta_streams_differ() && + nested_streams_differ(); } } streams; } diff --git a/tools/random-benchmark.cpp b/tools/random-benchmark.cpp index 3b89f2b011..a2e683580e 100644 --- a/tools/random-benchmark.cpp +++ b/tools/random-benchmark.cpp @@ -114,8 +114,6 @@ int main(int argc, char** argv) { } return sum; }); - // Warm the shared jump table before measuring xorshift splitting. - (void) xs.split(0); measure("split.xorshift64star",draws,[&] { uint64_t sum=0; for (uint64_t i=0; i Date: Tue, 3 Nov 2026 20:44:17 +0100 Subject: [PATCH 16/16] Trim the random generator branch Drop the planning notes, the benchmark tool, and the custom-engine example; the tests already run a custom engine through the selectors. Shorten the changelog entries, keep main's C++17 CI setup by avoiding if constexpr in random.hpp, and share one archive replay loop between the LDSB and generic replay tests. --- .github/ci/cmake-consumer-smoke.ps1 | 1 - .github/ci/custom-vis-smoke.sh | 2 +- Makefile.dep | 95 --------------- Makefile.in | 2 +- changelog.in | 47 ++------ examples/CMakeLists.txt | 1 - examples/random-engine.cpp | 121 ------------------- gecode/kernel/data/rnd.hpp | 3 +- gecode/support/random.hpp | 2 +- plans/random.md | 151 ----------------------- test/random.cpp | 60 ++++------ tools/random-benchmark.cpp | 178 ---------------------------- tools/random-benchmark.py | 113 ------------------ 13 files changed, 38 insertions(+), 738 deletions(-) delete mode 100644 examples/random-engine.cpp delete mode 100644 plans/random.md delete mode 100644 tools/random-benchmark.cpp delete mode 100644 tools/random-benchmark.py diff --git a/.github/ci/cmake-consumer-smoke.ps1 b/.github/ci/cmake-consumer-smoke.ps1 index 1ede4169bc..c651484820 100644 --- a/.github/ci/cmake-consumer-smoke.ps1 +++ b/.github/ci/cmake-consumer-smoke.ps1 @@ -125,7 +125,6 @@ find_library(GECODE_SUPPORT_LIBRARY NO_DEFAULT_PATH REQUIRED) add_executable(manual_consumer main.cpp) -target_compile_features(manual_consumer PRIVATE cxx_std_17) target_include_directories(manual_consumer PRIVATE "${GECODE_INCLUDE_DIR}") target_link_libraries(manual_consumer PRIVATE "${GECODE_SUPPORT_LIBRARY}") '@ | Set-Content -Encoding utf8 (Join-Path $work "manual\CMakeLists.txt") diff --git a/.github/ci/custom-vis-smoke.sh b/.github/ci/custom-vis-smoke.sh index cf6d411a60..93ecc82c21 100644 --- a/.github/ci/custom-vis-smoke.sh +++ b/.github/ci/custom-vis-smoke.sh @@ -33,7 +33,7 @@ awk ' ' gecode/int/var-imp/int.vis > custom-two.vis CFLAGS="-fPIC" \ -CXXFLAGS="-std=c++17 -fPIC -DGECODE_MEMORY_ALIGNMENT=16" \ +CXXFLAGS="-std=c++11 -fPIC -DGECODE_MEMORY_ALIGNMENT=16" \ dash ./configure \ --with-vis="$PWD/custom-one.vis,$PWD/custom-two.vis" \ --disable-cpprofiler \ diff --git a/Makefile.dep b/Makefile.dep index ed5954d8da..348647035b 100644 --- a/Makefile.dep +++ b/Makefile.dep @@ -14919,101 +14919,6 @@ examples/queens$(OBJSUFFIX) examples/queens$(SBJSUFFIX): \ ./gecode/support/marked-pointer.hpp ./gecode/support/random.hpp ./gecode/support/ref-count.hpp \ ./gecode/support/run-jobs.hpp ./gecode/support/sort.hpp ./gecode/support/static-stack.hpp \ ./gecode/support/thread.hpp ./gecode/support/thread/thread.hpp ./gecode/support/timer.hpp -examples/random-engine$(OBJSUFFIX) examples/random-engine$(SBJSUFFIX): \ - ./gecode/float.hh ./gecode/float/array-traits.hpp ./gecode/float/array.hpp \ - ./gecode/float/branch.hpp ./gecode/float/branch/action.hpp ./gecode/float/branch/afc.hpp \ - ./gecode/float/branch/assign.hpp ./gecode/float/branch/chb.hpp ./gecode/float/branch/traits.hpp \ - ./gecode/float/branch/val.hpp ./gecode/float/branch/var.hpp ./gecode/float/channel.hpp \ - ./gecode/float/exception.hpp ./gecode/float/limits.hpp ./gecode/float/nextafter.hpp \ - ./gecode/float/num.hpp ./gecode/float/rounding.hpp ./gecode/float/trace.hpp \ - ./gecode/float/trace/delta.hpp ./gecode/float/trace/trace-view.hpp ./gecode/float/trace/traits.hpp \ - ./gecode/float/val.hpp ./gecode/float/var-imp.hpp ./gecode/float/var-imp/delta.hpp \ - ./gecode/float/var-imp/float.hpp ./gecode/float/var/float.hpp ./gecode/float/var/print.hpp \ - ./gecode/float/view.hpp ./gecode/float/view/float.hpp ./gecode/float/view/minus.hpp \ - ./gecode/float/view/offset.hpp ./gecode/float/view/print.hpp ./gecode/float/view/rel-test.hpp \ - ./gecode/float/view/scale.hpp ./gecode/int.hh ./gecode/int/array-traits.hpp \ - ./gecode/int/array.hpp ./gecode/int/branch.hh ./gecode/int/branch.hpp \ - ./gecode/int/branch/action.hpp ./gecode/int/branch/afc.hpp ./gecode/int/branch/assign.hpp \ - ./gecode/int/branch/cbs.hpp ./gecode/int/branch/chb.hpp ./gecode/int/branch/merit.hpp \ - ./gecode/int/branch/ngl.hpp ./gecode/int/branch/open.hpp ./gecode/int/branch/traits.hpp \ - ./gecode/int/branch/val-commit.hpp ./gecode/int/branch/val-sel.hpp ./gecode/int/branch/val.hpp \ - ./gecode/int/branch/var.hpp ./gecode/int/branch/view-values.hpp ./gecode/int/channel.hpp \ - ./gecode/int/div.hh ./gecode/int/div.hpp ./gecode/int/exception.hpp \ - ./gecode/int/extensional.hpp ./gecode/int/extensional/dfa.hpp ./gecode/int/extensional/tuple-set.hpp \ - ./gecode/int/int-set-1.hpp ./gecode/int/int-set-2.hpp ./gecode/int/ipl.hpp \ - ./gecode/int/irt.hpp ./gecode/int/limits.hpp ./gecode/int/min-distance.hpp \ - ./gecode/int/open-var-list.hpp ./gecode/int/propagator.hpp ./gecode/int/reify.hpp \ - ./gecode/int/trace.hpp ./gecode/int/trace/bool-delta.hpp ./gecode/int/trace/bool-trace-view.hpp \ - ./gecode/int/trace/int-delta.hpp ./gecode/int/trace/int-trace-view.hpp ./gecode/int/trace/traits.hpp \ - ./gecode/int/var-imp.hpp ./gecode/int/var-imp/bool.hpp ./gecode/int/var-imp/delta.hpp \ - ./gecode/int/var-imp/int.hpp ./gecode/int/var/bool.hpp ./gecode/int/var/int.hpp \ - ./gecode/int/var/print.hpp ./gecode/int/view.hpp ./gecode/int/view/bool-test.hpp \ - ./gecode/int/view/bool.hpp ./gecode/int/view/cached.hpp ./gecode/int/view/constint.hpp \ - ./gecode/int/view/int.hpp ./gecode/int/view/iter.hpp ./gecode/int/view/minus.hpp \ - ./gecode/int/view/neg-bool.hpp ./gecode/int/view/offset.hpp ./gecode/int/view/print.hpp \ - ./gecode/int/view/rel-test.hpp ./gecode/int/view/scale.hpp ./gecode/int/view/zero.hpp \ - ./gecode/iter.hh ./gecode/iter/ranges-add.hpp ./gecode/iter/ranges-append.hpp \ - ./gecode/iter/ranges-array.hpp ./gecode/iter/ranges-cache.hpp ./gecode/iter/ranges-compl.hpp \ - ./gecode/iter/ranges-diff.hpp ./gecode/iter/ranges-empty.hpp ./gecode/iter/ranges-inter.hpp \ - ./gecode/iter/ranges-list.hpp ./gecode/iter/ranges-map.hpp ./gecode/iter/ranges-minmax.hpp \ - ./gecode/iter/ranges-minus.hpp ./gecode/iter/ranges-negative.hpp ./gecode/iter/ranges-offset.hpp \ - ./gecode/iter/ranges-operations.hpp ./gecode/iter/ranges-positive.hpp ./gecode/iter/ranges-rangelist.hpp \ - ./gecode/iter/ranges-scale.hpp ./gecode/iter/ranges-singleton-append.hpp ./gecode/iter/ranges-singleton.hpp \ - ./gecode/iter/ranges-size.hpp ./gecode/iter/ranges-union.hpp ./gecode/iter/ranges-values.hpp \ - ./gecode/iter/values-array.hpp ./gecode/iter/values-bitset.hpp ./gecode/iter/values-bitsetoffset.hpp \ - ./gecode/iter/values-inter.hpp ./gecode/iter/values-list.hpp ./gecode/iter/values-map.hpp \ - ./gecode/iter/values-minus.hpp ./gecode/iter/values-negative.hpp ./gecode/iter/values-offset.hpp \ - ./gecode/iter/values-positive.hpp ./gecode/iter/values-ranges.hpp ./gecode/iter/values-singleton.hpp \ - ./gecode/iter/values-union.hpp ./gecode/iter/values-unique.hpp ./gecode/kernel.hh \ - ./gecode/kernel/archive.hpp ./gecode/kernel/branch/action.hpp ./gecode/kernel/branch/afc.hpp \ - ./gecode/kernel/branch/chb.hpp ./gecode/kernel/branch/filter.hpp ./gecode/kernel/branch/merit.hpp \ - ./gecode/kernel/branch/print.hpp ./gecode/kernel/branch/tiebreak.hpp ./gecode/kernel/branch/traits.hpp \ - ./gecode/kernel/branch/val-commit.hpp ./gecode/kernel/branch/val-sel-commit.hpp ./gecode/kernel/branch/val-sel.hpp \ - ./gecode/kernel/branch/val.hpp ./gecode/kernel/branch/var.hpp ./gecode/kernel/branch/view-sel.hpp \ - ./gecode/kernel/branch/view-val.hpp ./gecode/kernel/branch/view.hpp ./gecode/kernel/core.hpp \ - ./gecode/kernel/data/array.hpp ./gecode/kernel/data/rnd.hpp ./gecode/kernel/data/shared-array.hpp \ - ./gecode/kernel/data/shared-data.hpp ./gecode/kernel/exception.hpp ./gecode/kernel/gpi.hpp \ - ./gecode/kernel/macros.hpp ./gecode/kernel/memory/allocators.hpp ./gecode/kernel/memory/config.hpp \ - ./gecode/kernel/memory/manager.hpp ./gecode/kernel/memory/region.hpp ./gecode/kernel/modevent.hpp \ - ./gecode/kernel/propagator/advisor.hpp ./gecode/kernel/propagator/pattern.hpp ./gecode/kernel/propagator/subscribed.hpp \ - ./gecode/kernel/propagator/wait.hpp ./gecode/kernel/range-list.hpp ./gecode/kernel/shared-object.hpp \ - ./gecode/kernel/shared-space-data.hpp ./gecode/kernel/trace/filter.hpp ./gecode/kernel/trace/general.hpp \ - ./gecode/kernel/trace/print.hpp ./gecode/kernel/trace/recorder.hpp ./gecode/kernel/trace/tracer.hpp \ - ./gecode/kernel/trace/traits.hpp ./gecode/kernel/var-imp.hpp ./gecode/kernel/var-type.hpp \ - ./gecode/kernel/var.hpp ./gecode/kernel/view.hpp ./gecode/minimodel.hh \ - ./gecode/minimodel/aliases.hpp ./gecode/minimodel/bool-expr.hpp ./gecode/minimodel/channel.hpp \ - ./gecode/minimodel/exception.hpp ./gecode/minimodel/float-expr.hpp ./gecode/minimodel/float-rel.hpp \ - ./gecode/minimodel/int-expr.hpp ./gecode/minimodel/int-rel.hpp ./gecode/minimodel/ipl.hpp \ - ./gecode/minimodel/ldsb.hpp ./gecode/minimodel/matrix.hpp ./gecode/minimodel/optimize.hpp \ - ./gecode/minimodel/reg.hpp ./gecode/minimodel/set-expr.hpp ./gecode/minimodel/set-rel.hpp \ - ./gecode/search.hh ./gecode/search/astar.hpp ./gecode/search/bab.hpp \ - ./gecode/search/base.hpp ./gecode/search/build.hpp ./gecode/search/cabs.hpp \ - ./gecode/search/cutoff.hpp ./gecode/search/dfs.hpp ./gecode/search/engine.hpp \ - ./gecode/search/exception.hpp ./gecode/search/lds.hpp ./gecode/search/options.hpp \ - ./gecode/search/pbs.hpp ./gecode/search/rbs.hpp ./gecode/search/sebs.hpp \ - ./gecode/search/seq/dead.hh ./gecode/search/statistics.hpp ./gecode/search/stop.hpp \ - ./gecode/search/support.hh ./gecode/search/trace-recorder.hpp ./gecode/search/tracer.hpp \ - ./gecode/search/traits.hpp ./gecode/search/worker-control.hh ./gecode/set.hh \ - ./gecode/set/array-traits.hpp ./gecode/set/array.hpp ./gecode/set/branch.hpp \ - ./gecode/set/branch/action.hpp ./gecode/set/branch/afc.hpp ./gecode/set/branch/assign.hpp \ - ./gecode/set/branch/chb.hpp ./gecode/set/branch/traits.hpp ./gecode/set/branch/val.hpp \ - ./gecode/set/branch/var.hpp ./gecode/set/exception.hpp ./gecode/set/int.hpp \ - ./gecode/set/limits.hpp ./gecode/set/trace.hpp ./gecode/set/trace/delta.hpp \ - ./gecode/set/trace/trace-view.hpp ./gecode/set/trace/traits.hpp ./gecode/set/var-imp.hpp \ - ./gecode/set/var-imp/delta.hpp ./gecode/set/var-imp/integerset.hpp ./gecode/set/var-imp/iter.hpp \ - ./gecode/set/var-imp/set.hpp ./gecode/set/var/print.hpp ./gecode/set/var/set.hpp \ - ./gecode/set/view.hpp ./gecode/set/view/cached.hpp ./gecode/set/view/complement.hpp \ - ./gecode/set/view/const.hpp ./gecode/set/view/print.hpp ./gecode/set/view/set.hpp \ - ./gecode/set/view/singleton.hpp ./gecode/support.hh ./gecode/support/allocator.hpp \ - ./gecode/support/auto-link.hpp ./gecode/support/bitset-base.hpp ./gecode/support/bitset-offset.hpp \ - ./gecode/support/bitset.hpp ./gecode/support/block-allocator.hpp ./gecode/support/cast.hpp \ - ./gecode/support/config.hpp ./gecode/support/dynamic-array.hpp ./gecode/support/dynamic-queue.hpp \ - ./gecode/support/dynamic-stack.hpp ./gecode/support/exception.hpp ./gecode/support/failpoint.hpp \ - ./gecode/support/hash.hpp ./gecode/support/heap.hpp ./gecode/support/hw-rnd.hpp \ - ./gecode/support/int-type.hpp ./gecode/support/macros.hpp ./gecode/support/marked-pointer.hpp \ - ./gecode/support/random.hpp ./gecode/support/ref-count.hpp ./gecode/support/run-jobs.hpp \ - ./gecode/support/sort.hpp ./gecode/support/static-stack.hpp ./gecode/support/thread.hpp \ - ./gecode/support/thread/thread.hpp ./gecode/support/timer.hpp examples/sudoku$(OBJSUFFIX) examples/sudoku$(SBJSUFFIX): \ ./gecode/driver.hh ./gecode/driver/options.hpp ./gecode/driver/script.hpp \ ./gecode/float.hh ./gecode/float/array-traits.hpp ./gecode/float/array.hpp \ diff --git a/Makefile.in b/Makefile.in index 774d834cd0..9852988585 100755 --- a/Makefile.in +++ b/Makefile.in @@ -964,7 +964,7 @@ INTEXAMPLESRC0 = \ alpha bacp bibd donald efpa eq20 golomb-ruler \ graph-color grocery ind-set magic-sequence magic-square \ magic-square-partial \ - money ortho-latin partition photo queens random-engine sudoku sudoku-advanced kakuro \ + money ortho-latin partition photo queens sudoku sudoku-advanced kakuro \ nonogram pentominoes crowded-chess black-hole \ minesweeper domino steel-mill sports-league \ all-interval langford-number warehouses radiotherapy \ diff --git a/changelog.in b/changelog.in index aaaa8a6e2e..c6f2c6ee8b 100755 --- a/changelog.in +++ b/changelog.in @@ -73,41 +73,23 @@ Version: 7.0.0 Date: unreleased [DESCRIPTION] -Development changes for Gecode 7; source, binary, and seeded-sequence -compatibility with Gecode 6 is not preserved. +This is the development changelog for the next Gecode release. [ENTRY] Module: kernel What: new Rank: major [DESCRIPTION] -Added user-extensible random number generators with reproducible -stream splitting. Splittable SplitMix replaces the old default generator; -xorshift64* is available as a smaller-state alternative. The default is -configurable at build time, and users can supply their own engines. -Randomized branching splits its generator state by alternative on commit, -so the state at a node depends only on the path and is reproduced during -recomputation. Restart and portfolio search split each slave's state, the -first slave included, with separate streams for restarts and portfolio assets, -so slaves explore different random paths, also when engines nest. -Complete-state save/restore and command-line state input support exact -replay, including test failures. +Replaced the random number generator by splittable engines with complete +state save/restore: SplitMix by default, xorshift64* at build time, or a +user-supplied engine. Randomized branching splits its state on commit, so +cloning and recomputation make the same random decisions, while each +restart and portfolio asset gets its own stream. [MORE] -Generators store their compact state directly in their consumers, and copying -a generator produces independent state. Posting splits each selector by its -brancher and role, so branchers given the same generator draw independent -streams. Choices store no generator state. Bounded draws take their value -from the high bits of the raw output. Xorshift64* hashes its seed, as its -recurrence is linear. Drivers accept checked 64-bit seeds as well as complete -state. APIs and seeded sequences change; no legacy -sequence mode is provided. This design remains -provisional for a future breaking-change release. Replace the old seed() -snapshot accessor with state() and restore with state(text). The driver and -FlatZinc options no longer provide a seed() accessor; use opt.rnd() instead -of Rnd(opt.seed()) to honor time, hardware, and complete-state input. -The seed constructor is explicit, so an integer no longer converts to a -generator: write INT_VAL_RND(Rnd(42)) instead of INT_VAL_RND(42). -Engine contracts are documented on Support::Random and RndGenerator. +Rnd is a value type with an explicit seed constructor: write +INT_VAL_RND(Rnd(42)) instead of INT_VAL_RND(42). Use state() and +state(text) instead of seed(), and opt.rnd() instead of Rnd(opt.seed()). +Drivers accept -state with a complete state. Seeded sequences change. [ENTRY] Module: search @@ -115,18 +97,15 @@ What: change Rank: minor [DESCRIPTION] Restart-based search numbers a restart after a solution like any other -restart, so every slave's MetaInfo::restart() is unique. FlatZinc -large neighbourhood search therefore relaxes from the first restart after -the first solution. +restart, so MetaInfo::restart() is unique for every slave. [ENTRY] Module: test What: change Rank: minor [DESCRIPTION] -The test harness option -replay takes the complete random state printed for -a failed iteration instead of a 64-bit seed, together with the failed -test's name passed to -test. +The test option -replay takes the complete random state printed for a +failed iteration. [RELEASE] Version: 6.5.0 diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index d389b76f86..f5c12bdcb9 100755 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -53,7 +53,6 @@ set(GECODE_EXAMPLE_SOURCES queen-armies.cpp queens.cpp radiotherapy.cpp - random-engine.cpp sat.cpp schurs-lemma.cpp sports-league.cpp diff --git a/examples/random-engine.cpp b/examples/random-engine.cpp deleted file mode 100644 index c5cf164121..0000000000 --- a/examples/random-engine.cpp +++ /dev/null @@ -1,121 +0,0 @@ -/* -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */ -/* - * Main authors: - * Mikael Zayenz Lagerkvist - * - * Copyright: - * Mikael Zayenz Lagerkvist, 2026 - * - * This file is part of Gecode, the generic constraint - * development environment: - * http://www.gecode.dev - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - */ - -#include -#include -#include -#include -#include - -/** \brief A user engine that counts raw draws along each path - * - * Build the random-engine target and run it with an optional complete state, - * for example counted-splitmix-v1:000000000000002a:9e3779b97f4a7c15:0000000000000000. - * The program prints its initial state, then enumerates all 24 permutations. - * Generation and indexed splitting retain SplitMix's algorithms and guarantees. - */ -class CountedSplitMix { - Gecode::Support::SplitMix engine; - uint64_t draws = 0; -public: - using State = std::array; - explicit CountedSplitMix(uint64_t seed=1) : engine(seed) {} - static const char* name(void) { return "counted-splitmix-v1"; } - static constexpr uint64_t min(void) { return 0; } - static constexpr uint64_t max(void) { return UINT64_MAX; } - void seed(uint64_t value) { engine.seed(value); draws = 0; } - uint64_t next(void) { ++draws; return engine.next(); } - State state(void) const { - auto s = engine.state(); - return {{s[0],s[1],draws}}; - } - void state(const State& s) { - engine.state({{s[0],s[1]}}); // Validate before changing the counter. - draws = s[2]; - } - CountedSplitMix split(uint32_t alternative) const { - auto child = *this; - child.engine = engine.split(alternative); - return child; - } -}; - -/// Enumerate permutations with independent values of a user-defined engine. -class Permutations : public Gecode::Space { - Gecode::IntVarArray x; -public: - using Random = Gecode::RndGenerator; - explicit Permutations(const Random& source) : x(*this,4,0,3) { - using namespace Gecode; - using namespace Gecode::Int; - distinct(*this,x); - IntVarArgs variables(x); - ViewArray views(*this,variables); - ViewSel* selectors[] = { - new (*this) ViewSelRnd(*this,source) - }; - using Values = ValSelCommit, - Branch::ValCommitEq>; - auto* values = new (*this) Values(*this,INT_VAL_MIN(),source.split(0)); - postviewvalbrancher(*this,views,selectors,values,nullptr,nullptr); - } - Permutations(Permutations& s) - : Space(s) { x.update(*this,s.x); } - Gecode::Space* copy(void) override { return new Permutations(*this); } - void print(void) const { - std::cout << x << '\n'; - } -}; - -int main(int argc, char* argv[]) { - try { - if (argc > 2) - throw std::invalid_argument("Usage: random-engine [complete-state]"); - Gecode::Support::Random engine(42); - if (argc == 2) - engine.state(std::string(argv[1])); - std::cout << "Initial state: " << engine.state_string() << '\n'; - Permutations::Random source(engine); - auto root = std::make_unique(source); - Gecode::DFS search(root.get()); - root.reset(); - while (auto solution = std::unique_ptr(search.next())) - solution->print(); - return 0; - } catch (const std::exception& e) { - std::cerr << e.what() << '\n'; - return 1; - } -} - -// STATISTICS: example-any diff --git a/gecode/kernel/data/rnd.hpp b/gecode/kernel/data/rnd.hpp index 1d5233f8ff..8db901bbac 100755 --- a/gecode/kernel/data/rnd.hpp +++ b/gecode/kernel/data/rnd.hpp @@ -63,8 +63,7 @@ namespace Gecode { * changes them; a custom brancher using one must make its state a * function of the path, for example by splitting it on commit. Space * cloning does not copy mutable state in shared callback captures. Engine - * must meet the Support::Random contract, including splitting. See - * examples/random-engine.cpp for a user-defined engine and selectors. + * must meet the Support::Random contract, including splitting. * \ingroup TaskModel */ template diff --git a/gecode/support/random.hpp b/gecode/support/random.hpp index 8c0bb06322..2aa69ec331 100755 --- a/gecode/support/random.hpp +++ b/gecode/support/random.hpp @@ -388,7 +388,7 @@ namespace Gecode { namespace Support { uint64_t n = static_cast(bound); // Bucket size: 2^64 raw values, or 2^64-1 for nonzero engines, over n. uint64_t bucket; - if constexpr (Engine::min() == 0) + if (Engine::min() == 0) bucket = (uint64_t(0)-n)/n + 1; else bucket = UINT64_MAX/n; diff --git a/plans/random.md b/plans/random.md deleted file mode 100644 index 55bb9bbc34..0000000000 --- a/plans/random.md +++ /dev/null @@ -1,151 +0,0 @@ -# Plan: Extensible random generators and reproducible splitting - -> Provisional draft for Gecode 7 or another future breaking-change release only. -> Status: implemented and locally verified; design remains under review in draft PR #241. - -## Goal - -Replace the old default generator, provide user-extensible alternatives and exact -state replay, and split randomized branching states by alternative. Keep states -small enough to store directly in the model, selector, or brancher that uses them. - -## Required behavior - -1. A generator is a value containing its engine state inline. Copying it produces - independent state, with no allocation, registry, or aliasing for built-in engines. -2. Model members and selectors own their own copies. Posting or cloning copies - state without drawing. Posting splits each selector by brancher id and role, - so passing the same value to two consumers gives them independent streams. -3. Selection draws from a copy, so computing a choice leaves selector state - unchanged. Committing alternative a splits the state of every selector of - that brancher by a. The state at a node is thus a function of the posted - state and the path, and cloning, recomputation, and archived replay agree. -4. Each sibling index produces a distinct successor state. Cloning does not - split. Late alternatives do not replay preceding siblings. Choices store no - generator state. -5. Deterministic branchers, unrelated branchers, and model-owned RNGs are not - implicitly advanced. Custom owners explicitly record/split their own state - when required; hidden callback randomness is not automatically enrolled. -6. Space and Choice representations have no additional RNG fields or archive - words, for randomized and deterministic branchers alike. -7. Users can define engines with different state sizes and use generic branching - machinery. No runtime engine registry or fixed custom-state-size cap is needed. -8. Checked 64-bit seeds and canonical full-state text remain distinct interfaces. - Drivers use one configured engine. Test failures and exceptions report exact - iteration-state replay commands. -9. Restart and portfolio engines split every brancher of each slave by the - restart or asset number, behind separate separators, so slaves explore - different random paths even when the engines nest. Number 0 splits too, so - nested slaves (0,1) and (1,0) differ. -10. This remains a provisional breaking-change design, not a compatibility release. - -## Implementation - -### Value ownership - -- [x] Replace shared-handle Rnd with RndGenerator and a configured Rnd alias. -- [x] Copy selector and model generators normally; remove random-handle disposal - overhead for trivially destructible built-in engines. -- [x] Make relaxation take a generator reference, explicitly advancing its owner. - -### Brancher-local replay - -- [x] Add a selector random_split hook with a no-op default, and a virtual - Brancher::random_split that splits all selectors of a brancher. -- [x] Draw from copies in selection; split on commit only. -- [x] Key selectors at posting by brancher id and role (value 0, variable - selector i at 1+i). -- [x] Add Space::random_split, called by RBS and PBS on each slave. -- [x] Cover binary, multiway, assignment, reversed alternatives, LDSB, open-list, - and FlatZinc int-bool branchers. -- [x] Support user-parameterized variable and integer/Boolean value selectors - through the existing generic brancher machinery. -- [x] Keep model/custom-brancher state transitions explicit. - -### Tests and documentation - -- [x] Test independent copies, untouched model/later-selector state, and - consumer-local splitting. -- [x] Test that branchers sharing a source differ, that open-list and int-bool - results do not depend on the recomputation distance, and that restarts - find different solutions. -- [x] Check direct/archived choices, perturbed destination selector state, - sibling exploration, cloning/recomputation, and parallel solution agreement. -- [x] Exercise a three-word external engine in selectors and a runnable example. -- [x] Retain engine vectors, full-state failure replay, CLI validation, and - failed-clone resource checks. -- [x] Document engine extension, splitting, full-state replay, and migration. -- [x] Run full relevant checks for both defaults and the reduced static/no-thread build. -- [x] Measure compactness and representative costs against main. -- [x] Review the implementation and update the provisional draft PR. - -## Verification strategy - -Use the focused Random::Contract, Random::BranchReplay, Random::CommitBoundary, -Random::Streams, and FlatZinc::random_intbool_branch tests. Fault::Random::CloneFailures counts inline -custom-engine instances across failed clones, including a failure inside each -selector copy of a brancher. Existing Boolean, set, float, -assignment, LDSB, and FlatZinc restart cases cover their integration paths. - -Run random-options and random-state-replay CTests with both configured engines. -Verify that the custom example's initial state replays its output. Keep direct -assertions on state and choice archives; different first outputs alone do not -prove sibling-state distinction. - -Measure against the same main baseline (6b7de57b04), using the existing controlled -tree and queens harness. Report actual generator/description/selector/choice sizes -and any performance costs. - -## Algorithm and CLI decisions - -Splittable SplitMix has two state words and constant-time indexed splitting. -Xorshift64* has one state word and splits by hashing s + (2a+1)g with Mix13. -Jumps along its recurrence were rejected: they add up, so split(0) of split(0) -equals split(1), correlating subtrees. A zero input hashes (2a+1)g instead, so -each split is a bijection on nonzero states. Seeds are hashed the same way, as -related raw seeds of the linear recurrence give related streams. Bounded draws -use the high bits, since xorshift64*'s low bits are linear. Both engines have -documented sibling-state arguments in gecode/support/random.hpp. The default remains -provisional SplitMix; xorshift64* remains configurable through CMake and Autoconf. - -Support::Random defines bounded integer conversion and canonical full-state -encoding. RndGenerator provides the modeling value interface. Full state -includes every word needed for future draws and splits. Seed expansion never -substitutes for state restoration. - -Drivers retain -seed (examples), -r (FlatZinc), and -state. Time/hardware -initialization reports concrete state. Incompatible, malformed, and conflicting -input is rejected. No legacy sequence mode is required. - -## Split analysis - -PractRand 0.95 (expanded tests, full folding) passes 32 GB of first draws for -both engines along leftmost, random binary, and random 10-way paths, in -depth-first order over complete trees, for all 2^32 siblings of one node, for -siblings under many parents, for parent and child draws side by side, for -posting keys, and for restart slaves; binary draws pass 16 GB. Splits without -the hash fail all of these at 32-64 MB, and the old jump split fails the -depth-first stream at 32 MB. Single-bit avalanche of every split derivation is -at the noise floor, and trees of 1.1-1.3 * 10^8 nodes repeat no state. -Unsplit xorshift64* low bits fail at 128 MB, which motivated high-bit draws. -Repeated states at about 2^32 nodes are expected for the one-word engine and -harmless: two nodes then make the same random choices. - -## Verification results - -With the split-on-commit design, both engine configurations (xorshift64* built -with experimental engines) pass CMake check and all CTests. The Random and -FlatZinc random tests pass with both engines. The SplitMix build also passes -all Branch, Assign, FlatZinc, Search, and NoGoods tests. The custom-engine -example's full-state replay agrees with its output. - -The Space and base Choice implementation matches main exactly, and randomized -choices are as large as deterministic ones. Rnd is 16 bytes with SplitMix and -8 with xorshift64*, without shared allocation. Against the merge base -(f1a6fa4a75), with Apple Clang 21 on ARM64 and seven alternating runs, bounded -draws take 0.11x (SplitMix) and 0.24x (xorshift64*) of the old time. -Cloning stays within run-to-run noise (0.92-1.09x). Recomputation in the -controlled tree is 12-13% (SplitMix) and 6% (xorshift64*) slower with random -branching, and 5-8% slower with deterministic branching, as every commit now -calls Brancher::random_split. Draft PR #241 remains provisional, for a future -breaking-change release only. diff --git a/test/random.cpp b/test/random.cpp index c6c31aa915..03fe4af863 100644 --- a/test/random.cpp +++ b/test/random.cpp @@ -245,18 +245,24 @@ namespace Test { return true; } + /// Spaces without a model-owned generator have no owner state to compare. + template + bool owners_agree(const Model&, const Model&) { return true; } template - bool choice_replay(const Random& source, bool different, bool multi, bool callback) { + bool owners_agree(const ReplaySpace& a, const ReplaySpace& b) { + return a.own.state()==b.own.state(); + } + + /// Committing an archived choice must match committing it directly. + template + bool archive_replay(Model& root) { using namespace Gecode; - using Model=ReplaySpace; - std::unique_ptr root(new Model(source,different,multi,callback)); - while (root->status()==SS_BRANCH) { - std::unique_ptr before(static_cast(root->clone())); - const auto owner_state=root->own.state(); - std::unique_ptr choice(root->choice()); - if (root->own.state()!=owner_state) return false; + while (root.status()==SS_BRANCH) { + std::unique_ptr before(static_cast(root.clone())); + std::unique_ptr choice(root.choice()); + if (!owners_agree(root,*before)) return false; for (unsigned int a=choice->alternatives(); a--;) { - std::unique_ptr direct(static_cast(root->clone())); + std::unique_ptr direct(static_cast(root.clone())); std::unique_ptr replay(static_cast(before->clone())); Archive archive; choice->archive(archive); std::unique_ptr restored(replay->choice(archive)); @@ -264,7 +270,7 @@ namespace Test { direct->commit(*choice,a); replay->commit(*restored,a); auto status=direct->status(); - if (status!=replay->status() || direct->own.state()!=replay->own.state()) + if (status!=replay->status() || !owners_agree(*direct,*replay)) return false; if (status==SS_BRANCH) { std::unique_ptr next(direct->choice()); @@ -272,7 +278,7 @@ namespace Test { if (!same_archive(*next,*next_replay)) return false; } } - root->commit(*choice,0); + root.commit(*choice,0); } return true; } @@ -301,7 +307,8 @@ namespace Test { for (bool different : {false,true}) for (bool multi : {false,true}) for (bool callback : {false,true}) { - if (!choice_replay(source,different,multi,callback)) return false; + ReplaySpace root(source,different,multi,callback); + if (!archive_replay(root)) return false; auto cloned=solutions(source,1,different,multi,callback); auto recomputed=solutions(source,100,different,multi,callback); if (cloned.size()!=81 || cloned!=recomputed) return false; @@ -327,37 +334,12 @@ namespace Test { Space* copy() override { return new LDSBSpace(*this); } }; - bool ldsb_replay() { - using namespace Gecode; - LDSBSpace root; - while (root.status()==SS_BRANCH) { - std::unique_ptr before(root.clone()); - std::unique_ptr choice(root.choice()); - for (unsigned int a=choice->alternatives(); a--;) { - std::unique_ptr direct(root.clone()); - std::unique_ptr replay(before->clone()); - Archive archive; choice->archive(archive); - std::unique_ptr restored(replay->choice(archive)); - if (!same_archive(*choice,*restored)) return false; - direct->commit(*choice,a); replay->commit(*restored,a); - auto status=direct->status(); - if (status!=replay->status()) return false; - if (status==SS_BRANCH) { - std::unique_ptr next(direct->choice()); - std::unique_ptr next_replay(replay->choice()); - if (!same_archive(*next,*next_replay)) return false; - } - } - root.commit(*choice,0); - } - return true; - } - class BranchReplay : public Base { public: BranchReplay() : Base("Random::BranchReplay", TestTags::check()) {} bool run() override { - return ldsb_replay() && branch_replay(Gecode::Rnd(42)) && + LDSBSpace ldsb; + return archive_replay(ldsb) && branch_replay(Gecode::Rnd(42)) && branch_replay(Gecode::RndGenerator(42)) && branch_replay(Gecode::RndGenerator(42)); } diff --git a/tools/random-benchmark.cpp b/tools/random-benchmark.cpp deleted file mode 100644 index a2e683580e..0000000000 --- a/tools/random-benchmark.cpp +++ /dev/null @@ -1,178 +0,0 @@ -/* -*- 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. - * - */ - -// Compile against main without RANDOM_NEW, or against feature/random with it. -#include -#include -#include -#include -#include -#include - -using namespace Gecode; -using Clock = std::chrono::steady_clock; - -template -void measure(const char* name, uint64_t operations, F work) { - auto start=Clock::now(); - uint64_t checksum=work(); - double ns=std::chrono::duration(Clock::now()-start).count(); - std::cout << name << '\t' << ns/operations << "\tns/op\t" << checksum << '\n'; -} - -class Model : public Space { -public: - IntVarArray x; - Model(unsigned int n, unsigned int seed, bool queens, bool random) - : x(*this,n,0,queens ? n-1 : 1) { - if (queens) { - distinct(*this,x,IPL_DOM); - IntArgs up(n); - IntArgs down(n); - for (unsigned int i=0; i1 ? std::stoull(argv[1]) : 1000000; - const unsigned int seed=argc>2 ? std::stoul(argv[2]) : 42; - if (!draws) return 1; - std::cout << "size.engine\t" << sizeof(Support::RandomGenerator) << "\tbytes\t0\n" - << "size.rnd\t" << sizeof(Rnd) << "\tbytes\t0\n" - << "size.var_selector\t" << sizeof(ViewSelRnd) << "\tbytes\t0\n" - << "size.val_selector\t" << sizeof(Int::Branch::ValSelRnd) << "\tbytes\t0\n" - << "size.var_description\t" << sizeof(IntVarBranch) << "\tbytes\t0\n" - << "size.val_description\t" << sizeof(IntValBranch) << "\tbytes\t0\n" - << "size.space\t" << sizeof(Space) << "\tbytes\t0\n" - << "size.choice\t" << sizeof(PosValChoice) << "\tbytes\t0\n"; - Support::RandomGenerator raw(seed); - measure("raw.default",draws,[&] { - uint64_t sum=0; - for (uint64_t i=0; i xs(seed); - measure("raw.xorshift64star",draws,[&] { - uint64_t sum=0; - for (uint64_t i=0; i choice(root.choice()); - Archive archive; - choice->archive(archive); - std::cout << (random ? "size.random_archive" : "size.plain_archive") - << '\t' << archive.size()*sizeof(unsigned int) << "\tbytes\t0\n"; -#ifdef RANDOM_NEW - // Randomized choices store no generator state. - std::cout << (random ? "size.random_choice" : "size.plain_choice") - << '\t' << sizeof(PosValChoice) << "\tbytes\t0\n"; -#endif - measure(random ? "clone.random" : "clone.plain",10000,[&] { - uint64_t sum=0; - for (int i=0; i<10000; ++i) { - std::unique_ptr copy(root.clone()); - sum += copy->status(); - } - return sum; - }); - for (unsigned int distance : {1U,16U}) { - Search::Options options; - options.c_d=distance; - options.a_d=distance; - const char* name=random ? (distance==1 ? "tree.random.clone" : "tree.random.recompute") - : (distance==1 ? "tree.plain.clone" : "tree.plain.recompute"); - // Complete binary tree: exactly 32767 nodes for every engine/seed. - measure(name,32767,[&] { - DFS search(&root,options); - uint64_t solutions=0; - while (std::unique_ptr s{search.next()}) ++solutions; - if (solutions!=16384 || search.statistics().node!=32767) - throw std::runtime_error("Controlled tree changed"); - return solutions; - }); - } - } - Model queens(10,seed,true,true); - Search::Options options; - uint64_t nodes=0; - measure("queens.random",1,[&] { - DFS search(&queens,options); - uint64_t solutions=0; - while (std::unique_ptr s{search.next()}) ++solutions; - nodes=search.statistics().node; - if (solutions!=724) throw std::runtime_error("Queens solutions changed"); - return solutions; - }); - std::cout << "queens.nodes\t" << nodes << "\tnodes\t0\n"; -} - -// STATISTICS: support-any diff --git a/tools/random-benchmark.py b/tools/random-benchmark.py deleted file mode 100644 index ec25fb6e78..0000000000 --- a/tools/random-benchmark.py +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env python3 - -# -# 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. -# - -r"""Compare compiled random-benchmark executables with bounded repeated runs. - -Example: python3 tools/random-benchmark.py build/random/random-benchmark \ - --baseline /tmp/baseline/random-benchmark --repeat 5 --output results.json -Compile the candidate against the configured build: - clang++ -O3 -DNDEBUG -std=c++17 -DRANDOM_NEW -Ibuild/random -I. \ - tools/random-benchmark.cpp -Lbuild/random -lgecodeint -lgecodesearch \ - -lgecodekernel -lgecodesupport -Wl,-rpath,build/random \ - -o build/random/random-benchmark -Compile the same source against baseline headers/libraries without -DRANDOM_NEW. -Runs alternate order, discard one warmup, and report medians. The controlled tree -has 32767 nodes and 16384 solutions; queens node counts can vary with the engine. -Avoid concurrent compilation while measuring. -""" -import argparse -import json -import platform -import statistics -import subprocess -from pathlib import Path - - -def main(): - parser = argparse.ArgumentParser( - description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) - parser.add_argument("candidate", type=Path) - parser.add_argument("--baseline", type=Path) - parser.add_argument("--repeat", type=int, default=5) - parser.add_argument("--draws", type=int, default=1_000_000) - parser.add_argument("--seed", type=int, default=42) - parser.add_argument("--output", type=Path) - args = parser.parse_args() - if args.repeat < 1 or args.draws < 1: - parser.error("repeat and draws must be positive") - if not 0 <= args.seed <= 0xffffffff: - parser.error("the baseline comparison requires a 32-bit unsigned seed") - binaries = {"candidate": args.candidate.resolve()} - if args.baseline: - binaries["baseline"] = args.baseline.resolve() - records = {name: [] for name in binaries} - for iteration in range(args.repeat + 1): - # Reverse order on alternating repetitions; first repetition is warmup. - names = list(binaries) - if iteration % 2: - names.reverse() - for name in names: - run = subprocess.run( - [str(binaries[name]), str(args.draws), str(args.seed)], - capture_output=True, text=True, timeout=120, - ) - if run.returncode: - parser.exit(1, f"{binaries[name]} failed:\n{run.stdout}\n{run.stderr}") - rows = {} - for line in run.stdout.splitlines(): - case, value, unit, checksum = line.split("\t") - rows[case] = {"value": float(value), "unit": unit, "checksum": checksum} - if iteration: - records[name].append(rows) - medians = { - name: {case: statistics.median(run[case]["value"] for run in runs) - for case in runs[0]} - for name, runs in records.items() - } - for case, value in medians["candidate"].items(): - unit = records["candidate"][0][case]["unit"] - previous = medians.get("baseline", {}).get(case) - comparison = f" (baseline {previous:.2f}, ratio {value / previous:.3f})" if previous else "" - print(f"{case}: {value:.2f} {unit}{comparison}") - if args.output: - # Preserve earlier runs; choose a new output name to collect another run. - with args.output.open("x") as out: - json.dump({"platform": platform.platform(), "machine": platform.machine(), - "draws": args.draws, "seed": args.seed, - "binaries": {k: str(v) for k, v in binaries.items()}, - "runs": records, "medians": medians}, out, indent=2) - out.write("\n") - - -if __name__ == "__main__": - main()