diff --git a/CMakeLists.txt b/CMakeLists.txt index e9d0600f6b..350308f5d6 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) @@ -1399,6 +1406,29 @@ 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 + test/test.cpp test/random-replay.cpp) + target_link_libraries(gecode-random-replay PRIVATE ${GECODE_TEST_LINK_LIBS}) + 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) + 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}) + 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) + 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 ${GECODE_FAULT_TEST_SOURCES}) @@ -1418,6 +1448,9 @@ if(BUILD_TESTING) endif() set(GECODE_CHECK_TESTS + Random::Contract + Random::BranchReplay + Random::CommitBoundary Branch::Int::Dense::3 Int::Arithmetic::Abs Int::Arithmetic::ArgMax diff --git a/Makefile.in b/Makefile.in index b446405125..2aed374fa1 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 \ @@ -896,7 +895,7 @@ INTEXAMPLEHDR0 = \ INTEXAMPLESRC0 = \ alpha bacp bibd donald efpa eq20 golomb-ruler \ graph-color grocery ind-set magic-sequence magic-square \ - 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 \ @@ -1276,7 +1275,7 @@ BLACKBOXEXECSRC = test/flatzinc/blackbox-exec.cpp BLACKBOXDLLSRC = test/flatzinc/blackbox-dll.cpp BLACKBOXSRC = $(BLACKBOXEXECSRC) $(BLACKBOXDLLSRC) -TESTSRC0 = test/test.cpp test/afc.cpp test/ldsb.cpp test/region.cpp \ +TESTSRC0 = test/test.cpp 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. @@ -1345,6 +1344,7 @@ test: mkcompiledirs $(BLACKBOXFIXTURES) @$(MAKE) $(VARIMP) $(TESTEXE) CHECKTESTS = Branch::Int::Dense::3 \ + Random::Contract Random::BranchReplay Random::CommitBoundary \ FlatZinc::magic_square \ Int::Arithmetic::Abs \ Int::Arithmetic::ArgMax \ diff --git a/changelog.in b/changelog.in index 5356b24303..f7cb0160d6 100755 --- a/changelog.in +++ b/changelog.in @@ -67,6 +67,34 @@ # optional section in the html page. # +[RELEASE] +Version: 7.0.0 +Date: unreleased +[DESCRIPTION] +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] +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 Date: unreleased diff --git a/cmake/GecodeSources.cmake b/cmake/GecodeSources.cmake index 0c9defc0f1..2f189a35e5 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 @@ -411,6 +410,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/configure b/configure index 0a59cb0548..7797bc119a 100755 --- a/configure +++ b/configure @@ -821,6 +821,7 @@ SHELL' ac_subst_files='' ac_user_opts=' enable_option_checking +with_random_engine with_host_os with_compiler_vendor enable_resource @@ -1546,6 +1547,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, @@ -3280,6 +3284,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=51 GECODE_SOVERSION=${ac_gecode_soversion} diff --git a/configure.ac b/configure.ac index b2c3ef0d58..4493755a75 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 new file mode 100644 index 0000000000..6fe23fffc2 --- /dev/null +++ b/docs/random.md @@ -0,0 +1,248 @@ +# 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()`. 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. + +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/CMakeLists.txt b/examples/CMakeLists.txt index a223ae844a..498de28fad 100755 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -50,6 +50,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/job-shop.cpp b/examples/job-shop.cpp index 356fac4cfd..e564c3bac4 100755 --- a/examples/job-shop.cpp +++ b/examples/job-shop.cpp @@ -465,7 +465,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(o.rnd()) { if (opt.propagation() == PROP_UNARY) nooverload(); @@ -616,7 +616,7 @@ print(const Search::Statistics& stat, bool restart) { /// Solver void solve(const JobShopOptions& opt) { - Rnd rnd(opt.seed()); + Rnd rnd = opt.rnd(); /* * Invariant: @@ -828,4 +828,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..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(opt.seed()), p(opt.relax()) + rnd(opt.rnd()), 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)) { + 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; diff --git a/examples/random-engine.cpp b/examples/random-engine.cpp new file mode 100644 index 0000000000..a2582282c1 --- /dev/null +++ b/examples/random-engine.cpp @@ -0,0 +1,114 @@ +/* -*- 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 + +/// 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 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; + } +} diff --git a/gecode/driver.hh b/gecode/driver.hh index 409968d979..d3694889ca 100755 --- a/gecode/driver.hh +++ b/gecode/driver.hh @@ -143,6 +143,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 * @@ -417,7 +435,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 //@} @@ -509,9 +527,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 5c6c1fbf87..8793832f6a 100755 --- a/gecode/driver/options.cpp +++ b/gecode/driver/options.cpp @@ -108,6 +108,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 2aa4c337c4..34bc8add3f 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 f4d934d3d1..d78497246c 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(); } @@ -410,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; @@ -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/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/flatzinc/flatzinc.cpp b/gecode/flatzinc/flatzinc.cpp index e35bd3638c..52fdbb467d 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(); @@ -2078,6 +2077,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 = _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 40542fdf9e..8a2fa95b6e 100755 --- a/gecode/float.hh +++ b/gecode/float.hh @@ -2042,7 +2042,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 811663f4ab..f506b99a90 100644 --- a/gecode/float/branch/val-sel.hpp +++ b/gecode/float/branch/val-sel.hpp @@ -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&) { @@ -100,4 +100,3 @@ namespace Gecode { namespace Float { namespace Branch { }}} // STATISTICS: float-branch - 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 2b6c2ea796..64d04f8976 100755 --- a/gecode/int.hh +++ b/gecode/int.hh @@ -5827,7 +5827,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 @@ -5852,7 +5852,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 1192ee1282..7ccebbd09c 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 7d91368c3e..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) + ValSelRnd::ValSelRnd + (Space& home, const ValBranch::Var>& vb) : ValSel(home,vb), r(vb.rnd()) {} - template + template forceinline - ValSelRnd::ValSelRnd(Space& home, ValSelRnd& vs) + 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 @@ -166,4 +166,3 @@ namespace Gecode { namespace Int { namespace Branch { }}} // STATISTICS: int-branch - 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 124acac92f..02263d320a 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..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; @@ -189,7 +184,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 +209,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 2ffa423cd0..4beb10f1be 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) + ViewSelRnd::ViewSelRnd(Space& home, const VarBranch& vb) : ViewSel(home,vb), r(vb.rnd()) {} - template + template forceinline - ViewSelRnd::ViewSelRnd(Space& home, ViewSelRnd& vs) + ViewSelRnd::ViewSelRnd(Space& home, ViewSelRnd& vs) : ViewSel(home,vs), r(vs.r) {} - template + 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 333d014cfd..33f63f1e38 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 8bdca1d1e1..1700c7d632 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/data/rnd.hpp b/gecode/kernel/data/rnd.hpp index 9cdd4afd2b..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: @@ -36,147 +38,53 @@ namespace Gecode { /** - * \brief Random number generator + * \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. * \ingroup TaskModel */ - class Rnd : public SharedHandle { - 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(unsigned int s); - /// Return seed - unsigned int seed(void) const; - /// Set seed to \a s - void seed(unsigned int 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); - }; - /// Set the current seed to \a s (initializes if needed) - void _seed(unsigned int s); + template + class RndGenerator { + Support::Random r; 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(unsigned int s); - /// Set the current seed to \a s (initializes if needed) - GECODE_KERNEL_EXPORT - void seed(unsigned int 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; - /// 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); + 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()); + } + 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); + } + /// 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(); + } }; - forceinline unsigned int - Rnd::IMP::seed(void) const { - unsigned int s; - const_cast(*this).m.acquire(); - s = rg.seed(); - const_cast(*this).m.release(); - return s; - } - forceinline void - Rnd::IMP::seed(unsigned int 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 unsigned int - Rnd::seed(void) const { - const IMP* i = static_cast(object()); - return i->seed(); - } - 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); - } + /// Build-configured default, with exactly the engine's inline state size. + using Rnd = RndGenerator; } diff --git a/gecode/search/relax.hh b/gecode/search/relax.hh index 5e8b1aee60..ecf2b03a18 100755 --- a/gecode/search/relax.hh +++ b/gecode/search/relax.hh @@ -42,13 +42,13 @@ 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; 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 113c6314e4..972fe154c8 100644 --- a/gecode/set/branch/val-sel.hpp +++ b/gecode/set/branch/val-sel.hpp @@ -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&) { @@ -120,4 +120,3 @@ namespace Gecode { namespace Set { namespace Branch { }}} // STATISTICS: set-branch - 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/config.hpp.in b/gecode/support/config.hpp.in index 9c9588878f..85beb03270 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 760176ee64..27d1becb3f 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,248 @@ 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; + 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: + 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 with indexed jump 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. + * \ingroup FuncSupport + */ + class Xorshift64Star { + public: + 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"; } + 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 = 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 + * + * 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 EngineType = Engine; + 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; +#else + using RandomGenerator = Random; +#endif }} diff --git a/plans/random.md b/plans/random.md new file mode 100644 index 0000000000..2f818c8fc6 --- /dev/null +++ b/plans/random.md @@ -0,0 +1,118 @@ +# 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. 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] 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] 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] 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 existing focused Random::Contract, Random::BranchReplay, and +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. + +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 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. + +## 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 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. Draft PR #241 remains provisional, for a future +breaking-change release only. diff --git a/test/fault.cpp b/test/fault.cpp index d0cd8ce1f1..3ff428779a 100644 --- a/test/fault.cpp +++ b/test/fault.cpp @@ -32,6 +32,7 @@ */ #include +#include #include #include "test/test.hh" @@ -1174,6 +1175,77 @@ namespace Test { namespace Fault { } }; + // Count inline engine instances across selector cloning and failed clones. + 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; + 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(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 inline engines 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/flatzinc.cpp b/test/flatzinc.cpp index b7c1873b0b..f9bfe7dd42 100755 --- a/test/flatzinc.cpp +++ b/test/flatzinc.cpp @@ -142,8 +142,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..78f492ca4a --- /dev/null +++ b/test/random-options.cmake @@ -0,0 +1,95 @@ +# +# 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) +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/gecode/flatzinc/restart-random.hpp b/test/random-options.cpp similarity index 56% rename from gecode/flatzinc/restart-random.hpp rename to test/random-options.cpp index c272d2aaac..bb9bc5a8f2 100644 --- a/gecode/flatzinc/restart-random.hpp +++ b/test/random-options.cpp @@ -31,37 +31,34 @@ * */ -#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); +#include +#ifdef TEST_RANDOM_FLATZINC +#include +#endif +#include - // 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; +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 - -// STATISTICS: flatzinc-other + return check(argc,argv); +} diff --git a/test/random-replay.cmake b/test/random-replay.cmake new file mode 100644 index 0000000000..bbc4febf65 --- /dev/null +++ b/test/random-replay.cmake @@ -0,0 +1,53 @@ +# +# 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-exact "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/gecode/kernel/data/rnd.cpp b/test/random-replay.cpp similarity index 59% rename from gecode/kernel/data/rnd.cpp rename to test/random-replay.cpp index f7dc782889..9eaa16d8fa 100644 --- a/gecode/kernel/data/rnd.cpp +++ b/test/random-replay.cpp @@ -1,12 +1,10 @@ /* -*- 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 + * Mikael Zayenz Lagerkvist, 2026 * * This file is part of Gecode, the generic constraint * development environment: @@ -33,52 +31,24 @@ * */ -#include - -namespace Gecode { - - Support::Mutex Rnd::IMP::m; - - forceinline - Rnd::IMP::IMP(unsigned int s) - : rg(s) {} - - Rnd::IMP::~IMP(void) {} - - forceinline void - Rnd::_seed(unsigned int s) { - if (object() == nullptr) { - object(new IMP(s)); - } else { - static_cast(object())->seed(s); +// 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; } - } - - Rnd::Rnd(void) {} - Rnd::Rnd(unsigned int s) { - object(new IMP(s)); - } - Rnd::Rnd(const Rnd& r) - : SharedHandle(r) {} - Rnd& - Rnd::operator =(const Rnd& r) { - (void) SharedHandle::operator =(r); - return *this; - } - Rnd::~Rnd(void) {} - - void - Rnd::seed(unsigned int s) { - _seed(s); - } - void - Rnd::time(void) { - _seed(static_cast(::time(nullptr))); - } - void - Rnd::hw(void) { - _seed(Support::hwrnd()); - } + } failure(false), exception(true); } - -// STATISTICS: kernel-other diff --git a/test/random.cpp b/test/random.cpp new file mode 100644 index 0000000000..a600f82b49 --- /dev/null +++ b/test/random.cpp @@ -0,0 +1,386 @@ +/* -*- 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/test.hh" +#include + +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") {} + bool run() override { + // SplitMix64 reference sequence, seed zero and golden-ratio increment. + Gecode::Support::Random 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() || + !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. + Gecode::Support::Random 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; + + // The model's RNG is an ordinary value, independent of selector copies. + template + class ReplaySpace : public Gecode::Space { + public: + Gecode::IntVarArray x; + Random own; + Gecode::ViewSel* variable = nullptr; + Gecode::ValSelCommitBase* value = nullptr; + void post(const Gecode::IntVarArgs& vars, bool multi, bool different) { + using namespace Gecode; + 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 { + 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(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; + for (int i=0; i + bool choice_replay(const Random& source, bool different, bool multi, bool callback) { + 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; + 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)); + if (!same_archive(*choice,*restored)) return false; + direct->commit(*choice,a); + replay->commit(*restored,a); + 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; + } + } + root->commit(*choice,0); + } + return true; + } + + template + std::vector solutions(const Random& source, unsigned int distance, + bool different, bool multi, bool callback, + unsigned int threads=1) { + using namespace Gecode; + 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); + std::vector result; + while (std::unique_ptr s{search.next()}) { + std::ostringstream item; + 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") {} + bool run() override { + return ldsb_replay() && branch_replay(Gecode::Rnd(42)) && + branch_replay(Gecode::RndGenerator(42)) && + branch_replay(Gecode::RndGenerator(42)); + } + } branch_replay_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; + } + return true; + } + + class CommitBoundary : public Base { + public: + CommitBoundary() : Base("Random::CommitBoundary") {} + bool run() override { + using namespace Gecode; + 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/test/test.cpp b/test/test.cpp index 5d808d8dd2..d6da2b67f9 100644 --- a/test/test.cpp +++ b/test/test.cpp @@ -94,11 +94,18 @@ namespace Test { Options opt; - void report_error(const std::string& name, unsigned int seed, Options& options, std::ostream& ostream) { - ostream << "Options: -seed " << seed; + void report_error(const std::string& name, const std::string& state, + const Options& options, std::ostream& ostream) { + ostream << "Options: -state " << state << " -iter 1"; if (options.fixprob != Test::Options::deffixprob) ostream << " -fixprob " << options.fixprob; - ostream << " -test " << name << std::endl; + ostream << " -test-exact '"; + for (char c : name) + if (c == '\'') + ostream << "'\\''"; + else + ostream << c; + ostream << "'" << std::endl; if (options.log) ostream << olog.str(); } @@ -106,6 +113,7 @@ namespace Test { void Options::parse(int argc, char* argv[]) { int i = 1; + bool seed_given = false; while (i < argc) { if (!strcmp(argv[i],"-help") || !strcmp(argv[i],"--help")) { std::cerr << "Options for testing:" << std::endl @@ -113,12 +121,16 @@ 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-state (complete random state)" << std::endl + << "\t\treplay one test directly; requires -test-exact" << std::endl + << "\t-test-exact (string)" << std::endl + << "\t\texact name of the test to run" << std::endl << "\t-fixprob (unsigned int) default: " << fixprob << std::endl << "\t\t1/fixprob is the probability of computing a fixpoint" @@ -158,11 +170,30 @@ namespace Test { } } else if (!strcmp(argv[i],"-seed")) { if (++i == argc) goto missing; + seed_given = true; 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],"-state")) { + if (++i == argc) goto missing; + random_state = argv[i]; + try { + Gecode::Support::RandomGenerator check; + check.state(random_state); + } catch (const std::invalid_argument& e) { + std::cerr << e.what() << std::endl; + exit(EXIT_FAILURE); } + } else if (!strcmp(argv[i],"-test-exact")) { + if (++i == argc) goto missing; + exact_test = argv[i]; } else if (!strcmp(argv[i],"-iter")) { if (++i == argc) goto missing; iter = static_cast(atoi(argv[i])); @@ -195,6 +226,12 @@ namespace Test { i++; } + if (!random_state.empty() && + (seed_given || exact_test.empty() || threads != 1)) { + std::cerr << "State replay requires -test-exact, one thread, and no -seed." + << std::endl; + exit(EXIT_FAILURE); + } if (threads > 1 && log) { std::cerr << "Logging and multi threading can not be used jointly." << std::endl; exit(EXIT_FAILURE); @@ -208,6 +245,8 @@ namespace Test { } bool Options::is_test_name_matching(const std::string& test_name) { + if (!exact_test.empty()) + return test_name == exact_test; if (!testpat.empty()) { bool positive_patterns = false; bool match_found = false; @@ -248,19 +287,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) { + bool run_test(Base* test, uint64_t test_seed, const Options& options, std::ostream& ostream) { + test->_rand.seed(test_seed); + if (!options.random_state.empty()) + test->_rand.state(options.random_state); + std::string iteration_state = test->_rand.state_string(); try { ostream << test->name() << " "; ostream.flush(); - test->_rand.seed(test_seed); for (unsigned int i = options.iter; i--;) { - unsigned int 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, opt, ostream); + report_error(test->name(), iteration_state, options, ostream); return false; } } @@ -270,7 +312,11 @@ namespace Test { ostream << "Exception in \"Gecode::" << e.what() << "." << std::endl << "Stopping..." << std::endl; - report_error(test->name(), options.seed, opt, 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; } } @@ -280,7 +326,7 @@ namespace Test { Gecode::Support::RandomGenerator seed_sequence(options.seed); int result = EXIT_SUCCESS; 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)) { if (opt.stop) { return EXIT_FAILURE; @@ -396,10 +442,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 { @@ -422,7 +468,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; if (!run_test(test, test_seed, tec.options, test_output)) { tec.set_failure(); @@ -489,6 +535,10 @@ main(int argc, char* argv[]) { } } + if (!opt.exact_test.empty() && tests.size() != 1) { + std::cerr << "Exact test name did not select a test." << std::endl; + return EXIT_FAILURE; + } if (opt.threads > 1) { return run_tests_parallel(tests, opt); } else { diff --git a/test/test.hh b/test/test.hh index 9092165615..fd9f82cf0b 100755 --- a/test/test.hh +++ b/test/test.hh @@ -85,7 +85,11 @@ namespace Test { /// Number of threads to use unsigned int threads; /// The random seed to be used - unsigned int seed; + uint64_t seed; + /// Complete state for replaying exactly one named test + std::string random_state; + /// Exact test name (also used by failure replay commands) + std::string exact_test; /// Number of iterations for each test unsigned int iter; /// Default number of iterations 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); diff --git a/tools/random-benchmark.cpp b/tools/random-benchmark.cpp new file mode 100644 index 0000000000..6e3699bc5d --- /dev/null +++ b/tools/random-benchmark.cpp @@ -0,0 +1,180 @@ +/* -*- 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),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 + std::cout << (random ? "size.random_snapshot" : "size.plain_snapshot") + << '\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; + 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..7f0e1c429a --- /dev/null +++ b/tools/random-benchmark.py @@ -0,0 +1,104 @@ +#!/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 \ + --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()